Update UI for shareable link

feat/share-solutions
Kamran Ahmed 3 months ago
parent e39fadb032
commit af8c3b11d6
  1. 1
      .astro/types.d.ts
  2. 23
      src/components/Projects/SubmitProjectModal.tsx
  3. 71
      src/components/Projects/SubmitSuccessModal.tsx
  4. 17
      src/components/ReactIcons/FacebookIcon.tsx
  5. 52
      src/components/ReactIcons/LinkedInIcon.tsx
  6. 4
      src/components/ReactIcons/ShareIcon.tsx
  7. 16
      src/components/ReactIcons/TwitterIcon.tsx
  8. 53
      src/data/projects/basic-html-website.md

1
.astro/types.d.ts vendored

@ -0,0 +1 @@
/// <reference types="astro/client" />

@ -1,5 +1,4 @@
import { CheckIcon, CopyIcon, X } from 'lucide-react'; import { CheckIcon, CopyIcon, X } from 'lucide-react';
import { CheckIcon as ReactCheckIcon } from '../ReactIcons/CheckIcon.tsx';
import { Modal } from '../Modal'; import { Modal } from '../Modal';
import { type FormEvent, useState } from 'react'; import { type FormEvent, useState } from 'react';
import { httpPost } from '../../lib/http'; import { httpPost } from '../../lib/http';
@ -40,7 +39,7 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) {
const { isCopied, copyText } = useCopyText(); 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 [isSuccess, setIsSuccess] = useState(false);
const [repoUrl, setRepoUrl] = useState(defaultRepositoryUrl); const [repoUrl, setRepoUrl] = useState(defaultRepositoryUrl);
const [verificationChecks, setVerificationChecks] = const [verificationChecks, setVerificationChecks] =
useState<VerificationChecksType>({ useState<VerificationChecksType>({
@ -62,7 +61,7 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) {
setIsLoading(true); setIsLoading(true);
setError(''); setError('');
setSuccessMessage(''); setIsSuccess(false);
if (!repoUrl) { if (!repoUrl) {
setVerificationChecks({ setVerificationChecks({
@ -199,7 +198,7 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) {
); );
} }
setSuccessMessage('Solution submitted successfully!'); setIsSuccess(true);
setIsLoading(false); setIsLoading(false);
onSubmit(submitResponse); onSubmit(submitResponse);
@ -210,14 +209,8 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) {
} }
}; };
if (successMessage) { if (isSuccess) {
return ( return <SubmitSuccessModal projectId={projectId} onClose={onClose} />;
<SubmitSuccessModal
projectId={projectId}
onClose={onClose}
successMessage={successMessage}
/>
);
} }
return ( return (
@ -296,12 +289,6 @@ export function SubmitProjectModal(props: SubmitProjectModalProps) {
{error && ( {error && (
<p className="mt-2 text-sm font-medium text-red-500">{error}</p> <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>
)}
</form> </form>
<button <button

@ -1,10 +1,4 @@
import { import { CheckCircle2, Clipboard } from 'lucide-react';
CheckCircle2,
Clipboard,
Facebook,
Linkedin,
Twitter,
} from 'lucide-react';
import { getUser } from '../../lib/jwt.ts'; import { getUser } from '../../lib/jwt.ts';
import { Modal } from '../Modal'; import { Modal } from '../Modal';
import { CheckIcon as ReactCheckIcon } from '../ReactIcons/CheckIcon.tsx'; import { CheckIcon as ReactCheckIcon } from '../ReactIcons/CheckIcon.tsx';
@ -14,62 +8,43 @@ import { cn } from '../../lib/classname.ts';
type SubmitSuccessModalProps = { type SubmitSuccessModalProps = {
projectId: string; projectId: string;
onClose: () => void; onClose: () => void;
successMessage: string;
}; };
export function SubmitSuccessModal(props: SubmitSuccessModalProps) { export function SubmitSuccessModal(props: SubmitSuccessModalProps) {
const { onClose, successMessage, projectId } = props; const { onClose, projectId } = props;
const user = getUser(); 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}/solutions?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(); const { isCopied, copyText } = useCopyText();
const socialShareLinks = [
{
title: 'Twitter',
href: `https://x.com/intent/tweet?text=${description}&url=${projectSolutionUrl}`,
icon: <Twitter className="size-4 text-gray-700" />,
},
{
title: 'Facebook',
href: `https://www.facebook.com/sharer/sharer.php?quote=${description}&u=${projectSolutionUrl}`,
icon: <Facebook className="size-4 text-gray-700" />,
},
{
title: 'Linkedin',
href: `https://www.linkedin.com/sharing/share-offsite/?url=${projectSolutionUrl}`,
icon: <Linkedin className="size-4 text-gray-700" />,
},
];
return ( return (
<Modal onClose={onClose} bodyClassName="h-auto p-4"> <Modal onClose={onClose} bodyClassName="h-auto p-4">
<div className="flex flex-col items-center justify-center pb-5 pt-12"> <div className="flex flex-col items-center justify-center pb-3 pt-12">
<ReactCheckIcon additionalClasses="h-12 text-green-500 w-12" /> <ReactCheckIcon additionalClasses="h-12 text-green-500 w-12" />
<p className="mt-4 text-lg font-medium">{successMessage}</p> <p className="mt-4 text-lg font-medium">Solution Submitted</p>
<p className="mt-0.5 text-center text-sm text-gray-500"> <p className="mt-0.5 text-center text-sm text-gray-500">
You can use the link to share your solution with others. Congrats! Your solution has been submitted.
</p> </p>
<div className="mt-4 flex w-full items-stretch rounded-md border bg-gray-50"> <div className="mt-4 w-full">
<input <input
type="text" type="text"
readOnly={true} readOnly={true}
value={projectSolutionUrl} value={projectSolutionUrl}
className="w-full bg-transparent px-2.5 py-2 text-sm text-gray-700 focus:outline-none" className="w-full rounded-md border bg-gray-50 px-2.5 py-2 text-sm text-gray-700 focus:outline-none"
onClick={(e) => { onClick={(e) => {
e.currentTarget.select(); e.currentTarget.select();
copyText(projectSolutionUrl);
}} }}
/> />
<button <button
className={cn( className={cn(
'm-1 ml-0 flex items-center gap-1 rounded-md bg-gray-200 px-2 py-1.5 text-xs font-medium text-black', 'mt-2 flex w-full items-center justify-center gap-1 rounded-md px-2 py-2 text-sm font-medium transition-colors',
isCopied ? 'bg-green-200 text-green-900' : '', isCopied
? 'bg-green-600 text-white hover:bg-green-700'
: 'bg-black text-white hover:bg-gray-800'
)} )}
onClick={() => { onClick={() => {
copyText(projectSolutionUrl); copyText(projectSolutionUrl);
@ -77,35 +52,17 @@ export function SubmitSuccessModal(props: SubmitSuccessModalProps) {
> >
{isCopied ? ( {isCopied ? (
<> <>
<CheckCircle2 className="size-3 stroke-[2.5px]" /> <CheckCircle2 className="size-4 stroke-[2.5px]" />
Copied Copied
</> </>
) : ( ) : (
<> <>
<Clipboard className="size-3 stroke-[2.5px]" /> <Clipboard className="size-4 stroke-[2.5px]" />
Copy Copy Shareable Link
</> </>
)} )}
</button> </button>
</div> </div>
<div className="mt-8 flex justify-center gap-2">
{socialShareLinks.map((socialLink) => (
<a
key={socialLink.title}
href={socialLink.href}
target="_blank"
rel="noreferrer"
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md border bg-gray-50 hover:bg-gray-100"
>
{socialLink.icon}
</a>
))}
</div>
<p className="mt-4 text-sm text-gray-500">
Share your solution with the others!
</p>
</div> </div>
</Modal> </Modal>
); );

@ -0,0 +1,17 @@
interface FacebookIconProps {
className?: string;
}
export function FacebookIcon(props: FacebookIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
fill="currentColor"
className={className}
>
<path d="M400 32H48A48 48 0 0 0 0 80v352a48 48 0 0 0 48 48h137.25V327.69h-63V256h63v-54.64c0-62.15 37-96.48 93.67-96.48 27.14 0 55.52 4.84 55.52 4.84v61h-31.27c-30.81 0-40.42 19.12-40.42 38.73V256h68.78l-11 71.69h-57.78V480H400a48 48 0 0 0 48-48V80a48 48 0 0 0-48-48z" />
</svg>
);
}

@ -1,49 +1,29 @@
type LinkedInIconProps = { interface LinkedInIconProps {
className?: string; className?: string;
}; }
export function LinkedInIcon(props: LinkedInIconProps) { export function LinkedInIcon(props: LinkedInIconProps) {
const { className } = props; const { className } = props;
return ( return (
<svg <svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className={className} className={className}
x="0px"
y="0px"
width="100"
height="100"
viewBox="0,0,256,256"
> >
<g transform="translate(-26.66667,-26.66667) scale(1.20833,1.20833)"> <g clipPath="url(#clip0_2344_20)">
<g <path
fill="none" d="M0 0V24H24V0H0ZM8 19H5V8H8V19ZM6.5 6.732C5.534 6.732 4.75 5.942 4.75 4.968C4.75 3.994 5.534 3.204 6.5 3.204C7.466 3.204 8.25 3.994 8.25 4.968C8.25 5.942 7.467 6.732 6.5 6.732ZM20 19H17V13.396C17 10.028 13 10.283 13 13.396V19H10V8H13V9.765C14.397 7.179 20 6.988 20 12.241V19Z"
fillRule="nonzero" fill="currentColor"
stroke="none" />
strokeWidth="1"
strokeLinecap="butt"
strokeLinejoin="miter"
strokeMiterlimit="10"
strokeDasharray=""
strokeDashoffset="0"
fontFamily="none"
fontWeight="none"
fontSize="none"
textAnchor="none"
style={{ mixBlendMode: 'normal' }}
>
<g transform="scale(5.33333,5.33333)">
<path
d="M42,37c0,2.762 -2.238,5 -5,5h-26c-2.761,0 -5,-2.238 -5,-5v-26c0,-2.762 2.239,-5 5,-5h26c2.762,0 5,2.238 5,5z"
fill="#0288d1"
></path>
<path
d="M12,19h5v17h-5zM14.485,17h-0.028c-1.492,0 -2.457,-1.112 -2.457,-2.501c0,-1.419 0.995,-2.499 2.514,-2.499c1.521,0 2.458,1.08 2.486,2.499c0,1.388 -0.965,2.501 -2.515,2.501zM36,36h-5v-9.099c0,-2.198 -1.225,-3.698 -3.192,-3.698c-1.501,0 -2.313,1.012 -2.707,1.99c-0.144,0.35 -0.101,1.318 -0.101,1.807v9h-5v-17h5v2.616c0.721,-1.116 1.85,-2.616 4.738,-2.616c3.578,0 6.261,2.25 6.261,7.274l0.001,9.726z"
fill="#ffffff"
></path>
</g>
</g>
</g> </g>
<defs>
<clipPath id="clip0_2344_20">
<rect width="24" height="24" rx="2" fill="white" />
</clipPath>
</defs>
</svg> </svg>
); );
} }

@ -1,6 +1,6 @@
import type { JSX } from "preact/jsx-runtime"; import type { SVGAttributes } from 'react';
type ShareIconProps = JSX.SVGAttributes<SVGSVGElement> type ShareIconProps = SVGAttributes<SVGSVGElement>;
export function ShareIcon(props: ShareIconProps) { export function ShareIcon(props: ShareIconProps) {
return ( return (

@ -1,22 +1,22 @@
type TwitterIconProps = { interface TwitterIconProps {
className?: string; className?: string;
}; }
export function TwitterIcon(props: TwitterIconProps) { export function TwitterIcon(props: TwitterIconProps) {
const { className } = props; const { className } = props;
return ( return (
<svg <svg
width="15" width="23"
height="15" height="23"
viewBox="0 0 15 15" viewBox="0 0 23 23"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className={className} className={className}
> >
<rect width="23" height="23" rx="3" fill="currentColor" />
<path <path
d="M8.9285 6.35221L14.5135 0H13.1905L8.339 5.5144L4.467 0H0L5.8565 8.33955L0 15H1.323L6.443 9.17535L10.533 15H15M1.8005 0.976187H3.833L13.1895 14.0718H11.1565" d="M12.9285 10.3522L18.5135 4H17.1905L12.339 9.5144L8.467 4H4L9.8565 12.3395L4 19H5.323L10.443 13.1754L14.533 19H19M5.8005 4.97619H7.833L17.1895 18.0718H15.1565"
fill="currentColor" fill="#E5E5E5"
/> />
</svg> </svg>
); );

@ -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.
<hr />
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.
Loading…
Cancel
Save