From d36af2d3fa51ee211007cdacfb8cde3cbbd92ffd Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Tue, 22 Apr 2025 16:36:36 +0600 Subject: [PATCH] feat: project without submission (#8530) --- .../Projects/CompleteProjectConfirmation.tsx | 74 +++++++++++ src/components/Projects/ProjectTabs.tsx | 36 ++--- .../Projects/StartProjectConfirmation.tsx | 71 ++++++++++ src/components/Projects/StartProjectModal.tsx | 30 ++--- .../Projects/StatusStepper/ProjectStepper.tsx | 28 ++-- .../StatusStepper/ProjectTrackingActions.tsx | 123 ++++++++++++++++++ src/lib/project.ts | 3 +- src/pages/projects/[projectId]/index.astro | 37 ++++-- .../projects/[projectId]/solutions.astro | 1 + src/queries/project.ts | 26 ++++ 10 files changed, 364 insertions(+), 65 deletions(-) create mode 100644 src/components/Projects/CompleteProjectConfirmation.tsx create mode 100644 src/components/Projects/StartProjectConfirmation.tsx create mode 100644 src/components/Projects/StatusStepper/ProjectTrackingActions.tsx create mode 100644 src/queries/project.ts 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? +

+ +
+ + +
+
+ ); +} 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? +

+ +
+ + +
+
+ ); +} 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 ( -
+

{error}

@@ -77,7 +63,7 @@ export function StartProjectModal(props: StartProjectModalProps) { if (isStartingProject) { return ( -
+

Starting project ..

@@ -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 && ( + + )} + + {startedAt && !isLoading && ( +

+ )} + + ); +} 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}`, + {}, + ); + }, + }); +}