From e277ab00c9ed346d1897f9d548097556b023bc0c Mon Sep 17 00:00:00 2001 From: jp calvo Date: Thu, 29 Feb 2024 18:53:46 +0800 Subject: [PATCH 1/5] init switch to rsc --- website/src/app/(index)/_.tsx | 19 ++ website/src/app/(index)/gallery.tsx | 52 ------ website/src/app/(index)/icon-details.tsx | 162 ------------------ website/src/app/(index)/page-context.tsx | 64 ------- website/src/app/(index)/page.tsx | 81 +++++++-- website/src/app/(index)/searchbar.tsx | 49 ++---- website/src/app/(index)/total.tsx | 30 ---- website/src/app/(index)/types.ts | 9 - website/src/app/(index)/utils.server.ts | 51 ------ website/src/app/(index)/utils.ts | 54 ++++++ .../spinner.tsx => app/[slug]/loading.tsx} | 11 +- website/src/app/[slug]/page.tsx | 104 +++++++++++ website/src/app/{(index) => [slug]}/utils.tsx | 134 +++++++++++---- website/src/app/layout.tsx | 6 +- website/src/app/navbar.tsx | 19 +- website/src/app/theme-picker.tsx | 2 +- website/src/lib/dash-to-pascal.ts | 6 + website/src/lib/syntax.tsx | 34 ---- website/src/lib/use-debounce.tsx | 17 -- website/src/types/index.ts | 19 +- 20 files changed, 403 insertions(+), 520 deletions(-) create mode 100644 website/src/app/(index)/_.tsx delete mode 100644 website/src/app/(index)/gallery.tsx delete mode 100644 website/src/app/(index)/icon-details.tsx delete mode 100644 website/src/app/(index)/page-context.tsx delete mode 100644 website/src/app/(index)/total.tsx delete mode 100644 website/src/app/(index)/types.ts delete mode 100644 website/src/app/(index)/utils.server.ts create mode 100644 website/src/app/(index)/utils.ts rename website/src/{lib/spinner.tsx => app/[slug]/loading.tsx} (77%) create mode 100644 website/src/app/[slug]/page.tsx rename website/src/app/{(index) => [slug]}/utils.tsx (50%) create mode 100644 website/src/lib/dash-to-pascal.ts delete mode 100644 website/src/lib/syntax.tsx delete mode 100644 website/src/lib/use-debounce.tsx diff --git a/website/src/app/(index)/_.tsx b/website/src/app/(index)/_.tsx new file mode 100644 index 00000000..690efdad --- /dev/null +++ b/website/src/app/(index)/_.tsx @@ -0,0 +1,19 @@ +import {Dialog} from '@ark-ui/react'; +import {XCloseIcon} from '@untitled-theme/icons-react'; + +export function IconDetails() { + return ( + + + + +
{/* TODO: content */}
+ + + + +
+
+
+ ); +} diff --git a/website/src/app/(index)/gallery.tsx b/website/src/app/(index)/gallery.tsx deleted file mode 100644 index 05c2a02e..00000000 --- a/website/src/app/(index)/gallery.tsx +++ /dev/null @@ -1,52 +0,0 @@ -'use client'; - -import {Tooltip} from '@ark-ui/react'; -import {twMerge} from 'tailwind-merge'; -import {usePageContext} from './page-context'; -import type {Icon} from './types'; - -export function Gallery() { - const context = usePageContext(); - - return ( -
- {context.icons.map((item) => ( - - ))} -
- ); -} - -function GalleryItem({data}: {data: Icon}) { - const context = usePageContext(); - - return ( - - context.inspect(data)} - dangerouslySetInnerHTML={{ - __html: data.content, - }} - className="flex aspect-square items-center justify-center rounded border p-2 transition duration-200 hover:bg-gray-true-50 focus:shadow-outline dark:hover:bg-gray-true-800/10" - aria-label={data.displayName} - /> - - - - - - - - {data.displayName} - - - - ); -} diff --git a/website/src/app/(index)/icon-details.tsx b/website/src/app/(index)/icon-details.tsx deleted file mode 100644 index 81b125c4..00000000 --- a/website/src/app/(index)/icon-details.tsx +++ /dev/null @@ -1,162 +0,0 @@ -'use client'; - -import {Spinner} from '@/lib/spinner'; -import {Syntax} from '@/lib/syntax'; -import {Clipboard, Dialog, Tabs} from '@ark-ui/react'; -import {CheckIcon, Copy01Icon, XCloseIcon} from '@untitled-theme/icons-react'; -import {useCallback, useEffect, useState} from 'react'; -import {usePageContext} from './page-context'; -import {toHtmlComponent, toReactComponent, toSvelteComponent} from './utils'; - -export function IconDetails() { - const context = usePageContext(); - const [loading, setLoading] = useState(false); - - const [htmlComponent, setHtmlComponent] = useState(''); - const [reactComponent, setReactComponent] = useState(''); - const [svelteComponent, setSvelteComponent] = useState(''); - - const parseAll = useCallback(async () => { - if (!context.inspectionSubject) return; - - setLoading(true); - - const [h, r, s] = await Promise.all([ - toHtmlComponent(context.inspectionSubject), - toReactComponent(context.inspectionSubject), - toSvelteComponent(context.inspectionSubject), - ]); - - setHtmlComponent(h); - setReactComponent(r); - setSvelteComponent(s); - - setLoading(false); - }, [context.inspectionSubject]); - - useEffect(() => { - parseAll(); - }, [parseAll]); - - useEffect(() => { - return () => { - setHtmlComponent(''); - setReactComponent(''); - setSvelteComponent(''); - setLoading(false); - }; - }, []); - - const items = [ - { - label: 'SVG', - value: 'html' as const, - content: htmlComponent, - }, - { - label: 'React', - value: 'tsx' as const, - content: reactComponent, - }, - { - label: 'Svelte', - value: 'svelte' as const, - content: svelteComponent, - }, - ]; - - return ( - { - if (!open) { - context.inspect.dismiss(); - } - }} - lazyMount - unmountOnExit - > - - - -
-
- -
- <{context.inspectionSubject?.displayName ?? ''} /> - - - Copy - - - } - > - - - - -
- - - - {items.map((item) => ( - - {item.label} - - ))} - - - {items.map((item) => { - return ( - -
- {loading && } - {!loading && ( - <> - - Copy - - - } - > - - - - - - - {item.content} - - - )} -
-
- ); - })} -
-
- - - - - - - - ); -} diff --git a/website/src/app/(index)/page-context.tsx b/website/src/app/(index)/page-context.tsx deleted file mode 100644 index ced9b7a4..00000000 --- a/website/src/app/(index)/page-context.tsx +++ /dev/null @@ -1,64 +0,0 @@ -'use client'; - -import {useDebounce} from '@/lib/use-debounce'; -import { - createContext, - useContext, - useEffect, - useMemo, - useState, - type PropsWithChildren, -} from 'react'; -import type {Icon} from './types'; - -export const PageContext = createContext(undefined as unknown as UsePageReturn); -export const usePageContext = () => useContext(PageContext); -export const PageProvider = (props: PropsWithChildren) => { - return {props.children}; -}; - -interface UsePageProps { - items?: Icon[]; -} - -type UsePageReturn = ReturnType; - -function usePage(props: UsePageProps) { - const [searchKeyword, setSearchKeyword] = useState(''); - const debouncedKeyword = useDebounce(searchKeyword); - const search = (keyword: string) => setSearchKeyword(keyword); - search.stop = () => setSearchKeyword(''); - - const icons = useMemo(() => { - if (!props.items) return []; - - return props.items.filter((item) => { - return item.displayName - .toLowerCase() - .replace(/ /g, '') - .includes(debouncedKeyword.toLowerCase().replace(/ /g, '')); - }); - }, [props.items, debouncedKeyword]); - - const iconsCount = icons.length; - - const [inspectionSubject, setInspectionSubject] = useState(); - const inspect = (subject: Icon) => setInspectionSubject(subject); - inspect.dismiss = () => setInspectionSubject(null); - - useEffect(() => { - return function reset() { - setSearchKeyword(''); - setInspectionSubject(null); - }; - }, []); - - return { - icons, - iconsCount, - search, - searchKeyword, - inspect, - inspectionSubject, - }; -} diff --git a/website/src/app/(index)/page.tsx b/website/src/app/(index)/page.tsx index 50a60419..8b6c992c 100644 --- a/website/src/app/(index)/page.tsx +++ b/website/src/app/(index)/page.tsx @@ -1,19 +1,76 @@ -import {Gallery} from './gallery'; -import {IconDetails} from './icon-details'; -import {PageProvider} from './page-context'; +import {Tooltip} from '@ark-ui/react'; +import Link from 'next/link'; +import {twMerge} from 'tailwind-merge'; import {Searchbar} from './searchbar'; -import {Total} from './total'; -import {getIcons} from './utils.server'; +import {getIcons} from './utils'; + +interface Props { + searchParams: { + search?: string | string[]; + }; +} + +export default async function IconsPage({searchParams}: Props) { + const icons = await getIcons({search: searchParams.search?.toString()}); + + let totalHtml = ''; + + if (icons.length === 0) { + totalHtml += 'No icons found'; + } else if (icons.length === 1) { + totalHtml += '1 icon found'; + } else { + totalHtml += `${icons.length} icons found`; + } + + if (searchParams.search) { + totalHtml += ` for '${searchParams.search}'`; + } -export default async function Landing() { return ( - + <> -
- - + +
+ +
+ {icons.map((icon) => ( + + + + + + + + + + + + {icon.name} + + + + ))}
- - + ); } diff --git a/website/src/app/(index)/searchbar.tsx b/website/src/app/(index)/searchbar.tsx index 19cb84d3..7031437f 100644 --- a/website/src/app/(index)/searchbar.tsx +++ b/website/src/app/(index)/searchbar.tsx @@ -1,50 +1,33 @@ 'use client'; -import {Presence} from '@ark-ui/react'; -import {SearchLgIcon, XCloseIcon} from '@untitled-theme/icons-react'; -import {useRef} from 'react'; -import {twMerge} from 'tailwind-merge'; -import {usePageContext} from './page-context'; +import {SearchLgIcon} from '@untitled-theme/icons-react'; +import {usePathname, useRouter, useSearchParams} from 'next/navigation'; export function Searchbar() { - const context = usePageContext(); - const inputRef = useRef(null); + const router = useRouter(); + const params = useSearchParams(); + const pathname = usePathname(); return (
{ - context.search(e.target.value); + const newParams = new URLSearchParams(params); + + if (e.target.value) { + newParams.set('search', e.target.value); + } else { + newParams.delete('search'); + } + + router.replace(`${pathname}?${newParams.toString()}`); }} placeholder="Search" - className={twMerge( - 'h-12 w-full rounded border py-2 pl-12 outline-none focus:shadow-outline', - context.searchKeyword ? 'pr-10' : 'pr-4', - )} + className="h-12 w-full rounded border py-2 pl-12 pr-4 outline-none focus:shadow-outline" /> - - - -
); } diff --git a/website/src/app/(index)/total.tsx b/website/src/app/(index)/total.tsx deleted file mode 100644 index 9200e31a..00000000 --- a/website/src/app/(index)/total.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; - -import {usePageContext} from './page-context'; - -export function Total() { - const context = usePageContext(); - - let s: string[] = []; - - if (context.iconsCount === 0) { - s.push('No icons found'); - } else if (context.iconsCount === 1) { - s.push('1 icon found'); - } else { - s.push(`${context.iconsCount} icons found`); - } - - if (context.searchKeyword) { - s.push(`for '${context.searchKeyword}'`); - } - - return ( -
- ); -} diff --git a/website/src/app/(index)/types.ts b/website/src/app/(index)/types.ts deleted file mode 100644 index deb27baf..00000000 --- a/website/src/app/(index)/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type {Html} from '@/types'; - -export interface Icon { - displayName: string; - content: Html; - meta: { - fileName: string; - }; -} diff --git a/website/src/app/(index)/utils.server.ts b/website/src/app/(index)/utils.server.ts deleted file mode 100644 index 613630af..00000000 --- a/website/src/app/(index)/utils.server.ts +++ /dev/null @@ -1,51 +0,0 @@ -import fs from 'fs/promises'; -import {unstable_cache as cache} from 'next/cache'; -import path from 'path'; -import * as svgson from 'svgson'; -import type {Icon} from './types'; - -export const getIcons = cache(async () => { - const root = path.resolve(process.cwd(), '../assets/icons'); - const files = await fs.readdir(root, 'utf-8'); - - const promises = files.map>(async (fileName) => { - const parsedPath = path.parse(fileName); - const displayName = dashToPascal(parsedPath.name) + 'Icon'; - const fileContent = await fs.readFile(path.join(root, fileName), 'utf-8'); - - const parsed = await svgson.parse(fileContent, { - transformNode(node) { - if (node.name === 'svg') { - node.attributes['width'] = '32'; - node.attributes['height'] = '32'; - } - - return node; - }, - }); - - const content = svgson.stringify(parsed, { - selfClose: true, - transformAttr(key, value, escape) { - if (key === 'stroke') return `${key}="currentColor"`; - if (key === 'stroke-width') return `${key}="1.5"`; - return `${key}="${escape(value)}"`; - }, - }); - - return { - displayName, - content, - meta: {fileName}, - }; - }); - - return await Promise.all(promises); -}, ['items']); - -function dashToPascal(value: string) { - return value - .split('-') - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(''); -} diff --git a/website/src/app/(index)/utils.ts b/website/src/app/(index)/utils.ts new file mode 100644 index 00000000..0fd1ea97 --- /dev/null +++ b/website/src/app/(index)/utils.ts @@ -0,0 +1,54 @@ +import {dashToPascal} from '@/lib/dash-to-pascal'; +import type {Icon} from '@/types'; +import fs from 'fs/promises'; +import {unstable_cache as cache} from 'next/cache'; +import path from 'path'; +import * as svgson from 'svgson'; + +export const getIcons = cache( + async ({search}: {search?: string}) => { + const root = path.resolve(process.cwd(), '../assets/icons'); + const files = await fs.readdir(root, 'utf-8'); + + const promises = files.map>(async (fileName) => { + const content = await fs.readFile(path.join(root, fileName), 'utf-8'); + const parsed = await svgson.parse(content, { + transformNode(node) { + if (node.name === 'svg') { + node.attributes['width'] = '32'; + node.attributes['height'] = '32'; + } + + return node; + }, + }); + + const html = svgson.stringify(parsed, { + selfClose: true, + transformAttr(key, value, escape) { + if (key === 'stroke') return `${key}="currentColor"`; + if (key === 'stroke-width') return `${key}="1.5"`; + return `${key}="${escape(value)}"`; + }, + }); + + const slug = path.parse(fileName).name; + const name = dashToPascal(slug) + 'Icon'; + + return { + slug, + name, + html, + }; + }); + + return await Promise.all(promises).then((array) => { + if (!search) return array; + + return array.filter((item) => + item.name.toLowerCase().replace(/ /g, '').includes(search.toLowerCase().replace(/ /g, '')), + ); + }); + }, + ['icons'], +); diff --git a/website/src/lib/spinner.tsx b/website/src/app/[slug]/loading.tsx similarity index 77% rename from website/src/lib/spinner.tsx rename to website/src/app/[slug]/loading.tsx index 9d3ab061..3b3a82ac 100644 --- a/website/src/lib/spinner.tsx +++ b/website/src/app/[slug]/loading.tsx @@ -1,14 +1,11 @@ -import {forwardRef, type SVGProps} from 'react'; - -export const Spinner = forwardRef>((props, ref) => { +export default function Loading() { return ( >((props ); -}); - -Spinner.displayName = 'Spinner'; +} diff --git a/website/src/app/[slug]/page.tsx b/website/src/app/[slug]/page.tsx new file mode 100644 index 00000000..5aab88a6 --- /dev/null +++ b/website/src/app/[slug]/page.tsx @@ -0,0 +1,104 @@ +import {Clipboard, Tabs} from '@ark-ui/react'; +import {CheckIcon, Copy01Icon} from '@untitled-theme/icons-react'; +import {notFound} from 'next/navigation'; +import {getIcon} from './utils'; + +interface Props { + params: { + slug: string; + }; +} + +export default async function IconDetailsPage({params}: Props) { + const icon = await getIcon(params.slug); + + if (!icon) return notFound(); + + const items = [ + { + label: 'Html', + value: 'Html', + content: icon.snippet.html, + }, + { + label: 'React', + value: 'React', + content: icon.snippet.react, + }, + { + label: 'Svelte', + value: 'Svelte', + content: icon.snippet.svelte, + }, + ]; + + return ( +
+
+ +
+ <{icon.name} /> + + + Copy + + } + > + + + + +
+ + + + {items.map((item) => ( + + {item.label} + + ))} + + + {items.map((item) => { + return ( + +
+ + Copy + + + } + > + + + + + +
+
+ + ); + })} + +
+ ); +} diff --git a/website/src/app/(index)/utils.tsx b/website/src/app/[slug]/utils.tsx similarity index 50% rename from website/src/app/(index)/utils.tsx rename to website/src/app/[slug]/utils.tsx index 13408430..267146a1 100644 --- a/website/src/app/(index)/utils.tsx +++ b/website/src/app/[slug]/utils.tsx @@ -1,15 +1,17 @@ -import prettierEsTreePlugin from 'prettier/plugins/estree'; -import prettierHtmlPlugin from 'prettier/plugins/html'; -import prettierTsPlugin from 'prettier/plugins/typescript'; -import prettier from 'prettier/standalone'; +import {dashToPascal} from '@/lib/dash-to-pascal'; +import type {Html, Icon} from '@/types'; +import fs from 'fs/promises'; +import {unstable_cache as cache} from 'next/cache'; +import path from 'path'; +import prettier from 'prettier'; +import {codeToHtml} from 'shiki'; import * as svgson from 'svgson'; -import type {Icon} from './types'; const REF = 'REF'; const REST = 'REST'; -export async function toReactComponent(icon: Icon) { - const node = await svgson.parse(icon.content, { +async function toReactSnippet(svg: Html, name: string) { + const node = await svgson.parse(svg, { camelcase: true, transformNode(node) { if (node.name === 'svg') { @@ -50,25 +52,32 @@ export async function toReactComponent(icon: Icon) { const component = ` import * as React from 'react'; - export const ${icon.displayName} = React.forwardRef>((props, ref) => { + export const ${name} = React.forwardRef>((props, ref) => { return ${reactSvg}; }); - ${icon.displayName}.displayName = '${icon.displayName}' + ${name}.displayName = '${name}' - export default ${icon.displayName}; + export default ${name}; `; - return await prettier.format(component, { - parser: 'typescript', - plugins: [prettierTsPlugin, prettierEsTreePlugin], - printWidth: 80, - bracketSpacing: false, - }); + return await codeToHtml( + await prettier.format(component, { + parser: 'typescript', + printWidth: 80, + }), + { + lang: 'typescript', + themes: { + dark: 'vitesse-dark', + light: 'vitesse-light', + }, + }, + ); } -export async function toSvelteComponent(icon: Icon) { - const node = await svgson.parse(icon.content, { +async function toSvelteSnippet(svg: Html) { + const node = await svgson.parse(svg, { camelcase: true, transformNode(node) { if (node.name === 'svg') { @@ -116,16 +125,23 @@ export async function toSvelteComponent(icon: Icon) { ${svelteSvg} `; - return await prettier.format(component, { - parser: 'html', - plugins: [prettierHtmlPlugin], - printWidth: 80, - bracketSpacing: false, - }); + return await codeToHtml( + await prettier.format(component, { + parser: 'html', + printWidth: 80, + }), + { + lang: 'svelte', + themes: { + dark: 'vitesse-dark', + light: 'vitesse-light', + }, + }, + ); } -export async function toHtmlComponent(icon: Icon) { - const node = await svgson.parse(icon.content, { +async function toHtmlSnippet(svg: Html) { + const node = await svgson.parse(svg, { camelcase: true, transformNode(node) { if (node.name === 'svg') { @@ -157,10 +173,64 @@ export async function toHtmlComponent(icon: Icon) { }, }); - return await prettier.format(htmlSvg, { - parser: 'html', - plugins: [prettierHtmlPlugin], - printWidth: 80, - bracketSpacing: false, - }); + return await codeToHtml( + await prettier.format(htmlSvg, { + parser: 'html', + printWidth: 80, + }), + { + lang: 'html', + themes: { + dark: 'vitesse-dark', + light: 'vitesse-light', + }, + }, + ); } + +export const getIcon = cache( + async (slug: string): Promise | null> => { + const fullPath = path.resolve(process.cwd(), '../assets/icons', `${slug}.svg`); + + try { + await fs.stat(fullPath); + } catch { + return null; + } + + const content = await fs.readFile(fullPath, 'utf-8'); + const parsed = await svgson.parse(content, { + transformNode(node) { + if (node.name === 'svg') { + node.attributes['width'] = '32'; + node.attributes['height'] = '32'; + } + + return node; + }, + }); + + const html = svgson.stringify(parsed, { + selfClose: true, + transformAttr(key, value, escape) { + if (key === 'stroke') return `${key}="currentColor"`; + if (key === 'stroke-width') return `${key}="1.5"`; + return `${key}="${escape(value)}"`; + }, + }); + + const name = dashToPascal(slug) + 'Icon'; + + return { + slug, + name, + html, + snippet: { + html: await toHtmlSnippet(html), + react: await toReactSnippet(html, name), + svelte: await toSvelteSnippet(html), + }, + }; + }, + ['icon'], +); diff --git a/website/src/app/layout.tsx b/website/src/app/layout.tsx index 9d41cd5d..48c1785d 100644 --- a/website/src/app/layout.tsx +++ b/website/src/app/layout.tsx @@ -8,14 +8,14 @@ import {Providers} from './providers'; export const metadata: Metadata = { title: { - default: '@untitled-theme/icons-{react|svelte}', - template: '@untitled-theme/icons-{react|svelte} | %s', + default: '@untitled-theme', + template: '@untitled-theme | %s', }, description: 'Untitled UI icons for React and Svelte', metadataBase: new URL('https://untitled-theme-docs.vercel.app'), openGraph: { type: 'website', - title: '@untitled-theme/icons-{react|svelte}', + title: '@untitled-theme', description: 'Untitled UI icons for React and Svelte', images: ['/opengraph.png'], }, diff --git a/website/src/app/navbar.tsx b/website/src/app/navbar.tsx index 8d38740b..7ac73dc4 100644 --- a/website/src/app/navbar.tsx +++ b/website/src/app/navbar.tsx @@ -1,18 +1,15 @@ +import Link from 'next/link'; import {ThemePicker} from './theme-picker'; export function Navbar() { return ( -
-
- - pnpm - install - - {'@untitled-theme/icons-{react|svelte}'} - - {'@untitled-theme'} - -
+
+ + @untitled-theme +
diff --git a/website/src/app/theme-picker.tsx b/website/src/app/theme-picker.tsx index 6b211197..d9082aa1 100644 --- a/website/src/app/theme-picker.tsx +++ b/website/src/app/theme-picker.tsx @@ -24,7 +24,7 @@ export function ThemePicker() { unmountOnExit > - + {selected.icon} diff --git a/website/src/lib/dash-to-pascal.ts b/website/src/lib/dash-to-pascal.ts new file mode 100644 index 00000000..9f3a0ec7 --- /dev/null +++ b/website/src/lib/dash-to-pascal.ts @@ -0,0 +1,6 @@ +export function dashToPascal(value: string) { + return value + .split('-') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(''); +} diff --git a/website/src/lib/syntax.tsx b/website/src/lib/syntax.tsx deleted file mode 100644 index b367034e..00000000 --- a/website/src/lib/syntax.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type {Assign} from '@/types'; -import {ark, type HTMLArkProps} from '@ark-ui/react'; -import {forwardRef, useEffect, useState} from 'react'; -import {codeToHtml, type BundledLanguage} from 'shiki'; - -interface SyntaxProps extends Assign, {children: string}> { - language: BundledLanguage; -} - -export const Syntax = forwardRef( - ({children, language, ...props}, ref) => { - const [parsed, setParsed] = useState(''); - - useEffect(() => { - codeToHtml(children, { - lang: language, - themes: { - dark: 'vitesse-dark', - light: 'vitesse-light', - }, - }) - .then(setParsed) - .catch(console.error); - - return () => { - setParsed(''); - }; - }, [children, language]); - - return ; - }, -); - -Syntax.displayName = 'Syntax'; diff --git a/website/src/lib/use-debounce.tsx b/website/src/lib/use-debounce.tsx deleted file mode 100644 index 9b538d17..00000000 --- a/website/src/lib/use-debounce.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import {useEffect, useState} from 'react'; - -export function useDebounce(value: T, delay: number = 500) { - const [debouncedValue, setDebouncedValue] = useState(value); - - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedValue(value); - }, delay); - - return () => { - clearTimeout(timer); - }; - }, [value, delay]); - - return debouncedValue; -} diff --git a/website/src/types/index.ts b/website/src/types/index.ts index 7601c1c2..4f6043bc 100644 --- a/website/src/types/index.ts +++ b/website/src/types/index.ts @@ -1,7 +1,24 @@ export type Alias = T & {_?: never}; -export type Html = Alias; export type GenericObject = Record; export type Pretty = {} & {[P in keyof T]: T[P]}; export type Assign = Pretty< Source & Omit >; +export type Html = Alias; +export type Snippet = Alias; +export type Icon = WithSnippet extends true + ? { + slug: string; + name: string; + html: Html; + snippet: { + html: Snippet; + react: Snippet; + svelte: Snippet; + }; + } + : { + slug: string; + name: string; + html: Html; + }; From 1eb4e7ef9dc8cf1f8a1bbf9210e6312fa3b25ed9 Mon Sep 17 00:00:00 2001 From: jp calvo Date: Thu, 29 Feb 2024 19:29:12 +0800 Subject: [PATCH 2/5] use canary version and route intercept --- pnpm-lock.yaml | 100 ++++++++++-------- website/package.json | 2 +- .../src/@modal/(.)icons/[slug]/default.tsx | 3 + .../_.tsx => @modal/(.)icons/[slug]/page.tsx} | 6 +- website/src/@modal/(.)icons/default.tsx | 3 + .../src/app/@modal/(.)icons/[slug]/page.tsx | 19 ++++ website/src/app/@modal/(.)icons/default.tsx | 3 + website/src/app/@modal/default.tsx | 3 + .../[slug]/__loading.tsx} | 0 website/src/app/{ => icons}/[slug]/page.tsx | 0 website/src/app/{ => icons}/[slug]/utils.tsx | 0 website/src/app/layout.tsx | 7 +- website/src/app/{(index) => }/page.tsx | 2 +- website/src/app/{(index) => }/searchbar.tsx | 0 website/src/app/{(index) => }/utils.ts | 12 +-- 15 files changed, 103 insertions(+), 57 deletions(-) create mode 100644 website/src/@modal/(.)icons/[slug]/default.tsx rename website/src/{app/(index)/_.tsx => @modal/(.)icons/[slug]/page.tsx} (85%) create mode 100644 website/src/@modal/(.)icons/default.tsx create mode 100644 website/src/app/@modal/(.)icons/[slug]/page.tsx create mode 100644 website/src/app/@modal/(.)icons/default.tsx create mode 100644 website/src/app/@modal/default.tsx rename website/src/app/{[slug]/loading.tsx => icons/[slug]/__loading.tsx} (100%) rename website/src/app/{ => icons}/[slug]/page.tsx (100%) rename website/src/app/{ => icons}/[slug]/utils.tsx (100%) rename website/src/app/{(index) => }/page.tsx (97%) rename website/src/app/{(index) => }/searchbar.tsx (100%) rename website/src/app/{(index) => }/utils.ts (84%) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8c101f5..99e3e948 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,7 +88,7 @@ importers: version: 1.0.3 '@types/node': specifier: 20.11.21 - version: 20.11.22 + version: 20.11.21 '@types/yargs': specifier: 17.0.32 version: 17.0.32 @@ -121,13 +121,13 @@ importers: version: link:../packages/icons/react geist: specifier: 1.2.2 - version: 1.2.2(next@14.1.0) + version: 1.2.2(next@14.1.1-canary.80) next: - specifier: 14.1.0 - version: 14.1.0(react-dom@18.2.0)(react@18.2.0) + specifier: 14.1.1-canary.80 + version: 14.1.1-canary.80(react-dom@18.2.0)(react@18.2.0) next-themes: specifier: 0.2.1 - version: 0.2.1(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) + version: 0.2.1(next@14.1.1-canary.80)(react-dom@18.2.0)(react@18.2.0) react: specifier: ^18 version: 18.2.0 @@ -747,8 +747,8 @@ packages: resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} dev: true - /@next/env@14.1.0: - resolution: {integrity: sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==} + /@next/env@14.1.1-canary.80: + resolution: {integrity: sha512-wOC/paUahuvf/AE/oe3mnvZgi1lTKdM1NUlutr9+4mGz+C+Mw4doPu628vYu7UiKtTuUxRHFgGslKC9oeTvjhw==} dev: false /@next/eslint-plugin-next@14.1.0: @@ -757,8 +757,8 @@ packages: glob: 10.3.10 dev: true - /@next/swc-darwin-arm64@14.1.0: - resolution: {integrity: sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==} + /@next/swc-darwin-arm64@14.1.1-canary.80: + resolution: {integrity: sha512-rRvpAq7BNzsaqqktu/NuFUt9ZkH9ZADEAFsgsFEsyUmpAjLE6d/o+GeyfiRWvvs8Lcl+NDoEta/iT4FYwEUG1g==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -766,8 +766,8 @@ packages: dev: false optional: true - /@next/swc-darwin-x64@14.1.0: - resolution: {integrity: sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==} + /@next/swc-darwin-x64@14.1.1-canary.80: + resolution: {integrity: sha512-lEC0LUr7ZIpGnsdNccFmEQ1I/YXIZBBfwmCMpFtF3DjJXSX92HK31IWbBzL9cGHUQcaq3L+Wyva8TlsNR0e80w==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -775,8 +775,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-gnu@14.1.0: - resolution: {integrity: sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==} + /@next/swc-linux-arm64-gnu@14.1.1-canary.80: + resolution: {integrity: sha512-I+wuvjUUh9KFYXNwPwV4UWwBDXthLV6C5LCtcp0jPwDJrgdXTyMD3v+P6aJ/xOWbyv/nWOJVW9PBOVIeYL+TqA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -784,8 +784,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-musl@14.1.0: - resolution: {integrity: sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==} + /@next/swc-linux-arm64-musl@14.1.1-canary.80: + resolution: {integrity: sha512-6WJ+Agyiidq2o5rUJOyBINmbUBL1EdXOIDF6zuNgC1REoeXdhqFf3oB1KoJatXgjlhMyltOoepgutuZWJDZ+Zg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -793,8 +793,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-gnu@14.1.0: - resolution: {integrity: sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==} + /@next/swc-linux-x64-gnu@14.1.1-canary.80: + resolution: {integrity: sha512-szjzs8GvhdxyC0fbXT0JlDMDVkAgqYQs3iN3yzF4UMhfRM9VVyBSJYiHPhTrXUatlZ8h9SlMoP5YuN0ZBSbqoA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -802,8 +802,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-musl@14.1.0: - resolution: {integrity: sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==} + /@next/swc-linux-x64-musl@14.1.1-canary.80: + resolution: {integrity: sha512-Nek2e3aKt9tjN3YdAGFetjRVtxdIBK8A9iPnVCGrYuy2ERpAwsupOk8nmMzIlp1kGxxGsLWL1vBojQwjukKWkg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -811,8 +811,8 @@ packages: dev: false optional: true - /@next/swc-win32-arm64-msvc@14.1.0: - resolution: {integrity: sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==} + /@next/swc-win32-arm64-msvc@14.1.1-canary.80: + resolution: {integrity: sha512-aPnjiYjduvB5ut7juZ4g/mc9aY8MCsQESrSH5/dQVZJF5o6JzcNj+0D21J1/oeqiBOVsvn1LMjNOMU762t0gHQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -820,8 +820,8 @@ packages: dev: false optional: true - /@next/swc-win32-ia32-msvc@14.1.0: - resolution: {integrity: sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==} + /@next/swc-win32-ia32-msvc@14.1.1-canary.80: + resolution: {integrity: sha512-n0u0CofRBEx35Nn6mxS1o4o1+dseR9GhTuVqj6Zy2xIby66IjuuxKuLD49Ufg9qsj1oCAkJpsToJqmqisc4Eyg==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] @@ -829,8 +829,8 @@ packages: dev: false optional: true - /@next/swc-win32-x64-msvc@14.1.0: - resolution: {integrity: sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==} + /@next/swc-win32-x64-msvc@14.1.1-canary.80: + resolution: {integrity: sha512-eNTkUJVB2JyPxnf9iDEMEkMFPrxb1lA32Mk0H7kl7EndQ0hNmSnDbcBLji3J2jBFcTK8ok0FH5sZH3c/lxR8aw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1212,9 +1212,14 @@ packages: - supports-color dev: true - /@swc/helpers@0.5.2: - resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} + /@swc/counter@0.1.3: + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + dev: false + + /@swc/helpers@0.5.5: + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} dependencies: + '@swc/counter': 0.1.3 tslib: 2.6.2 dev: false @@ -1267,10 +1272,17 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true + /@types/node@20.11.21: + resolution: {integrity: sha512-/ySDLGscFPNasfqStUuWWPfL78jompfIoVzLJPVVAHBh6rpG68+pI2Gk+fNLeI8/f1yPYL4s46EleVIc20F1Ow==} + dependencies: + undici-types: 5.26.5 + dev: false + /@types/node@20.11.22: resolution: {integrity: sha512-/G+IxWxma6V3E+pqK1tSl2Fo1kl41pK1yeCyDsgkF9WlVAme4j5ISYM2zR11bgLFJGLN5sVK40T4RJNuiZbEjA==} dependencies: undici-types: 5.26.5 + dev: true /@types/prop-types@15.7.11: resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} @@ -3561,12 +3573,12 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true - /geist@1.2.2(next@14.1.0): + /geist@1.2.2(next@14.1.1-canary.80): resolution: {integrity: sha512-uRDrxhvdnPwWJmh+K5+/5LXSKwvJzaYCl9tDXgiBi4hj7hB4K7+n/WLcvJMFs5btvyn0r9OSwCd1s6CmqAsxEw==} peerDependencies: next: '>=13.2.0 <15' dependencies: - next: 14.1.0(react-dom@18.2.0)(react@18.2.0) + next: 14.1.1-canary.80(react-dom@18.2.0)(react@18.2.0) dev: false /get-caller-file@2.0.5: @@ -4684,20 +4696,20 @@ packages: type-fest: 2.19.0 dev: true - /next-themes@0.2.1(next@14.1.0)(react-dom@18.2.0)(react@18.2.0): + /next-themes@0.2.1(next@14.1.1-canary.80)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} peerDependencies: next: '*' react: '*' react-dom: '*' dependencies: - next: 14.1.0(react-dom@18.2.0)(react@18.2.0) + next: 14.1.1-canary.80(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /next@14.1.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==} + /next@14.1.1-canary.80(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-mjAz6ssshgYAtYxH4dzvZ57P0jTgbcMHsBLx6Rc7Nlh5vL2STILlDDndd4gAubbop7K4+ZTjvaI5l+sE8BHc7g==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -4711,8 +4723,8 @@ packages: sass: optional: true dependencies: - '@next/env': 14.1.0 - '@swc/helpers': 0.5.2 + '@next/env': 14.1.1-canary.80 + '@swc/helpers': 0.5.5 busboy: 1.6.0 caniuse-lite: 1.0.30001591 graceful-fs: 4.2.11 @@ -4721,15 +4733,15 @@ packages: react-dom: 18.2.0(react@18.2.0) styled-jsx: 5.1.1(react@18.2.0) optionalDependencies: - '@next/swc-darwin-arm64': 14.1.0 - '@next/swc-darwin-x64': 14.1.0 - '@next/swc-linux-arm64-gnu': 14.1.0 - '@next/swc-linux-arm64-musl': 14.1.0 - '@next/swc-linux-x64-gnu': 14.1.0 - '@next/swc-linux-x64-musl': 14.1.0 - '@next/swc-win32-arm64-msvc': 14.1.0 - '@next/swc-win32-ia32-msvc': 14.1.0 - '@next/swc-win32-x64-msvc': 14.1.0 + '@next/swc-darwin-arm64': 14.1.1-canary.80 + '@next/swc-darwin-x64': 14.1.1-canary.80 + '@next/swc-linux-arm64-gnu': 14.1.1-canary.80 + '@next/swc-linux-arm64-musl': 14.1.1-canary.80 + '@next/swc-linux-x64-gnu': 14.1.1-canary.80 + '@next/swc-linux-x64-musl': 14.1.1-canary.80 + '@next/swc-win32-arm64-msvc': 14.1.1-canary.80 + '@next/swc-win32-ia32-msvc': 14.1.1-canary.80 + '@next/swc-win32-x64-msvc': 14.1.1-canary.80 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros diff --git a/website/package.json b/website/package.json index e41d8edc..a68e262e 100644 --- a/website/package.json +++ b/website/package.json @@ -14,7 +14,7 @@ "@untitled-theme/colors": "workspace:*", "@untitled-theme/icons-react": "workspace:*", "geist": "1.2.2", - "next": "14.1.0", + "next": "14.1.1-canary.80", "next-themes": "0.2.1", "react": "^18", "react-dom": "^18", diff --git a/website/src/@modal/(.)icons/[slug]/default.tsx b/website/src/@modal/(.)icons/[slug]/default.tsx new file mode 100644 index 00000000..6ddf1b76 --- /dev/null +++ b/website/src/@modal/(.)icons/[slug]/default.tsx @@ -0,0 +1,3 @@ +export default function Default() { + return null; +} diff --git a/website/src/app/(index)/_.tsx b/website/src/@modal/(.)icons/[slug]/page.tsx similarity index 85% rename from website/src/app/(index)/_.tsx rename to website/src/@modal/(.)icons/[slug]/page.tsx index 690efdad..d5834629 100644 --- a/website/src/app/(index)/_.tsx +++ b/website/src/@modal/(.)icons/[slug]/page.tsx @@ -1,13 +1,13 @@ import {Dialog} from '@ark-ui/react'; import {XCloseIcon} from '@untitled-theme/icons-react'; -export function IconDetails() { +export default function IconDetailsModal() { return ( - + -
{/* TODO: content */}
+
It works
diff --git a/website/src/@modal/(.)icons/default.tsx b/website/src/@modal/(.)icons/default.tsx new file mode 100644 index 00000000..6ddf1b76 --- /dev/null +++ b/website/src/@modal/(.)icons/default.tsx @@ -0,0 +1,3 @@ +export default function Default() { + return null; +} diff --git a/website/src/app/@modal/(.)icons/[slug]/page.tsx b/website/src/app/@modal/(.)icons/[slug]/page.tsx new file mode 100644 index 00000000..d5834629 --- /dev/null +++ b/website/src/app/@modal/(.)icons/[slug]/page.tsx @@ -0,0 +1,19 @@ +import {Dialog} from '@ark-ui/react'; +import {XCloseIcon} from '@untitled-theme/icons-react'; + +export default function IconDetailsModal() { + return ( + + + + +
It works
+ + + + +
+
+
+ ); +} diff --git a/website/src/app/@modal/(.)icons/default.tsx b/website/src/app/@modal/(.)icons/default.tsx new file mode 100644 index 00000000..6ddf1b76 --- /dev/null +++ b/website/src/app/@modal/(.)icons/default.tsx @@ -0,0 +1,3 @@ +export default function Default() { + return null; +} diff --git a/website/src/app/@modal/default.tsx b/website/src/app/@modal/default.tsx new file mode 100644 index 00000000..6ddf1b76 --- /dev/null +++ b/website/src/app/@modal/default.tsx @@ -0,0 +1,3 @@ +export default function Default() { + return null; +} diff --git a/website/src/app/[slug]/loading.tsx b/website/src/app/icons/[slug]/__loading.tsx similarity index 100% rename from website/src/app/[slug]/loading.tsx rename to website/src/app/icons/[slug]/__loading.tsx diff --git a/website/src/app/[slug]/page.tsx b/website/src/app/icons/[slug]/page.tsx similarity index 100% rename from website/src/app/[slug]/page.tsx rename to website/src/app/icons/[slug]/page.tsx diff --git a/website/src/app/[slug]/utils.tsx b/website/src/app/icons/[slug]/utils.tsx similarity index 100% rename from website/src/app/[slug]/utils.tsx rename to website/src/app/icons/[slug]/utils.tsx diff --git a/website/src/app/layout.tsx b/website/src/app/layout.tsx index 48c1785d..0c8cc356 100644 --- a/website/src/app/layout.tsx +++ b/website/src/app/layout.tsx @@ -1,6 +1,7 @@ import {GeistMono} from 'geist/font/mono'; import {GeistSans} from 'geist/font/sans'; import type {Metadata} from 'next'; +import type {ReactNode} from 'react'; import {twMerge} from 'tailwind-merge'; import './globals.css'; import {Navbar} from './navbar'; @@ -21,7 +22,7 @@ export const metadata: Metadata = { }, }; -export default function RootLayout({children}: Readonly<{children: React.ReactNode}>) { +export default function RootLayout(props: Readonly<{modal: ReactNode; children: ReactNode}>) { return ( -
{children}
+
{props.children}
+ + {props.modal}
diff --git a/website/src/app/(index)/page.tsx b/website/src/app/page.tsx similarity index 97% rename from website/src/app/(index)/page.tsx rename to website/src/app/page.tsx index 8b6c992c..43047639 100644 --- a/website/src/app/(index)/page.tsx +++ b/website/src/app/page.tsx @@ -50,7 +50,7 @@ export default async function IconsPage({searchParams}: Props) { aria-label={icon.name} asChild > - + diff --git a/website/src/app/(index)/searchbar.tsx b/website/src/app/searchbar.tsx similarity index 100% rename from website/src/app/(index)/searchbar.tsx rename to website/src/app/searchbar.tsx diff --git a/website/src/app/(index)/utils.ts b/website/src/app/utils.ts similarity index 84% rename from website/src/app/(index)/utils.ts rename to website/src/app/utils.ts index 0fd1ea97..5da515a6 100644 --- a/website/src/app/(index)/utils.ts +++ b/website/src/app/utils.ts @@ -42,13 +42,13 @@ export const getIcons = cache( }; }); - return await Promise.all(promises).then((array) => { - if (!search) return array; + const icons = await Promise.all(promises); - return array.filter((item) => - item.name.toLowerCase().replace(/ /g, '').includes(search.toLowerCase().replace(/ /g, '')), - ); - }); + if (!search) return icons; + + return icons.filter((icon) => + icon.name.toLowerCase().replace(/ /g, '').includes(search.toLowerCase().replace(/ /g, '')), + ); }, ['icons'], ); From 4ebe8756c52801a392acc5426e41714e546baede Mon Sep 17 00:00:00 2001 From: jp calvo Date: Thu, 29 Feb 2024 19:34:23 +0800 Subject: [PATCH 3/5] refactor --- .../src/app/@modal/(.)icons/[slug]/page.tsx | 11 +- website/src/app/icons/[slug]/__loading.tsx | 29 --- website/src/app/icons/[slug]/page.tsx | 2 +- website/src/app/icons/[slug]/utils.tsx | 236 ----------------- website/src/{types/index.ts => app/types.ts} | 0 website/src/app/utils.ts | 240 +++++++++++++++++- website/src/lib/dash-to-pascal.ts | 6 - 7 files changed, 249 insertions(+), 275 deletions(-) delete mode 100644 website/src/app/icons/[slug]/__loading.tsx delete mode 100644 website/src/app/icons/[slug]/utils.tsx rename website/src/{types/index.ts => app/types.ts} (100%) delete mode 100644 website/src/lib/dash-to-pascal.ts diff --git a/website/src/app/@modal/(.)icons/[slug]/page.tsx b/website/src/app/@modal/(.)icons/[slug]/page.tsx index d5834629..9e43899d 100644 --- a/website/src/app/@modal/(.)icons/[slug]/page.tsx +++ b/website/src/app/@modal/(.)icons/[slug]/page.tsx @@ -1,7 +1,16 @@ +import {getIcon} from '@/app/utils'; import {Dialog} from '@ark-ui/react'; import {XCloseIcon} from '@untitled-theme/icons-react'; -export default function IconDetailsModal() { +interface Props { + params: { + slug: string; + }; +} + +export default async function IconDetailsModal({params}: Props) { + const icon = await getIcon(params.slug); + return ( diff --git a/website/src/app/icons/[slug]/__loading.tsx b/website/src/app/icons/[slug]/__loading.tsx deleted file mode 100644 index 3b3a82ac..00000000 --- a/website/src/app/icons/[slug]/__loading.tsx +++ /dev/null @@ -1,29 +0,0 @@ -export default function Loading() { - return ( - - - - - - - ); -} diff --git a/website/src/app/icons/[slug]/page.tsx b/website/src/app/icons/[slug]/page.tsx index 5aab88a6..10dbca90 100644 --- a/website/src/app/icons/[slug]/page.tsx +++ b/website/src/app/icons/[slug]/page.tsx @@ -1,7 +1,7 @@ +import {getIcon} from '@/app/utils'; import {Clipboard, Tabs} from '@ark-ui/react'; import {CheckIcon, Copy01Icon} from '@untitled-theme/icons-react'; import {notFound} from 'next/navigation'; -import {getIcon} from './utils'; interface Props { params: { diff --git a/website/src/app/icons/[slug]/utils.tsx b/website/src/app/icons/[slug]/utils.tsx deleted file mode 100644 index 267146a1..00000000 --- a/website/src/app/icons/[slug]/utils.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import {dashToPascal} from '@/lib/dash-to-pascal'; -import type {Html, Icon} from '@/types'; -import fs from 'fs/promises'; -import {unstable_cache as cache} from 'next/cache'; -import path from 'path'; -import prettier from 'prettier'; -import {codeToHtml} from 'shiki'; -import * as svgson from 'svgson'; - -const REF = 'REF'; -const REST = 'REST'; - -async function toReactSnippet(svg: Html, name: string) { - const node = await svgson.parse(svg, { - camelcase: true, - transformNode(node) { - if (node.name === 'svg') { - return { - ...node, - attributes: { - [REF]: '', - ...node.attributes, - width: '24', - height: '24', - viewBox: '0 0 24 24', - [REST]: '', - }, - }; - } - - return node; - }, - }); - - const reactSvg = svgson.stringify(node, { - selfClose: true, - transformAttr(key, value, esc) { - if (key === REF) { - return 'ref={ref}'; - } else if (key === REST) { - return '{...props}'; - } else if (key === 'stroke') { - return `${key}="currentColor"`; - } else if (key === 'strokeWidth') { - return `${key}="2"`; - } else { - return `${key}="${esc(value)}"`; - } - }, - }); - - const component = ` - import * as React from 'react'; - - export const ${name} = React.forwardRef>((props, ref) => { - return ${reactSvg}; - }); - - ${name}.displayName = '${name}' - - export default ${name}; - `; - - return await codeToHtml( - await prettier.format(component, { - parser: 'typescript', - printWidth: 80, - }), - { - lang: 'typescript', - themes: { - dark: 'vitesse-dark', - light: 'vitesse-light', - }, - }, - ); -} - -async function toSvelteSnippet(svg: Html) { - const node = await svgson.parse(svg, { - camelcase: true, - transformNode(node) { - if (node.name === 'svg') { - return { - ...node, - attributes: { - [REF]: '', - ...node.attributes, - width: '24', - height: '24', - viewBox: '0 0 24 24', - [REST]: '', - }, - }; - } - - return node; - }, - }); - - const svelteSvg = svgson.stringify(node, { - selfClose: true, - transformAttr(key, value, esc) { - if (key === REF) { - return 'ref={ref}'; - } else if (key === REST) { - return '{...props}'; - } else if (key === 'stroke') { - return `${key}="currentColor"`; - } else if (key === 'strokeWidth') { - return `${key}="2"`; - } else { - return `${key}="${esc(value)}"`; - } - }, - }); - - const component = ` - - - ${svelteSvg} - `; - - return await codeToHtml( - await prettier.format(component, { - parser: 'html', - printWidth: 80, - }), - { - lang: 'svelte', - themes: { - dark: 'vitesse-dark', - light: 'vitesse-light', - }, - }, - ); -} - -async function toHtmlSnippet(svg: Html) { - const node = await svgson.parse(svg, { - camelcase: true, - transformNode(node) { - if (node.name === 'svg') { - return { - ...node, - attributes: { - ...node.attributes, - width: '24', - height: '24', - viewBox: '0 0 24 24', - }, - }; - } - - return node; - }, - }); - - const htmlSvg = svgson.stringify(node, { - selfClose: false, - transformAttr(key, value, esc) { - if (key === 'stroke') { - return `${key}="currentColor"`; - } else if (key === 'strokeWidth') { - return `${key}="2"`; - } else { - return `${key}="${esc(value)}"`; - } - }, - }); - - return await codeToHtml( - await prettier.format(htmlSvg, { - parser: 'html', - printWidth: 80, - }), - { - lang: 'html', - themes: { - dark: 'vitesse-dark', - light: 'vitesse-light', - }, - }, - ); -} - -export const getIcon = cache( - async (slug: string): Promise | null> => { - const fullPath = path.resolve(process.cwd(), '../assets/icons', `${slug}.svg`); - - try { - await fs.stat(fullPath); - } catch { - return null; - } - - const content = await fs.readFile(fullPath, 'utf-8'); - const parsed = await svgson.parse(content, { - transformNode(node) { - if (node.name === 'svg') { - node.attributes['width'] = '32'; - node.attributes['height'] = '32'; - } - - return node; - }, - }); - - const html = svgson.stringify(parsed, { - selfClose: true, - transformAttr(key, value, escape) { - if (key === 'stroke') return `${key}="currentColor"`; - if (key === 'stroke-width') return `${key}="1.5"`; - return `${key}="${escape(value)}"`; - }, - }); - - const name = dashToPascal(slug) + 'Icon'; - - return { - slug, - name, - html, - snippet: { - html: await toHtmlSnippet(html), - react: await toReactSnippet(html, name), - svelte: await toSvelteSnippet(html), - }, - }; - }, - ['icon'], -); diff --git a/website/src/types/index.ts b/website/src/app/types.ts similarity index 100% rename from website/src/types/index.ts rename to website/src/app/types.ts diff --git a/website/src/app/utils.ts b/website/src/app/utils.ts index 5da515a6..718df50c 100644 --- a/website/src/app/utils.ts +++ b/website/src/app/utils.ts @@ -1,10 +1,18 @@ -import {dashToPascal} from '@/lib/dash-to-pascal'; -import type {Icon} from '@/types'; +import type {Html, Icon} from '@/app/types'; import fs from 'fs/promises'; import {unstable_cache as cache} from 'next/cache'; import path from 'path'; +import prettier from 'prettier'; +import {codeToHtml} from 'shiki'; import * as svgson from 'svgson'; +function dashToPascal(value: string) { + return value + .split('-') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(''); +} + export const getIcons = cache( async ({search}: {search?: string}) => { const root = path.resolve(process.cwd(), '../assets/icons'); @@ -52,3 +60,231 @@ export const getIcons = cache( }, ['icons'], ); + +const REF = 'REF'; +const REST = 'REST'; + +async function toReactSnippet(svg: Html, name: string) { + const node = await svgson.parse(svg, { + camelcase: true, + transformNode(node) { + if (node.name === 'svg') { + return { + ...node, + attributes: { + [REF]: '', + ...node.attributes, + width: '24', + height: '24', + viewBox: '0 0 24 24', + [REST]: '', + }, + }; + } + + return node; + }, + }); + + const reactSvg = svgson.stringify(node, { + selfClose: true, + transformAttr(key, value, esc) { + if (key === REF) { + return 'ref={ref}'; + } else if (key === REST) { + return '{...props}'; + } else if (key === 'stroke') { + return `${key}="currentColor"`; + } else if (key === 'strokeWidth') { + return `${key}="2"`; + } else { + return `${key}="${esc(value)}"`; + } + }, + }); + + const component = ` + import * as React from 'react'; + + export const ${name} = React.forwardRef>((props, ref) => { + return ${reactSvg}; + }); + + ${name}.displayName = '${name}' + + export default ${name}; + `; + + return await codeToHtml( + await prettier.format(component, { + parser: 'typescript', + printWidth: 80, + }), + { + lang: 'typescript', + themes: { + dark: 'vitesse-dark', + light: 'vitesse-light', + }, + }, + ); +} + +async function toSvelteSnippet(svg: Html) { + const node = await svgson.parse(svg, { + camelcase: true, + transformNode(node) { + if (node.name === 'svg') { + return { + ...node, + attributes: { + [REF]: '', + ...node.attributes, + width: '24', + height: '24', + viewBox: '0 0 24 24', + [REST]: '', + }, + }; + } + + return node; + }, + }); + + const svelteSvg = svgson.stringify(node, { + selfClose: true, + transformAttr(key, value, esc) { + if (key === REF) { + return 'ref={ref}'; + } else if (key === REST) { + return '{...props}'; + } else if (key === 'stroke') { + return `${key}="currentColor"`; + } else if (key === 'strokeWidth') { + return `${key}="2"`; + } else { + return `${key}="${esc(value)}"`; + } + }, + }); + + const component = ` + + + ${svelteSvg} + `; + + return await codeToHtml( + await prettier.format(component, { + parser: 'html', + printWidth: 80, + }), + { + lang: 'svelte', + themes: { + dark: 'vitesse-dark', + light: 'vitesse-light', + }, + }, + ); +} + +async function toHtmlSnippet(svg: Html) { + const node = await svgson.parse(svg, { + camelcase: true, + transformNode(node) { + if (node.name === 'svg') { + return { + ...node, + attributes: { + ...node.attributes, + width: '24', + height: '24', + viewBox: '0 0 24 24', + }, + }; + } + + return node; + }, + }); + + const htmlSvg = svgson.stringify(node, { + selfClose: false, + transformAttr(key, value, esc) { + if (key === 'stroke') { + return `${key}="currentColor"`; + } else if (key === 'strokeWidth') { + return `${key}="2"`; + } else { + return `${key}="${esc(value)}"`; + } + }, + }); + + return await codeToHtml( + await prettier.format(htmlSvg, { + parser: 'html', + printWidth: 80, + }), + { + lang: 'html', + themes: { + dark: 'vitesse-dark', + light: 'vitesse-light', + }, + }, + ); +} + +export const getIcon = cache( + async (slug: string): Promise | null> => { + const fullPath = path.resolve(process.cwd(), '../assets/icons', `${slug}.svg`); + + try { + await fs.stat(fullPath); + } catch { + return null; + } + + const content = await fs.readFile(fullPath, 'utf-8'); + const parsed = await svgson.parse(content, { + transformNode(node) { + if (node.name === 'svg') { + node.attributes['width'] = '32'; + node.attributes['height'] = '32'; + } + + return node; + }, + }); + + const html = svgson.stringify(parsed, { + selfClose: true, + transformAttr(key, value, escape) { + if (key === 'stroke') return `${key}="currentColor"`; + if (key === 'stroke-width') return `${key}="1.5"`; + return `${key}="${escape(value)}"`; + }, + }); + + const name = dashToPascal(slug) + 'Icon'; + + return { + slug, + name, + html, + snippet: { + html: await toHtmlSnippet(html), + react: await toReactSnippet(html, name), + svelte: await toSvelteSnippet(html), + }, + }; + }, + ['icon'], +); diff --git a/website/src/lib/dash-to-pascal.ts b/website/src/lib/dash-to-pascal.ts deleted file mode 100644 index 9f3a0ec7..00000000 --- a/website/src/lib/dash-to-pascal.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function dashToPascal(value: string) { - return value - .split('-') - .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) - .join(''); -} From 91e3a689bf0892c0bfd4f71aad709662be6d162c Mon Sep 17 00:00:00 2001 From: jp calvo Date: Thu, 29 Feb 2024 19:47:19 +0800 Subject: [PATCH 4/5] intercept routes enhancement --- .../src/app/@modal/(.)icons/[slug]/modal.tsx | 32 ++++++ .../src/app/@modal/(.)icons/[slug]/page.tsx | 22 ++--- website/src/app/icon-details.tsx | 97 +++++++++++++++++++ website/src/app/icons/[slug]/page.tsx | 93 +----------------- 4 files changed, 139 insertions(+), 105 deletions(-) create mode 100644 website/src/app/@modal/(.)icons/[slug]/modal.tsx create mode 100644 website/src/app/icon-details.tsx diff --git a/website/src/app/@modal/(.)icons/[slug]/modal.tsx b/website/src/app/@modal/(.)icons/[slug]/modal.tsx new file mode 100644 index 00000000..e1643172 --- /dev/null +++ b/website/src/app/@modal/(.)icons/[slug]/modal.tsx @@ -0,0 +1,32 @@ +'use client'; + +import {Dialog} from '@ark-ui/react'; +import {XCloseIcon} from '@untitled-theme/icons-react'; +import {useRouter} from 'next/navigation'; +import {PropsWithChildren} from 'react'; + +export function Modal({children}: PropsWithChildren) { + const router = useRouter(); + + return ( + { + if (!open) router.back(); + }} + > + + + + {children} + + + + + + + + ); +} diff --git a/website/src/app/@modal/(.)icons/[slug]/page.tsx b/website/src/app/@modal/(.)icons/[slug]/page.tsx index 9e43899d..fe91b881 100644 --- a/website/src/app/@modal/(.)icons/[slug]/page.tsx +++ b/website/src/app/@modal/(.)icons/[slug]/page.tsx @@ -1,6 +1,7 @@ +import {IconDetails} from '@/app/icon-details'; import {getIcon} from '@/app/utils'; -import {Dialog} from '@ark-ui/react'; -import {XCloseIcon} from '@untitled-theme/icons-react'; +import {notFound} from 'next/navigation'; +import {Modal} from './modal'; interface Props { params: { @@ -11,18 +12,11 @@ interface Props { export default async function IconDetailsModal({params}: Props) { const icon = await getIcon(params.slug); - return ( - - - - -
It works
+ if (!icon) return notFound(); - - - -
-
-
+ return ( + + + ); } diff --git a/website/src/app/icon-details.tsx b/website/src/app/icon-details.tsx new file mode 100644 index 00000000..a978ce85 --- /dev/null +++ b/website/src/app/icon-details.tsx @@ -0,0 +1,97 @@ +import {Clipboard, Tabs} from '@ark-ui/react'; +import {CheckIcon, Copy01Icon} from '@untitled-theme/icons-react'; +import type {Icon} from './types'; + +interface Props { + data: Icon; +} + +export async function IconDetails({data}: Props) { + const items = [ + { + label: 'Html', + value: 'Html', + content: data.snippet.html, + }, + { + label: 'React', + value: 'React', + content: data.snippet.react, + }, + { + label: 'Svelte', + value: 'Svelte', + content: data.snippet.svelte, + }, + ]; + + return ( +
+
+ +
+ <{data.name} /> + + + Copy + + } + > + + + + +
+ + + + {items.map((item) => ( + + {item.label} + + ))} + + + {items.map((item) => { + return ( + +
+ + Copy + + + } + > + + + + + +
+
+ + ); + })} + +
+ ); +} diff --git a/website/src/app/icons/[slug]/page.tsx b/website/src/app/icons/[slug]/page.tsx index 10dbca90..1c3c4838 100644 --- a/website/src/app/icons/[slug]/page.tsx +++ b/website/src/app/icons/[slug]/page.tsx @@ -1,6 +1,5 @@ +import {IconDetails} from '@/app/icon-details'; import {getIcon} from '@/app/utils'; -import {Clipboard, Tabs} from '@ark-ui/react'; -import {CheckIcon, Copy01Icon} from '@untitled-theme/icons-react'; import {notFound} from 'next/navigation'; interface Props { @@ -12,93 +11,5 @@ interface Props { export default async function IconDetailsPage({params}: Props) { const icon = await getIcon(params.slug); - if (!icon) return notFound(); - - const items = [ - { - label: 'Html', - value: 'Html', - content: icon.snippet.html, - }, - { - label: 'React', - value: 'React', - content: icon.snippet.react, - }, - { - label: 'Svelte', - value: 'Svelte', - content: icon.snippet.svelte, - }, - ]; - - return ( -
-
- -
- <{icon.name} /> - - - Copy - - } - > - - - - -
- - - - {items.map((item) => ( - - {item.label} - - ))} - - - {items.map((item) => { - return ( - -
- - Copy - - - } - > - - - - - -
-
- - ); - })} - -
- ); + return !icon ? notFound() : ; } From ffa41b041ef868f6d2e32c4aa8463bb3899e9f5d Mon Sep 17 00:00:00 2001 From: jp calvo Date: Thu, 29 Feb 2024 20:43:24 +0800 Subject: [PATCH 5/5] fix shiki height on modal --- website/src/app/@modal/(.)icons/[slug]/modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/app/@modal/(.)icons/[slug]/modal.tsx b/website/src/app/@modal/(.)icons/[slug]/modal.tsx index e1643172..c28f8332 100644 --- a/website/src/app/@modal/(.)icons/[slug]/modal.tsx +++ b/website/src/app/@modal/(.)icons/[slug]/modal.tsx @@ -20,7 +20,7 @@ export function Modal({children}: PropsWithChildren) { - {children} +
{children}