From b0d80f6769dc402b072b5177dc87907d906faa79 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed Date: Fri, 16 Aug 2024 02:02:50 +0100 Subject: [PATCH] Refactor project stepper --- src/components/Projects/ProjectStepper.tsx | 166 ------------------ .../Projects/StatusStepper/MilestoneStep.tsx | 27 +++ .../Projects/StatusStepper/ProjectStepper.tsx | 128 ++++++++++++++ .../Projects/StatusStepper/StepperAction.tsx | 51 ++++++ .../StatusStepper/StepperStepSeparator.tsx | 17 ++ src/hooks/use-sticky-stuck.tsx | 24 +++ src/pages/projects/[projectId]/index.astro | 4 +- 7 files changed, 249 insertions(+), 168 deletions(-) delete mode 100644 src/components/Projects/ProjectStepper.tsx create mode 100644 src/components/Projects/StatusStepper/MilestoneStep.tsx create mode 100644 src/components/Projects/StatusStepper/ProjectStepper.tsx create mode 100644 src/components/Projects/StatusStepper/StepperAction.tsx create mode 100644 src/components/Projects/StatusStepper/StepperStepSeparator.tsx create mode 100644 src/hooks/use-sticky-stuck.tsx diff --git a/src/components/Projects/ProjectStepper.tsx b/src/components/Projects/ProjectStepper.tsx deleted file mode 100644 index 60cd3d374..000000000 --- a/src/components/Projects/ProjectStepper.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { - Blocks, - Check, - Flag, - Hammer, - type LucideIcon, - Play, - PlayCircle, - Send, -} from 'lucide-react'; -import { useEffect, useRef, useState } from 'react'; -import { cn } from '../../lib/classname.ts'; - -type StepperActionProps = { - isActive?: boolean; - isCompleted?: boolean; - onClick?: () => void; - icon: LucideIcon; - text: string; - number: number; -}; - -function StepperAction(props: StepperActionProps) { - const { - isActive, - onClick = () => null, - isCompleted, - icon: DisplayIcon, - text, - number, - } = props; - - if (isActive) { - return ( - - ); - } - - if (isCompleted) { - return ( - - - {text} - - ); - } - - return ( - - - {number} - - {text} - - ); -} - -type StepperStepSeparatorProps = { - isActive: boolean; -}; - -function StepperStepSeparator(props: StepperStepSeparatorProps) { - const { isActive } = props; - - return ( -
- ); -} - -type MilestoneStepProps = { - icon: LucideIcon; - text: string; - isCompleted?: boolean; -}; - -function MilestoneStep(props: MilestoneStepProps) { - const { icon: DisplayIcon, text, isCompleted } = props; - - if (isCompleted) { - return ( - - - {text} - - ); - } - - return ( - - - {text} - - ); -} - -export function ProjectStepper() { - const stickyElRef = useRef(null); - const [isSticky, setIsSticky] = useState(false); - - // on scroll check if the element has sticky class in effect - useEffect(() => { - const handleScroll = () => { - if (stickyElRef.current) { - setIsSticky(stickyElRef.current.getBoundingClientRect().top <= 8); - } - }; - - window.addEventListener('scroll', handleScroll); - return () => { - window.removeEventListener('scroll', handleScroll); - }; - }, []); - - return ( -
-
- Start building, submit solution and get feedback from the community. -
- -
- - - - - - - -
-
- ); -} diff --git a/src/components/Projects/StatusStepper/MilestoneStep.tsx b/src/components/Projects/StatusStepper/MilestoneStep.tsx new file mode 100644 index 000000000..8866c7554 --- /dev/null +++ b/src/components/Projects/StatusStepper/MilestoneStep.tsx @@ -0,0 +1,27 @@ +import { Check, type LucideIcon } from 'lucide-react'; + +type MilestoneStepProps = { + icon: LucideIcon; + text: string; + isCompleted?: boolean; +}; + +export function MilestoneStep(props: MilestoneStepProps) { + const { icon: DisplayIcon, text, isCompleted } = props; + + if (isCompleted) { + return ( + + + {text} + + ); + } + + return ( + + + {text} + + ); +} \ No newline at end of file diff --git a/src/components/Projects/StatusStepper/ProjectStepper.tsx b/src/components/Projects/StatusStepper/ProjectStepper.tsx new file mode 100644 index 000000000..8bb0f7e54 --- /dev/null +++ b/src/components/Projects/StatusStepper/ProjectStepper.tsx @@ -0,0 +1,128 @@ +import { Flag, Play, Send } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; +import { cn } from '../../../lib/classname.ts'; +import { useStickyStuck } from '../../../hooks/use-sticky-stuck.tsx'; +import { StepperAction } from './StepperAction.tsx'; +import { StepperStepSeparator } from './StepperStepSeparator.tsx'; +import { MilestoneStep } from './MilestoneStep.tsx'; +import { httpGet } from '../../../lib/http.ts'; + +type ProjectStatusResponse = { + id?: string; + + startedAt?: Date; + submittedAt?: Date; + repositoryUrl?: string; + + upvotes: number; + downvotes: number; +}; + +type ProjectStepperProps = { + projectId: string; +}; + +export function ProjectStepper(props: ProjectStepperProps) { + const { projectId } = props; + + const stickyElRef = useRef(null); + const isSticky = useStickyStuck(stickyElRef, 8); + + const [error, setError] = useState(null); + const [activeStep, setActiveStep] = useState(0); + const [isLoadingStatus, setIsLoadingStatus] = useState(true); + const [projectStatus, setProjectStatus] = useState({ + upvotes: 0, + downvotes: 0, + }); + + async function loadProjectStatus() { + setIsLoadingStatus(true); + + const { response, error } = await httpGet( + `${import.meta.env.PUBLIC_API_URL}/v1-project-status/${projectId}`, + {}, + ); + + if (error || !response) { + setError(error?.message || 'Error loading project status'); + setIsLoadingStatus(false); + return; + } + + const { startedAt, submittedAt, upvotes } = response; + + if (upvotes >= 10) { + setActiveStep(4); + } else if (upvotes >= 5) { + setActiveStep(4); + } else if (submittedAt) { + setActiveStep(3); + } else if (startedAt) { + setActiveStep(1); + } + + setProjectStatus(response); + setIsLoadingStatus(false); + } + + useEffect(() => { + loadProjectStatus().finally(() => {}); + }, []); + + return ( +
+ {isLoadingStatus && ( +
+ )} +
+ Start building, submit solution and get feedback from the community. +
+ +
+ 0} + icon={Play} + text={activeStep > 0 ? 'Started Working' : 'Start Working'} + number={1} + /> + 0} /> + 1} + icon={Send} + text={activeStep > 1 ? 'Submitted' : 'Submit Solution'} + number={2} + /> + 1} /> + 2} + icon={Flag} + text={'5 upvotes'} + /> + 2} /> + 3} + icon={Flag} + text={'10 upvotes'} + /> +
+
+ ); +} diff --git a/src/components/Projects/StatusStepper/StepperAction.tsx b/src/components/Projects/StatusStepper/StepperAction.tsx new file mode 100644 index 000000000..a6555fe13 --- /dev/null +++ b/src/components/Projects/StatusStepper/StepperAction.tsx @@ -0,0 +1,51 @@ +import { Check, type LucideIcon } from 'lucide-react'; + +type StepperActionProps = { + isActive?: boolean; + isCompleted?: boolean; + onClick?: () => void; + icon: LucideIcon; + text: string; + number: number; +}; + +export function StepperAction(props: StepperActionProps) { + const { + isActive, + onClick = () => null, + isCompleted, + icon: DisplayIcon, + text, + number, + } = props; + + if (isActive) { + return ( + + ); + } + + if (isCompleted) { + return ( + + + {text} + + ); + } + + return ( + + + {number} + + {text} + + ); +} diff --git a/src/components/Projects/StatusStepper/StepperStepSeparator.tsx b/src/components/Projects/StatusStepper/StepperStepSeparator.tsx new file mode 100644 index 000000000..ba3ee3ba7 --- /dev/null +++ b/src/components/Projects/StatusStepper/StepperStepSeparator.tsx @@ -0,0 +1,17 @@ +import { cn } from '../../../lib/classname.ts'; + +type StepperStepSeparatorProps = { + isActive: boolean; +}; + +export function StepperStepSeparator(props: StepperStepSeparatorProps) { + const { isActive } = props; + + return ( +
+ ); +} diff --git a/src/hooks/use-sticky-stuck.tsx b/src/hooks/use-sticky-stuck.tsx new file mode 100644 index 000000000..60743840d --- /dev/null +++ b/src/hooks/use-sticky-stuck.tsx @@ -0,0 +1,24 @@ +import { type RefObject, useEffect, useState } from 'react'; + +// Checks if the sticky element is stuck or not +export function useStickyStuck( + ref: RefObject, + offset: number = 0, +): boolean { + const [isSticky, setIsSticky] = useState(false); + + useEffect(() => { + const handleScroll = () => { + if (ref.current) { + setIsSticky(ref.current.getBoundingClientRect().top <= offset); + } + }; + + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, [ref, offset]); + + return isSticky; +} diff --git a/src/pages/projects/[projectId]/index.astro b/src/pages/projects/[projectId]/index.astro index e7ef720e1..d08db9a86 100644 --- a/src/pages/projects/[projectId]/index.astro +++ b/src/pages/projects/[projectId]/index.astro @@ -8,7 +8,7 @@ import { } from '../../../lib/project'; import AstroIcon from '../../../components/AstroIcon.astro'; import { ProjectMilestoneStrip } from '../../../components/Projects/ProjectMilestoneStrip'; -import { ProjectStepper } from "../../../components/Projects/ProjectStepper"; +import { ProjectStepper } from "../../../components/Projects/StatusStepper/ProjectStepper"; import { ProjectTabs } from '../../../components/Projects/ProjectTabs'; export async function getStaticPaths() { @@ -72,7 +72,7 @@ const githubUrl = `https://github.com/kamranahmedse/developer-roadmap/tree/maste
- +