Add projects tip popup

pull/6513/head
Kamran Ahmed 3 months ago
parent b0d80f6769
commit 05d09335b5
  1. 4
      src/components/Modal.tsx
  2. 2
      src/components/OpenSourceStat.astro
  3. 176
      src/components/Projects/StartProjectModal.tsx
  4. 64
      src/components/Projects/StatusStepper/ProjectStepper.tsx

@ -33,7 +33,7 @@ export function Modal(props: ModalProps) {
return ( return (
<div <div
className={cn( className={cn(
'popup fixed left-0 right-0 top-0 z-[99] flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50', 'fixed left-0 right-0 top-0 z-[99] flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50',
overlayClassName, overlayClassName,
)} )}
> >
@ -46,7 +46,7 @@ export function Modal(props: ModalProps) {
<div <div
ref={popupBodyEl} ref={popupBodyEl}
className={cn( className={cn(
'popup-body relative h-full rounded-lg bg-white shadow', 'relative h-full rounded-lg bg-white shadow',
bodyClassName, bodyClassName,
)} )}
> >

@ -44,7 +44,7 @@ const isDiscordMembers = text.toLowerCase() === 'discord members';
} }
<div class="flex flex-row items-center sm:flex-col my-1 sm:my-0"> <div class="flex flex-row items-center sm:flex-col my-1 sm:my-0">
<p <p
class='relative my-0 sm:my-4 mr-1 sm:mr-0 text-base font-bold sm:w-auto sm:text-5xl' class='my-0 sm:my-4 mr-1 sm:mr-0 text-base font-bold sm:w-auto sm:text-5xl'
> >
{value} {value}
</p> </p>

@ -1,57 +1,175 @@
import { X } from 'lucide-react'; import {
Check,
CheckCircle,
CheckCircle2,
CopyCheck,
CopyIcon,
ServerCrash,
X,
} from 'lucide-react';
import { Modal } from '../Modal'; import { Modal } from '../Modal';
import { getRelativeTimeString } from '../../lib/date'; import { getRelativeTimeString } from '../../lib/date';
import { useEffect, useState } from 'react';
import { Spinner } from '../ReactIcons/Spinner.tsx';
import { httpPost } from '../../lib/http.ts';
import { ErrorIcon } from '../ReactIcons/ErrorIcon.tsx';
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 (
<span className="flex-shrink-0 rounded-full bg-gray-200 px-2 py-1 text-xs text-gray-600">
{label}
</span>
);
}
type StartProjectModalProps = { type StartProjectModalProps = {
projectId: string;
onClose: () => void; onClose: () => void;
startedAt?: Date; startedAt?: Date;
onStarted: (startedAt: Date) => void;
}; };
export function StartProjectModal(props: StartProjectModalProps) { export function StartProjectModal(props: StartProjectModalProps) {
const { onClose, startedAt } = props; const { onClose, startedAt, onStarted, projectId } = props;
const [isStartingProject, setIsStartingProject] = useState(true);
const [error, setError] = useState<string | null>();
const { isCopied, copyText } = useCopyText();
const projectUrl = `${import.meta.env.PUBLIC_APP_URL}/projects/${projectId}`;
const projectTips = [ const projectTips = [
'Create a repository on GitHub', 'Create a repository on GitHub',
'Develop the required functionality', 'Complete the task and push it to GitHub',
'Add a readme and make sure to link to the project page', 'Add a readme file with instructions on how to run the project',
'Once you are done, make sure to come back and submit your solution to get feedback from others.', 'Submit your project once you are done to get feedback from the community',
'Feel free to join our discord and ask for help if you get stuck.',
]; ];
const formattedStartedAt = startedAt ? getRelativeTimeString(startedAt) : ''; const formattedStartedAt = startedAt ? getRelativeTimeString(startedAt) : '';
async function handleStartProject() {
if (!projectId || startedAt) {
return;
}
setIsStartingProject(true);
const { response, error } = await httpPost<{
startedAt: Date;
}>(`${import.meta.env.PUBLIC_API_URL}/v1-start-project/${projectId}`, {});
if (error || !response) {
setError(error?.message || 'Failed to start project');
setIsStartingProject(false);
return;
}
onStarted(response.startedAt);
}
useEffect(() => {
handleStartProject().finally(() => setIsStartingProject(false));
}, []);
if (error) {
return ( return (
<Modal onClose={onClose} bodyClassName="h-auto p-4"> <Modal onClose={onClose} bodyClassName="h-auto text-red-500">
<h2 className="mb-0.5 text-xl font-semibold"> <div className="flex flex-col items-center justify-center gap-2 pb-10 pt-12">
Started working... <ServerCrash className={'h-6 w-6'} />
{formattedStartedAt ? ( <p className="font-medium">{error}</p>
<span className="ml-1 rounded-md border border-yellow-400 bg-yellow-200 px-1 py-0.5 text-sm font-normal leading-none text-yellow-800"> </div>
{formattedStartedAt} </Modal>
</span> );
) : null} }
</h2>
<p className="text-balance text-sm text-gray-500">
You have started working on the project. Here are some tips to get most
out of it.
</p>
<ul className="ml-4 mt-4 list-disc space-y-1.5 marker:text-gray-400"> if (isStartingProject) {
{projectTips.map((tip) => {
return ( return (
<li key={tip} className="text-balance"> <Modal onClose={onClose} bodyClassName="h-auto">
{tip} <div className="flex flex-col items-center justify-center gap-4 pb-10 pt-12">
</li> <Spinner className={'h-6 w-6'} isDualRing={false} />
<p className="font-medium">Starting project ..</p>
</div>
</Modal>
); );
})} }
</ul>
return (
<Modal
onClose={onClose}
bodyClassName="h-auto p-4 relative overflow-hidden"
wrapperClassName={'max-w-md'}
>
<p className="-mx-4 -mt-4 flex items-center bg-yellow-200 px-3 py-2 text-sm text-yellow-900">
<CheckIcon additionalClasses="mr-1.5 w-[15px] text-yellow-800 h-[15px]" />
<span className="mr-1.5 font-normal">Project started</span>{' '}
<span className="font-bold">{formattedStartedAt}</span>
</p>
<h2 className="mb-0.5 mt-5 text-2xl font-semibold text-gray-800">
Start Building
</h2>
<p className="text-gray-700">
Follow these steps to complete the project.
</p>
<div className="my-5 space-y-1.5 marker:text-gray-400">
<div className="flex flex-row items-start gap-2">
<StepLabel label={'1'} />
<p className="text-gray-700">Create a repository on GitHub</p>
</div>
<div className="flex flex-row items-start gap-2">
<StepLabel label={'2'} />
<p className="text-gray-700">
Complete the task and push it to GitHub
</p>
</div>
<div className="flex flex-row items-start gap-2">
<StepLabel label={'3'} />
<p className="text-gray-700">
Add a readme file with instructions on how to run the project. Make
sure to include the{' '}
<button
onClick={() => {
copyText(projectUrl);
}}
className="font-semibold"
>
project page URL{' '}
{!isCopied && (
<CopyIcon className="inline-block h-4 w-4" strokeWidth={2.5} />
)}
{isCopied && (
<Check className="inline-block h-4 w-4" strokeWidth={2.5} />
)}
</button>{' '}
in the readme file.
</p>
</div>
<div className="flex flex-row items-start gap-2">
<StepLabel label={'4'} />
<p className="text-gray-700">
Submit your repository URL to help others learn and get feedback
from the community.
</p>
</div>
</div>
<p className="mt-4 font-medium">Happy coding!</p> <div className='mb-5'>
<p>If you get stuck, you can always ask for help in the community <a href='https://roadmap.sh/discord' target='_blank' className='underline underline-offset-2 font-medium'>chat on discord</a>.</p>
</div>
<button <button
className="absolute right-2.5 top-2.5 text-gray-600 hover:text-black" className="w-full rounded-md bg-blue-600 py-2 text-sm font-medium text-white hover:bg-blue-700"
onClick={onClose} onClick={onClose}
> >
<X className="h-5 w-5" /> Close
</button> </button>
</Modal> </Modal>
); );

@ -5,7 +5,12 @@ import { useStickyStuck } from '../../../hooks/use-sticky-stuck.tsx';
import { StepperAction } from './StepperAction.tsx'; import { StepperAction } from './StepperAction.tsx';
import { StepperStepSeparator } from './StepperStepSeparator.tsx'; import { StepperStepSeparator } from './StepperStepSeparator.tsx';
import { MilestoneStep } from './MilestoneStep.tsx'; import { MilestoneStep } from './MilestoneStep.tsx';
import { httpGet } from '../../../lib/http.ts'; import { httpGet, httpPost } from '../../../lib/http.ts';
import { StartProjectModal } from '../StartProjectModal.tsx';
import { pageProgressMessage } from '../../../stores/page.ts';
import { getRelativeTimeString } from '../../../lib/date.ts';
import { isLoggedIn } from '../../../lib/jwt.ts';
import { showLoginPopup } from '../../../lib/popup.ts';
type ProjectStatusResponse = { type ProjectStatusResponse = {
id?: string; id?: string;
@ -28,6 +33,7 @@ export function ProjectStepper(props: ProjectStepperProps) {
const stickyElRef = useRef<HTMLDivElement>(null); const stickyElRef = useRef<HTMLDivElement>(null);
const isSticky = useStickyStuck(stickyElRef, 8); const isSticky = useStickyStuck(stickyElRef, 8);
const [isStartingProject, setIsStartingProject] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [activeStep, setActiveStep] = useState<number>(0); const [activeStep, setActiveStep] = useState<number>(0);
const [isLoadingStatus, setIsLoadingStatus] = useState(true); const [isLoadingStatus, setIsLoadingStatus] = useState(true);
@ -80,6 +86,26 @@ export function ProjectStepper(props: ProjectStepperProps) {
}, },
)} )}
> >
{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 && ( {isLoadingStatus && (
<div className={cn('striped-loader absolute inset-0 z-10 bg-white')} /> <div className={cn('striped-loader absolute inset-0 z-10 bg-white')} />
)} )}
@ -91,7 +117,35 @@ export function ProjectStepper(props: ProjectStepperProps) {
}, },
)} )}
> >
{activeStep === 0 && (
<>
Start building, submit solution and get feedback from the community. 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-200 hover:text-white': isSticky,
})}
onClick={() => {
setIsStartingProject(true);
}}
>
these tips
</button>{' '}
to get most out of it.
</>
)}
</div> </div>
<div className="flex min-h-[60px] items-center justify-between gap-3 px-4"> <div className="flex min-h-[60px] items-center justify-between gap-3 px-4">
@ -101,6 +155,14 @@ export function ProjectStepper(props: ProjectStepperProps) {
icon={Play} icon={Play}
text={activeStep > 0 ? 'Started Working' : 'Start Working'} text={activeStep > 0 ? 'Started Working' : 'Start Working'}
number={1} number={1}
onClick={() => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
setIsStartingProject(true);
}}
/> />
<StepperStepSeparator isActive={activeStep > 0} /> <StepperStepSeparator isActive={activeStep > 0} />
<StepperAction <StepperAction

Loading…
Cancel
Save