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
-
+