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..0cda0b5cf --- /dev/null +++ b/src/components/Projects/ProjectSolutionModal.tsx @@ -0,0 +1,192 @@ +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, Trophy } 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(); + }} + wrapperClassName={'max-w-lg'} + bodyClassName={'h-auto'} + > +
+

{projectTitle}

+

{projectDescription}

+ +
+
+ {solution?.user?.name} +
+

{solution?.user.name}'s Solution

+

+ Submitted their solution{' '} + {getRelativeTimeString(solution?.submittedAt!)} +

+
+
+
+ +
+ + + View Solution on GitHub + + + +
+ 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..f4c4d3b02 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} @@ -200,21 +232,46 @@ export function ProjectStepper(props: ProjectStepperProps) { }} /> 0} /> - 1} - icon={Send} - onClick={() => { - if (!isLoggedIn()) { - showLoginPopup(); - return; - } +
+ 1} + icon={Send} + onClick={() => { + if (!isLoggedIn()) { + showLoginPopup(); + return; + } - setIsSubmittingProject(true); - }} - text={activeStep > 1 ? 'Submitted' : 'Submit Solution'} - number={2} - /> + setIsSubmittingProject(true); + }} + text={activeStep > 1 ? 'Submitted' : 'Submit Solution'} + number={2} + /> + + · + +
1} /> ({ @@ -61,7 +61,7 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) { setIsLoading(true); setError(''); - setSuccessMessage(''); + setIsSuccess(false); if (!repoUrl) { setVerificationChecks({ @@ -198,7 +198,7 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) { ); } - setSuccessMessage('Solution submitted successfully!'); + setIsSuccess(true); setIsLoading(false); onSubmit(submitResponse); @@ -209,15 +209,8 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) { } }; - if (successMessage) { - return ( - -
- -

{successMessage}

-
-
- ); + if (isSuccess) { + return ; } return ( @@ -296,12 +289,6 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) { {error && (

{error}

)} - - {successMessage && ( -

- {successMessage} -

- )} +
+
+ + ); +} diff --git a/src/components/ReactIcons/FacebookIcon.tsx b/src/components/ReactIcons/FacebookIcon.tsx new file mode 100644 index 000000000..e9f88a7d5 --- /dev/null +++ b/src/components/ReactIcons/FacebookIcon.tsx @@ -0,0 +1,17 @@ +interface FacebookIconProps { + className?: string; +} + +export function FacebookIcon(props: FacebookIconProps) { + const { className } = props; + return ( + + + + ); +} \ No newline at end of file diff --git a/src/components/ReactIcons/LinkedInIcon.tsx b/src/components/ReactIcons/LinkedInIcon.tsx index 0c276044d..c2db043fa 100644 --- a/src/components/ReactIcons/LinkedInIcon.tsx +++ b/src/components/ReactIcons/LinkedInIcon.tsx @@ -1,49 +1,29 @@ -type LinkedInIconProps = { +interface LinkedInIconProps { className?: string; -}; +} export function LinkedInIcon(props: LinkedInIconProps) { const { className } = props; - return ( - - - - - - - + + + + + + + ); } diff --git a/src/components/ReactIcons/ShareIcon.tsx b/src/components/ReactIcons/ShareIcon.tsx index 1a75f5316..3745caecb 100644 --- a/src/components/ReactIcons/ShareIcon.tsx +++ b/src/components/ReactIcons/ShareIcon.tsx @@ -1,6 +1,6 @@ -import type { JSX } from "preact/jsx-runtime"; +import type { SVGAttributes } from 'react'; -type ShareIconProps = JSX.SVGAttributes +type ShareIconProps = SVGAttributes; export function ShareIcon(props: ShareIconProps) { return ( diff --git a/src/components/ReactIcons/TwitterIcon.tsx b/src/components/ReactIcons/TwitterIcon.tsx index 59ccb4e71..9297dc141 100644 --- a/src/components/ReactIcons/TwitterIcon.tsx +++ b/src/components/ReactIcons/TwitterIcon.tsx @@ -1,22 +1,22 @@ -type TwitterIconProps = { +interface TwitterIconProps { className?: string; -}; +} export function TwitterIcon(props: TwitterIconProps) { const { className } = props; - return ( + ); diff --git a/src/data/projects/basic-html-website.md b/src/data/projects/basic-html-website.md new file mode 100644 index 000000000..0895bac59 --- /dev/null +++ b/src/data/projects/basic-html-website.md @@ -0,0 +1,53 @@ +--- +title: 'Basic HTML Website' +description: 'Create simple HTML only website with multiple pages.' +isNew: false +sort: 1 +difficulty: 'beginner' +nature: 'HTML' +skills: + - 'HTML' + - 'Layouts' + - 'semantic HTML' +seo: + title: 'Basic HTML Website Project' + description: 'Create a simple HTML only website with multiple pages.' + keywords: + - 'basic html' + - 'html project idea' +roadmapIds: + - 'frontend' +--- + +> Goal of this project is to teach you how to structure a website using HTML i.e. different sections of a website like header, footer, navigation, main content, sidebars etc. Do not style the website, only focus on the structure. Styling will be done in separate projects. + +In this project, you are required to create a simple HTML only website with multiple pages. The website should have following pages: + +- Homepage +- Projects +- Articles +- Contact + +The website should have a navigation bar that should be present on all pages and link to all the pages. + +You are not required to style the website, you are only required to create the structure of the website using HTML. Goals of this project are: + +- Learn how to create multiple pages in a website. +- Structure a website using HTML in a semantic way. +- Structure in a way that you can easily add styles later. +- Add SEO meta tags to the website. + +You can use the following mockup example to create the structure of the website (remember, you are not required to style the website, only focus on the structure that you can style later): + +![Basic HTML Website](https://assets.roadmap.sh/guest/portfolio-design-83lku.png) + +Again, make sure that your submission includes the following: + +- Semantically correct HTML structure. +- Multiple pages with a navigation bar. +- SEO meta tags in the head of each page. +- Contact page should have a form with fields like name, email, message etc. + +
+ +After completing this project, you will have a good understanding of how to structure a website using HTML, basic SEO meta tags, HTML tags, forms etc. You can now move on to the next project where you will learn how to style this website using CSS. 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 >
- + + +