diff --git a/src/components/Projects/CompleteProjectConfirmation.tsx b/src/components/Projects/CompleteProjectConfirmation.tsx
new file mode 100644
index 000000000..d30fe9c25
--- /dev/null
+++ b/src/components/Projects/CompleteProjectConfirmation.tsx
@@ -0,0 +1,74 @@
+import { useMutation } from '@tanstack/react-query';
+import { queryClient } from '../../stores/query-client';
+import { Modal } from '../Modal';
+import { httpPost } from '../../lib/query-http';
+import { useToast } from '../../hooks/use-toast';
+import { Loader2Icon } from 'lucide-react';
+import { projectStatusOptions } from '../../queries/project';
+
+type CompleteProjectConfirmationProps = {
+ projectId: string;
+ onClose: () => void;
+};
+
+export function CompleteProjectConfirmation(
+ props: CompleteProjectConfirmationProps,
+) {
+ const { onClose, projectId } = props;
+
+ const toast = useToast();
+
+ const { mutate: completeProject, isPending: isCompletingProject } =
+ useMutation(
+ {
+ mutationFn: () => {
+ return httpPost<{
+ startedAt: Date;
+ }>(
+ `${import.meta.env.PUBLIC_API_URL}/v1-mark-as-done-project/${projectId}`,
+ {},
+ );
+ },
+ onSettled: () => {
+ queryClient.invalidateQueries(projectStatusOptions(projectId));
+ },
+ onError: (error) => {
+ toast.error(error?.message || 'Failed to start project');
+ },
+ onSuccess: () => {
+ onClose();
+ },
+ },
+ queryClient,
+ );
+
+ return (
+
+
+ Complete Project
+
+
+ Are you sure you want to mark this project as completed?
+
+
+
+
+ Cancel
+
+ completeProject()}
+ className="flex h-9 items-center justify-center gap-2 rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700"
+ >
+ {isCompletingProject ? (
+
+ ) : (
+ 'Complete Project'
+ )}
+
+
+
+ );
+}
diff --git a/src/components/Projects/ProjectTabs.tsx b/src/components/Projects/ProjectTabs.tsx
index 1c7350b07..3ff14814c 100644
--- a/src/components/Projects/ProjectTabs.tsx
+++ b/src/components/Projects/ProjectTabs.tsx
@@ -1,13 +1,5 @@
import { cn } from '../../lib/classname';
-import {
- ArrowLeft,
- Blocks,
- BoxSelect,
- type LucideIcon,
- StepBackIcon,
- StickyNote,
- Text,
-} from 'lucide-react';
+import { ArrowLeft, Blocks, type LucideIcon, Text } from 'lucide-react';
export const allowedProjectTabs = ['details', 'solutions'] as const;
export type AllowedProjectTab = (typeof allowedProjectTabs)[number];
@@ -36,7 +28,7 @@ function TabButton(props: TabButtonProps) {
{smText && {smText} }
{isActive && (
-
+
)}
);
@@ -46,10 +38,16 @@ type ProjectTabsProps = {
activeTab: AllowedProjectTab;
projectId: string;
parentRoadmapId?: string;
+ hasNoSubmission?: boolean;
};
export function ProjectTabs(props: ProjectTabsProps) {
- const { activeTab, parentRoadmapId, projectId } = props;
+ const {
+ activeTab,
+ parentRoadmapId,
+ projectId,
+ hasNoSubmission = false,
+ } = props;
return (
@@ -69,13 +67,15 @@ export function ProjectTabs(props: ProjectTabsProps) {
isActive={activeTab === 'details'}
href={`/projects/${projectId}`}
/>
-
+ {!hasNoSubmission && (
+
+ )}
);
}
diff --git a/src/components/Projects/StartProjectConfirmation.tsx b/src/components/Projects/StartProjectConfirmation.tsx
new file mode 100644
index 000000000..c44b64fbc
--- /dev/null
+++ b/src/components/Projects/StartProjectConfirmation.tsx
@@ -0,0 +1,71 @@
+import { useMutation } from '@tanstack/react-query';
+import { queryClient } from '../../stores/query-client';
+import { Modal } from '../Modal';
+import { httpPost } from '../../lib/query-http';
+import { useToast } from '../../hooks/use-toast';
+import { Loader2Icon } from 'lucide-react';
+import { projectStatusOptions } from '../../queries/project';
+
+type StartProjectConfirmationProps = {
+ projectId: string;
+ onClose: () => void;
+};
+
+export function StartProjectConfirmation(props: StartProjectConfirmationProps) {
+ const { onClose, projectId } = props;
+
+ const toast = useToast();
+
+ const { mutate: startProject, isPending: isStartingProject } = useMutation(
+ {
+ mutationFn: () => {
+ return httpPost<{
+ startedAt: Date;
+ }>(
+ `${import.meta.env.PUBLIC_API_URL}/v1-start-project/${projectId}`,
+ {},
+ );
+ },
+ onSettled: () => {
+ queryClient.invalidateQueries(projectStatusOptions(projectId));
+ },
+ onSuccess: () => {
+ onClose();
+ },
+ onError: (error) => {
+ toast.error(error?.message || 'Failed to start project');
+ },
+ },
+ queryClient,
+ );
+
+ return (
+
+
+ Start Project
+
+
+ Are you sure you want to start this project?
+
+
+
+
+ Cancel
+
+ startProject()}
+ className="flex h-9 items-center justify-center gap-2 rounded-lg bg-purple-600 px-4 py-2 text-sm font-medium text-white hover:bg-purple-700"
+ >
+ {isStartingProject ? (
+
+ ) : (
+ 'Start Project'
+ )}
+
+
+
+ );
+}
diff --git a/src/components/Projects/StartProjectModal.tsx b/src/components/Projects/StartProjectModal.tsx
index 820889a96..dacf0d969 100644
--- a/src/components/Projects/StartProjectModal.tsx
+++ b/src/components/Projects/StartProjectModal.tsx
@@ -7,20 +7,6 @@ import { httpPost } from '../../lib/http.ts';
import { CheckIcon } from '../ReactIcons/CheckIcon.tsx';
import { useCopyText } from '../../hooks/use-copy-text.ts';
-type StepLabelProps = {
- label: string;
-};
-
-function StepLabel(props: StepLabelProps) {
- const { label } = props;
-
- return (
-
- {label}
-
- );
-}
-
type StartProjectModalProps = {
projectId: string;
onClose: () => void;
@@ -66,7 +52,7 @@ export function StartProjectModal(props: StartProjectModalProps) {
if (error) {
return (
-
+
@@ -77,7 +63,7 @@ export function StartProjectModal(props: StartProjectModalProps) {
if (isStartingProject) {
return (
-
+
@@ -96,7 +82,7 @@ export function StartProjectModal(props: StartProjectModalProps) {
Project started {' '}
{formattedStartedAt}
-
+
Start Building
@@ -109,8 +95,8 @@ export function StartProjectModal(props: StartProjectModalProps) {
- 2. Complete the project according to the requirements and push your code
- to the GitHub repository.
+ 2. Complete the project according to the requirements and push your
+ code to the GitHub repository.
@@ -139,13 +125,13 @@ export function StartProjectModal(props: StartProjectModalProps) {
- 4. Once done, submit your solution to help the others learn and get feedback
- from the community.
+ 4. Once done, submit your solution to help the others learn and get
+ feedback from the community.
-
+
If you get stuck, you can always ask for help in the community{' '}
{
+ if (!projectStatus) {
+ return;
+ }
+
+ setIsLoading(false);
+ }, [projectStatus]);
+
+ const { startedAt, submittedAt } = projectStatus || {};
+ const formattedStartedAt = startedAt ? getRelativeTimeString(startedAt) : '';
+ const formattedSubmittedAt = submittedAt
+ ? getRelativeTimeString(submittedAt)
+ : '';
+ const isCompleted = !!submittedAt;
+
+ return (
+ <>
+ {isStartingProject && (
+ setIsStartingProject(false)}
+ projectId={projectId}
+ />
+ )}
+
+ {isCompletingProject && (
+ setIsCompletingProject(false)}
+ projectId={projectId}
+ />
+ )}
+
+ {!startedAt && (
+ {
+ if (!isLoggedIn()) {
+ showLoginPopup();
+ return;
+ }
+
+ setIsStartingProject(true);
+ }}
+ className={cn(
+ 'relative flex items-center gap-1.5 overflow-hidden rounded-full bg-purple-600 py-1 pr-2.5 pl-2 text-sm text-white hover:bg-purple-700',
+ isLoading && 'bg-white text-gray-500',
+ )}
+ disabled={isLoading}
+ >
+
+ Start Working
+
+ {isLoading && (
+
+ )}
+
+ )}
+
+ {startedAt && !isLoading && (
+
+
setIsCompletingProject(true)}
+ className={cn(
+ 'relative flex items-center gap-1.5 overflow-hidden rounded-full bg-green-600 py-1 pr-2.5 pl-2 text-sm text-white hover:bg-green-700',
+ isCompleted &&
+ 'cursor-default bg-gray-200 text-gray-500 hover:bg-gray-200',
+ )}
+ disabled={isCompleted}
+ >
+
+ {isCompleted ? (
+ Completed
+ ) : (
+ Mark as Completed
+ )}
+
+
+
+ {isCompleted ? (
+ <>
+ Completed{' '}
+ {formattedSubmittedAt}
+ >
+ ) : (
+ <>
+ Started working{' '}
+ {formattedStartedAt}
+ >
+ )}
+
+
+ )}
+ >
+ );
+}
diff --git a/src/lib/project.ts b/src/lib/project.ts
index 011f829f7..41cef4841 100644
--- a/src/lib/project.ts
+++ b/src/lib/project.ts
@@ -1,5 +1,5 @@
import type { MarkdownFileType } from './file';
-import { getRoadmapById, type RoadmapFileType } from './roadmap.ts';
+import { getRoadmapById, type RoadmapFileType } from './roadmap';
export const projectDifficulties = [
'beginner',
@@ -22,6 +22,7 @@ export interface ProjectFrontmatter {
keywords: string[];
ogImageUrl: string;
};
+ hasNoSubmission: boolean;
roadmapIds: string[];
}
diff --git a/src/pages/projects/[projectId]/index.astro b/src/pages/projects/[projectId]/index.astro
index 5783b6337..3c28a5763 100644
--- a/src/pages/projects/[projectId]/index.astro
+++ b/src/pages/projects/[projectId]/index.astro
@@ -8,6 +8,7 @@ import {
} from '../../../lib/project';
import AstroIcon from '../../../components/AstroIcon.astro';
import { ProjectStepper } from '../../../components/Projects/StatusStepper/ProjectStepper';
+import { ProjectTrackingActions } from '../../../components/Projects/StatusStepper/ProjectTrackingActions';
import { ProjectTabs } from '../../../components/Projects/ProjectTabs';
export const prerender = true;
@@ -51,7 +52,12 @@ const parentRoadmapId = projectData?.roadmapIds?.[0] || '';
>
-
+
-
-
- {projectData.title}
-
-
- {projectData.description}
-
+
+
+
+ {projectData.title}
+
+
+ {projectData.description}
+
+
+ {
+ projectData?.hasNoSubmission && (
+
+ )
+ }
-
+ {
+ !projectData?.hasNoSubmission && (
+
+ )
+ }
diff --git a/src/pages/projects/[projectId]/solutions.astro b/src/pages/projects/[projectId]/solutions.astro
index 848326b74..343ea1cc8 100644
--- a/src/pages/projects/[projectId]/solutions.astro
+++ b/src/pages/projects/[projectId]/solutions.astro
@@ -15,6 +15,7 @@ export async function getStaticPaths() {
const projects = await getAllProjects();
return projects
+ .filter((project) => !(project?.frontmatter?.hasNoSubmission || false))
.map((project) => project.id)
.map((projectId) => ({
params: { projectId },
diff --git a/src/queries/project.ts b/src/queries/project.ts
new file mode 100644
index 000000000..f7cd482ac
--- /dev/null
+++ b/src/queries/project.ts
@@ -0,0 +1,26 @@
+import { queryOptions } from '@tanstack/react-query';
+import { httpGet } from '../lib/query-http';
+import { isLoggedIn } from '../lib/jwt';
+
+type ProjectStatusResponse = {
+ id?: string;
+
+ startedAt?: Date;
+ submittedAt?: Date;
+ repositoryUrl?: string;
+
+ upvotes: number;
+ downvotes: number;
+};
+
+export function projectStatusOptions(projectId: string) {
+ return queryOptions({
+ queryKey: ['project-status', projectId],
+ queryFn: () => {
+ return httpGet
(
+ `${import.meta.env.PUBLIC_API_URL}/v1-project-status/${projectId}`,
+ {},
+ );
+ },
+ });
+}