refactor: remove BlockCategory pages, and BlockPreview components

This commit is contained in:
javayhu 2025-08-25 23:43:45 +08:00
parent fa4b9a19a1
commit 4bad9714fa
3 changed files with 0 additions and 438 deletions

View File

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

View File

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

View File

@ -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;