Requirement verification functionality

pull/6513/head
Kamran Ahmed 3 months ago
parent d7e81bd13a
commit d0e76c85ce
  1. 4
      src/components/Projects/SubmissionRequirement.tsx
  2. 128
      src/components/Projects/SubmitProjectModal.tsx

@ -1,6 +1,6 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { cn } from '../../lib/classname.ts'; import { cn } from '../../lib/classname.ts';
import { CheckIcon, CircleDashed } from 'lucide-react'; import {CheckIcon, CircleDashed, X} from 'lucide-react';
type SubmissionRequirementProps = { type SubmissionRequirementProps = {
status: 'pending' | 'success' | 'error'; status: 'pending' | 'success' | 'error';
@ -23,7 +23,7 @@ export function SubmissionRequirement(props: SubmissionRequirementProps) {
) : status === 'success' ? ( ) : status === 'success' ? (
<CheckIcon className="h-4 w-4 text-green-800" /> <CheckIcon className="h-4 w-4 text-green-800" />
) : ( ) : (
<CheckIcon className="h-4 w-4 text-yellow-800" /> <X className="h-4 w-4 text-yellow-800" />
)} )}
<span className="ml-2">{children}</span> <span className="ml-2">{children}</span>
</div> </div>

@ -1,4 +1,4 @@
import { CheckIcon, CircleDashed, X } from 'lucide-react'; import { CheckIcon, CircleDashed, CopyIcon, X } from 'lucide-react';
import { Modal } from '../Modal'; import { Modal } from '../Modal';
import { useState, type FormEvent, type ReactNode } from 'react'; import { useState, type FormEvent, type ReactNode } from 'react';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
@ -6,12 +6,19 @@ import { httpPost } from '../../lib/http';
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx'; import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
import { cn } from '../../lib/classname.ts'; import { cn } from '../../lib/classname.ts';
import { SubmissionRequirement } from './SubmissionRequirement.tsx'; import { SubmissionRequirement } from './SubmissionRequirement.tsx';
import { useCopyText } from '../../hooks/use-copy-text.ts';
type SubmitProjectResponse = { type SubmitProjectResponse = {
repositoryUrl: string; repositoryUrl: string;
submittedAt: Date; submittedAt: Date;
}; };
type VerificationChecksType = {
repositoryExists: 'pending' | 'success' | 'error';
readmeExists: 'pending' | 'success' | 'error';
projectUrlExists: 'pending' | 'success' | 'error';
};
type SubmitProjectModalProps = { type SubmitProjectModalProps = {
onClose: () => void; onClose: () => void;
projectId: string; projectId: string;
@ -27,19 +34,40 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) {
repositoryUrl: defaultRepositoryUrl = '', repositoryUrl: defaultRepositoryUrl = '',
} = props; } = props;
const { isCopied, copyText } = useCopyText();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [successMessage, setSuccessMessage] = useState(''); const [successMessage, setSuccessMessage] = useState('');
const [repoUrl, setRepoUrl] = useState(defaultRepositoryUrl); const [repoUrl, setRepoUrl] = useState(defaultRepositoryUrl);
const [verificationChecks, setVerificationChecks] =
useState<VerificationChecksType>({
repositoryExists: 'pending',
readmeExists: 'pending',
projectUrlExists: 'pending',
});
const projectUrl = `${import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh'}/projects/${projectId}`;
const handleSubmit = async (e: FormEvent) => { const handleSubmit = async (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
try { try {
setVerificationChecks({
repositoryExists: 'pending',
readmeExists: 'pending',
projectUrlExists: 'pending',
});
setIsLoading(true); setIsLoading(true);
setError(''); setError('');
setSuccessMessage(''); setSuccessMessage('');
if (!repoUrl) { if (!repoUrl) {
setVerificationChecks({
repositoryExists: 'error',
readmeExists: 'pending',
projectUrlExists: 'pending',
});
throw new Error('Repository URL is required'); throw new Error('Repository URL is required');
} }
@ -50,6 +78,12 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) {
const repoName = repoUrlParts[2]; const repoName = repoUrlParts[2];
if (!username || !repoName) { if (!username || !repoName) {
setVerificationChecks({
repositoryExists: 'error',
readmeExists: 'pending',
projectUrlExists: 'pending',
});
throw new Error('Invalid GitHub repository URL'); throw new Error('Invalid GitHub repository URL');
} }
@ -58,6 +92,12 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) {
const allContentsUrl = `${mainApiUrl}/contents`; const allContentsUrl = `${mainApiUrl}/contents`;
const allContentsResponse = await fetch(allContentsUrl); const allContentsResponse = await fetch(allContentsUrl);
if (!allContentsResponse.ok) { if (!allContentsResponse.ok) {
setVerificationChecks({
repositoryExists: 'error',
readmeExists: 'pending',
projectUrlExists: 'pending',
});
if (allContentsResponse?.status === 404) { if (allContentsResponse?.status === 404) {
throw new Error( throw new Error(
'Repository not found. Make sure it exists and is public.', 'Repository not found. Make sure it exists and is public.',
@ -69,6 +109,12 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) {
const allContentsData = await allContentsResponse.json(); const allContentsData = await allContentsResponse.json();
if (!Array.isArray(allContentsData)) { if (!Array.isArray(allContentsData)) {
setVerificationChecks({
repositoryExists: 'error',
readmeExists: 'pending',
projectUrlExists: 'pending',
});
throw new Error('Failed to fetch repository contents'); throw new Error('Failed to fetch repository contents');
} }
@ -76,24 +122,47 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) {
(file) => file.name.toLowerCase() === 'readme.md', (file) => file.name.toLowerCase() === 'readme.md',
); );
if (!readmeFile || !readmeFile.url) { if (!readmeFile || !readmeFile.url) {
setVerificationChecks({
repositoryExists: 'success',
readmeExists: 'error',
projectUrlExists: 'pending',
});
throw new Error('Readme file not found'); throw new Error('Readme file not found');
} }
const readmeUrl = readmeFile.url; const readmeUrl = readmeFile.url;
const response = await fetch(readmeUrl); const response = await fetch(readmeUrl);
if (!response.ok || response.status === 404) { if (!response.ok || response.status === 404) {
setVerificationChecks({
repositoryExists: 'success',
readmeExists: 'error',
projectUrlExists: 'pending',
});
throw new Error('Readme file not found'); throw new Error('Readme file not found');
} }
const data = await response.json(); const data = await response.json();
if (!data.content) { if (!data.content) {
setVerificationChecks({
repositoryExists: 'success',
readmeExists: 'error',
projectUrlExists: 'pending',
});
throw new Error('Readme file not found'); throw new Error('Readme file not found');
} }
const readmeContent = window.atob(data.content); const readmeContent = window.atob(data.content);
const projectUrl = `${window.location.origin}/projects/${projectId}`;
if (!readmeContent.includes(projectUrl)) { if (!readmeContent.includes(projectUrl)) {
throw new Error('Project URL not found in the readme file'); setVerificationChecks({
repositoryExists: 'success',
readmeExists: 'success',
projectUrlExists: 'error',
});
throw new Error('Add the project page URL to the readme file');
} }
const submitProjectUrl = `${import.meta.env.PUBLIC_API_URL}/v1-submit-project/${projectId}`; const submitProjectUrl = `${import.meta.env.PUBLIC_API_URL}/v1-submit-project/${projectId}`;
@ -108,6 +177,7 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) {
setSuccessMessage('Repository verified successfully'); setSuccessMessage('Repository verified successfully');
setIsLoading(false); setIsLoading(false);
onSubmit(submitResponse); onSubmit(submitResponse);
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
@ -116,21 +186,6 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) {
} }
}; };
const verificationChecks = {
valid_url: {
status: 'pending',
message: 'URL must point to a public GitHub repository',
},
valid_repo: {
status: 'pending',
message: 'Repository must contain a readme file',
},
valid_readme: {
status: 'pending',
message: 'Readme file must contain the project URL',
},
};
return ( return (
<Modal onClose={onClose} bodyClassName="h-auto p-4"> <Modal onClose={onClose} bodyClassName="h-auto p-4">
<h2 className="mb-2 flex items-center gap-2.5 text-2xl font-semibold"> <h2 className="mb-2 flex items-center gap-2.5 text-2xl font-semibold">
@ -141,21 +196,48 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) {
</p> </p>
<div className="my-4 flex flex-col gap-1"> <div className="my-4 flex flex-col gap-1">
<SubmissionRequirement status="pending"> <SubmissionRequirement status={verificationChecks.repositoryExists}>
URL must point to a public GitHub repository URL must point to a public GitHub repository
</SubmissionRequirement> </SubmissionRequirement>
<SubmissionRequirement status="pending"> <SubmissionRequirement status={verificationChecks.readmeExists}>
Repository must contain a README file Repository must contain a README file
</SubmissionRequirement> </SubmissionRequirement>
<SubmissionRequirement status="pending"> <SubmissionRequirement status={verificationChecks.projectUrlExists}>
README file must contain the project URL README file must contain the{' '}
<button
className={
'font-medium underline underline-offset-2 hover:text-purple-700'
}
onClick={() => {
copyText(projectUrl);
}}
>
{!isCopied && (
<>
project URL{' '}
<CopyIcon
className="relative -top-0.5 inline-block h-3 w-3"
strokeWidth={2.5}
/>
</>
)}
{isCopied && (
<>
copied URL{' '}
<CheckIcon
className="relative -top-0.5 inline-block h-3 w-3"
strokeWidth={2.5}
/>
</>
)}
</button>
</SubmissionRequirement> </SubmissionRequirement>
</div> </div>
<form className="mt-4" onSubmit={handleSubmit}> <form className="mt-4" onSubmit={handleSubmit}>
<input <input
type="text" type="text"
className="w-full rounded-lg border border-gray-300 p-2 focus:border-gray-500 focus:outline-none text-sm" className="w-full rounded-lg border border-gray-300 p-2 text-sm focus:border-gray-500 focus:outline-none"
placeholder="https://github.com/you/solution-repo" placeholder="https://github.com/you/solution-repo"
value={repoUrl} value={repoUrl}
onChange={(e) => setRepoUrl(e.target.value)} onChange={(e) => setRepoUrl(e.target.value)}

Loading…
Cancel
Save