refactor: remove BlockCategory pages, and BlockPreview components
This commit is contained in:
parent
fa4b9a19a1
commit
4bad9714fa
@ -1,16 +0,0 @@
|
|||||||
import { categories } from '@/components/tailark/blocks';
|
|
||||||
import BlocksNav from '@/components/tailark/blocks-nav';
|
|
||||||
import type { PropsWithChildren } from 'react';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The locale inconsistency issue has been fixed in the BlocksNav component
|
|
||||||
*/
|
|
||||||
export default function BlockCategoryLayout({ children }: PropsWithChildren) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<BlocksNav categories={categories} />
|
|
||||||
|
|
||||||
<main>{children}</main>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
import BlockPreview from '@/components/tailark/block-preview';
|
|
||||||
import { blocks, categories } from '@/components/tailark/blocks';
|
|
||||||
import { constructMetadata } from '@/lib/metadata';
|
|
||||||
import { getUrlWithLocale } from '@/lib/urls/urls';
|
|
||||||
import type { Metadata } from 'next';
|
|
||||||
import type { Locale } from 'next-intl';
|
|
||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
import { notFound } from 'next/navigation';
|
|
||||||
|
|
||||||
export const dynamic = 'force-static';
|
|
||||||
export const revalidate = 3600;
|
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
|
||||||
return categories.map((category) => ({
|
|
||||||
category: category,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateMetadata({
|
|
||||||
params,
|
|
||||||
}: {
|
|
||||||
params: Promise<{ locale: Locale; category: string }>;
|
|
||||||
}): Promise<Metadata | undefined> {
|
|
||||||
const { locale, category } = await params;
|
|
||||||
const t = await getTranslations({ locale, namespace: 'Metadata' });
|
|
||||||
return constructMetadata({
|
|
||||||
title: category + ' | ' + t('title'),
|
|
||||||
description: t('description'),
|
|
||||||
canonicalUrl: getUrlWithLocale(`/blocks/${category}`, locale),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BlockCategoryPageProps {
|
|
||||||
params: Promise<{ category: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function BlockCategoryPage({
|
|
||||||
params,
|
|
||||||
}: BlockCategoryPageProps) {
|
|
||||||
const { category } = await params;
|
|
||||||
const categoryBlocks = blocks.filter((b) => b.category === category);
|
|
||||||
|
|
||||||
if (categoryBlocks.length === 0) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{categoryBlocks.map((block, index) => (
|
|
||||||
<BlockPreview {...block} key={index} />
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,368 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { useCopyToClipboard } from '@/hooks/use-clipboard';
|
|
||||||
import { isUrlCached } from '@/lib/serviceWorker';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import * as RadioGroup from '@radix-ui/react-radio-group';
|
|
||||||
import { Check, Code2, Copy, Eye, Maximize, Terminal } from 'lucide-react';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import type React from 'react';
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import {
|
|
||||||
Panel,
|
|
||||||
PanelGroup,
|
|
||||||
PanelResizeHandle,
|
|
||||||
type ImperativePanelGroupHandle,
|
|
||||||
} from 'react-resizable-panels';
|
|
||||||
import { useMedia } from 'use-media';
|
|
||||||
|
|
||||||
export interface BlockPreviewProps {
|
|
||||||
code?: string;
|
|
||||||
preview: string;
|
|
||||||
title: string;
|
|
||||||
category: string;
|
|
||||||
previewOnly?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const radioItem =
|
|
||||||
'rounded-(--radius) duration-200 flex items-center justify-center h-8 px-2.5 gap-2 transition-[color] data-[state=checked]:bg-muted';
|
|
||||||
|
|
||||||
const DEFAULTSIZE = 100;
|
|
||||||
const SMSIZE = 30;
|
|
||||||
const MDSIZE = 62;
|
|
||||||
const LGSIZE = 82;
|
|
||||||
|
|
||||||
const getCacheKey = (src: string) => `iframe-cache-${src}`;
|
|
||||||
|
|
||||||
const titleToNumber = (title: string): number => {
|
|
||||||
const titles = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty"];
|
|
||||||
return titles.indexOf(title.toLowerCase()) + 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BlockPreview: React.FC<BlockPreviewProps> = ({
|
|
||||||
code,
|
|
||||||
preview,
|
|
||||||
title,
|
|
||||||
category,
|
|
||||||
previewOnly,
|
|
||||||
}) => {
|
|
||||||
const [width, setWidth] = useState(DEFAULTSIZE);
|
|
||||||
const [mode, setMode] = useState<'preview' | 'code'>('preview');
|
|
||||||
const [iframeHeight, setIframeHeight] = useState(0);
|
|
||||||
const [shouldLoadIframe, setShouldLoadIframe] = useState(false);
|
|
||||||
const [cachedHeight, setCachedHeight] = useState<number | null>(null);
|
|
||||||
const [isIframeCached, setIsIframeCached] = useState(false);
|
|
||||||
|
|
||||||
const terminalCode = `pnpm dlx shadcn@canary add https://nsui.irung.me/r/${category}-${titleToNumber(title)}.json`;
|
|
||||||
const { copied, copy } = useCopyToClipboard({ code: code as string, title, category, eventName: 'block_copy' })
|
|
||||||
const { copied: cliCopied, copy: cliCopy } = useCopyToClipboard({ code: terminalCode, title, category, eventName: 'block_cli_copy' })
|
|
||||||
|
|
||||||
const ref = useRef<ImperativePanelGroupHandle>(null);
|
|
||||||
const isLarge = useMedia('(min-width: 1024px)');
|
|
||||||
|
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
||||||
const observer = useRef<IntersectionObserver | null>(null);
|
|
||||||
const blockRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
observer.current = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
if (entries[0].isIntersecting) {
|
|
||||||
setShouldLoadIframe(true);
|
|
||||||
observer.current?.disconnect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.1 }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (blockRef.current) {
|
|
||||||
observer.current.observe(blockRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
observer.current?.disconnect();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkCache = async () => {
|
|
||||||
try {
|
|
||||||
const isCached = await isUrlCached(preview);
|
|
||||||
setIsIframeCached(isCached);
|
|
||||||
if (isCached) {
|
|
||||||
setShouldLoadIframe(true);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking cache status:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkCache();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cacheKey = getCacheKey(preview);
|
|
||||||
const cached = localStorage.getItem(cacheKey);
|
|
||||||
if (cached) {
|
|
||||||
const { height, timestamp } = JSON.parse(cached);
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - timestamp < 24 * 60 * 60 * 1000) {
|
|
||||||
setCachedHeight(height);
|
|
||||||
setIframeHeight(height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error retrieving cache:', error);
|
|
||||||
}
|
|
||||||
}, [preview]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const iframe = iframeRef.current;
|
|
||||||
if (!iframe || !shouldLoadIframe) return;
|
|
||||||
|
|
||||||
const handleLoad = () => {
|
|
||||||
try {
|
|
||||||
const contentHeight = iframe.contentWindow!.document.body.scrollHeight;
|
|
||||||
setIframeHeight(contentHeight);
|
|
||||||
|
|
||||||
const cacheKey = getCacheKey(preview);
|
|
||||||
const cacheValue = JSON.stringify({
|
|
||||||
height: contentHeight,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
localStorage.setItem(cacheKey, cacheValue);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error accessing iframe content:', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
iframe.addEventListener('load', handleLoad);
|
|
||||||
return () => {
|
|
||||||
iframe.removeEventListener('load', handleLoad);
|
|
||||||
};
|
|
||||||
}, [shouldLoadIframe, preview]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!blockRef.current || shouldLoadIframe) return;
|
|
||||||
|
|
||||||
const linkElement = document.createElement('link');
|
|
||||||
linkElement.rel = 'preload';
|
|
||||||
linkElement.href = preview;
|
|
||||||
linkElement.as = 'document';
|
|
||||||
|
|
||||||
if (
|
|
||||||
!document.head.querySelector(`link[rel="preload"][href="${preview}"]`)
|
|
||||||
) {
|
|
||||||
document.head.appendChild(linkElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const existingLink = document.head.querySelector(
|
|
||||||
`link[rel="preload"][href="${preview}"]`
|
|
||||||
);
|
|
||||||
if (existingLink) {
|
|
||||||
document.head.removeChild(existingLink);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [preview, shouldLoadIframe]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="group mb-16 border-b [--color-border:color-mix(in_oklab,var(--color-zinc-200)_75%,transparent)] dark:[--color-border:color-mix(in_oklab,var(--color-zinc-800)_60%,transparent)]">
|
|
||||||
<div className="relative border-y">
|
|
||||||
<div
|
|
||||||
aria-hidden
|
|
||||||
className="absolute inset-x-4 -top-14 bottom-0 mx-auto max-w-7xl lg:inset-x-0"
|
|
||||||
>
|
|
||||||
<div className="to-(--color-border) absolute bottom-0 left-0 top-0 w-px bg-gradient-to-b from-transparent to-75%"></div>
|
|
||||||
<div className="to-(--color-border) absolute bottom-0 right-0 top-0 w-px bg-gradient-to-b from-transparent to-75%"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative z-10 mx-auto flex max-w-7xl justify-between py-1.5 pl-8 pr-6 [--color-border:var(--color-zinc-200)] md:py-2 lg:pl-6 lg:pr-2 dark:[--color-border:var(--color-zinc-800)]">
|
|
||||||
<div className="-ml-3 flex items-center gap-3">
|
|
||||||
{code && (
|
|
||||||
<>
|
|
||||||
<RadioGroup.Root className="flex gap-0.5">
|
|
||||||
<RadioGroup.Item
|
|
||||||
onClick={() => setMode('preview')}
|
|
||||||
aria-label="Block preview"
|
|
||||||
value="100"
|
|
||||||
checked={mode == 'preview'}
|
|
||||||
className={radioItem}
|
|
||||||
>
|
|
||||||
<Eye className="size-3.5 sm:opacity-50" />
|
|
||||||
<span className="hidden text-[13px] sm:block">Preview</span>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
|
|
||||||
<RadioGroup.Item
|
|
||||||
onClick={() => setMode('code')}
|
|
||||||
aria-label="Code"
|
|
||||||
value="0"
|
|
||||||
checked={mode == 'code'}
|
|
||||||
className={radioItem}
|
|
||||||
>
|
|
||||||
<Code2 className="size-3.5 sm:opacity-50" />
|
|
||||||
<span className="hidden text-[13px] sm:block">Code</span>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
</RadioGroup.Root>
|
|
||||||
|
|
||||||
<Separator
|
|
||||||
orientation="vertical"
|
|
||||||
className="hidden !h-4 lg:block"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{previewOnly && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
<span className="ml-2 text-sm capitalize">{title}</span>
|
|
||||||
<Separator orientation="vertical" className="!h-4" />{' '}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{/* <Button asChild variant="ghost" size="sm" className="size-8">
|
|
||||||
<Link href={preview} passHref target="_blank">
|
|
||||||
<Maximize className="size-4" />
|
|
||||||
</Link>
|
|
||||||
</Button> */}
|
|
||||||
<Separator
|
|
||||||
orientation="vertical"
|
|
||||||
className="hidden !h-4 lg:block"
|
|
||||||
/>
|
|
||||||
<span className="text-muted-foreground hidden text-sm lg:block">
|
|
||||||
{width < MDSIZE
|
|
||||||
? 'Mobile'
|
|
||||||
: width < LGSIZE
|
|
||||||
? 'Tablet'
|
|
||||||
: 'Desktop'}
|
|
||||||
</span>{' '}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{code && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
onClick={cliCopy}
|
|
||||||
size="sm"
|
|
||||||
className="size-8 shadow-none md:w-fit"
|
|
||||||
variant="outline"
|
|
||||||
aria-label="copy code">
|
|
||||||
{cliCopied ? <Check className="size-4" /> : <Terminal className="!size-3.5" />}
|
|
||||||
<span className="hidden font-mono text-xs md:block">
|
|
||||||
pnpm dlx shadcn@canary add {category}-{titleToNumber(title)}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
<Separator className="!h-4" orientation="vertical" />
|
|
||||||
{/* <OpenInV0Button
|
|
||||||
{...{ title, category }}
|
|
||||||
block={`${category}-${titleToNumber(title)}`}
|
|
||||||
/> */}
|
|
||||||
<Separator className="!h-4" orientation="vertical" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={copy}
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
aria-label="copy code"
|
|
||||||
className="size-8">
|
|
||||||
{copied ? <Check className="size-4" /> : <Copy className="!size-3.5" />}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!code && (
|
|
||||||
<span className="hidden font-mono text-sm md:block">
|
|
||||||
{/* pnpm dlx shadcn@canary add */}{category}-{titleToNumber(title)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
aria-hidden
|
|
||||||
className="absolute inset-x-4 -bottom-14 mx-auto h-14 max-w-7xl lg:inset-x-0"
|
|
||||||
>
|
|
||||||
<div className="from-(--color-border) absolute bottom-0 left-0 top-0 w-px bg-gradient-to-b"></div>
|
|
||||||
<div className="from-(--color-border) absolute bottom-0 right-0 top-0 w-px bg-gradient-to-b"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative z-10 mx-auto max-w-7xl px-4 lg:border-r lg:px-0">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'bg-white dark:bg-transparent',
|
|
||||||
mode == 'code' && 'hidden'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<PanelGroup direction="horizontal" tagName="div" ref={ref}>
|
|
||||||
<Panel
|
|
||||||
id={`block-${title}`}
|
|
||||||
order={1}
|
|
||||||
onResize={(size) => {
|
|
||||||
setWidth(Number(size));
|
|
||||||
}}
|
|
||||||
defaultSize={DEFAULTSIZE}
|
|
||||||
minSize={SMSIZE}
|
|
||||||
className="h-fit border-x"
|
|
||||||
>
|
|
||||||
<div ref={blockRef}>
|
|
||||||
{shouldLoadIframe ? (
|
|
||||||
<iframe
|
|
||||||
key={`${category}-${title}-iframe`}
|
|
||||||
loading={isIframeCached ? 'eager' : 'lazy'}
|
|
||||||
allowFullScreen
|
|
||||||
ref={iframeRef}
|
|
||||||
title={title}
|
|
||||||
height={cachedHeight || iframeHeight}
|
|
||||||
className={cn(
|
|
||||||
'h-(--iframe-height) block min-h-56 w-full duration-200 will-change-auto',
|
|
||||||
!cachedHeight &&
|
|
||||||
'@starting:opacity-0 @starting:blur-xl',
|
|
||||||
isIframeCached && '!opacity-100 !blur-none'
|
|
||||||
)}
|
|
||||||
src={preview}
|
|
||||||
id={`block-${title}`}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
'--iframe-height': `${cachedHeight || iframeHeight}px`,
|
|
||||||
display: 'block',
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex min-h-56 items-center justify-center">
|
|
||||||
<div className="border-primary size-6 animate-spin rounded-full border-2 border-t-transparent" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
{isLarge && (
|
|
||||||
<>
|
|
||||||
<PanelResizeHandle className="relative w-2 before:absolute before:inset-0 before:m-auto before:h-12 before:w-1 before:rounded-full before:bg-zinc-300 before:transition-[height,background] hover:before:h-16 hover:before:bg-zinc-400 focus:before:bg-zinc-400 dark:before:bg-zinc-600 dark:hover:before:bg-zinc-500 dark:focus:before:bg-zinc-400" />
|
|
||||||
<Panel
|
|
||||||
id={`code-${title}`}
|
|
||||||
order={2}
|
|
||||||
defaultSize={100 - DEFAULTSIZE}
|
|
||||||
className="-mr-[0.5px] ml-px"
|
|
||||||
></Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</PanelGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-transparent">
|
|
||||||
{/* {mode == 'code' && (
|
|
||||||
<CodeBlock
|
|
||||||
code={code as string}
|
|
||||||
lang="tsx"
|
|
||||||
maxHeight={iframeHeight}
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BlockPreview;
|
|
Loading…
Reference in New Issue
Block a user