mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-10 11:14:09 +02:00
Add support for roadcards
This commit is contained in:
@@ -75,6 +75,8 @@ export default defineConfig({
|
|||||||
css: false,
|
css: false,
|
||||||
js: false,
|
js: false,
|
||||||
}),
|
}),
|
||||||
preact(),
|
preact({
|
||||||
|
compat: true,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
1751
pnpm-lock.yaml
generated
1751
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -13,35 +13,48 @@ const sidebarLinks = [
|
|||||||
href: '/account',
|
href: '/account',
|
||||||
title: 'Activity',
|
title: 'Activity',
|
||||||
id: 'activity',
|
id: 'activity',
|
||||||
|
isNew: false,
|
||||||
icon: {
|
icon: {
|
||||||
glyph: 'analytics',
|
glyph: 'analytics',
|
||||||
classes: 'h-3 w-4',
|
classes: 'h-3 w-4',
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/account/road-card',
|
||||||
|
title: 'Card',
|
||||||
|
id: 'road-card',
|
||||||
|
isNew: true,
|
||||||
|
icon: {
|
||||||
|
glyph: 'badge',
|
||||||
|
classes: 'h-4 w-4',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/account/update-profile',
|
href: '/account/update-profile',
|
||||||
title: 'Profile',
|
title: 'Profile',
|
||||||
id: 'profile',
|
id: 'profile',
|
||||||
|
isNew: false,
|
||||||
icon: {
|
icon: {
|
||||||
glyph: 'user',
|
glyph: 'user',
|
||||||
classes: 'h-4 w-4',
|
classes: 'h-4 w-4',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/account/update-password',
|
href: '/account/update-password',
|
||||||
title: 'Security',
|
title: 'Security',
|
||||||
id: 'change-password',
|
id: 'change-password',
|
||||||
|
isNew: false,
|
||||||
icon: {
|
icon: {
|
||||||
glyph: 'security',
|
glyph: 'security',
|
||||||
classes: 'h-4 w-4'
|
classes: 'h-4 w-4',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class='relative mb-5 block border-b p-4 shadow-inner md:hidden'>
|
<div class='relative mb-5 block border-b p-4 shadow-inner md:hidden'>
|
||||||
<button
|
<button
|
||||||
class='flex h-10 w-full items-center justify-between rounded-md border bg-white px-2 text-center text-gray-900 text-sm font-medium'
|
class='flex h-10 w-full items-center justify-between rounded-md border bg-white px-2 text-center text-sm font-medium text-gray-900'
|
||||||
id='settings-menu'
|
id='settings-menu'
|
||||||
>
|
>
|
||||||
{activePageTitle}
|
{activePageTitle}
|
||||||
@@ -52,42 +65,67 @@ const sidebarLinks = [
|
|||||||
class='absolute left-0 right-0 z-10 mt-1 hidden space-y-1.5 bg-white p-2 shadow-lg'
|
class='absolute left-0 right-0 z-10 mt-1 hidden space-y-1.5 bg-white p-2 shadow-lg'
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
sidebarLinks.map((sidebarLink) => (
|
sidebarLinks.map((sidebarLink) => {
|
||||||
|
const isActive = activePageId === sidebarLink.id;
|
||||||
|
|
||||||
|
return (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href={sidebarLink.href}
|
href={sidebarLink.href}
|
||||||
class={`flex items-center w-full rounded px-3 py-1.5 text-slate-900 hover:bg-slate-200 text-sm ${
|
class={`flex w-full items-center rounded px-3 py-1.5 text-sm text-slate-900 hover:bg-slate-200 ${
|
||||||
activePageId === sidebarLink.id ? 'bg-slate-100' : ''
|
isActive ? 'bg-slate-100' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<AstroIcon icon={sidebarLink.icon.glyph} class={`${sidebarLink.icon.classes} mr-2`} />
|
<AstroIcon
|
||||||
|
icon={sidebarLink.icon.glyph}
|
||||||
|
class={`${sidebarLink.icon.classes} mr-2`}
|
||||||
|
/>
|
||||||
{sidebarLink.title}
|
{sidebarLink.title}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='container flex min-h-screen items-stretch'>
|
<div class='container flex min-h-screen items-stretch'>
|
||||||
<!-- Start Desktop Sidebar -->
|
<!-- Start Desktop Sidebar -->
|
||||||
<aside class='hidden shrink-0 w-44 border-r border-slate-200 py-10 md:block'>
|
<aside class='hidden w-44 shrink-0 border-r border-slate-200 py-10 md:block'>
|
||||||
<nav>
|
<nav>
|
||||||
<ul class='space-y-1'>
|
<ul class='space-y-1'>
|
||||||
{
|
{
|
||||||
sidebarLinks.map((sidebarLink) => (
|
sidebarLinks.map((sidebarLink) => {
|
||||||
|
const isActive = activePageId === sidebarLink.id;
|
||||||
|
|
||||||
|
return (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href={sidebarLink.href}
|
href={sidebarLink.href}
|
||||||
class={`font-regular flex w-full items-center gap-2 px-2 py-1.5 text-sm border-r-2 ${
|
class={`font-regular flex w-full items-center border-r-2 px-2 py-1.5 text-sm ${
|
||||||
activePageId === sidebarLink.id ? 'text-black border-r-black bg-gray-100' : 'text-gray-500 border-r-transparent hover:border-r-gray-300'
|
isActive
|
||||||
|
? 'border-r-black bg-gray-100 text-black'
|
||||||
|
: 'border-r-transparent text-gray-500 hover:border-r-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<AstroIcon icon={sidebarLink.icon.glyph} class={`${sidebarLink.icon.classes} mr-0`} />
|
<span class='flex flex-grow items-center'>
|
||||||
|
<AstroIcon
|
||||||
|
icon={sidebarLink.icon.glyph}
|
||||||
|
class={`${sidebarLink.icon.classes} mr-2`}
|
||||||
|
/>
|
||||||
{sidebarLink.title}
|
{sidebarLink.title}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{sidebarLink.isNew && !isActive && (
|
||||||
|
<span class='relative mr-1 flex items-center'>
|
||||||
|
<span class='relative rounded-full bg-gray-200 p-1 text-xs' />
|
||||||
|
<span class='absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-gray-400 p-1 text-xs' />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
42
src/components/RoadCard/Editor.tsx
Normal file
42
src/components/RoadCard/Editor.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { useCopyText } from '../../hooks/use-copy-text';
|
||||||
|
import CopyIcon from '../../icons/copy.svg';
|
||||||
|
|
||||||
|
type EditorProps = {
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Editor(props: EditorProps) {
|
||||||
|
const { text, title } = props;
|
||||||
|
|
||||||
|
const { isCopied, copyText } = useCopyText();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-grow flex-col overflow-hidden rounded border border-gray-300 bg-gray-50">
|
||||||
|
<div className="flex items-center justify-between gap-2 border-b border-gray-300 px-3 py-2">
|
||||||
|
<span className="text-xs uppercase leading-none text-gray-400">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<button className="flex items-center" onClick={() => copyText(text)}>
|
||||||
|
{isCopied && (
|
||||||
|
<span className="mr-1 text-xs leading-none text-gray-700">
|
||||||
|
Copied!
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<img src={CopyIcon} alt="Copy" className="inline-block h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
className="no-scrollbar block h-12 w-full overflow-x-auto whitespace-nowrap bg-gray-200/70 p-3 text-sm text-gray-900 focus:bg-gray-50 focus:outline-0"
|
||||||
|
readOnly
|
||||||
|
onClick={(e: any) => {
|
||||||
|
e.target.select();
|
||||||
|
copyText(e.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
15
src/components/RoadCard/GitHubReadmeBanner.tsx
Normal file
15
src/components/RoadCard/GitHubReadmeBanner.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export function GitHubReadmeBanner() {
|
||||||
|
return (
|
||||||
|
<p className="mt-3 rounded-md border p-2 text-sm w-full bg-yellow-100 border-yellow-400 text-yellow-900">
|
||||||
|
Add this badge to your{' '}
|
||||||
|
<a
|
||||||
|
href="https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/customizing-your-profile/managing-your-profile-readme"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 underline hover:text-blue-800"
|
||||||
|
>
|
||||||
|
GitHub profile readme.
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
172
src/components/RoadCard/RoadCardPage.tsx
Normal file
172
src/components/RoadCard/RoadCardPage.tsx
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
import { useCopyText } from '../../hooks/use-copy-text';
|
||||||
|
import { useAuth } from '../../hooks/use-auth';
|
||||||
|
import CopyIcon from '../../icons/copy.svg';
|
||||||
|
import { RoadmapSelect } from './RoadmapSelect';
|
||||||
|
import { GitHubReadmeBanner } from './GitHubReadmeBanner';
|
||||||
|
import { downloadImage } from '../../helper/download-image';
|
||||||
|
import { SelectionButton } from './SelectionButton';
|
||||||
|
import { StepCounter } from './StepCounter';
|
||||||
|
import { Editor } from './Editor';
|
||||||
|
|
||||||
|
type StepLabelProps = {
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
function StepLabel(props: StepLabelProps) {
|
||||||
|
const { label } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="mb-3 flex items-center gap-2 text-sm leading-none text-gray-400">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoadCardPage() {
|
||||||
|
const { isCopied, copyText } = useCopyText();
|
||||||
|
const [roadmaps, setRoadmaps] = useState<string[]>([]);
|
||||||
|
const [version, setVersion] = useState<'tall' | 'wide'>('tall');
|
||||||
|
const [variant, setVariant] = useState<'dark' | 'light'>('dark');
|
||||||
|
const user = useAuth();
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const badgeUrl = new URL(
|
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-badge/${version}/${user?.id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
badgeUrl.searchParams.set('variant', variant);
|
||||||
|
if (roadmaps.length > 0) {
|
||||||
|
badgeUrl.searchParams.set('roadmaps', roadmaps.join(','));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-5 flex items-start gap-4 pt-2">
|
||||||
|
<StepCounter step={1} />
|
||||||
|
<div>
|
||||||
|
<StepLabel label="Pick progress to show (Max. 4)" />
|
||||||
|
|
||||||
|
<div className="flex min-h-[30px] flex-wrap">
|
||||||
|
<RoadmapSelect
|
||||||
|
selectedRoadmaps={roadmaps}
|
||||||
|
setSelectedRoadmaps={setRoadmaps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-5 flex items-start gap-4">
|
||||||
|
<StepCounter step={2} />
|
||||||
|
<div>
|
||||||
|
<StepLabel label="Select Mode (Dark vs Light)" />
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<SelectionButton
|
||||||
|
text={'Dark'}
|
||||||
|
isDisabled={false}
|
||||||
|
isSelected={variant === 'dark'}
|
||||||
|
onClick={() => {
|
||||||
|
setVariant('dark');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectionButton
|
||||||
|
text={'Light'}
|
||||||
|
isDisabled={false}
|
||||||
|
isSelected={variant === 'light'}
|
||||||
|
onClick={() => {
|
||||||
|
setVariant('light');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-5 flex items-start gap-4">
|
||||||
|
<StepCounter step={3} />
|
||||||
|
<div>
|
||||||
|
<StepLabel label="Select Version" />
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<SelectionButton
|
||||||
|
text={'Tall'}
|
||||||
|
isDisabled={false}
|
||||||
|
isSelected={version === 'tall'}
|
||||||
|
onClick={() => {
|
||||||
|
setVersion('tall');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SelectionButton
|
||||||
|
text={'Wide'}
|
||||||
|
isDisabled={false}
|
||||||
|
isSelected={version === 'wide'}
|
||||||
|
onClick={() => {
|
||||||
|
setVersion('wide');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-5 flex items-start gap-4">
|
||||||
|
<StepCounter step={4} />
|
||||||
|
<div class="flex-grow">
|
||||||
|
<StepLabel label="Share your #RoadCard with others" />
|
||||||
|
<div className={'rounded-md border bg-gray-50 p-2 text-center'}>
|
||||||
|
<a
|
||||||
|
href={badgeUrl.toString()}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={`relative block hover:cursor-pointer ${
|
||||||
|
version === 'tall' ? ' max-w-[270px] ' : ' w-full '
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<img src={badgeUrl.toString()} alt="RoadCard" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center rounded border border-gray-300 p-1.5 px-2 text-sm font-medium"
|
||||||
|
onClick={() =>
|
||||||
|
downloadImage({
|
||||||
|
url: badgeUrl.toString(),
|
||||||
|
name: 'road-card',
|
||||||
|
scale: 4,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
disabled={isCopied}
|
||||||
|
className="flex cursor-pointer items-center justify-center rounded border border-gray-300 p-1.5 px-2 text-sm font-medium disabled:bg-blue-50"
|
||||||
|
onClick={() => copyText(badgeUrl.toString())}
|
||||||
|
>
|
||||||
|
<img alt="Copy" src={CopyIcon} className="mr-1" />
|
||||||
|
|
||||||
|
{isCopied ? 'Copied!' : 'Copy Link'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 flex flex-col gap-3">
|
||||||
|
<Editor
|
||||||
|
title={'HTML'}
|
||||||
|
text={`<a href="https://roadmap.sh"><img src="${badgeUrl}" alt="roadmap.sh"/></a>`.trim()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Editor
|
||||||
|
title={'Markdown'}
|
||||||
|
text={`[](https://roadmap.sh)`.trim()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GitHubReadmeBanner />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
69
src/components/RoadCard/RoadmapSelect.tsx
Normal file
69
src/components/RoadCard/RoadmapSelect.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { httpGet } from '../../lib/http';
|
||||||
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
import { pageProgressMessage } from '../../stores/page';
|
||||||
|
import type { UserProgressResponse } from '../HeroSection/FavoriteRoadmaps';
|
||||||
|
import { SelectionButton } from './SelectionButton';
|
||||||
|
|
||||||
|
type RoadmapSelectProps = {
|
||||||
|
selectedRoadmaps: string[];
|
||||||
|
setSelectedRoadmaps: (updatedRoadmaps: string[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RoadmapSelect(props: RoadmapSelectProps) {
|
||||||
|
const { selectedRoadmaps, setSelectedRoadmaps } = props;
|
||||||
|
|
||||||
|
const [progressList, setProgressList] = useState<UserProgressResponse>();
|
||||||
|
|
||||||
|
const fetchProgress = async () => {
|
||||||
|
const { response, error } = await httpGet<UserProgressResponse>(
|
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-all-progress`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error || !response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgressList(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProgress().finally(() => {
|
||||||
|
pageProgressMessage.set('');
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const canSelectMore = selectedRoadmaps.length < 4;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{progressList
|
||||||
|
?.filter((progress) => progress.resourceType === 'roadmap')
|
||||||
|
.map((progress) => {
|
||||||
|
const isSelected = selectedRoadmaps.includes(progress.resourceId);
|
||||||
|
const canSelect = isSelected || canSelectMore;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectionButton
|
||||||
|
text={progress.resourceTitle}
|
||||||
|
isDisabled={!canSelect}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onClick={() => {
|
||||||
|
if (isSelected) {
|
||||||
|
setSelectedRoadmaps(
|
||||||
|
selectedRoadmaps.filter(
|
||||||
|
(roadmap) => roadmap !== progress.resourceId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (selectedRoadmaps.length < 4) {
|
||||||
|
setSelectedRoadmaps([
|
||||||
|
...selectedRoadmaps,
|
||||||
|
progress.resourceId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
23
src/components/RoadCard/SelectionButton.tsx
Normal file
23
src/components/RoadCard/SelectionButton.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
type SelectionButtonProps = {
|
||||||
|
text: string;
|
||||||
|
isDisabled: boolean;
|
||||||
|
isSelected: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SelectionButton(props: SelectionButtonProps) {
|
||||||
|
const { text, isDisabled, isSelected, onClick } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`rounded-md border p-1 px-2 text-sm ${
|
||||||
|
isSelected ? ' border-gray-500 bg-gray-300 ' : ''
|
||||||
|
} ${
|
||||||
|
!isDisabled ? ' cursor-pointer ' : ' cursor-not-allowed opacity-40 '
|
||||||
|
}`}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
17
src/components/RoadCard/StepCounter.tsx
Normal file
17
src/components/RoadCard/StepCounter.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
type StepCounterProps = {
|
||||||
|
step: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function StepCounter(props: StepCounterProps) {
|
||||||
|
const { step } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
'flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-gray-300 text-white'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{step}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
@@ -80,7 +80,7 @@ export default function UpdatePasswordForm() {
|
|||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div class="hidden md:block mb-8">
|
<div class="hidden md:block mb-8">
|
||||||
<h2 className="text-3xl font-bold sm:text-4xl">Password</h2>
|
<h2 className="text-3xl font-bold sm:text-4xl">Password</h2>
|
||||||
<p className="mt-2">Use the form below to update your password.</p>
|
<p className="mt-2 text-gray-400">Use the form below to update your password.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{authProvider === 'email' && (
|
{authProvider === 'email' && (
|
||||||
|
@@ -83,7 +83,7 @@ export function UpdateProfileForm() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="mb-8 hidden md:block">
|
<div className="mb-8 hidden md:block">
|
||||||
<h2 className="text-3xl font-bold sm:text-4xl">Profile</h2>
|
<h2 className="text-3xl font-bold sm:text-4xl">Profile</h2>
|
||||||
<p className="mt-2">Update your profile details below.</p>
|
<p className="mt-2 text-gray-400">Update your profile details below.</p>
|
||||||
</div>
|
</div>
|
||||||
<UploadProfilePicture
|
<UploadProfilePicture
|
||||||
avatarUrl={
|
avatarUrl={
|
||||||
|
36
src/helper/download-image.ts
Normal file
36
src/helper/download-image.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
type DownloadImageProps = {
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
extension?: 'png' | 'jpg';
|
||||||
|
scale?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function downloadImage({
|
||||||
|
url,
|
||||||
|
name,
|
||||||
|
extension = 'png',
|
||||||
|
scale = 1,
|
||||||
|
}: DownloadImageProps) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url);
|
||||||
|
const svg = await res.text();
|
||||||
|
|
||||||
|
const image = `data:image/svg+xml;base64,${window.btoa(svg)}`;
|
||||||
|
const img = new Image();
|
||||||
|
img.src = image;
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = img.width * scale;
|
||||||
|
canvas.height = img.height * scale;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx?.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
const png = canvas.toDataURL('image/png', 1.0); // Increase the quality by setting a higher value (0.0 - 1.0)
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = png;
|
||||||
|
a.download = `${name}.${extension}`;
|
||||||
|
a.click();
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error downloading image');
|
||||||
|
}
|
||||||
|
}
|
12
src/hooks/use-auth.ts
Normal file
12
src/hooks/use-auth.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Cookies from 'js-cookie';
|
||||||
|
import { TOKEN_COOKIE_NAME, decodeToken } from '../lib/jwt';
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const token = Cookies.get(TOKEN_COOKIE_NAME);
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const user = decodeToken(token);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
22
src/hooks/use-copy-text.ts
Normal file
22
src/hooks/use-copy-text.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
export function useCopyText() {
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
|
||||||
|
const copyText = (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text).then();
|
||||||
|
setIsCopied(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timeout: ReturnType<typeof setTimeout>;
|
||||||
|
if (isCopied) {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
setIsCopied(false);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [isCopied]);
|
||||||
|
|
||||||
|
return { isCopied, copyText };
|
||||||
|
}
|
1
src/icons/badge.svg
Normal file
1
src/icons/badge.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 384 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M256 48V64c0 17.7-14.3 32-32 32H160c-17.7 0-32-14.3-32-32V48H64c-8.8 0-16 7.2-16 16V448c0 8.8 7.2 16 16 16H320c8.8 0 16-7.2 16-16V64c0-8.8-7.2-16-16-16H256zM0 64C0 28.7 28.7 0 64 0H320c35.3 0 64 28.7 64 64V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V64zM160 320h64c44.2 0 80 35.8 80 80c0 8.8-7.2 16-16 16H96c-8.8 0-16-7.2-16-16c0-44.2 35.8-80 80-80zm-32-96a64 64 0 1 1 128 0 64 64 0 1 1 -128 0z"></path></svg>
|
After Width: | Height: | Size: 571 B |
1
src/icons/copy.svg
Normal file
1
src/icons/copy.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg>
|
After Width: | Height: | Size: 419 B |
@@ -1,4 +1,3 @@
|
|||||||
<svg viewBox="0 0 14 14" focusable="false" class='h-3 w-3' aria-hidden="true">
|
<svg viewBox="0 0 14 14" focusable="false" class='h-3 w-3' aria-hidden="true">
|
||||||
<path fill="currentColor"
|
<path fill="currentColor" d="M11.2857,6.05714 L10.08571,4.85714 L7.85714,7.14786 L7.85714,1 L6.14286,1 L6.14286,7.14786 L3.91429,4.85714 L2.71429,6.05714 L7,10.42857 L11.2857,6.05714 Z M1,11.2857 L1,13 L13,13 L13,11.2857 L1,11.2857 Z"></path>
|
||||||
d="M11.2857,6.05714 L10.08571,4.85714 L7.85714,7.14786 L7.85714,1 L6.14286,1 L6.14286,7.14786 L3.91429,4.85714 L2.71429,6.05714 L7,10.42857 L11.2857,6.05714 Z M1,11.2857 L1,13 L13,13 L13,11.2857 L1,11.2857 Z"></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 331 B |
15
src/pages/account/road-card.astro
Normal file
15
src/pages/account/road-card.astro
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
import AccountSidebar from '../../components/AccountSidebar.astro';
|
||||||
|
import AccountLayout from '../../layouts/AccountLayout.astro';
|
||||||
|
import { RoadCardPage } from '../../components/RoadCard/RoadCardPage';
|
||||||
|
---
|
||||||
|
|
||||||
|
<AccountLayout
|
||||||
|
title='Road Card'
|
||||||
|
noIndex={true}
|
||||||
|
initialLoadingMessage='Preparing card..'
|
||||||
|
>
|
||||||
|
<AccountSidebar activePageId='road-card' activePageTitle='Road Card'>
|
||||||
|
<RoadCardPage client:load />
|
||||||
|
</AccountSidebar>
|
||||||
|
</AccountLayout>
|
@@ -6,6 +6,16 @@
|
|||||||
.container {
|
.container {
|
||||||
@apply mx-auto max-w-[830px] px-4;
|
@apply mx-auto max-w-[830px] px-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Chrome, Safari and Opera */
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote p:before {
|
blockquote p:before {
|
||||||
|
Reference in New Issue
Block a user