mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-09-24 15:00:31 +08:00
345 lines
10 KiB
TypeScript
345 lines
10 KiB
TypeScript
import { Flag, Play, Send, Share, Square, StopCircle, X } from 'lucide-react';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { cn } from '../../../lib/classname';
|
|
import { useStickyStuck } from '../../../hooks/use-sticky-stuck';
|
|
import { StepperAction } from './StepperAction';
|
|
import { StepperStepSeparator } from './StepperStepSeparator';
|
|
import { MilestoneStep } from './MilestoneStep';
|
|
import { httpGet, httpPost } from '../../../lib/http';
|
|
import { StartProjectModal } from '../StartProjectModal';
|
|
import { getRelativeTimeString } from '../../../lib/date';
|
|
import { getUser, isLoggedIn } from '../../../lib/jwt';
|
|
import { showLoginPopup } from '../../../lib/popup';
|
|
import { SubmitProjectModal } from '../SubmitProjectModal';
|
|
import { useCopyText } from '../../../hooks/use-copy-text';
|
|
import { CheckIcon } from '../../ReactIcons/CheckIcon';
|
|
import { pageProgressMessage } from '../../../stores/page';
|
|
|
|
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<HTMLDivElement>(null);
|
|
const isSticky = useStickyStuck(stickyElRef, 8);
|
|
const currentUser = getUser();
|
|
|
|
const [isStartingProject, setIsStartingProject] = useState(false);
|
|
const [isSubmittingProject, setIsSubmittingProject] = useState(false);
|
|
const { copyText, isCopied } = useCopyText();
|
|
|
|
const [isStoppingProject, setIsStoppingProject] = useState(false);
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [activeStep, setActiveStep] = useState<number>(0);
|
|
const [isLoadingStatus, setIsLoadingStatus] = useState(true);
|
|
const [projectStatus, setProjectStatus] = useState<ProjectStatusResponse>({
|
|
upvotes: 0,
|
|
downvotes: 0,
|
|
});
|
|
|
|
async function loadProjectStatus() {
|
|
setIsLoadingStatus(true);
|
|
|
|
const { response, error } = await httpGet<ProjectStatusResponse>(
|
|
`${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(3);
|
|
} else if (submittedAt) {
|
|
setActiveStep(2);
|
|
} else if (startedAt) {
|
|
setActiveStep(1);
|
|
}
|
|
|
|
setProjectStatus(response);
|
|
setIsLoadingStatus(false);
|
|
}
|
|
|
|
const stopProject = async () => {
|
|
setIsStoppingProject(true);
|
|
const { response, error } = await httpPost(
|
|
`${import.meta.env.PUBLIC_API_URL}/v1-stop-project/${projectId}`,
|
|
{},
|
|
);
|
|
|
|
if (error || !response) {
|
|
setIsStoppingProject(false);
|
|
setError(error?.message || 'Error stopping project');
|
|
return;
|
|
}
|
|
|
|
pageProgressMessage.set('Update project status');
|
|
setActiveStep(0);
|
|
loadProjectStatus().finally(() => {
|
|
pageProgressMessage.set('');
|
|
setIsStoppingProject(false);
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadProjectStatus().finally(() => {});
|
|
}, []);
|
|
|
|
const projectSolutionUrl = `${import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh'}/projects/${projectId}/solutions?u=${currentUser?.id}`;
|
|
|
|
return (
|
|
<div
|
|
ref={stickyElRef}
|
|
className={cn(
|
|
'relative top-0 -mx-4 my-5 overflow-hidden rounded-none border border-x-0 bg-white transition-all sm:sticky sm:mx-0 sm:rounded-lg sm:border-x',
|
|
{
|
|
'sm:-mx-5 sm:rounded-none sm:border-x-0 sm:border-t-0 sm:bg-gray-50':
|
|
isSticky,
|
|
},
|
|
)}
|
|
>
|
|
{isSubmittingProject && (
|
|
<SubmitProjectModal
|
|
onClose={() => setIsSubmittingProject(false)}
|
|
projectId={projectId}
|
|
onSubmit={(response) => {
|
|
const { repositoryUrl, submittedAt } = response;
|
|
|
|
setProjectStatus({
|
|
...projectStatus,
|
|
repositoryUrl,
|
|
submittedAt,
|
|
});
|
|
|
|
setActiveStep(2);
|
|
}}
|
|
repositoryUrl={projectStatus.repositoryUrl}
|
|
/>
|
|
)}
|
|
{isStartingProject && (
|
|
<StartProjectModal
|
|
projectId={projectId}
|
|
onStarted={(startedAt) => {
|
|
setProjectStatus({
|
|
...projectStatus,
|
|
startedAt,
|
|
});
|
|
setActiveStep(1);
|
|
}}
|
|
startedAt={projectStatus?.startedAt}
|
|
onClose={() => setIsStartingProject(false)}
|
|
/>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="absolute inset-0 bg-red-100 p-2 text-sm text-red-500">
|
|
{error}
|
|
</div>
|
|
)}
|
|
{isLoadingStatus && (
|
|
<div className={cn('striped-loader absolute inset-0 z-10 bg-white')} />
|
|
)}
|
|
<div
|
|
className={cn(
|
|
'bg-gray-100 px-4 py-2 text-sm text-gray-500 transition-colors sm:flex sm:items-center',
|
|
{
|
|
'bg-purple-600 text-white': isSticky,
|
|
},
|
|
)}
|
|
>
|
|
{activeStep === 0 && (
|
|
<>
|
|
Start building, submit solution and get feedback from the community.
|
|
</>
|
|
)}
|
|
{activeStep === 1 && (
|
|
<>
|
|
Started working
|
|
<span
|
|
className={cn('font-medium text-gray-800', {
|
|
'text-purple-200': isSticky,
|
|
})}
|
|
>
|
|
{getRelativeTimeString(projectStatus.startedAt!)}
|
|
</span>
|
|
. Follow
|
|
<button
|
|
className={cn('underline underline-offset-2 hover:text-black', {
|
|
'text-purple-100 hover:text-white': isSticky,
|
|
})}
|
|
onClick={() => {
|
|
setIsStartingProject(true);
|
|
}}
|
|
>
|
|
these tips
|
|
</button>
|
|
to get most out of it.
|
|
</>
|
|
)}
|
|
{activeStep >= 2 && (
|
|
<>
|
|
Congrats on submitting your solution.
|
|
<button
|
|
className={cn('underline underline-offset-2 hover:text-black', {
|
|
'text-purple-100 hover:text-white': isSticky,
|
|
})}
|
|
onClick={() => {
|
|
setIsSubmittingProject(true);
|
|
}}
|
|
>
|
|
View or update your submission.
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
{projectStatus?.startedAt && !projectStatus?.submittedAt && (
|
|
<button
|
|
className={cn(
|
|
'ml-auto hidden items-center gap-1.5 text-sm hover:text-black disabled:opacity-50 sm:flex',
|
|
)}
|
|
onClick={stopProject}
|
|
disabled={isStoppingProject}
|
|
>
|
|
<Square className="h-3 w-3 fill-current stroke-[2.5px]" />
|
|
<span className="hidden md:inline">Stop Working</span>
|
|
<span className="md:hidden">Stop</span>
|
|
</button>
|
|
)}
|
|
|
|
{activeStep >= 2 && (
|
|
<button
|
|
className={cn(
|
|
'ml-auto hidden items-center gap-1 text-sm sm:flex',
|
|
isCopied ? 'text-green-500' : '',
|
|
)}
|
|
onClick={() => {
|
|
copyText(projectSolutionUrl);
|
|
}}
|
|
>
|
|
{isCopied ? (
|
|
<>
|
|
<CheckIcon additionalClasses="h-3 w-3" />
|
|
Copied
|
|
</>
|
|
) : (
|
|
<>
|
|
<Share className="h-3 w-3 stroke-[2.5px]" />
|
|
<span className="hidden md:inline">Share your Solution</span>
|
|
<span className="md:hidden">Share</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex min-h-[60px] flex-col items-start justify-between gap-2 px-4 py-4 sm:flex-row sm:items-center sm:gap-3 sm:py-0">
|
|
<StepperAction
|
|
isActive={activeStep === 0}
|
|
isCompleted={activeStep > 0}
|
|
icon={Play}
|
|
text={activeStep > 0 ? 'Started Working' : 'Start Working'}
|
|
number={1}
|
|
onClick={() => {
|
|
if (!isLoggedIn()) {
|
|
showLoginPopup();
|
|
return;
|
|
}
|
|
|
|
setIsStartingProject(true);
|
|
}}
|
|
/>
|
|
<StepperStepSeparator isActive={activeStep > 0} />
|
|
<div className="flex items-center gap-2">
|
|
<StepperAction
|
|
isActive={activeStep === 1}
|
|
isCompleted={activeStep > 1}
|
|
icon={Send}
|
|
onClick={() => {
|
|
if (!isLoggedIn()) {
|
|
showLoginPopup();
|
|
return;
|
|
}
|
|
|
|
setIsSubmittingProject(true);
|
|
}}
|
|
text={activeStep > 1 ? 'Submitted' : 'Submit Solution'}
|
|
number={2}
|
|
/>
|
|
|
|
{activeStep > 1 && (
|
|
<>
|
|
<span className="text-gray-600 sm:hidden">·</span>
|
|
<button
|
|
className={cn(
|
|
'flex items-center gap-2 text-sm sm:hidden',
|
|
isCopied ? 'text-green-500' : 'text-gray-600',
|
|
)}
|
|
onClick={() => {
|
|
copyText(projectSolutionUrl);
|
|
}}
|
|
>
|
|
{isCopied ? (
|
|
<>
|
|
<CheckIcon additionalClasses="h-3 w-3" />
|
|
URL Copied
|
|
</>
|
|
) : (
|
|
<>
|
|
<Share className="h-3 w-3 stroke-[2.5px]" />
|
|
Share your Solution
|
|
</>
|
|
)}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
<StepperStepSeparator isActive={activeStep > 1} />
|
|
<MilestoneStep
|
|
isActive={activeStep === 2}
|
|
isCompleted={activeStep > 2}
|
|
icon={Flag}
|
|
text={
|
|
activeStep == 2
|
|
? `${projectStatus.upvotes} / 5 upvotes`
|
|
: `5 upvotes`
|
|
}
|
|
/>
|
|
<StepperStepSeparator isActive={activeStep > 2} />
|
|
<MilestoneStep
|
|
isActive={activeStep === 3}
|
|
isCompleted={activeStep > 3}
|
|
icon={Flag}
|
|
text={
|
|
activeStep == 3
|
|
? `${projectStatus.upvotes} / 10 upvotes`
|
|
: activeStep > 3
|
|
? `${projectStatus.upvotes} upvotes`
|
|
: `10 upvotes`
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|