parent
169a8f0356
commit
8db22e81b1
5 changed files with 298 additions and 26 deletions
@ -0,0 +1,102 @@ |
|||||||
|
import { useState } from 'react'; |
||||||
|
import { cn } from '../../lib/classname'; |
||||||
|
import { StartProjectModal } from './StartProjectModal'; |
||||||
|
import { SubmitProjectModal } from './SubmitProjectModal'; |
||||||
|
|
||||||
|
type ProjectMilestoneStripProps = { |
||||||
|
projectId: string; |
||||||
|
}; |
||||||
|
|
||||||
|
export function ProjectMilestoneStrip(props: ProjectMilestoneStripProps) { |
||||||
|
const { projectId } = props; |
||||||
|
|
||||||
|
const [stepIndex, setStepIndex] = useState(0); |
||||||
|
const [isStartProjectModalOpen, setIsStartProjectModalOpen] = useState(false); |
||||||
|
const [isSubmitProjectModalOpen, setIsSubmitProjectModalOpen] = |
||||||
|
useState(false); |
||||||
|
|
||||||
|
const startProjectModal = isStartProjectModalOpen ? ( |
||||||
|
<StartProjectModal onClose={() => setIsStartProjectModalOpen(false)} /> |
||||||
|
) : null; |
||||||
|
const submitProjectModal = isSubmitProjectModalOpen ? ( |
||||||
|
<SubmitProjectModal |
||||||
|
onClose={() => setIsSubmitProjectModalOpen(false)} |
||||||
|
projectId={projectId} |
||||||
|
onSubmit={() => setStepIndex(2)} |
||||||
|
/> |
||||||
|
) : null; |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
{startProjectModal} |
||||||
|
{submitProjectModal} |
||||||
|
|
||||||
|
<div className="relative -mx-2 -mt-2 mb-5 rounded-lg bg-gray-100/70 p-5"> |
||||||
|
<div className="grid grid-cols-4"> |
||||||
|
<div className="flex flex-col"> |
||||||
|
<MilestoneStep isActive={stepIndex === 1} /> |
||||||
|
|
||||||
|
<button |
||||||
|
className="mt-3 text-left text-sm font-medium text-blue-600 underline underline-offset-2" |
||||||
|
onClick={() => setIsStartProjectModalOpen(true)} |
||||||
|
> |
||||||
|
Start Project |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<div className="flex flex-col"> |
||||||
|
<MilestoneStep isActive={stepIndex === 2} position="middle" /> |
||||||
|
|
||||||
|
<button |
||||||
|
className="mt-3 text-sm font-medium text-blue-600 underline underline-offset-2" |
||||||
|
onClick={() => setIsSubmitProjectModalOpen(true)} |
||||||
|
> |
||||||
|
Submit Solution |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<div className="flex flex-col"> |
||||||
|
<MilestoneStep isActive={stepIndex === 2} position="middle" /> |
||||||
|
|
||||||
|
<span className="mt-3 w-full text-center text-sm font-medium"> |
||||||
|
Get 5 Likes |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div className="flex flex-col"> |
||||||
|
<MilestoneStep isActive={stepIndex === 2} position="end" /> |
||||||
|
|
||||||
|
<span className="mt-3 w-full text-right text-sm font-medium"> |
||||||
|
Get 10 Likes |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
type MilestoneStepProps = { |
||||||
|
isActive: boolean; |
||||||
|
position?: 'start' | 'middle' | 'end'; |
||||||
|
}; |
||||||
|
|
||||||
|
function MilestoneStep(props: MilestoneStepProps) { |
||||||
|
const { isActive = false, position = 'start' } = props; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className={cn( |
||||||
|
'relative h-1 w-full bg-gray-300', |
||||||
|
isActive && 'bg-gray-500', |
||||||
|
)} |
||||||
|
> |
||||||
|
<span |
||||||
|
className={cn( |
||||||
|
'absolute -top-[4px] size-3 -translate-x-1/2 rounded-full border bg-white', |
||||||
|
isActive && 'border-black bg-black', |
||||||
|
position === 'start' && 'left-0', |
||||||
|
position === 'middle' && 'left-1/2', |
||||||
|
position === 'end' && 'right-0 translate-x-1/2', |
||||||
|
)} |
||||||
|
></span> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
import { X } from 'lucide-react'; |
||||||
|
import { Modal } from '../Modal'; |
||||||
|
|
||||||
|
type StartProjectModalProps = { |
||||||
|
onClose: () => void; |
||||||
|
}; |
||||||
|
|
||||||
|
export function StartProjectModal(props: StartProjectModalProps) { |
||||||
|
const { onClose } = props; |
||||||
|
|
||||||
|
const projectTips = [ |
||||||
|
'Create a repository on GitHub', |
||||||
|
'Develop the required functionality', |
||||||
|
'Add a readme and make sure to link to the project page', |
||||||
|
'Once you are done, make sure to come back and submit your solution to get feedback from others.', |
||||||
|
'Feel free to join our discord and ask for help if you get stuck.', |
||||||
|
]; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Modal onClose={onClose} bodyClassName="h-auto p-4"> |
||||||
|
<h2 className="mb-0.5 text-xl font-semibold">Started working...</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"> |
||||||
|
{projectTips.map((tip) => { |
||||||
|
return ( |
||||||
|
<li key={tip} className="text-balance"> |
||||||
|
{tip} |
||||||
|
</li> |
||||||
|
); |
||||||
|
})} |
||||||
|
</ul> |
||||||
|
|
||||||
|
<p className="mt-4 font-medium">Happy coding!</p> |
||||||
|
|
||||||
|
<button |
||||||
|
className="absolute right-2.5 top-2.5 text-gray-600 hover:text-black" |
||||||
|
onClick={onClose} |
||||||
|
> |
||||||
|
<X className="h-5 w-5" /> |
||||||
|
</button> |
||||||
|
</Modal> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,136 @@ |
|||||||
|
import { X } from 'lucide-react'; |
||||||
|
import { Modal } from '../Modal'; |
||||||
|
import { useState, type FormEvent } from 'react'; |
||||||
|
import { useToast } from '../../hooks/use-toast'; |
||||||
|
|
||||||
|
type SubmitProjectModalProps = { |
||||||
|
onClose: () => void; |
||||||
|
projectId: string; |
||||||
|
onSubmit: () => void; |
||||||
|
}; |
||||||
|
|
||||||
|
export function SubmitProjectModal(props: SubmitProjectModalProps) { |
||||||
|
const { onClose, projectId, onSubmit } = props; |
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false); |
||||||
|
const [error, setError] = useState(''); |
||||||
|
const [successMessage, setSuccessMessage] = useState(''); |
||||||
|
const [repoUrl, setRepoUrl] = useState(''); |
||||||
|
|
||||||
|
const handleSubmit = async (e: FormEvent) => { |
||||||
|
e.preventDefault(); |
||||||
|
try { |
||||||
|
setIsLoading(true); |
||||||
|
setError(''); |
||||||
|
setSuccessMessage(''); |
||||||
|
|
||||||
|
const repoUrlParts = repoUrl |
||||||
|
.replace(/https?:\/\/(www\.)?github\.com/, '') |
||||||
|
.split('/'); |
||||||
|
const username = repoUrlParts[1]; |
||||||
|
const repoName = repoUrlParts[2]; |
||||||
|
|
||||||
|
if (!username || !repoName) { |
||||||
|
throw new Error('Invalid GitHub repository URL'); |
||||||
|
} |
||||||
|
|
||||||
|
const mainApiUrl = `https://api.github.com/repos/${username}/${repoName}`; |
||||||
|
|
||||||
|
const allContentsUrl = `${mainApiUrl}/contents`; |
||||||
|
const allContentsResponse = await fetch(allContentsUrl); |
||||||
|
if (!allContentsResponse.ok) { |
||||||
|
const errorData = await allContentsResponse.json(); |
||||||
|
if (errorData?.status === 404) { |
||||||
|
throw new Error('Repository not found'); |
||||||
|
} |
||||||
|
|
||||||
|
throw new Error('Failed to fetch repository contents'); |
||||||
|
} |
||||||
|
|
||||||
|
const allContentsData = await allContentsResponse.json(); |
||||||
|
if (!Array.isArray(allContentsData)) { |
||||||
|
throw new Error('Failed to fetch repository contents'); |
||||||
|
} |
||||||
|
|
||||||
|
const readmeFile = allContentsData.find( |
||||||
|
(file) => file.name.toLowerCase() === 'readme.md', |
||||||
|
); |
||||||
|
if (!readmeFile || !readmeFile.url) { |
||||||
|
throw new Error('Readme file not found'); |
||||||
|
} |
||||||
|
|
||||||
|
const readmeUrl = readmeFile.url; |
||||||
|
const response = await fetch(readmeUrl); |
||||||
|
if (!response.ok || response.status === 404) { |
||||||
|
throw new Error('Readme file not found'); |
||||||
|
} |
||||||
|
|
||||||
|
const data = await response.json(); |
||||||
|
if (!data.content) { |
||||||
|
throw new Error('Readme file not found'); |
||||||
|
} |
||||||
|
|
||||||
|
const readmeContent = window.atob(data.content); |
||||||
|
const projectUrl = `${window.location.origin}/projects/${projectId}`; |
||||||
|
if (!readmeContent.includes(projectUrl)) { |
||||||
|
throw new Error('Project URL not found in the readme file'); |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: Make API call to update the project status
|
||||||
|
setSuccessMessage('Repository verified successfully'); |
||||||
|
setIsLoading(false); |
||||||
|
onSubmit(); |
||||||
|
} catch (error: any) { |
||||||
|
console.error(error); |
||||||
|
setError(error?.message || 'Failed to verify repository'); |
||||||
|
setIsLoading(false); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Modal onClose={onClose} bodyClassName="h-auto p-4"> |
||||||
|
<h2 className="mb-0.5 text-xl font-semibold"> |
||||||
|
Submit Link to the GitHub repository |
||||||
|
</h2> |
||||||
|
<p className="text-balance text-sm text-gray-500"> |
||||||
|
Make sure to have a readme file in your project repository containing |
||||||
|
the link to this project page. |
||||||
|
</p> |
||||||
|
|
||||||
|
<form className="mt-4" onSubmit={handleSubmit}> |
||||||
|
<input |
||||||
|
type="text" |
||||||
|
className="w-full rounded-lg border border-gray-300 p-2 focus:border-gray-500 focus:outline-none" |
||||||
|
placeholder="https://github.com/kamranahmedse/developer-roadmap" |
||||||
|
value={repoUrl} |
||||||
|
onChange={(e) => setRepoUrl(e.target.value)} |
||||||
|
/> |
||||||
|
|
||||||
|
{error && ( |
||||||
|
<p className="mt-2 text-sm font-medium text-red-500">{error}</p> |
||||||
|
)} |
||||||
|
|
||||||
|
{successMessage && ( |
||||||
|
<p className="mt-2 text-sm font-medium text-green-500"> |
||||||
|
{successMessage} |
||||||
|
</p> |
||||||
|
)} |
||||||
|
|
||||||
|
<button |
||||||
|
type="submit" |
||||||
|
className="mt-2 w-full rounded-lg bg-black p-2 font-medium text-white disabled:opacity-50" |
||||||
|
disabled={isLoading} |
||||||
|
> |
||||||
|
{isLoading ? 'Verifying...' : 'Verify'} |
||||||
|
</button> |
||||||
|
</form> |
||||||
|
|
||||||
|
<button |
||||||
|
className="absolute right-2.5 top-2.5 text-gray-600 hover:text-black" |
||||||
|
onClick={onClose} |
||||||
|
> |
||||||
|
<X className="h-5 w-5" /> |
||||||
|
</button> |
||||||
|
</Modal> |
||||||
|
); |
||||||
|
} |
Loading…
Reference in new issue