feat: add Header component for consistent page titles and layout
This commit is contained in:
parent
8676a7d05a
commit
31e27d4214
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 129 KiB |
|
|
@ -4,65 +4,48 @@
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 98%;
|
||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
--primary: 222.2 47.4% 11.2%;
|
--primary: 222.2 47.4% 11.2%;
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
--muted: 210 40% 96.1%;
|
--accent: 240 4.8% 95.9%;
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
--accent: 210 40% 96.1%;
|
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
|
||||||
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
--border: 214.3 31.8% 91.4%;
|
--input: 240 5.9% 90%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--ring: 240 10% 3.9%;
|
||||||
--ring: 222.2 84% 4.9%;
|
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: 240 10% 3.9%;
|
||||||
--foreground: 210 40% 98%;
|
--foreground: 0 0% 98%;
|
||||||
|
--card: 240 10% 3.9%;
|
||||||
--card: 222.2 84% 4.9%;
|
--card-foreground: 0 0% 98%;
|
||||||
--card-foreground: 210 40% 98%;
|
--popover: 240 10% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
--popover: 222.2 84% 4.9%;
|
|
||||||
--popover-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--primary: 210 40% 98%;
|
--primary: 210 40% 98%;
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--secondary-foreground: 0 0% 98%;
|
||||||
--secondary-foreground: 210 40% 98%;
|
--muted: 240 3.7% 15.9%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
--muted: 217.2 32.6% 17.5%;
|
--accent: 240 3.7% 15.9%;
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--accent: 217.2 32.6% 17.5%;
|
|
||||||
--accent-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
--border: 217.2 32.6% 17.5%;
|
--input: 240 3.7% 15.9%;
|
||||||
--input: 217.2 32.6% 17.5%;
|
--ring: 215 20.2% 65.1%;
|
||||||
--ring: 212.7 26.8% 83.9%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,4 +57,4 @@
|
||||||
background-color: hsl(var(--background));
|
background-color: hsl(var(--background));
|
||||||
color: hsl(var(--foreground));
|
color: hsl(var(--foreground));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,10 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import Sidebar from "@/components/sidebar";
|
import Sidebar from "@/components/sidebar";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const inter = Inter({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-inter",
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -26,7 +21,7 @@ export default function RootLayout({
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-gray-100 dark:bg-gray-900`}
|
className={`${inter.variable} antialiased bg-gray-100 dark:bg-gray-900`}
|
||||||
>
|
>
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
|
||||||
106
src/app/page.tsx
106
src/app/page.tsx
|
|
@ -1,103 +1,13 @@
|
||||||
import Image from "next/image";
|
import { Header } from "@/components/ui/header";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
<Header title="Home" />
|
||||||
<Image
|
<div className="flex flex-col items-center justify-center mt-8">
|
||||||
className="dark:invert"
|
<h2 className="text-xl font-semibold">Welcome to NextAV</h2>
|
||||||
src="/next.svg"
|
<p className="text-gray-500 dark:text-gray-400">Select a library from the sidebar to get started.</p>
|
||||||
alt="Next.js logo"
|
</div>
|
||||||
width={180}
|
|
||||||
height={38}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
|
||||||
<li className="mb-2 tracking-[-.01em]">
|
|
||||||
Get started by editing{" "}
|
|
||||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
|
||||||
src/app/page.tsx
|
|
||||||
</code>
|
|
||||||
.
|
|
||||||
</li>
|
|
||||||
<li className="tracking-[-.01em]">
|
|
||||||
Save and see your changes instantly.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
Deploy now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Read our docs
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/file.svg"
|
|
||||||
alt="File icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Learn
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/window.svg"
|
|
||||||
alt="Window icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Examples
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/globe.svg"
|
|
||||||
alt="Globe icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Go to nextjs.org →
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Header } from "@/components/ui/header";
|
||||||
|
|
||||||
interface Library {
|
interface Library {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -64,7 +65,7 @@ const SettingsPage = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<h1 className="text-2xl font-bold mb-4">Settings</h1>
|
<Header title="Settings" />
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import { Header } from "@/components/ui/header";
|
||||||
|
|
||||||
interface Video {
|
interface Video {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -33,7 +34,7 @@ const VideosPage = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<h1 className="text-2xl font-bold mb-4">Videos</h1>
|
<Header title="Videos" />
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||||
{videos.map((video) => (
|
{videos.map((video) => (
|
||||||
<Card key={video.id} className="overflow-hidden rounded-lg shadow-lg hover:shadow-xl transition-shadow duration-300 ease-in-out">
|
<Card key={video.id} className="overflow-hidden rounded-lg shadow-lg hover:shadow-xl transition-shadow duration-300 ease-in-out">
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,13 @@ import {
|
||||||
Folder,
|
Folder,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
const [libraries, setLibraries] = useState<{ id: number; path: string }[]>([]);
|
const [libraries, setLibraries] = useState<{ id: number; path: string }[]>([]);
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchLibraries();
|
fetchLibraries();
|
||||||
|
|
@ -32,50 +35,70 @@ const Sidebar = () => {
|
||||||
setIsCollapsed(!isCollapsed);
|
setIsCollapsed(!isCollapsed);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ href: "/", label: "Home", icon: Home },
|
||||||
|
{ href: "/settings", label: "Settings", icon: Settings },
|
||||||
|
{ href: "/videos", label: "Videos", icon: Video },
|
||||||
|
{ href: "/photos", label: "Photos", icon: Image },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col bg-white dark:bg-gray-900 text-gray-700 dark:text-gray-200 border-r border-gray-200 dark:border-gray-800 transition-all duration-300 ${isCollapsed ? "w-20" : "w-64"}`}>
|
className={cn(
|
||||||
|
"flex flex-col bg-gray-100 dark:bg-gray-900 text-gray-700 dark:text-gray-200 border-r border-gray-200 dark:border-gray-800 transition-all duration-300",
|
||||||
|
isCollapsed ? "w-20" : "w-64"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800">
|
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800">
|
||||||
{!isCollapsed && <h1 className="text-xl font-bold">NextAV</h1>}
|
{!isCollapsed && (
|
||||||
|
<h1 className="text-2xl font-bold text-primary">NextAV</h1>
|
||||||
|
)}
|
||||||
<Button onClick={toggleSidebar} variant="ghost" size="icon">
|
<Button onClick={toggleSidebar} variant="ghost" size="icon">
|
||||||
{isCollapsed ? <ChevronRight /> : <ChevronLeft />}
|
{isCollapsed ? <ChevronRight /> : <ChevronLeft />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex flex-col mt-4 space-y-2 px-4">
|
<nav className="flex-1 mt-4 space-y-2 px-4">
|
||||||
<Link href="/" passHref>
|
{navItems.map((item) => (
|
||||||
<Button variant="ghost" className="w-full justify-start">
|
<Link href={item.href} key={item.href} passHref>
|
||||||
<Home className="mr-4" />
|
<Button
|
||||||
{!isCollapsed && "Home"}
|
variant={pathname === item.href ? "secondary" : "ghost"}
|
||||||
</Button>
|
className="w-full justify-start"
|
||||||
</Link>
|
>
|
||||||
<Link href="/settings" passHref>
|
<item.icon className="mr-4 h-5 w-5" />
|
||||||
<Button variant="ghost" className="w-full justify-start">
|
{!isCollapsed && item.label}
|
||||||
<Settings className="mr-4" />
|
</Button>
|
||||||
{!isCollapsed && "Settings"}
|
</Link>
|
||||||
</Button>
|
))}
|
||||||
</Link>
|
|
||||||
<Link href="/videos" passHref>
|
|
||||||
<Button variant="ghost" className="w-full justify-start">
|
|
||||||
<Video className="mr-4" />
|
|
||||||
{!isCollapsed && "Videos"}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Link href="/photos" passHref>
|
|
||||||
<Button variant="ghost" className="w-full justify-start">
|
|
||||||
<Image className="mr-4" />
|
|
||||||
{!isCollapsed && "Photos"}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<h2 className={`text-lg font-semibold p-2 ${isCollapsed ? 'text-center' : ''}`}>
|
<h2
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold p-2",
|
||||||
|
isCollapsed && "text-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{!isCollapsed ? "Folder Viewer" : "Folders"}
|
{!isCollapsed ? "Folder Viewer" : "Folders"}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-2">
|
||||||
{libraries.map((lib) => (
|
{libraries.map((lib) => (
|
||||||
<Link href={`/folder-viewer?path=${lib.path}`} key={lib.id} passHref>
|
<Link
|
||||||
<Button variant="ghost" className="w-full justify-start">
|
href={`/folder-viewer?path=${lib.path}`}
|
||||||
<Folder className="mr-4" />
|
key={lib.id}
|
||||||
{!isCollapsed && <span className="truncate">{lib.path}</span>}
|
passHref
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant={
|
||||||
|
pathname === "/folder-viewer" &&
|
||||||
|
new URLSearchParams(window.location.search).get("path") ===
|
||||||
|
lib.path
|
||||||
|
? "secondary"
|
||||||
|
: "ghost"
|
||||||
|
}
|
||||||
|
className="w-full justify-start"
|
||||||
|
>
|
||||||
|
<Folder className="mr-4 h-5 w-5" />
|
||||||
|
{!isCollapsed && (
|
||||||
|
<span className="truncate">{lib.path}</span>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -2,91 +2,78 @@ import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>((
|
||||||
return (
|
{ className, ...props },
|
||||||
<div
|
ref
|
||||||
data-slot="card"
|
) => (
|
||||||
className={cn(
|
<div
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
ref={ref}
|
||||||
className
|
className={cn(
|
||||||
)}
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
{...props}
|
className
|
||||||
/>
|
)}
|
||||||
)
|
{...props}
|
||||||
}
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>((
|
||||||
return (
|
{ className, ...props },
|
||||||
<div
|
ref
|
||||||
data-slot="card-header"
|
) => (
|
||||||
className={cn(
|
<div
|
||||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
ref={ref}
|
||||||
className
|
className={cn("flex flex-col space-y-1.5 p-4", className)}
|
||||||
)}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
))
|
||||||
)
|
CardHeader.displayName = "CardHeader"
|
||||||
}
|
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>((
|
||||||
return (
|
{ className, ...props },
|
||||||
<div
|
ref
|
||||||
data-slot="card-title"
|
) => (
|
||||||
className={cn("leading-none font-semibold", className)}
|
<h3
|
||||||
{...props}
|
ref={ref}
|
||||||
/>
|
className={cn(
|
||||||
)
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
}
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
const CardDescription = React.forwardRef<
|
||||||
return (
|
HTMLParagraphElement,
|
||||||
<div
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
data-slot="card-description"
|
>(({ className, ...props }, ref) => (
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
<p
|
||||||
{...props}
|
ref={ref}
|
||||||
/>
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
)
|
{...props}
|
||||||
}
|
/>
|
||||||
|
))
|
||||||
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>((
|
||||||
return (
|
{ className, ...props },
|
||||||
<div
|
ref
|
||||||
data-slot="card-action"
|
) => (
|
||||||
className={cn(
|
<div ref={ref} className={cn("p-4 pt-0", className)} {...props} />
|
||||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
))
|
||||||
className
|
CardContent.displayName = "CardContent"
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>((
|
||||||
return (
|
{ className, ...props },
|
||||||
<div
|
ref
|
||||||
data-slot="card-content"
|
) => (
|
||||||
className={cn("px-6", className)}
|
<div
|
||||||
{...props}
|
ref={ref}
|
||||||
/>
|
className={cn("flex items-center p-4 pt-0", className)}
|
||||||
)
|
{...props}
|
||||||
}
|
/>
|
||||||
|
))
|
||||||
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-footer"
|
|
||||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardFooter,
|
|
||||||
CardTitle,
|
|
||||||
CardAction,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Header({ title, className, ...props }: HeaderProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-800",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<h1 className="text-2xl font-bold text-primary">{title}</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue