🌐 AI搜索 & 代理 主页
Skip to content

Commit 9a76918

Browse files
committed
feat(ui): redesign snippet modal and code preview
1 parent 502357e commit 9a76918

File tree

7 files changed

+216
-9
lines changed

7 files changed

+216
-9
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@radix-ui/react-separator": "^1.1.7",
2222
"@radix-ui/react-slot": "^1.2.3",
2323
"@radix-ui/react-switch": "^1.2.5",
24+
"@radix-ui/react-tabs": "^1.1.13",
2425
"@radix-ui/react-tooltip": "^1.2.8",
2526
"@types/mdx": "^2.0.13",
2627
"class-variance-authority": "^0.7.1",

src/app/snippets/[category]/[snippet]/page.tsx

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,21 @@
33
import { use } from "react";
44

55
import { useRouter } from "next/navigation";
6+
import Link from "next/link";
67

7-
import { Button } from "@/components/ui/button";
8+
import { Loader, XIcon } from "lucide-react";
89

9-
import { unslugify } from "@/lib/utils";
10+
import { FullSnippet } from "@/types";
11+
import { useFetch } from "@/hooks/use-fetch";
12+
import { Button } from "@/components/ui/button";
13+
import CodePreview from "@/components/layouts/code-preview";
14+
import {
15+
Card,
16+
CardContent,
17+
CardFooter,
18+
CardHeader,
19+
} from "@/components/ui/card";
20+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
1021

1122
interface Props {
1223
params: Promise<{ category: string; snippet: string }>;
@@ -15,6 +26,9 @@ interface Props {
1526
export default function SnippetPage({ params }: Props) {
1627
const router = useRouter();
1728
const { category, snippet } = use(params);
29+
const { data, loading } = useFetch<FullSnippet>(
30+
`/data/snippets/${category}/${snippet}.json`
31+
);
1832

1933
const handleCloseModal = () => {
2034
/**
@@ -26,15 +40,55 @@ export default function SnippetPage({ params }: Props) {
2640
else router.push("/snippets");
2741
};
2842

43+
if (loading) return <Loader />;
44+
if (!data) return null;
45+
2946
return (
3047
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
31-
<div className="bg-secondary text-secondary-foreground p-6 rounded-lg space-y-4 shadow-lg w-[400px]">
32-
<h2 className="text-lg font-bold">
33-
{unslugify(category)} / {unslugify(snippet)}
34-
</h2>
35-
<p>Snippet details here</p>
36-
<Button onClick={handleCloseModal}>Close</Button>
37-
</div>
48+
<Card className="wrapper-xs">
49+
<CardHeader className="flex items-center justify-between gap-4">
50+
<h2 className="text-2xl font-bold">{data.title}</h2>
51+
<Button onClick={handleCloseModal} size="icon">
52+
<XIcon />
53+
</Button>
54+
</CardHeader>
55+
<CardContent className="space-y-4">
56+
<p>{data.description}</p>
57+
<CodePreview languages={data.languages} snippets={data.snippets} />
58+
</CardContent>
59+
<CardFooter className="grid gap-4">
60+
<div className="flex items-center gap-4">
61+
<p className="font-bold">Contributors: </p>
62+
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
63+
{data.contributors.map((contributor) => (
64+
<Avatar
65+
key={contributor}
66+
className="w-8 h-8"
67+
title={`@${contributor}`}
68+
>
69+
<AvatarImage
70+
src={`https://github.com/${contributor}.png`}
71+
alt={`@${contributor}`}
72+
/>
73+
<AvatarFallback>{contributor.slice(0, 2)}</AvatarFallback>
74+
</Avatar>
75+
))}
76+
</div>
77+
</div>
78+
<ul className="flex items-center flex-wrap gap-2">
79+
{data.tags.map((tag) => (
80+
<li key={tag}>
81+
<Link
82+
className="border border-border font-semibold pt-1 pb-2 px-3 rounded-md leading-tight"
83+
href={`/snippets/tags/${tag}`}
84+
>
85+
{tag}
86+
</Link>
87+
</li>
88+
))}
89+
</ul>
90+
</CardFooter>
91+
</Card>
3892
</div>
3993
);
4094
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
2+
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
3+
4+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
5+
6+
// import CopyToClipboard from "./CopyToClipboard";
7+
// import CopyURLButton from "./CopyURLButton";
8+
9+
type Props = {
10+
languages: string[];
11+
snippets: Record<string, string>;
12+
};
13+
14+
const CodePreview = ({ languages, snippets }: Props) => {
15+
return (
16+
<div className="w-full">
17+
<Tabs defaultValue={languages[0]}>
18+
<TabsList>
19+
{languages.map((language) => (
20+
<TabsTrigger key={language} value={language}>
21+
{language}
22+
</TabsTrigger>
23+
))}
24+
</TabsList>
25+
{Object.keys(snippets).map((language) => {
26+
const code = snippets[language as keyof typeof snippets];
27+
28+
return (
29+
<TabsContent value={language} key={language}>
30+
<SyntaxHighlighter
31+
language={language}
32+
style={oneDark}
33+
wrapLines={true}
34+
customStyle={{ margin: "0", maxHeight: "22rem" }}
35+
>
36+
{code}
37+
</SyntaxHighlighter>
38+
</TabsContent>
39+
);
40+
})}
41+
</Tabs>
42+
</div>
43+
);
44+
};
45+
46+
export default CodePreview;

src/components/ui/tabs.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import * as TabsPrimitive from "@radix-ui/react-tabs"
5+
6+
import { cn } from "@/lib/utils"
7+
8+
function Tabs({
9+
className,
10+
...props
11+
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
12+
return (
13+
<TabsPrimitive.Root
14+
data-slot="tabs"
15+
className={cn("flex flex-col gap-2", className)}
16+
{...props}
17+
/>
18+
)
19+
}
20+
21+
function TabsList({
22+
className,
23+
...props
24+
}: React.ComponentProps<typeof TabsPrimitive.List>) {
25+
return (
26+
<TabsPrimitive.List
27+
data-slot="tabs-list"
28+
className={cn(
29+
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
30+
className
31+
)}
32+
{...props}
33+
/>
34+
)
35+
}
36+
37+
function TabsTrigger({
38+
className,
39+
...props
40+
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
41+
return (
42+
<TabsPrimitive.Trigger
43+
data-slot="tabs-trigger"
44+
className={cn(
45+
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
46+
className
47+
)}
48+
{...props}
49+
/>
50+
)
51+
}
52+
53+
function TabsContent({
54+
className,
55+
...props
56+
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
57+
return (
58+
<TabsPrimitive.Content
59+
data-slot="tabs-content"
60+
className={cn("flex-1 outline-none", className)}
61+
{...props}
62+
/>
63+
)
64+
}
65+
66+
export { Tabs, TabsList, TabsTrigger, TabsContent }

src/hooks/use-fetch.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useEffect, useState } from "react";
2+
3+
export const useFetch = <T>(url: string) => {
4+
const [data, setData] = useState<T | null>(null);
5+
const [error, setError] = useState<string | null>(null);
6+
const [loading, setLoading] = useState<boolean>(true);
7+
8+
useEffect(() => {
9+
const fetchData = async () => {
10+
try {
11+
const res = await fetch(url);
12+
if (!res.ok) {
13+
throw new Error(`Failed to fetch data from ${url}`);
14+
}
15+
const result: T = await res.json();
16+
setData(result);
17+
} catch (err) {
18+
setError((err as Error).message);
19+
} finally {
20+
setLoading(false);
21+
}
22+
};
23+
24+
fetchData();
25+
}, [url]);
26+
27+
return { data, loading, error };
28+
};

src/types/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,14 @@ export type SnippetType = {
1616
contributors: string[];
1717
tags: string[];
1818
};
19+
20+
export type FullSnippet = {
21+
id: string;
22+
category: string;
23+
title: string;
24+
description: string;
25+
languages: string[];
26+
contributors: string[];
27+
tags: string[];
28+
snippets: Record<string, string>;
29+
};

0 commit comments

Comments
 (0)