From a78e29608299acd92df289decf1445c83eaf8e41 Mon Sep 17 00:00:00 2001 From: v0 Date: Tue, 31 Mar 2026 05:10:28 +0000 Subject: [PATCH] refactor: optimize various features with internationalization and UX improvements Improved community stats card, leaderboard, playground preview, SEO style guides, recipe system, and guides page with internationalization, UX enhancements, and structural fixes. Co-authored-by: Anx <130662349+AnxForever@users.noreply.github.com> --- app/community/_content.tsx | 49 +++-- app/guides/page.tsx | 117 +++++++----- app/recipes/page.tsx | 31 +++- components/community/community-stats-card.tsx | 109 ++++++++---- components/playground/playground-preview.tsx | 34 ++-- components/recipes/recipe-card.tsx | 168 +++++++++++------- lib/community/leaderboard.ts | 85 +++++---- lib/seo/style-guides.ts | 164 +++++++++++++++++ lib/styles/recipes.ts | 33 ++++ 9 files changed, 573 insertions(+), 217 deletions(-) diff --git a/app/community/_content.tsx b/app/community/_content.tsx index c328a225..81640056 100644 --- a/app/community/_content.tsx +++ b/app/community/_content.tsx @@ -3,14 +3,22 @@ import Image from "next/image"; import { useEffect, useMemo, useState } from "react"; import { usePathname, useRouter } from "next/navigation"; -import { Loader2, LogIn, RefreshCw, Send, TrendingUp } from "lucide-react"; +import { Loader2, LogIn, RefreshCw, Send, TrendingUp, Clock, Heart, Zap } from "lucide-react"; import { LocalizedLink } from "@/components/i18n/localized-link"; import { useCommunityFeed } from "@/lib/swr"; import { useUser } from "@/lib/auth/use-user"; import { useI18n } from "@/lib/i18n/context"; -import { SORT_OPTIONS, type SortMethod } from "@/lib/community/leaderboard"; +import { SORT_OPTIONS_DEF, type SortMethod, type SortOptionDef } from "@/lib/community/leaderboard"; import { CommunityStatsCard } from "@/components/community/community-stats-card"; +// Icon mapping for sort options +const SORT_ICONS = { + TrendingUp: TrendingUp, + Clock: Clock, + Heart: Heart, + Zap: Zap, +} as const; + const PAGE_SIZE = 12; function formatDate(iso: string, locale: "zh" | "en"): string { @@ -163,21 +171,27 @@ export function CommunityContent({ {/* Sort Controls */}
- {SORT_OPTIONS.map((option) => ( - - ))} + {SORT_OPTIONS_DEF.map((option) => { + const Icon = SORT_ICONS[option.iconName]; + const label = locale === "zh" ? option.labelZh : option.labelEn; + const description = locale === "zh" ? option.descriptionZh : option.descriptionEn; + + return ( + + ); + })}
{error && ( @@ -358,6 +372,7 @@ export function CommunityContent({ )} + ); diff --git a/app/guides/page.tsx b/app/guides/page.tsx index 93189ebc..114aaef6 100644 --- a/app/guides/page.tsx +++ b/app/guides/page.tsx @@ -3,7 +3,17 @@ import { Header } from "@/components/layout/header"; import { Footer } from "@/components/layout/footer"; import { LocalizedLink } from "@/components/i18n/localized-link"; import { styleGuides } from "@/lib/seo/style-guides"; -import { BookOpen, ArrowRight } from "lucide-react"; +import { BookOpen, ArrowRight, Sparkles, History } from "lucide-react"; + +// Color scheme for each guide +const guideColors: Record = { + neumorphism: "from-rose-500/20 to-orange-500/20", + "minimalist-flat": "from-zinc-400/20 to-slate-500/20", + glassmorphism: "from-blue-500/20 to-purple-500/20", + "neo-brutalism": "from-yellow-400/20 to-pink-500/20", + editorial: "from-stone-400/20 to-amber-500/20", + "cyber-wafuu": "from-red-500/20 to-cyan-500/20", +}; export const metadata: Metadata = { title: "Design Style Guides - StyleKit", @@ -55,54 +65,71 @@ export default function GuidesPage() {
- {guides.map((guide) => ( - -
-
-

- {guide.nameEn} -

- {guide.name !== guide.nameEn && ( -

{guide.name}

- )} + {guides.map((guide) => { + const colorScheme = guideColors[guide.slug] || "from-gray-400/20 to-gray-500/20"; + + return ( + + {/* Gradient header */} +
+
+
+
+

+ {guide.nameEn} +

+ {guide.name !== guide.nameEn && ( +

{guide.name}

+ )} +
+
+ +
+
- -
+ +
+

+ {guide.descriptionEn} +

-

- {guide.descriptionEn} -

+ {/* Influenced by tags */} + {guide.influencedBy && guide.influencedBy.length > 0 && ( +
+ +
+ {guide.influencedBy.map((style) => ( + + {style} + + ))} +
+
+ )} - {guide.influencedBy && guide.influencedBy.length > 0 && ( -
- {guide.influencedBy.map((style) => ( - - {style} - - ))} +
+
+ + + {guide.useCases.length} use case{guide.useCases.length !== 1 ? "s" : ""} + + + + {guide.references.length} reference{guide.references.length !== 1 ? "s" : ""} + +
+
- )} - -
- - {guide.useCases.length} use case - {guide.useCases.length !== 1 ? "s" : ""} - - - - {guide.references.length} reference - {guide.references.length !== 1 ? "s" : ""} - -
- - ))} + + ); + })}
diff --git a/app/recipes/page.tsx b/app/recipes/page.tsx index 7ce98eb0..6d310745 100644 --- a/app/recipes/page.tsx +++ b/app/recipes/page.tsx @@ -37,22 +37,35 @@ export default function RecipesPage() { {/* Hero */} -
-
-

+
+ {/* Decorative gradient */} +
+ +
+
+ + Style + Layout + Animation +
+ +

Design Recipes

-

+

Curated combinations of visual styles, layouts, and animations. Each recipe is optimized for a specific use case and includes reasoning for why it works.

-
- - {allRecipes.length} Recipes + +
+ + + {allRecipes.length} + Recipes - - {allRecipes.filter((r) => r.featured).length} Featured + + + {allRecipes.filter((r) => r.featured).length} + Featured
diff --git a/components/community/community-stats-card.tsx b/components/community/community-stats-card.tsx index 891b99cd..9595a5f0 100644 --- a/components/community/community-stats-card.tsx +++ b/components/community/community-stats-card.tsx @@ -1,4 +1,7 @@ -import { Heart, Eye, Share2, TrendingUp, Clock, Award } from "lucide-react"; +"use client"; + +import { Heart, Eye, TrendingUp, Clock, Award, Users } from "lucide-react"; +import { useI18n } from "@/lib/i18n/context"; export interface CommunityStats { totalSubmissions: number; @@ -17,62 +20,92 @@ interface CommunityStatsCardProps { } export function CommunityStatsCard({ stats }: CommunityStatsCardProps) { + const { locale } = useI18n(); + const isZh = locale === "zh"; + const statItems = [ { - icon: , - label: "Total Styles", + icon: Award, + label: isZh ? "社区风格" : "Community Styles", value: stats.totalSubmissions.toLocaleString(), - color: "bg-blue-500/10 text-blue-600 dark:text-blue-400", + gradient: "from-blue-500/20 to-indigo-500/20", + iconColor: "text-blue-500", }, { - icon: , - label: "Contributors", + icon: Users, + label: isZh ? "贡献者" : "Contributors", value: stats.totalCollaborators.toLocaleString(), - color: "bg-purple-500/10 text-purple-600 dark:text-purple-400", + gradient: "from-purple-500/20 to-pink-500/20", + iconColor: "text-purple-500", }, { - icon: , - label: "This Month", + icon: Clock, + label: isZh ? "本月新增" : "This Month", value: stats.recentSubmissions.toLocaleString(), - color: "bg-green-500/10 text-green-600 dark:text-green-400", + gradient: "from-emerald-500/20 to-teal-500/20", + iconColor: "text-emerald-500", }, ]; return ( -
+
-
- {statItems.map((item, idx) => ( -
- {item.icon} -
-
{item.label}
-
{item.value}
+ {/* Stats Grid */} +
+ {statItems.map((item, idx) => { + const Icon = item.icon; + return ( +
+
+
+ +
+
+
+ {item.label} +
+
+ {item.value} +
+
+
-
- ))} + ); + })}
+ {/* Featured Style */} {stats.topStyle && ( -
-
- - Featured This Month -
-

{stats.topStyle.title}

-
-
by {stats.topStyle.author}
-
-
- - {stats.topStyle.likes} +
+
+ +
+
+ + + {isZh ? "本月精选" : "Featured This Month"} + +
+ +

{stats.topStyle.title}

+ +
+
+ {isZh ? "作者:" : "by "}{stats.topStyle.author}
-
- - {stats.topStyle.views} +
+
+ + {stats.topStyle.likes} +
+
+ + {stats.topStyle.views} +
diff --git a/components/playground/playground-preview.tsx b/components/playground/playground-preview.tsx index e2b2a70b..806c982c 100644 --- a/components/playground/playground-preview.tsx +++ b/components/playground/playground-preview.tsx @@ -5,14 +5,13 @@ import { Loader2, ZoomIn, ZoomOut, - RotateCcw, Maximize2, Minimize2, MousePointer, Eye, - EyeOff, X } from "lucide-react"; +import { useI18n } from "@/lib/i18n/context"; interface PlaygroundPreviewProps { code: string; @@ -175,6 +174,9 @@ export function PlaygroundPreview({ deviceWidth, onElementSelect, }: PlaygroundPreviewProps) { + const { locale } = useI18n(); + const isZh = locale === "zh"; + const [debouncedCode, setDebouncedCode] = useState(code); const [debouncedTokenCss, setDebouncedTokenCss] = useState(tokenCss); const [zoom, setZoom] = useState(1); @@ -278,12 +280,12 @@ export function PlaygroundPreview({ {/* 预览控制栏 */}
- {/* 缩放控制 */} + {/* Zoom Controls */} @@ -291,7 +293,7 @@ export function PlaygroundPreview({ @@ -300,14 +302,14 @@ export function PlaygroundPreview({ onClick={handleZoomIn} disabled={zoom >= ZOOM_LEVELS[ZOOM_LEVELS.length - 1]} className="p-1 text-muted hover:text-foreground disabled:opacity-30 disabled:cursor-not-allowed transition-colors" - title="放大" + title={isZh ? "放大" : "Zoom In"} >
- {/* 元素检查器 */} + {/* Element Inspector */}
- {/* 全屏切换 */} + {/* Fullscreen Toggle */}
- {/* 加载指示器 */} + {/* Loading Indicator */} {loading && (
- 更新中... + {isZh ? "更新中..." : "Updating..."}
)} @@ -408,11 +414,11 @@ export function PlaygroundPreview({
- {/* 检查器模式提示 */} + {/* Inspector Mode Hint */} {inspectorEnabled && (
- 点击元素查看类名 + {isZh ? "点击元素查看类名" : "Click elements to inspect"}
)}
diff --git a/components/recipes/recipe-card.tsx b/components/recipes/recipe-card.tsx index 57360a37..767fac82 100644 --- a/components/recipes/recipe-card.tsx +++ b/components/recipes/recipe-card.tsx @@ -1,6 +1,6 @@ "use client"; -import { ArrowRight, Layers, Layout, Sparkles } from "lucide-react"; +import { ArrowRight, Layers, Layout, Sparkles, Check } from "lucide-react"; import { LocalizedLink } from "@/components/i18n/localized-link"; import { useI18n } from "@/lib/i18n/context"; import type { StyleRecipe } from "@/lib/styles/recipes"; @@ -12,36 +12,49 @@ interface RecipeCardProps { export function RecipeCard({ recipe, variant = "default" }: RecipeCardProps) { const { locale } = useI18n(); + const isZh = locale === "zh"; - const name = locale === "zh" ? recipe.nameZh : recipe.name; - const description = locale === "zh" ? recipe.descriptionZh : recipe.description; - const reasoning = locale === "zh" ? recipe.reasoningZh : recipe.reasoning; + const name = isZh ? recipe.nameZh : recipe.name; + const description = isZh ? recipe.descriptionZh : recipe.description; + const reasoning = isZh ? recipe.reasoningZh : recipe.reasoning; if (variant === "compact") { return ( -
-
+ + {/* Subtle gradient overlay on hover */} +