Your Events
diff --git a/app/(auth)/guest-book/page.tsx b/app/(auth)/guest-book/page.tsx
index 8a2b4b2..4db8178 100644
--- a/app/(auth)/guest-book/page.tsx
+++ b/app/(auth)/guest-book/page.tsx
@@ -11,7 +11,7 @@ export default async function GuestBookPage({ searchParams }: { searchParams: {
const guestBookData = await queries.fetchGuestBookEntries({
page: currentPage,
- pageSize: 10,
+ pageSize: 20,
})
const { entries, totalPages, currentPage: verifiedPage } = !Array.isArray(guestBookData)
diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx
index 69083c8..6043603 100644
--- a/app/(auth)/layout.tsx
+++ b/app/(auth)/layout.tsx
@@ -3,20 +3,35 @@
import { SessionProvider } from 'next-auth/react'
import { ReactNode } from 'react'
import DashboardNavbar from '@/components/DashboardNavbar'
+import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'
+import { AppSidebar } from '@/components/app-sidebar'
+import { SiteHeader } from '@/components/site-header'
export default function AuthLayout({ children }: { children: ReactNode }) {
return (
<>
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
>
)
diff --git a/app/globals.css b/app/globals.css
index edbd5e6..31f1eec 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,4 +1,8 @@
@import "tailwindcss";
+@import "tw-animate-css";
+/*
+ ---break--- */
+@custom-variant dark (&:is(.dark *));
@plugin "@tailwindcss/typography";
@theme {
@@ -51,7 +55,7 @@
--color-brand-secondary-800: rgb(188, 220, 189);
--color-brand-secondary-900: rgb(222, 237, 222);
--color-brand-secondary-950: rgb(238, 246, 239); */
-
+
--color-brand-text: rgb(87, 77, 61);
--color-brand-background: rgb(242, 246, 248);
--color-brand-primary:rgb(134, 159, 207);
@@ -109,3 +113,137 @@
.textarea-bordered {
@apply border border-gray-300;
}
+
+/*
+ ---break--- */
+
+@theme inline {
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+
+}
+
+/*
+ ---break--- */
+
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.147 0.004 49.25);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.147 0.004 49.25);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.147 0.004 49.25);
+ --primary: oklch(0.216 0.006 56.043);
+ --primary-foreground: oklch(0.985 0.001 106.423);
+ --secondary: oklch(0.97 0.001 106.424);
+ --secondary-foreground: oklch(0.216 0.006 56.043);
+ --muted: oklch(0.97 0.001 106.424);
+ --muted-foreground: oklch(0.553 0.013 58.071);
+ --accent: oklch(0.97 0.001 106.424);
+ --accent-foreground: oklch(0.216 0.006 56.043);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.923 0.003 48.717);
+ --input: oklch(0.923 0.003 48.717);
+ --ring: oklch(0.709 0.01 56.259);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0.001 106.423);
+ --sidebar-foreground: oklch(0.147 0.004 49.25);
+ --sidebar-primary: oklch(0.216 0.006 56.043);
+ --sidebar-primary-foreground: oklch(0.985 0.001 106.423);
+ --sidebar-accent: oklch(0.97 0.001 106.424);
+ --sidebar-accent-foreground: oklch(0.216 0.006 56.043);
+ --sidebar-border: oklch(0.923 0.003 48.717);
+ --sidebar-ring: oklch(0.709 0.01 56.259);
+
+}
+
+/*
+ ---break--- */
+
+.dark {
+ --background: oklch(0.147 0.004 49.25);
+ --foreground: oklch(0.985 0.001 106.423);
+ --card: oklch(0.216 0.006 56.043);
+ --card-foreground: oklch(0.985 0.001 106.423);
+ --popover: oklch(0.216 0.006 56.043);
+ --popover-foreground: oklch(0.985 0.001 106.423);
+ --primary: oklch(0.923 0.003 48.717);
+ --primary-foreground: oklch(0.216 0.006 56.043);
+ --secondary: oklch(0.268 0.007 34.298);
+ --secondary-foreground: oklch(0.985 0.001 106.423);
+ --muted: oklch(0.268 0.007 34.298);
+ --muted-foreground: oklch(0.709 0.01 56.259);
+ --accent: oklch(0.268 0.007 34.298);
+ --accent-foreground: oklch(0.985 0.001 106.423);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.553 0.013 58.071);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.216 0.006 56.043);
+ --sidebar-foreground: oklch(0.985 0.001 106.423);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0.001 106.423);
+ --sidebar-accent: oklch(0.268 0.007 34.298);
+ --sidebar-accent-foreground: oklch(0.985 0.001 106.423);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.553 0.013 58.071);
+
+}
+
+/*
+ ---break--- */
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+
+ }
+ body {
+ @apply bg-background text-foreground;
+
+ }
+
+}
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..2883c94
--- /dev/null
+++ b/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "app/globals.css",
+ "baseColor": "stone",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
\ No newline at end of file
diff --git a/components/EventInfoDisplay.tsx b/components/EventInfoDisplay.tsx
index bb7fa5a..59f757e 100644
--- a/components/EventInfoDisplay.tsx
+++ b/components/EventInfoDisplay.tsx
@@ -116,7 +116,7 @@ export default function EventInfoDisplay({ event }: Props) {
const notAttendingGuests = eventGuests.filter((g) => g.rsvp === 'NO');
const pendingGuests = eventGuests.filter((g) => g.rsvp === 'PENDING');
- let daysLeft
+ let daysLeft: number | undefined
if (event.date !== null) {
daysLeft = getDaysUntilEvent(event.date);
@@ -202,11 +202,11 @@ export default function EventInfoDisplay({ event }: Props) {
Countdown
- {daysLeft && daysLeft > 0
+ {daysLeft !== undefined && daysLeft > 0
? `${daysLeft} day${daysLeft !== 1 ? 's' : ''} until the event`
: daysLeft === 0
? 'Today is the big day!'
- : `This event happened ${Math.abs(daysLeft)} day${Math.abs(daysLeft) !== 1 ? 's' : ''} ago`
+ : `This event happened ${daysLeft !== undefined && Math.abs(daysLeft)} day${daysLeft !== undefined && Math.abs(daysLeft) !== 1 ? 's' : ''} ago`
}
diff --git a/components/GuestBookPageClient.tsx b/components/GuestBookPageClient.tsx
index 2f5844b..ce53271 100644
--- a/components/GuestBookPageClient.tsx
+++ b/components/GuestBookPageClient.tsx
@@ -64,7 +64,7 @@ export default function GuestBookPageClient({
}
return (
-
+
Guest Book
diff --git a/components/ToDoList.tsx b/components/ToDoList.tsx
index 0c3b7a2..27495f4 100644
--- a/components/ToDoList.tsx
+++ b/components/ToDoList.tsx
@@ -6,6 +6,7 @@ interface Todo {
name: string
complete: boolean
dueDate?: string | null
+ notes?: string | null
}
interface Props {
@@ -18,6 +19,8 @@ export default function ToDoList({ eventId, initialTodos, onUpdate }: Props) {
const [todos, setTodos] = useState(initialTodos)
const [newName, setNewName] = useState('')
const [newDueDate, setNewDueDate] = useState('')
+ const [editingNoteId, setEditingNoteId] = useState
(null)
+ const [noteDraft, setNoteDraft] = useState('')
async function handleAdd() {
if (!newName.trim()) return
@@ -65,6 +68,25 @@ export default function ToDoList({ eventId, initialTodos, onUpdate }: Props) {
if (onUpdate) await onUpdate()
}
+ async function saveNote(id: string) {
+ const res = await fetch(`/api/events/${eventId}/todo/${id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ notes: noteDraft }),
+ })
+
+ if (res.ok) {
+ setTodos(prev =>
+ prev.map(todo =>
+ todo.id === id ? { ...todo, notes: noteDraft } : todo
+ )
+ )
+ setEditingNoteId(null)
+ setNoteDraft('')
+ }
+ if (onUpdate) await onUpdate()
+ }
+
return (
To Do List
@@ -82,33 +104,85 @@ export default function ToDoList({ eventId, initialTodos, onUpdate }: Props) {
value={newDueDate}
onChange={e => setNewDueDate(e.target.value)}
/>
- Add
+
+ Add
+
{todos.map(todo => (
-
+
+ {/* Notes Section */}
+
+ {editingNoteId === todo.id ? (
+
+ ) : (
+
{
+ setEditingNoteId(todo.id)
+ setNoteDraft(todo.notes || '')
+ }}
+ >
+ {todo.notes ? (
+ {todo.notes}
+ ) : (
+ Add note...
+ )}
+
)}
- handleDelete(todo.id)}>
- Delete
-
))}
diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx
new file mode 100644
index 0000000..3896bce
--- /dev/null
+++ b/components/app-sidebar.tsx
@@ -0,0 +1,173 @@
+"use client"
+
+import * as React from "react"
+import {
+ IconCamera,
+ IconChartBar,
+ IconDashboard,
+ IconDatabase,
+ IconFileAi,
+ IconFileDescription,
+ IconFileWord,
+ IconFolder,
+ IconHelp,
+ IconInnerShadowTop,
+ IconListDetails,
+ IconReport,
+ IconSearch,
+ IconSettings,
+ IconUsers,
+} from "@tabler/icons-react"
+
+import { NavDocuments } from "@/components/nav-documents"
+import { NavMain } from "@/components/nav-main"
+import { NavSecondary } from "@/components/nav-secondary"
+import { NavUser } from "@/components/nav-user"
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/ui/sidebar"
+import { useSession } from "next-auth/react"
+import Link from "next/link"
+
+const data = {
+ navMain: [
+ {
+ title: "Dashboard",
+ url: "/dashboard",
+ icon: IconDashboard,
+ },
+ {
+ title: "Events",
+ url: "/events",
+ icon: IconListDetails,
+ },
+ {
+ title: "Guest Book",
+ url: "/guest-book",
+ icon: IconUsers,
+ },
+ ],
+ // navClouds: [
+ // {
+ // title: "Capture",
+ // icon: IconCamera,
+ // isActive: true,
+ // url: "#",
+ // items: [
+ // {
+ // title: "Active Proposals",
+ // url: "#",
+ // },
+ // {
+ // title: "Archived",
+ // url: "#",
+ // },
+ // ],
+ // },
+ // {
+ // title: "Proposal",
+ // icon: IconFileDescription,
+ // url: "#",
+ // items: [
+ // {
+ // title: "Active Proposals",
+ // url: "#",
+ // },
+ // {
+ // title: "Archived",
+ // url: "#",
+ // },
+ // ],
+ // },
+ // {
+ // title: "Prompts",
+ // icon: IconFileAi,
+ // url: "#",
+ // items: [
+ // {
+ // title: "Active Proposals",
+ // url: "#",
+ // },
+ // {
+ // title: "Archived",
+ // url: "#",
+ // },
+ // ],
+ // },
+ // ],
+ navSecondary: [
+ {
+ title: "Settings",
+ url: "/admin",
+ icon: IconSettings,
+ },
+ {
+ title: "Get Help",
+ url: "#",
+ icon: IconHelp,
+ },
+ {
+ title: "Search",
+ url: "#",
+ icon: IconSearch,
+ },
+ ],
+ documents: [
+ {
+ name: "Data Library",
+ url: "#",
+ icon: IconDatabase,
+ },
+ {
+ name: "Reports",
+ url: "#",
+ icon: IconReport,
+ },
+ {
+ name: "Word Assistant",
+ url: "#",
+ icon: IconFileWord,
+ },
+ ],
+}
+
+export function AppSidebar({ ...props }: React.ComponentProps) {
+ const session = useSession()
+ const user = session.data?.user
+ return (
+
+
+
+
+
+
+
+ Event Planner
+
+
+
+
+
+
+
+ {/*
+ */}
+
+ {session && (
+
+
+
+ )}
+
+
+ )
+}
diff --git a/components/chart-area-interactive.tsx b/components/chart-area-interactive.tsx
new file mode 100644
index 0000000..4753f83
--- /dev/null
+++ b/components/chart-area-interactive.tsx
@@ -0,0 +1,292 @@
+"use client"
+
+import * as React from "react"
+import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
+
+import { useIsMobile } from "@/hooks/use-mobile"
+import {
+ Card,
+ CardAction,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card"
+import {
+ ChartConfig,
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import {
+ ToggleGroup,
+ ToggleGroupItem,
+} from "@/components/ui/toggle-group"
+
+export const description = "An interactive area chart"
+
+const chartData = [
+ { date: "2024-04-01", desktop: 222, mobile: 150 },
+ { date: "2024-04-02", desktop: 97, mobile: 180 },
+ { date: "2024-04-03", desktop: 167, mobile: 120 },
+ { date: "2024-04-04", desktop: 242, mobile: 260 },
+ { date: "2024-04-05", desktop: 373, mobile: 290 },
+ { date: "2024-04-06", desktop: 301, mobile: 340 },
+ { date: "2024-04-07", desktop: 245, mobile: 180 },
+ { date: "2024-04-08", desktop: 409, mobile: 320 },
+ { date: "2024-04-09", desktop: 59, mobile: 110 },
+ { date: "2024-04-10", desktop: 261, mobile: 190 },
+ { date: "2024-04-11", desktop: 327, mobile: 350 },
+ { date: "2024-04-12", desktop: 292, mobile: 210 },
+ { date: "2024-04-13", desktop: 342, mobile: 380 },
+ { date: "2024-04-14", desktop: 137, mobile: 220 },
+ { date: "2024-04-15", desktop: 120, mobile: 170 },
+ { date: "2024-04-16", desktop: 138, mobile: 190 },
+ { date: "2024-04-17", desktop: 446, mobile: 360 },
+ { date: "2024-04-18", desktop: 364, mobile: 410 },
+ { date: "2024-04-19", desktop: 243, mobile: 180 },
+ { date: "2024-04-20", desktop: 89, mobile: 150 },
+ { date: "2024-04-21", desktop: 137, mobile: 200 },
+ { date: "2024-04-22", desktop: 224, mobile: 170 },
+ { date: "2024-04-23", desktop: 138, mobile: 230 },
+ { date: "2024-04-24", desktop: 387, mobile: 290 },
+ { date: "2024-04-25", desktop: 215, mobile: 250 },
+ { date: "2024-04-26", desktop: 75, mobile: 130 },
+ { date: "2024-04-27", desktop: 383, mobile: 420 },
+ { date: "2024-04-28", desktop: 122, mobile: 180 },
+ { date: "2024-04-29", desktop: 315, mobile: 240 },
+ { date: "2024-04-30", desktop: 454, mobile: 380 },
+ { date: "2024-05-01", desktop: 165, mobile: 220 },
+ { date: "2024-05-02", desktop: 293, mobile: 310 },
+ { date: "2024-05-03", desktop: 247, mobile: 190 },
+ { date: "2024-05-04", desktop: 385, mobile: 420 },
+ { date: "2024-05-05", desktop: 481, mobile: 390 },
+ { date: "2024-05-06", desktop: 498, mobile: 520 },
+ { date: "2024-05-07", desktop: 388, mobile: 300 },
+ { date: "2024-05-08", desktop: 149, mobile: 210 },
+ { date: "2024-05-09", desktop: 227, mobile: 180 },
+ { date: "2024-05-10", desktop: 293, mobile: 330 },
+ { date: "2024-05-11", desktop: 335, mobile: 270 },
+ { date: "2024-05-12", desktop: 197, mobile: 240 },
+ { date: "2024-05-13", desktop: 197, mobile: 160 },
+ { date: "2024-05-14", desktop: 448, mobile: 490 },
+ { date: "2024-05-15", desktop: 473, mobile: 380 },
+ { date: "2024-05-16", desktop: 338, mobile: 400 },
+ { date: "2024-05-17", desktop: 499, mobile: 420 },
+ { date: "2024-05-18", desktop: 315, mobile: 350 },
+ { date: "2024-05-19", desktop: 235, mobile: 180 },
+ { date: "2024-05-20", desktop: 177, mobile: 230 },
+ { date: "2024-05-21", desktop: 82, mobile: 140 },
+ { date: "2024-05-22", desktop: 81, mobile: 120 },
+ { date: "2024-05-23", desktop: 252, mobile: 290 },
+ { date: "2024-05-24", desktop: 294, mobile: 220 },
+ { date: "2024-05-25", desktop: 201, mobile: 250 },
+ { date: "2024-05-26", desktop: 213, mobile: 170 },
+ { date: "2024-05-27", desktop: 420, mobile: 460 },
+ { date: "2024-05-28", desktop: 233, mobile: 190 },
+ { date: "2024-05-29", desktop: 78, mobile: 130 },
+ { date: "2024-05-30", desktop: 340, mobile: 280 },
+ { date: "2024-05-31", desktop: 178, mobile: 230 },
+ { date: "2024-06-01", desktop: 178, mobile: 200 },
+ { date: "2024-06-02", desktop: 470, mobile: 410 },
+ { date: "2024-06-03", desktop: 103, mobile: 160 },
+ { date: "2024-06-04", desktop: 439, mobile: 380 },
+ { date: "2024-06-05", desktop: 88, mobile: 140 },
+ { date: "2024-06-06", desktop: 294, mobile: 250 },
+ { date: "2024-06-07", desktop: 323, mobile: 370 },
+ { date: "2024-06-08", desktop: 385, mobile: 320 },
+ { date: "2024-06-09", desktop: 438, mobile: 480 },
+ { date: "2024-06-10", desktop: 155, mobile: 200 },
+ { date: "2024-06-11", desktop: 92, mobile: 150 },
+ { date: "2024-06-12", desktop: 492, mobile: 420 },
+ { date: "2024-06-13", desktop: 81, mobile: 130 },
+ { date: "2024-06-14", desktop: 426, mobile: 380 },
+ { date: "2024-06-15", desktop: 307, mobile: 350 },
+ { date: "2024-06-16", desktop: 371, mobile: 310 },
+ { date: "2024-06-17", desktop: 475, mobile: 520 },
+ { date: "2024-06-18", desktop: 107, mobile: 170 },
+ { date: "2024-06-19", desktop: 341, mobile: 290 },
+ { date: "2024-06-20", desktop: 408, mobile: 450 },
+ { date: "2024-06-21", desktop: 169, mobile: 210 },
+ { date: "2024-06-22", desktop: 317, mobile: 270 },
+ { date: "2024-06-23", desktop: 480, mobile: 530 },
+ { date: "2024-06-24", desktop: 132, mobile: 180 },
+ { date: "2024-06-25", desktop: 141, mobile: 190 },
+ { date: "2024-06-26", desktop: 434, mobile: 380 },
+ { date: "2024-06-27", desktop: 448, mobile: 490 },
+ { date: "2024-06-28", desktop: 149, mobile: 200 },
+ { date: "2024-06-29", desktop: 103, mobile: 160 },
+ { date: "2024-06-30", desktop: 446, mobile: 400 },
+]
+
+const chartConfig = {
+ visitors: {
+ label: "Visitors",
+ },
+ desktop: {
+ label: "Desktop",
+ color: "var(--primary)",
+ },
+ mobile: {
+ label: "Mobile",
+ color: "var(--primary)",
+ },
+} satisfies ChartConfig
+
+export function ChartAreaInteractive() {
+ const isMobile = useIsMobile()
+ const [timeRange, setTimeRange] = React.useState("90d")
+
+ React.useEffect(() => {
+ if (isMobile) {
+ setTimeRange("7d")
+ }
+ }, [isMobile])
+
+ const filteredData = chartData.filter((item) => {
+ const date = new Date(item.date)
+ const referenceDate = new Date("2024-06-30")
+ let daysToSubtract = 90
+ if (timeRange === "30d") {
+ daysToSubtract = 30
+ } else if (timeRange === "7d") {
+ daysToSubtract = 7
+ }
+ const startDate = new Date(referenceDate)
+ startDate.setDate(startDate.getDate() - daysToSubtract)
+ return date >= startDate
+ })
+
+ return (
+
+
+ Total Visitors
+
+
+ Total for the last 3 months
+
+ Last 3 months
+
+
+
+ Last 3 months
+ Last 30 days
+ Last 7 days
+
+
+
+
+
+
+
+ Last 3 months
+
+
+ Last 30 days
+
+
+ Last 7 days
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ const date = new Date(value)
+ return date.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ })
+ }}
+ />
+ {
+ return new Date(value).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ })
+ }}
+ indicator="dot"
+ />
+ }
+ />
+
+
+
+
+
+
+ )
+}
diff --git a/components/data-table.tsx b/components/data-table.tsx
new file mode 100644
index 0000000..4834681
--- /dev/null
+++ b/components/data-table.tsx
@@ -0,0 +1,807 @@
+"use client"
+
+import * as React from "react"
+import {
+ closestCenter,
+ DndContext,
+ KeyboardSensor,
+ MouseSensor,
+ TouchSensor,
+ useSensor,
+ useSensors,
+ type DragEndEvent,
+ type UniqueIdentifier,
+} from "@dnd-kit/core"
+import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
+import {
+ arrayMove,
+ SortableContext,
+ useSortable,
+ verticalListSortingStrategy,
+} from "@dnd-kit/sortable"
+import { CSS } from "@dnd-kit/utilities"
+import {
+ IconChevronDown,
+ IconChevronLeft,
+ IconChevronRight,
+ IconChevronsLeft,
+ IconChevronsRight,
+ IconCircleCheckFilled,
+ IconDotsVertical,
+ IconGripVertical,
+ IconLayoutColumns,
+ IconLoader,
+ IconPlus,
+ IconTrendingUp,
+} from "@tabler/icons-react"
+import {
+ ColumnDef,
+ ColumnFiltersState,
+ flexRender,
+ getCoreRowModel,
+ getFacetedRowModel,
+ getFacetedUniqueValues,
+ getFilteredRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ Row,
+ SortingState,
+ useReactTable,
+ VisibilityState,
+} from "@tanstack/react-table"
+import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
+import { toast } from "sonner"
+import { z } from "zod"
+
+import { useIsMobile } from "@/hooks/use-mobile"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import {
+ ChartConfig,
+ ChartContainer,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart"
+import { Checkbox } from "@/components/ui/checkbox"
+import {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerTrigger,
+} from "@/components/ui/drawer"
+import {
+ DropdownMenu,
+ DropdownMenuCheckboxItem,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { Separator } from "@/components/ui/separator"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import {
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from "@/components/ui/tabs"
+
+export const schema = z.object({
+ id: z.number(),
+ header: z.string(),
+ type: z.string(),
+ status: z.string(),
+ target: z.string(),
+ limit: z.string(),
+ reviewer: z.string(),
+})
+
+// Create a separate component for the drag handle
+function DragHandle({ id }: { id: number }) {
+ const { attributes, listeners } = useSortable({
+ id,
+ })
+
+ return (
+
+
+ Drag to reorder
+
+ )
+}
+
+const columns: ColumnDef>[] = [
+ {
+ id: "drag",
+ header: () => null,
+ cell: ({ row }) => ,
+ },
+ {
+ id: "select",
+ header: ({ table }) => (
+
+ table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ />
+
+ ),
+ cell: ({ row }) => (
+
+ row.toggleSelected(!!value)}
+ aria-label="Select row"
+ />
+
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ },
+ {
+ accessorKey: "header",
+ header: "Header",
+ cell: ({ row }) => {
+ return
+ },
+ enableHiding: false,
+ },
+ {
+ accessorKey: "type",
+ header: "Section Type",
+ cell: ({ row }) => (
+
+
+ {row.original.type}
+
+
+ ),
+ },
+ {
+ accessorKey: "status",
+ header: "Status",
+ cell: ({ row }) => (
+
+ {row.original.status === "Done" ? (
+
+ ) : (
+
+ )}
+ {row.original.status}
+
+ ),
+ },
+ {
+ accessorKey: "target",
+ header: () => Target
,
+ cell: ({ row }) => (
+
+ ),
+ },
+ {
+ accessorKey: "limit",
+ header: () => Limit
,
+ cell: ({ row }) => (
+
+ ),
+ },
+ {
+ accessorKey: "reviewer",
+ header: "Reviewer",
+ cell: ({ row }) => {
+ const isAssigned = row.original.reviewer !== "Assign reviewer"
+
+ if (isAssigned) {
+ return row.original.reviewer
+ }
+
+ return (
+ <>
+
+ Reviewer
+
+
+
+
+
+
+ Eddie Lake
+
+ Jamik Tashpulatov
+
+
+
+ >
+ )
+ },
+ },
+ {
+ id: "actions",
+ cell: () => (
+
+
+
+
+ Open menu
+
+
+
+ Edit
+ Make a copy
+ Favorite
+
+ Delete
+
+
+ ),
+ },
+]
+
+function DraggableRow({ row }: { row: Row> }) {
+ const { transform, transition, setNodeRef, isDragging } = useSortable({
+ id: row.original.id,
+ })
+
+ return (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
+
+ )
+}
+
+export function DataTable({
+ data: initialData,
+}: {
+ data: z.infer[]
+}) {
+ const [data, setData] = React.useState(() => initialData)
+ const [rowSelection, setRowSelection] = React.useState({})
+ const [columnVisibility, setColumnVisibility] =
+ React.useState({})
+ const [columnFilters, setColumnFilters] = React.useState(
+ []
+ )
+ const [sorting, setSorting] = React.useState([])
+ const [pagination, setPagination] = React.useState({
+ pageIndex: 0,
+ pageSize: 10,
+ })
+ const sortableId = React.useId()
+ const sensors = useSensors(
+ useSensor(MouseSensor, {}),
+ useSensor(TouchSensor, {}),
+ useSensor(KeyboardSensor, {})
+ )
+
+ const dataIds = React.useMemo(
+ () => data?.map(({ id }) => id) || [],
+ [data]
+ )
+
+ const table = useReactTable({
+ data,
+ columns,
+ state: {
+ sorting,
+ columnVisibility,
+ rowSelection,
+ columnFilters,
+ pagination,
+ },
+ getRowId: (row) => row.id.toString(),
+ enableRowSelection: true,
+ onRowSelectionChange: setRowSelection,
+ onSortingChange: setSorting,
+ onColumnFiltersChange: setColumnFilters,
+ onColumnVisibilityChange: setColumnVisibility,
+ onPaginationChange: setPagination,
+ getCoreRowModel: getCoreRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFacetedRowModel: getFacetedRowModel(),
+ getFacetedUniqueValues: getFacetedUniqueValues(),
+ })
+
+ function handleDragEnd(event: DragEndEvent) {
+ const { active, over } = event
+ if (active && over && active.id !== over.id) {
+ setData((data) => {
+ const oldIndex = dataIds.indexOf(active.id)
+ const newIndex = dataIds.indexOf(over.id)
+ return arrayMove(data, oldIndex, newIndex)
+ })
+ }
+ }
+
+ return (
+
+
+
+ View
+
+
+
+
+
+
+ Outline
+ Past Performance
+ Key Personnel
+ Focus Documents
+
+
+
+ Outline
+
+ Past Performance 3
+
+
+ Key Personnel 2
+
+ Focus Documents
+
+
+
+
+
+
+ Customize Columns
+ Columns
+
+
+
+
+ {table
+ .getAllColumns()
+ .filter(
+ (column) =>
+ typeof column.accessorFn !== "undefined" &&
+ column.getCanHide()
+ )
+ .map((column) => {
+ return (
+
+ column.toggleVisibility(!!value)
+ }
+ >
+ {column.id}
+
+ )
+ })}
+
+
+
+
+ Add Section
+
+
+
+
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => {
+ return (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+
+ )
+ })}
+
+ ))}
+
+
+ {table.getRowModel().rows?.length ? (
+
+ {table.getRowModel().rows.map((row) => (
+
+ ))}
+
+ ) : (
+
+
+ No results.
+
+
+ )}
+
+
+
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "}
+ {table.getFilteredRowModel().rows.length} row(s) selected.
+
+
+
+
+ Rows per page
+
+ {
+ table.setPageSize(Number(value))
+ }}
+ >
+
+
+
+
+ {[10, 20, 30, 40, 50].map((pageSize) => (
+
+ {pageSize}
+
+ ))}
+
+
+
+
+ Page {table.getState().pagination.pageIndex + 1} of{" "}
+ {table.getPageCount()}
+
+
+ table.setPageIndex(0)}
+ disabled={!table.getCanPreviousPage()}
+ >
+ Go to first page
+
+
+ table.previousPage()}
+ disabled={!table.getCanPreviousPage()}
+ >
+ Go to previous page
+
+
+ table.nextPage()}
+ disabled={!table.getCanNextPage()}
+ >
+ Go to next page
+
+
+ table.setPageIndex(table.getPageCount() - 1)}
+ disabled={!table.getCanNextPage()}
+ >
+ Go to last page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const chartData = [
+ { month: "January", desktop: 186, mobile: 80 },
+ { month: "February", desktop: 305, mobile: 200 },
+ { month: "March", desktop: 237, mobile: 120 },
+ { month: "April", desktop: 73, mobile: 190 },
+ { month: "May", desktop: 209, mobile: 130 },
+ { month: "June", desktop: 214, mobile: 140 },
+]
+
+const chartConfig = {
+ desktop: {
+ label: "Desktop",
+ color: "var(--primary)",
+ },
+ mobile: {
+ label: "Mobile",
+ color: "var(--primary)",
+ },
+} satisfies ChartConfig
+
+function TableCellViewer({ item }: { item: z.infer }) {
+ const isMobile = useIsMobile()
+
+ return (
+
+
+
+ {item.header}
+
+
+
+
+ {item.header}
+
+ Showing total visitors for the last 6 months
+
+
+
+ {!isMobile && (
+ <>
+
+
+
+ value.slice(0, 3)}
+ hide
+ />
+ }
+ />
+
+
+
+
+
+
+
+ Trending up by 5.2% this month{" "}
+
+
+
+ Showing total visitors for the last 6 months. This is just
+ some random text to test the layout. It spans multiple lines
+ and should wrap around.
+
+
+
+ >
+ )}
+
+
+
+ Submit
+
+ Done
+
+
+
+
+ )
+}
diff --git a/components/nav-documents.tsx b/components/nav-documents.tsx
new file mode 100644
index 0000000..b551e71
--- /dev/null
+++ b/components/nav-documents.tsx
@@ -0,0 +1,92 @@
+"use client"
+
+import {
+ IconDots,
+ IconFolder,
+ IconShare3,
+ IconTrash,
+ type Icon,
+} from "@tabler/icons-react"
+
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import {
+ SidebarGroup,
+ SidebarGroupLabel,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ useSidebar,
+} from "@/components/ui/sidebar"
+
+export function NavDocuments({
+ items,
+}: {
+ items: {
+ name: string
+ url: string
+ icon: Icon
+ }[]
+}) {
+ const { isMobile } = useSidebar()
+
+ return (
+
+ Documents
+
+ {items.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+ More
+
+
+
+
+
+ Open
+
+
+
+ Share
+
+
+
+
+ Delete
+
+
+
+
+ ))}
+
+
+
+ More
+
+
+
+
+ )
+}
diff --git a/components/nav-main.tsx b/components/nav-main.tsx
new file mode 100644
index 0000000..87ac31f
--- /dev/null
+++ b/components/nav-main.tsx
@@ -0,0 +1,41 @@
+"use client"
+
+import { IconCirclePlusFilled, IconMail, type Icon } from "@tabler/icons-react"
+
+import { Button } from "@/components/ui/button"
+import {
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/ui/sidebar"
+import Link from "next/link"
+
+export function NavMain({
+ items,
+}: {
+ items: {
+ title: string
+ url: string
+ icon?: Icon
+ }[]
+}) {
+ return (
+
+
+
+ {items.map((item) => (
+
+
+
+ {item.icon && }
+ {item.title}
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/components/nav-secondary.tsx b/components/nav-secondary.tsx
new file mode 100644
index 0000000..3f3636f
--- /dev/null
+++ b/components/nav-secondary.tsx
@@ -0,0 +1,42 @@
+"use client"
+
+import * as React from "react"
+import { type Icon } from "@tabler/icons-react"
+
+import {
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+} from "@/components/ui/sidebar"
+
+export function NavSecondary({
+ items,
+ ...props
+}: {
+ items: {
+ title: string
+ url: string
+ icon: Icon
+ }[]
+} & React.ComponentPropsWithoutRef) {
+ return (
+
+
+
+ {items.map((item) => (
+
+
+
+
+ {item.title}
+
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/components/nav-user.tsx b/components/nav-user.tsx
new file mode 100644
index 0000000..e2d4ba6
--- /dev/null
+++ b/components/nav-user.tsx
@@ -0,0 +1,116 @@
+"use client"
+
+import {
+ IconCreditCard,
+ IconDotsVertical,
+ IconLogout,
+ IconNotification,
+ IconUserCircle,
+} from "@tabler/icons-react"
+
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+} from "@/components/ui/avatar"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import {
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ useSidebar,
+} from "@/components/ui/sidebar"
+import Link from "next/link"
+import { signOut } from "next-auth/react"
+
+export function NavUser({
+ user,
+}: {
+ user: {
+ id: string
+ username: string
+ email: string
+ role: 'COUPLE' | 'PLANNER' | 'GUEST'
+ } | undefined
+}) {
+ const { isMobile } = useSidebar()
+
+ if (user) {
+ return (
+
+
+
+
+
+
+ {user.username}
+
+ {user.email}
+
+
+
+
+
+
+
+
+
+ {user.username}
+
+ {user.email}
+
+
+
+
+
+
+
+
+
+ Account
+
+
+ {/*
+
+ Billing
+
+
+
+ Notifications
+ */}
+
+
+ signOut()}
+ className="hover:cursor-pointer"
+ >
+
+
+ Log out
+
+
+
+
+
+
+ )
+ } else {
+ return
+ }
+}
diff --git a/components/section-cards.tsx b/components/section-cards.tsx
new file mode 100644
index 0000000..f714d25
--- /dev/null
+++ b/components/section-cards.tsx
@@ -0,0 +1,102 @@
+import { IconTrendingDown, IconTrendingUp } from "@tabler/icons-react"
+
+import { Badge } from "@/components/ui/badge"
+import {
+ Card,
+ CardAction,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card"
+
+export function SectionCards() {
+ return (
+
+
+
+ Total Revenue
+
+ $1,250.00
+
+
+
+
+ +12.5%
+
+
+
+
+
+ Trending up this month
+
+
+ Visitors for the last 6 months
+
+
+
+
+
+ New Customers
+
+ 1,234
+
+
+
+
+ -20%
+
+
+
+
+
+ Down 20% this period
+
+
+ Acquisition needs attention
+
+
+
+
+
+ Active Accounts
+
+ 45,678
+
+
+
+
+ +12.5%
+
+
+
+
+
+ Strong user retention
+
+ Engagement exceed targets
+
+
+
+
+ Growth Rate
+
+ 4.5%
+
+
+
+
+ +4.5%
+
+
+
+
+
+ Steady performance increase
+
+ Meets growth projections
+
+
+
+ )
+}
diff --git a/components/site-header.tsx b/components/site-header.tsx
new file mode 100644
index 0000000..713e7bc
--- /dev/null
+++ b/components/site-header.tsx
@@ -0,0 +1,30 @@
+import { Button } from "@/components/ui/button"
+import { Separator } from "@/components/ui/separator"
+import { SidebarTrigger } from "@/components/ui/sidebar"
+
+export function SiteHeader() {
+ return (
+
+
+
+
+ {/*
Documents */}
+
+
+
+ )
+}
diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx
new file mode 100644
index 0000000..71e428b
--- /dev/null
+++ b/components/ui/avatar.tsx
@@ -0,0 +1,53 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+function Avatar({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarImage({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
new file mode 100644
index 0000000..0205413
--- /dev/null
+++ b/components/ui/badge.tsx
@@ -0,0 +1,46 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
+ destructive:
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function Badge({
+ className,
+ variant,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"span"> &
+ VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "span"
+
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..eb88f32
--- /dev/null
+++ b/components/ui/breadcrumb.tsx
@@ -0,0 +1,109 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
+ return
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
+ return (
+
+ )
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+function BreadcrumbLink({
+ asChild,
+ className,
+ ...props
+}: React.ComponentProps<"a"> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+function BreadcrumbSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) {
+ return (
+ svg]:size-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+ )
+}
+
+function BreadcrumbEllipsis({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+
+ More
+
+ )
+}
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000..a2df8dc
--- /dev/null
+++ b/components/ui/button.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000..d05bbc6
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,92 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/components/ui/chart.tsx b/components/ui/chart.tsx
new file mode 100644
index 0000000..97cc280
--- /dev/null
+++ b/components/ui/chart.tsx
@@ -0,0 +1,353 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+function ChartContainer({
+ id,
+ className,
+ children,
+ config,
+ ...props
+}: React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+}) {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+}
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+