mirror of
https://github.com/jnsahaj/tweakcn.git
synced 2026-09-21 04:46:32 +08:00
update dashbaord UI
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import type { Theme } from "@/types/theme";
|
||||
import { ThemeCard } from "./theme-card";
|
||||
import { Search, ArrowUpDown, Layers } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
|
||||
interface ThemesListProps {
|
||||
themes: Theme[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export function ThemesList({ themes, totalCount }: ThemesListProps) {
|
||||
const [filteredThemes, setFilteredThemes] = useState<Theme[]>(themes);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [sortOption, setSortOption] = useState("newest");
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
useEffect(() => {
|
||||
const filtered = themes.filter((theme) =>
|
||||
theme.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
// Sort based on selected option
|
||||
const sorted = [...filtered].sort((a, b) => {
|
||||
switch (sortOption) {
|
||||
case "newest":
|
||||
return (b.createdAt?.getTime() || 0) - (a.createdAt?.getTime() || 0);
|
||||
case "oldest":
|
||||
return (a.createdAt?.getTime() || 0) - (b.createdAt?.getTime() || 0);
|
||||
case "name":
|
||||
return (a.name || "").localeCompare(b.name || "");
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
setFilteredThemes(sorted);
|
||||
}, [themes, searchTerm, sortOption]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-card rounded-lg border p-4">
|
||||
<div className="flex flex-col space-y-4 md:space-y-0 md:flex-row md:justify-between md:gap-4 mb-6">
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<div className="flex items-center gap-2 bg-accent px-3 py-1.5 rounded-full">
|
||||
<Layers className="h-4 w-4 text-accent-foreground" />
|
||||
<span className="font-medium">{totalCount}</span>
|
||||
<span className="text-accent-foreground font-normal">
|
||||
total themes
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 w-full md:w-auto">
|
||||
<div className="relative w-full">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search themes..."
|
||||
className="pl-8 w-full"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Select value={sortOption} onValueChange={setSortOption}>
|
||||
<SelectTrigger className="w-full w-[80px] md:w-[180px] gap-2">
|
||||
<ArrowUpDown className="h-4 w-4 text-muted-foreground" />
|
||||
{!isMobile && <SelectValue placeholder="Sort by" />}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="newest">Newest First</SelectItem>
|
||||
<SelectItem value="oldest">Oldest First</SelectItem>
|
||||
<SelectItem value="name">Name (A-Z)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredThemes.length === 0 && searchTerm ? (
|
||||
<div className="text-center py-12">
|
||||
<Search className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium mb-2">No themes found</h3>
|
||||
<p className="text-muted-foreground">
|
||||
No themes match your search term "{searchTerm}"
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredThemes.map((theme: Theme) => (
|
||||
<ThemeCard key={theme.id} theme={theme} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+65
-23
@@ -1,10 +1,9 @@
|
||||
import { Theme } from "@/types/theme";
|
||||
import { getThemes } from "@/actions/themes";
|
||||
import { ThemeCard } from "./components/theme-card";
|
||||
import { Header } from "@/components/editor/header";
|
||||
import { Palette } from "lucide-react";
|
||||
import { Palette, Plus } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ThemesList } from "@/app/dashboard/components/themes-list";
|
||||
|
||||
export default async function ProfilePage() {
|
||||
const themes = await getThemes();
|
||||
@@ -12,32 +11,75 @@ export default async function ProfilePage() {
|
||||
return b.createdAt?.getTime() - a.createdAt?.getTime();
|
||||
});
|
||||
|
||||
const oneWeekAgo = new Date();
|
||||
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
||||
const recentThemeObjects = themes.filter((theme) => {
|
||||
return theme.createdAt && theme.createdAt > oneWeekAgo;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-screen bg-gradient-to-b from-background to-muted/30">
|
||||
<Header />
|
||||
<div className="container mx-auto py-8">
|
||||
<h1 className="text-3xl font-bold mb-8">Your Themes</h1>
|
||||
{sortedThemes.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="text-6xl mb-4">
|
||||
<Palette className="size-12" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold mb-2">No themes yet</h2>
|
||||
<p className="text-gray-500 mb-4">
|
||||
Create your first theme to get started
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Your Themes</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Manage and explore your custom color themes
|
||||
</p>
|
||||
<Link href="/editor/theme">
|
||||
<Button>Create Theme</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<Link href="/editor/theme">
|
||||
<Button className="gap-1" variant="accent">
|
||||
New Theme
|
||||
<Plus className="size-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{sortedThemes.length === 0 ? (
|
||||
<div className="bg-card rounded-xl border shadow-sm p-8 flex flex-col items-center justify-center py-16 text-center">
|
||||
<div className="bg-primary/10 p-4 rounded-full mb-6">
|
||||
<Palette className="size-12 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold mb-2">
|
||||
No themes created yet
|
||||
</h2>
|
||||
<p className="text-muted-foreground max-w-md mb-6">
|
||||
Create your first custom theme to personalize your projects with
|
||||
unique color palettes
|
||||
</p>
|
||||
<div className="space-y-6 w-full max-w-md">
|
||||
<Link href="/editor/theme">
|
||||
<Button size="lg" className="w-full gap-2">
|
||||
<Plus className="size-4" />
|
||||
Create Your First Theme
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-card px-2 text-muted-foreground">
|
||||
or explore examples
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="bg-gradient-to-br from-pink-500 to-purple-600 h-24 rounded-md shadow-sm cursor-pointer hover:opacity-90 transition-opacity" />
|
||||
<div className="bg-gradient-to-br from-emerald-500 to-teal-600 h-24 rounded-md shadow-sm cursor-pointer hover:opacity-90 transition-opacity" />
|
||||
<div className="bg-gradient-to-br from-amber-500 to-orange-600 h-24 rounded-md shadow-sm cursor-pointer hover:opacity-90 transition-opacity" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{sortedThemes.map((theme: Theme) => (
|
||||
<ThemeCard key={theme.id} theme={theme} />
|
||||
))}
|
||||
</div>
|
||||
<ThemesList
|
||||
themes={sortedThemes}
|
||||
recentCount={recentThemeObjects.length}
|
||||
totalCount={themes.length}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ const buttonVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
@@ -20,6 +21,7 @@ const buttonVariants = cva(
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
accent: "bg-accent text-accent-foreground shadow-sm hover:bg-accent/80",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
|
||||
Reference in New Issue
Block a user