refactor: badges tab

pull/4053/head
Arik Chakma 1 year ago
parent 4f81d8ac53
commit 3f43af42bc
  1. 15
      src/components/RoadCard/GithubReadmeBanner.tsx
  2. 12
      src/components/RoadCard/LongBadge.tsx
  3. 62
      src/components/RoadCard/LongBadgeTab.tsx
  4. 160
      src/components/RoadCard/RoadCardPage.tsx
  5. 10
      src/components/RoadCard/WideBadge.tsx
  6. 62
      src/components/RoadCard/WideBadgeTab.tsx
  7. 36
      src/helper/download-image.ts
  8. 12
      src/hooks/use-auth.ts

@ -0,0 +1,15 @@
export function GithubReadmeBanner() {
return (
<p className="mt-3 rounded-md border p-2 text-sm">
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>
);
}

@ -1,3 +1,4 @@
import { downloadImage } from '../../helper/download-image';
import { useCopyText } from '../../hooks/use-copy-text'; import { useCopyText } from '../../hooks/use-copy-text';
import type { BadgeProps } from './RoadCardPage'; import type { BadgeProps } from './RoadCardPage';
@ -20,15 +21,16 @@ export function LongBadge({ badgeUrl }: BadgeProps) {
</a> </a>
<div className="mt-3 grid grid-cols-2 gap-2"> <div className="mt-3 grid grid-cols-2 gap-2">
<a <button
className="flex h-8 items-center justify-center whitespace-nowrap rounded border border-gray-300 bg-gray-50 px-2 text-sm font-medium leading-none hover:opacity-75" className="flex h-8 items-center justify-center whitespace-nowrap rounded border border-gray-300 bg-gray-50 px-2 text-sm font-medium leading-none hover:opacity-75"
href={badgeUrl + '&type=png'} onClick={() =>
download downloadImage({ url: badgeUrl, name: 'road-card', scale: 4 })
}
> >
Download Download
</a> </button>
<button <button
className="flex h-8 items-center justify-center whitespace-nowrap rounded border border-gray-300 bg-gray-50 px-2 text-sm font-medium leading-none cursor-pointer hover:opacity-75" className="flex h-8 cursor-pointer items-center justify-center whitespace-nowrap rounded border border-gray-300 bg-gray-50 px-2 text-sm font-medium leading-none hover:opacity-75"
onClick={() => copyText(badgeUrl)} onClick={() => copyText(badgeUrl)}
> >
{isCopied ? 'Copied!' : 'Copy Link'} {isCopied ? 'Copied!' : 'Copy Link'}

@ -0,0 +1,62 @@
import { useState } from 'preact/hooks';
import { LongBadge } from './LongBadge';
import { Editor, getBadgeLink } from './RoadCardPage';
import { GithubReadmeBanner } from './GithubReadmeBanner';
import { useAuth } from '../../hooks/use-auth';
export function LongBadgeTab() {
const [selectedVariant, setSelectedVariant] = useState<'dark' | 'light'>(
'dark'
);
const user = useAuth();
if (!user) {
return null;
}
const { badgeUrl, textareaContent, markdownSnippet } = getBadgeLink({
user,
variant: selectedVariant,
badge: 'long',
});
return (
<div className="grid gap-6 sm:grid-cols-5">
<LongBadge badgeUrl={badgeUrl} />
<div className="sm:col-span-3">
<div>
<span className="text-xs uppercase leading-none text-gray-400">
Variation
</span>
<div className="mt-2 flex items-center gap-2">
<button
className={`flex h-7 items-center justify-center rounded-lg border border-gray-200 px-3 text-sm leading-none hover:opacity-80 ${
selectedVariant === 'dark' && 'border-gray-300 bg-gray-100'
}`}
onClick={() => setSelectedVariant('dark')}
>
Dark
</button>
<button
className={`flex h-7 items-center justify-center rounded-lg border border-gray-200 px-3 text-sm leading-none hover:opacity-80 ${
selectedVariant === 'light' && 'border-gray-300 bg-gray-100'
}`}
onClick={() => setSelectedVariant('light')}
>
Light
</button>
</div>
</div>
<div className="mt-4 flex flex-col gap-2">
<Editor title={'HTML'} text={textareaContent} />
<Editor title={'Markdown'} text={markdownSnippet} />
</div>
<GithubReadmeBanner />
</div>
</div>
);
}

@ -1,11 +1,74 @@
import { useState } from 'preact/hooks'; import { useState } from 'preact/hooks';
import Cookies from 'js-cookie';
import { TOKEN_COOKIE_NAME, decodeToken } from '../../lib/jwt';
import { WideBadge } from './WideBadge';
import { LongBadge } from './LongBadge';
import { useCopyText } from '../../hooks/use-copy-text'; import { useCopyText } from '../../hooks/use-copy-text';
import {Editor} from "../Editor"; import { useAuth } from '../../hooks/use-auth';
import { LongBadgeTab } from './LongBadgeTab';
import WideBadgeTab from './WideBadgeTab';
import CopyIcon from '../../icons/copy.svg';
export type GetBadgeLinkProps = {
user: ReturnType<typeof useAuth>;
variant: 'dark' | 'light';
badge: 'long' | 'wide';
};
export function getBadgeLink({ user, variant, badge }: GetBadgeLinkProps) {
const badgeUrl = `${import.meta.env.PUBLIC_API_URL}/v1-badge/${badge}/${
user?.id
}${variant ? `?variant=${variant}` : ''}`;
const textareaContent = `
<a href="${badgeUrl}">
<img src="${badgeUrl}" alt="${user?.name}${user?.name && "'s"} Road Card"/>
</a>
`.trim();
const markdownSnippet = `
[![${user?.name}${
user?.name && "'s"
} Road Card](${badgeUrl})](${badgeUrl})
`.trim();
return {
badgeUrl,
textareaContent,
markdownSnippet,
};
}
type EditorProps = {
title: string;
text: string;
};
export function Editor(props: EditorProps) {
const { text, title } = props;
const { isCopied, copyText } = useCopyText();
return (
<div className="flex 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-green-500">
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"
readOnly
>
{text}
</textarea>
</div>
);
}
export type BadgeProps = { export type BadgeProps = {
badgeUrl: string; badgeUrl: string;
@ -13,32 +76,11 @@ export type BadgeProps = {
export function RoadCardPage() { export function RoadCardPage() {
const [selectedBadge, setSelectedBadge] = useState<'long' | 'wide'>('long'); const [selectedBadge, setSelectedBadge] = useState<'long' | 'wide'>('long');
const [selectedVariant, setSelectedVariant] = useState<'dark' | 'light'>(
'dark'
);
const { isCopied, copyText } = useCopyText();
const { isCopied: isMarkdownCopied, copyText: handleMarkdownCopy } =
useCopyText();
const token = Cookies.get(TOKEN_COOKIE_NAME); const user = useAuth();
if (!token) { if (!user) {
return null; return null;
} }
const user = decodeToken(token);
const badgeUrl = `${
import.meta.env.PUBLIC_API_URL
}/v1-badge/${selectedBadge}/${user.id}?variant=${selectedVariant}`;
const textareaContent = `
<a href="${badgeUrl}">
<img src="${badgeUrl}" alt="${user?.name}${user?.name && "'s"} Road Card"/>
</a>
`.trim();
const markdownSnippet = `
[![${user?.name}${user?.name && "'s"} Road Card](${badgeUrl})](${badgeUrl})
`.trim();
return ( return (
<> <>
@ -60,7 +102,6 @@ export function RoadCardPage() {
}`} }`}
onClick={() => { onClick={() => {
setSelectedBadge('long'); setSelectedBadge('long');
setSelectedVariant('dark');
}} }}
> >
Long Long
@ -74,7 +115,6 @@ export function RoadCardPage() {
}`} }`}
onClick={() => { onClick={() => {
setSelectedBadge('wide'); setSelectedBadge('wide');
setSelectedVariant('dark');
}} }}
> >
Wide Wide
@ -83,64 +123,8 @@ export function RoadCardPage() {
</div> </div>
</div> </div>
<div {selectedBadge === 'long' && <LongBadgeTab />}
className={`${ {selectedBadge === 'wide' && <WideBadgeTab />}
selectedBadge === 'long' && 'grid gap-6 sm:grid-cols-5'
} ${selectedBadge === 'wide' && 'flex flex-col gap-6'}`}
>
{selectedBadge === 'long' && <LongBadge badgeUrl={badgeUrl} />}
{selectedBadge === 'wide' && <WideBadge badgeUrl={badgeUrl} />}
<div className={`${selectedBadge === 'long' && 'sm:col-span-3'}`}>
<div>
<span className="text-xs uppercase leading-none text-gray-400">
Variation
</span>
<div className="mt-2 flex items-center gap-2">
<button
className={`flex h-7 items-center justify-center rounded-lg border border-gray-200 px-3 text-sm leading-none hover:opacity-80 ${
selectedVariant === 'dark' && 'border-gray-300 bg-gray-100'
}`}
onClick={() => setSelectedVariant('dark')}
>
Dark
</button>
<button
className={`flex h-7 items-center justify-center rounded-lg border border-gray-200 px-3 text-sm leading-none hover:opacity-80 ${
selectedVariant === 'light' && 'border-gray-300 bg-gray-100'
}`}
onClick={() => setSelectedVariant('light')}
>
Light
</button>
</div>
</div>
<div
className={`mt-4 flex gap-2 ${
selectedBadge === 'long' && 'flex-col'
}`}
>
<Editor title={'HTML'} text={textareaContent} />
<Editor title={'Markdown'} text={markdownSnippet} />
</div>
<p className="mt-3 rounded-md border p-2 px-3 text-sm">
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.
</a>
</p>
</div>
</div>
</> </>
); );
} }

@ -1,3 +1,4 @@
import { downloadImage } from '../../helper/download-image';
import { useCopyText } from '../../hooks/use-copy-text'; import { useCopyText } from '../../hooks/use-copy-text';
import type { BadgeProps } from './RoadCardPage'; import type { BadgeProps } from './RoadCardPage';
@ -19,13 +20,14 @@ export function WideBadge({ badgeUrl }: BadgeProps) {
</a> </a>
<div className="mt-3 grid grid-cols-2 gap-4"> <div className="mt-3 grid grid-cols-2 gap-4">
<a <button
className="flex h-8 items-center justify-center whitespace-nowrap rounded border border-gray-300 bg-gray-50 px-2 text-sm font-medium leading-none hover:opacity-75" className="flex h-8 items-center justify-center whitespace-nowrap rounded border border-gray-300 bg-gray-50 px-2 text-sm font-medium leading-none hover:opacity-75"
href={badgeUrl + '&type=png'} onClick={() =>
download downloadImage({ url: badgeUrl, name: 'road-card', scale: 4 })
}
> >
Download Download
</a> </button>
<button <button
className="flex h-8 cursor-pointer items-center justify-center whitespace-nowrap rounded border border-gray-300 bg-gray-50 px-2 text-sm font-medium leading-none hover:opacity-75" className="flex h-8 cursor-pointer items-center justify-center whitespace-nowrap rounded border border-gray-300 bg-gray-50 px-2 text-sm font-medium leading-none hover:opacity-75"
onClick={() => copyText(badgeUrl)} onClick={() => copyText(badgeUrl)}

@ -0,0 +1,62 @@
import { useState } from 'preact/hooks';
import { useAuth } from '../../hooks/use-auth';
import { WideBadge } from './WideBadge';
import { Editor, getBadgeLink } from './RoadCardPage';
import { GithubReadmeBanner } from './GithubReadmeBanner';
export default function WideBadgeTab() {
const [selectedVariant, setSelectedVariant] = useState<'dark' | 'light'>(
'dark'
);
const user = useAuth();
if (!user) {
return null;
}
const { badgeUrl, textareaContent, markdownSnippet } = getBadgeLink({
user,
variant: selectedVariant,
badge: 'wide',
});
return (
<div className="flex flex-col gap-6">
<WideBadge badgeUrl={badgeUrl} />
<div>
<div>
<span className="text-xs uppercase leading-none text-gray-400">
Variation
</span>
<div className="mt-2 flex items-center gap-2">
<button
className={`flex h-7 items-center justify-center rounded-lg border border-gray-200 px-3 text-sm leading-none hover:opacity-80 ${
selectedVariant === 'dark' && 'border-gray-300 bg-gray-100'
}`}
onClick={() => setSelectedVariant('dark')}
>
Dark
</button>
<button
className={`flex h-7 items-center justify-center rounded-lg border border-gray-200 px-3 text-sm leading-none hover:opacity-80 ${
selectedVariant === 'light' && 'border-gray-300 bg-gray-100'
}`}
onClick={() => setSelectedVariant('light')}
>
Light
</button>
</div>
</div>
<div className={`mt-4 flex gap-2`}>
<Editor title={'HTML'} text={textareaContent} />
<Editor title={'Markdown'} text={markdownSnippet} />
</div>
<GithubReadmeBanner />
</div>
</div>
);
}

@ -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');
}
}

@ -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;
}
Loading…
Cancel
Save