diff --git a/src/components/Projects/ListProjectSolutions.tsx b/src/components/Projects/ListProjectSolutions.tsx index be67ef1fb..a543d8a39 100644 --- a/src/components/Projects/ListProjectSolutions.tsx +++ b/src/components/Projects/ListProjectSolutions.tsx @@ -15,6 +15,7 @@ import { VoteButton } from './VoteButton.tsx'; import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx'; import { SelectLanguages } from './SelectLanguages.tsx'; import type { ProjectFrontmatter } from '../../lib/project.ts'; +import { ProjectSolutionModal } from './ProjectSolutionModal.tsx'; export interface ProjectStatusDocument { _id?: string; @@ -68,7 +69,7 @@ type ListProjectSolutionsProps = { projectId: string; }; -const submittedAlternatives = [ +export const submittedAlternatives = [ 'submitted their solution', 'got it done', 'submitted their take', diff --git a/src/components/Projects/ProjectSolutionModal.tsx b/src/components/Projects/ProjectSolutionModal.tsx new file mode 100644 index 000000000..7c6b291ca --- /dev/null +++ b/src/components/Projects/ProjectSolutionModal.tsx @@ -0,0 +1,198 @@ +import { useEffect, useState } from 'react'; +import { deleteUrlParam, getUrlParams } from '../../lib/browser'; +import { ModalLoader } from '../UserProgress/ModalLoader'; +import { Modal } from '../Modal'; +import { httpGet, httpPost } from '../../lib/http'; +import { + submittedAlternatives, + type AllowedVoteType, +} from './ListProjectSolutions'; +import { getRelativeTimeString } from '../../lib/date'; +import { ArrowUpRight, ThumbsDown, ThumbsUp } from 'lucide-react'; +import { VoteButton } from './VoteButton'; +import { GitHubIcon } from '../ReactIcons/GitHubIcon'; +import { isLoggedIn } from '../../lib/jwt'; +import { showLoginPopup } from '../../lib/popup'; +import { pageProgressMessage } from '../../stores/page'; +import { useToast } from '../../hooks/use-toast'; + +type UserProjectSolutionResponse = { + id?: string; + + startedAt?: Date; + submittedAt?: Date; + repositoryUrl?: string; + + upvotes?: number; + downvotes?: number; + + voteType?: AllowedVoteType | 'none'; + user: { + id: string; + name: string; + avatar: string; + }; +}; + +type ProjectSolutionModalProps = { + projectId: string; + projectTitle: string; + projectDescription: string; +}; + +export function ProjectSolutionModal(props: ProjectSolutionModalProps) { + const { projectId, projectTitle, projectDescription } = props; + + const { u: userId } = getUrlParams(); + if (!userId) { + return null; + } + + const toast = useToast(); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(''); + const [solution, setSolution] = useState(); + + const loadUserProjectSolution = async () => { + setIsLoading(true); + setError(''); + + const { response, error } = await httpGet( + `${import.meta.env.PUBLIC_API_URL}/v1-project-solution/${projectId}/${userId}`, + ); + + if (error || !response) { + setError(error?.message || 'Something went wrong'); + setIsLoading(false); + return; + } + + setSolution(response); + setIsLoading(false); + }; + + const handleSubmitVote = async ( + solutionId: string, + voteType: AllowedVoteType, + ) => { + if (!isLoggedIn()) { + showLoginPopup(); + return; + } + + pageProgressMessage.set('Submitting vote'); + const { response, error } = await httpPost( + `${import.meta.env.PUBLIC_API_URL}/v1-vote-project/${solutionId}`, + { + voteType, + }, + ); + + if (error || !response) { + toast.error(error?.message || 'Failed to submit vote'); + pageProgressMessage.set(''); + return; + } + + pageProgressMessage.set(''); + setSolution((prev) => { + if (!prev) { + return prev; + } + + return { + ...prev, + upvotes: response?.upvotes || 0, + downvotes: response?.downvotes || 0, + voteType, + }; + }); + }; + + useEffect(() => { + loadUserProjectSolution().finally(); + }, []); + + if (isLoading || error) { + return ( + + ); + } + + const avatar = solution?.user.avatar; + + return ( + { + deleteUrlParam('u'); + window.location.reload(); + }} + > +
+

{projectTitle}

+

+ {projectDescription} +

+ +
+ +
+ {solution?.user?.name} + {solution?.user.name} + + {submittedAlternatives[ + Math.floor(Math.random() * submittedAlternatives.length) + ] || 'submitted their solution'} + {' '} + + {getRelativeTimeString(solution?.submittedAt!)} + +
+ +
+ + + View Solution + + +
+ { + handleSubmitVote(solution?.id!, 'upvote'); + }} + /> + + { + handleSubmitVote(solution?.id!, 'downvote'); + }} + /> +
+
+
+
+ ); +} diff --git a/src/components/Projects/StatusStepper/ProjectStepper.tsx b/src/components/Projects/StatusStepper/ProjectStepper.tsx index dee1a485d..a98d7b977 100644 --- a/src/components/Projects/StatusStepper/ProjectStepper.tsx +++ b/src/components/Projects/StatusStepper/ProjectStepper.tsx @@ -1,4 +1,4 @@ -import { Flag, Play, Send } from 'lucide-react'; +import { Flag, Play, Send, Share } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { cn } from '../../../lib/classname.ts'; import { useStickyStuck } from '../../../hooks/use-sticky-stuck.tsx'; @@ -8,9 +8,11 @@ import { MilestoneStep } from './MilestoneStep.tsx'; import { httpGet } from '../../../lib/http.ts'; import { StartProjectModal } from '../StartProjectModal.tsx'; import { getRelativeTimeString } from '../../../lib/date.ts'; -import { isLoggedIn } from '../../../lib/jwt.ts'; +import { getUser, isLoggedIn } from '../../../lib/jwt.ts'; import { showLoginPopup } from '../../../lib/popup.ts'; import { SubmitProjectModal } from '../SubmitProjectModal.tsx'; +import { useCopyText } from '../../../hooks/use-copy-text.ts'; +import { CheckIcon } from '../../ReactIcons/CheckIcon.tsx'; type ProjectStatusResponse = { id?: string; @@ -32,9 +34,11 @@ export function ProjectStepper(props: ProjectStepperProps) { const stickyElRef = useRef(null); const isSticky = useStickyStuck(stickyElRef, 8); + const currentUser = getUser(); const [isStartingProject, setIsStartingProject] = useState(false); const [isSubmittingProject, setIsSubmittingProject] = useState(false); + const { copyText, isCopied } = useCopyText(); const [error, setError] = useState(null); const [activeStep, setActiveStep] = useState(0); @@ -78,13 +82,16 @@ export function ProjectStepper(props: ProjectStepperProps) { loadProjectStatus().finally(() => {}); }, []); + const projectSolutionUrl = `${import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh'}/projects/${projectId}/solutions?u=${currentUser?.id}`; + return (
@@ -131,7 +138,7 @@ export function ProjectStepper(props: ProjectStepperProps) { )}
- Started working{' '} + Started working  {getRelativeTimeString(projectStatus.startedAt!)} - . Follow{' '} + . Follow  {' '} - to get most out of it. + +   to get most out of it. )} {activeStep >= 2 && ( <> - Congrats on submitting your solution.{' '} + Congrats on submitting your solution.  + )}
-
+
0} diff --git a/src/components/Projects/SubmitSuccessModal.tsx b/src/components/Projects/SubmitSuccessModal.tsx index 14f7bec69..8afbe7455 100644 --- a/src/components/Projects/SubmitSuccessModal.tsx +++ b/src/components/Projects/SubmitSuccessModal.tsx @@ -23,7 +23,7 @@ export function SubmitSuccessModal(props: SubmitSuccessModalProps) { const user = getUser(); const description = 'Check out my solution to this project on Roadmap.sh'; - const projectSolutionUrl = `${import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh'}/projects/${projectId}?u=${user?.id}`; + const projectSolutionUrl = `${import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh'}/projects/${projectId}/solutions?u=${user?.id}`; const { isCopied, copyText } = useCopyText(); @@ -49,10 +49,7 @@ export function SubmitSuccessModal(props: SubmitSuccessModalProps) {
- {/*

{successMessage}

*/} -

- Solution submitted successfully! -

+

{successMessage}

You can use the link to share your solution with others.

diff --git a/src/pages/projects/[projectId]/solutions.astro b/src/pages/projects/[projectId]/solutions.astro index 35235bdc5..15b980730 100644 --- a/src/pages/projects/[projectId]/solutions.astro +++ b/src/pages/projects/[projectId]/solutions.astro @@ -1,14 +1,13 @@ --- import BaseLayout from '../../../layouts/BaseLayout.astro'; -import { Badge } from '../../../components/Badge'; import { getAllProjects, getProjectById, type ProjectFrontmatter, } from '../../../lib/project'; -import AstroIcon from '../../../components/AstroIcon.astro'; import { ProjectTabs } from '../../../components/Projects/ProjectTabs'; import { ListProjectSolutions } from '../../../components/Projects/ListProjectSolutions'; +import { ProjectSolutionModal } from '../../../components/Projects/ProjectSolutionModal'; export async function getStaticPaths() { const projects = await getAllProjects(); @@ -49,13 +48,24 @@ const githubUrl = `https://github.com/kamranahmedse/developer-roadmap/tree/maste >
- + + +