diff --git a/src/app/components/dashboard-card.tsx b/src/app/components/dashboard-card.tsx
new file mode 100644
index 0000000..b60ddaa
--- /dev/null
+++ b/src/app/components/dashboard-card.tsx
@@ -0,0 +1,43 @@
+import type { ReactNode } from "react";
+import { Card } from "./ui/card";
+
+interface DashboardCardProps {
+ title: string;
+ value: string | number;
+ subtitle?: string;
+ change?: string;
+ icon: ReactNode;
+ badge?: string;
+}
+
+export function DashboardCard({
+ title,
+ value,
+ subtitle,
+ change,
+ icon,
+ badge,
+}: DashboardCardProps) {
+ return (
+
+
+
+
{title}
+
+
{value}
+ {badge && (
+
+ {badge}
+
+ )}
+
+ {subtitle &&
{subtitle}
}
+ {change &&
↗ {change}
}
+
+
+ {icon}
+
+
+
+ );
+}
diff --git a/src/app/components/dashboard-content.tsx b/src/app/components/dashboard-content.tsx
new file mode 100644
index 0000000..d65f14b
--- /dev/null
+++ b/src/app/components/dashboard-content.tsx
@@ -0,0 +1,279 @@
+import {
+ Calendar,
+ CheckCircle,
+ FileText,
+ MessageCircle,
+ Users,
+} from "lucide-react";
+import {
+ Bar,
+ BarChart,
+ CartesianGrid,
+ Cell,
+ Pie,
+ PieChart,
+ ResponsiveContainer,
+ XAxis,
+ YAxis,
+} from "recharts";
+import { DashboardCard } from "./dashboard-card";
+import { Card } from "./ui/card";
+
+const barChartData = [
+ { month: "Jan", value: 145 },
+ { month: "Feb", value: 165 },
+ { month: "Mar", value: 195 },
+ { month: "Apr", value: 155 },
+ { month: "Mei", value: 205 },
+ { month: "Jun", value: 185 },
+];
+
+const pieChartData = [
+ { name: "Puas", value: 25 },
+ { name: "Cukup", value: 25 },
+ { name: "Kurang", value: 25 },
+ { name: "Sangat puas", value: 25 },
+];
+
+const COLORS = ["#4E5BA6", "#F4C542", "#8CC63F", "#E57373"];
+
+const divisiData = [
+ { name: "Kesejahteraan", value: 37 },
+ { name: "Pemerintahan", value: 26 },
+ { name: "Keuangan", value: 17 },
+ { name: "Sekretaris Desa", value: 15 },
+];
+
+const eventData = [
+ { date: "1 Oktober 2025", title: "Hari Kesaktian Pancasila" },
+ { date: "15 Oktober 2025", title: "Davest" },
+ { date: "19 Oktober 2025", title: "Rapat Koordinasi" },
+];
+
+export function DashboardContent() {
+ return (
+
+ {/* Stats Cards */}
+
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ badge="87%"
+ />
+
+
+ {/* Charts Section */}
+
+ {/* Bar Chart */}
+
+
+
+
+ Statistik Pengajuan Surat
+
+
+ Trend pengajuan surat 6 bulan terakhir
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Pie Chart */}
+
+ Tingkat Kepuasan
+ Tingkat kepuasan layanan
+
+
+
+ {pieChartData.map((_entry, index) => (
+ |
+ ))}
+
+
+
+
+
+
+
+ {/* Bottom Section */}
+
+ {/* Divisi Teraktif */}
+
+
+
+
Divisi Teraktif
+
+
+ {divisiData.map((divisi, index) => (
+
+
+ {divisi.name}
+
+ {divisi.value} Kegiatan
+
+
+
+
+ ))}
+
+
+
+ {/* Kalender */}
+
+
+
+
+ Kalender & Kegiatan Mendatang
+
+
+
+ {eventData.map((event, index) => (
+
+
{event.date}
+
{event.title}
+
+ ))}
+
+
+
+
+ {/* APBDes Chart */}
+
+ Grafik APBDes
+
+
+
+ );
+}
diff --git a/src/app/components/figma/ImageWithFallback.tsx b/src/app/components/figma/ImageWithFallback.tsx
new file mode 100644
index 0000000..617f7eb
--- /dev/null
+++ b/src/app/components/figma/ImageWithFallback.tsx
@@ -0,0 +1,42 @@
+import type React from "react";
+import { useState } from "react";
+
+const ERROR_IMG_SRC =
+ "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg==";
+
+export function ImageWithFallback(
+ props: React.ImgHTMLAttributes,
+) {
+ const [didError, setDidError] = useState(false);
+
+ const handleError = () => {
+ setDidError(true);
+ };
+
+ const { src, alt, style, className, ...rest } = props;
+
+ return didError ? (
+
+
+

+
+
+ ) : (
+
+ );
+}
diff --git a/src/app/components/header.tsx b/src/app/components/header.tsx
new file mode 100644
index 0000000..8d74c99
--- /dev/null
+++ b/src/app/components/header.tsx
@@ -0,0 +1,89 @@
+import { useLocation } from "@tanstack/react-router";
+import { Bell, Moon, Sun, User } from "lucide-react";
+import { useTheme } from "next-themes";
+
+export function Header() {
+ const location = useLocation();
+ const { theme, setTheme } = useTheme();
+
+ // Define page titles based on route
+ const getPageTitle = () => {
+ switch (location.pathname) {
+ case "/":
+ return "Dashboard";
+ case "/kinerja-divisi":
+ return "Kinerja Divisi";
+ case "/pengaduan":
+ return "Pengaduan & Layanan Publik";
+ case "/analytic":
+ return "Jenna Analytic";
+ case "/demografi":
+ return "Demografi & Kependudukan";
+ case "/keuangan":
+ return "Keuangan & Anggaran";
+ case "/bumdes":
+ return "Bumdes & UMKM Desa";
+ case "/sosial":
+ return "Sosial";
+ case "/keamanan":
+ return "Keamanan";
+ case "/bantuan":
+ return "Bantuan";
+ case "/pengaturan":
+ return "Pengaturan";
+ default:
+ return "Dashboard";
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/components/kinerja-divisi.tsx b/src/app/components/kinerja-divisi.tsx
new file mode 100644
index 0000000..25a2386
--- /dev/null
+++ b/src/app/components/kinerja-divisi.tsx
@@ -0,0 +1,267 @@
+
+import { Badge } from "@/app/components/ui/badge";
+import { Button } from "@/app/components/ui/button"; // Correct import for Button
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/app/components/ui/card";
+import { Progress } from "@/app/components/ui/progress";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/app/components/ui/table";
+
+const KinerjaDivisi = () => {
+ // Sample data for division performance
+ const divisions = [
+ {
+ id: 1,
+ name: "Divisi Teknologi",
+ target: 95,
+ achievement: 87,
+ status: "On Track",
+ projects: 12,
+ budget: "Rp 2.5M",
+ lastUpdate: "2 days ago",
+ },
+ {
+ id: 2,
+ name: "Divisi Keuangan",
+ target: 90,
+ achievement: 92,
+ status: "Above Target",
+ projects: 8,
+ budget: "Rp 1.8M",
+ lastUpdate: "1 day ago",
+ },
+ {
+ id: 3,
+ name: "Divisi SDM",
+ target: 85,
+ achievement: 78,
+ status: "Needs Attention",
+ projects: 6,
+ budget: "Rp 1.2M",
+ lastUpdate: "3 days ago",
+ },
+ {
+ id: 4,
+ name: "Divisi Operasional",
+ target: 92,
+ achievement: 89,
+ status: "On Track",
+ projects: 15,
+ budget: "Rp 3.2M",
+ lastUpdate: "5 hours ago",
+ },
+ {
+ id: 5,
+ name: "Divisi Pemasaran",
+ target: 88,
+ achievement: 91,
+ status: "Above Target",
+ projects: 10,
+ budget: "Rp 2.1M",
+ lastUpdate: "1 day ago",
+ },
+ ];
+
+ return (
+
+
+
+ Kinerja Divisi
+
+
+
+
+
+
+
+
+
+
+ Total Divisi
+
+
+
+ 5
+ Jumlah divisi aktif
+
+
+
+
+
+
+ Rata-rata Pencapaian
+
+
+
+
+ 87.4%
+ Target tercapai
+
+
+
+
+
+
+ Divisi Melebihi Target
+
+
+
+
+ 2
+ Dari total 5 divisi
+
+
+
+
+
+
+ Detail Kinerja Divisi
+
+
+
+
+
+ Nama Divisi
+ Target (%)
+ Pencapaian (%)
+ Status
+ Proyek Aktif
+ Anggaran
+ Terakhir Diperbarui
+
+
+
+ {divisions.map((division) => (
+
+
+ {division.name}
+
+
+ {division.target}%
+
+
+ {division.achievement}%
+
+
+
+
+
+ {division.status}
+
+
+
+
+ {division.projects}
+
+
+ {division.budget}
+
+
+ {division.lastUpdate}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ Grafik Pencapaian Divisi
+
+
+
+
+ Grafik pencapaian akan ditampilkan di sini
+
+
+
+
+
+
+
+ Distribusi Anggaran Divisi
+
+
+
+
+ Diagram distribusi anggaran akan ditampilkan di sini
+
+
+
+
+
+
+ );
+};
+
+export default KinerjaDivisi;
diff --git a/src/app/components/pengaduan-layanan-publik.tsx b/src/app/components/pengaduan-layanan-publik.tsx
new file mode 100644
index 0000000..287a002
--- /dev/null
+++ b/src/app/components/pengaduan-layanan-publik.tsx
@@ -0,0 +1,412 @@
+import type React from "react";
+import { useState } from "react";
+import { Badge } from "@/app/components/ui/badge";
+import { Button } from "@/app/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/app/components/ui/card";
+import { Input } from "@/app/components/ui/input";
+import { Select } from "@/app/components/ui/select";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/app/components/ui/table";
+import { Textarea } from "@/app/components/ui/textarea";
+
+const PengaduanLayananPublik = () => {
+ const [activeTab, setActiveTab] = useState<"complaints" | "services">(
+ "complaints",
+ );
+ const [newComplaint, setNewComplaint] = useState({
+ title: "",
+ category: "",
+ description: "",
+ });
+
+ // Sample data for complaints
+ const complaints = [
+ {
+ id: 1,
+ title: "Jalan Rusak di Jalan Raya",
+ category: "Infrastruktur",
+ status: "Pending",
+ priority: "High",
+ date: "2024-02-01",
+ reporter: "Bapak Ahmad",
+ },
+ {
+ id: 2,
+ title: "Pemadaman Listrik Berkelanjutan",
+ category: "Utilitas",
+ status: "In Progress",
+ priority: "Medium",
+ date: "2024-02-03",
+ reporter: "Ibu Sari",
+ },
+ {
+ id: 3,
+ title: "Pelayanan Administrasi Lambat",
+ category: "Administrasi",
+ status: "Resolved",
+ priority: "Low",
+ date: "2024-01-28",
+ reporter: "Pak Joko",
+ },
+ {
+ id: 4,
+ title: "Kebersihan Lingkungan",
+ category: "Sanitasi",
+ status: "Pending",
+ priority: "Medium",
+ date: "2024-02-05",
+ reporter: "Bu Dewi",
+ },
+ ];
+
+ // Sample data for public services
+ const services = [
+ {
+ id: 1,
+ name: "Pembuatan KTP",
+ description:
+ "Pelayanan pembuatan Kartu Tanda Penduduk baru atau perpanjangan",
+ status: "Available",
+ category: "Administrasi",
+ lastUpdated: "2024-02-01",
+ },
+ {
+ id: 2,
+ name: "Pembuatan Surat Keterangan Usaha",
+ description: "Surat keterangan untuk keperluan usaha atau perizinan",
+ status: "Available",
+ category: "Administrasi",
+ lastUpdated: "2024-02-02",
+ },
+ {
+ id: 3,
+ name: "Pelayanan Kesehatan",
+ description: "Pelayanan kesehatan dasar di puskesmas desa",
+ status: "Available",
+ category: "Kesehatan",
+ lastUpdated: "2024-01-30",
+ },
+ {
+ id: 4,
+ name: "Program Bantuan Sosial",
+ description:
+ "Informasi dan pendaftaran program bantuan sosial dari pemerintah",
+ status: "Limited",
+ category: "Sosial",
+ lastUpdated: "2024-02-04",
+ },
+ ];
+
+ const handleInputChange = (
+ e: React.ChangeEvent,
+ ) => {
+ const { name, value } = e.target;
+ setNewComplaint((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ };
+
+ const handleSelectChange = (value: string | null) => {
+ setNewComplaint((prev) => ({
+ ...prev,
+ category: value || "", // Ensure category is always a string
+ }));
+ };
+
+ const handleSubmitComplaint = (e: React.FormEvent) => {
+ e.preventDefault();
+ console.log("Submitting complaint:", newComplaint);
+ // Here you would typically send the complaint to your backend
+ alert("Pengaduan berhasil dikirim!");
+ setNewComplaint({ title: "", category: "", description: "" });
+ };
+
+ return (
+
+
+
+ Pengaduan & Layanan Publik
+
+
+
+
+
+
+
+ {activeTab === "complaints" ? (
+
+ {/* Complaint Submission Form */}
+
+
+
+
+ Ajukan Pengaduan
+
+
+
+
+
+
+
+
+ {/* Complaints List */}
+
+
+
+
+ Daftar Pengaduan
+
+
+
+
+
+
+
+ Judul
+
+
+ Kategori
+
+
+ Status
+
+
+ Prioritas
+
+
+ Tanggal
+
+
+
+
+ {complaints.map((complaint) => (
+
+
+ {complaint.title}
+
+
+ {complaint.category}
+
+
+
+ {complaint.status}
+
+
+
+
+ {complaint.priority}
+
+
+
+ {complaint.date}
+
+
+ ))}
+
+
+
+
+
+
+ ) : (
+
+
+
+
+ Layanan Publik Tersedia
+
+
+
+
+ {services.map((service) => (
+
+
+
+ {service.name}
+
+
+
+
+ {service.description}
+
+
+
+ {service.status}
+
+
+ {service.category}
+
+
+
+ Terakhir diperbarui: {service.lastUpdated}
+
+
+
+ ))}
+
+
+
+
+
+
+
+ Statistik Layanan
+
+
+
+
+
+
+ Jumlah Layanan Tersedia
+
+
+ 12
+
+
+
+
+ Layanan Terpopuler
+
+
+ 4
+
+
+
+
+ Permintaan Baru
+
+
+ 23
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default PengaduanLayananPublik;
diff --git a/src/app/components/sidebar.tsx b/src/app/components/sidebar.tsx
new file mode 100644
index 0000000..62cc8eb
--- /dev/null
+++ b/src/app/components/sidebar.tsx
@@ -0,0 +1,82 @@
+import { Link, useLocation } from "@tanstack/react-router";
+import { Search } from "lucide-react";
+import { cn } from "./ui/utils";
+
+interface SidebarProps {
+ className?: string;
+}
+
+export function Sidebar({ className }: SidebarProps) {
+ const location = useLocation();
+
+ // Define menu items with their paths
+ const menuItems = [
+ { name: "Beranda", path: "/dashboard" },
+ { name: "Kinerja Divisi", path: "/dashboard/kinerja-divisi" },
+ { name: "Pengaduan & Layanan Publik", path: "/dashboard/pengaduan" },
+ { name: "Jenna Analytic", path: "/dashboard/analytic" },
+ { name: "Demografi & Kependudukan", path: "/dashboard/demografi" },
+ { name: "Keuangan & Anggaran", path: "/dashboard/keuangan" },
+ { name: "Bumdes & UMKM Desa", path: "/dashboard/bumdes" },
+ { name: "Sosial", path: "/dashboard/sosial" },
+ { name: "Keamanan", path: "/dashboard/keamanan" },
+ { name: "Bantuan", path: "/dashboard/bantuan" },
+ { name: "Pengaturan", path: "/dashboard/pengaturan" },
+ ];
+
+ return (
+
+ {/* Logo */}
+
+
+
+ Digitalisasi Desa Transparansi Kerja
+
+
+
+ {/* Search */}
+
+
+ {/* Menu Items */}
+
+
+ );
+}
diff --git a/src/app/components/ui/accordion.tsx b/src/app/components/ui/accordion.tsx
new file mode 100644
index 0000000..a7a42da
--- /dev/null
+++ b/src/app/components/ui/accordion.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import * as AccordionPrimitive from "@radix-ui/react-accordion";
+import { ChevronDownIcon } from "lucide-react";
+import type * as React from "react";
+
+import { cn } from "./utils";
+
+function Accordion({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function AccordionItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ svg]:rotate-180",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+ );
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/src/app/components/ui/alert-dialog.tsx b/src/app/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..1fd8d3f
--- /dev/null
+++ b/src/app/components/ui/alert-dialog.tsx
@@ -0,0 +1,167 @@
+"use client";
+
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
+import type * as React from "react";
+
+import { cn } from "./utils";
+
+function AlertDialog({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function AlertDialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ );
+}
+
+function AlertDialogHeader({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function AlertDialogFooter({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function AlertDialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+const baseClasses =
+ "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50";
+
+function AlertDialogAction({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogCancel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+};
diff --git a/src/app/components/ui/alert.tsx b/src/app/components/ui/alert.tsx
new file mode 100644
index 0000000..79032bf
--- /dev/null
+++ b/src/app/components/ui/alert.tsx
@@ -0,0 +1,66 @@
+import { cva, type VariantProps } from "class-variance-authority";
+import type * as React from "react";
+
+import { cn } from "./utils";
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
+ {
+ variants: {
+ variant: {
+ default: "bg-card text-card-foreground",
+ destructive:
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+function Alert({
+ className,
+ variant,
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ );
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function AlertDescription({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/src/app/components/ui/aspect-ratio.tsx b/src/app/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..51fa2c6
--- /dev/null
+++ b/src/app/components/ui/aspect-ratio.tsx
@@ -0,0 +1,11 @@
+"use client";
+
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
+
+function AspectRatio({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+export { AspectRatio };
diff --git a/src/app/components/ui/avatar.tsx b/src/app/components/ui/avatar.tsx
new file mode 100644
index 0000000..a781969
--- /dev/null
+++ b/src/app/components/ui/avatar.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
+import type * as React from "react";
+
+import { cn } from "./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/src/app/components/ui/badge.tsx b/src/app/components/ui/badge.tsx
new file mode 100644
index 0000000..71b553b
--- /dev/null
+++ b/src/app/components/ui/badge.tsx
@@ -0,0 +1,53 @@
+import {
+ Badge as MantineBadge,
+ type BadgeProps as MantineBadgeProps,
+} from "@mantine/core";
+
+import { cn } from "./utils";
+
+interface BadgeProps extends MantineBadgeProps {
+ variant?: "default" | "secondary" | "destructive" | "success";
+}
+
+const Badge = ({
+ className,
+ variant = "default",
+ children,
+ ...props
+}: BadgeProps) => {
+ let mantineVariant: MantineBadgeProps["variant"];
+ let mantineColor: MantineBadgeProps["color"];
+
+ switch (variant) {
+ case "secondary":
+ mantineVariant = "light";
+ mantineColor = "gray";
+ break;
+ case "destructive":
+ mantineVariant = "filled";
+ mantineColor = "red";
+ break;
+ case "success":
+ mantineVariant = "filled";
+ mantineColor = "green";
+ break;
+
+ default:
+ mantineVariant = "filled";
+ mantineColor = "blue"; // Placeholder, should align with primary color
+ break;
+ }
+
+ return (
+
+ {children}
+
+ );
+};
+
+export { Badge };
diff --git a/src/app/components/ui/breadcrumb.tsx b/src/app/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..930347d
--- /dev/null
+++ b/src/app/components/ui/breadcrumb.tsx
@@ -0,0 +1,109 @@
+import { Slot } from "@radix-ui/react-slot";
+import { ChevronRight, MoreHorizontal } from "lucide-react";
+import type * as React from "react";
+
+import { cn } from "./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/src/app/components/ui/button.tsx b/src/app/components/ui/button.tsx
new file mode 100644
index 0000000..ade3d4b
--- /dev/null
+++ b/src/app/components/ui/button.tsx
@@ -0,0 +1,73 @@
+import {
+ Button as MantineButton,
+ type ButtonProps as MantineButtonCoreProps,
+} from "@mantine/core";
+import React from "react";
+import { cn } from "./utils";
+
+// Define the props for our custom Button component
+// We want to allow all MantineButtonProps and also our custom variant and className.
+// Native HTML button attributes are generally passed through MantineButtonProps.
+interface ButtonProps extends MantineButtonCoreProps {
+ variant?:
+ | "default"
+ | "destructive"
+ | "outline"
+ | "secondary"
+ | "ghost"
+ | "link";
+ className?: string;
+ // Explicitly add common HTML button attributes if they are causing issues
+ type?: React.ButtonHTMLAttributes["type"];
+ onClick?: React.MouseEventHandler;
+}
+
+const Button = React.forwardRef(
+ ({ children, className, variant, type, onClick, ...props }, ref) => {
+ // Destructure type and onClick
+ let mantineVariant: MantineButtonCoreProps["variant"];
+ let mantineColor: MantineButtonCoreProps["color"];
+
+ switch (variant) {
+ case "destructive":
+ mantineVariant = "filled";
+ mantineColor = "red";
+ break;
+ case "outline":
+ mantineVariant = "outline";
+ break;
+ case "secondary":
+ mantineVariant = "default";
+ break;
+ case "ghost":
+ mantineVariant = "subtle";
+ break;
+ case "link":
+ mantineVariant = "transparent";
+ mantineColor = "blue"; // Assuming primary maps to blue in Mantine for now
+ break;
+ case "default":
+ default:
+ mantineVariant = "filled";
+ mantineColor = "blue"; // Assuming primary maps to blue in Mantine for now
+ break;
+ }
+
+ return (
+
+ {children}
+
+ );
+ },
+);
+Button.displayName = "Button";
+
+export { Button };
diff --git a/src/app/components/ui/calendar.tsx b/src/app/components/ui/calendar.tsx
new file mode 100644
index 0000000..ec25475
--- /dev/null
+++ b/src/app/components/ui/calendar.tsx
@@ -0,0 +1,79 @@
+"use client";
+
+import { ChevronLeft, ChevronRight } from "lucide-react";
+import type * as React from "react";
+import { DayPicker } from "react-day-picker";
+
+import { cn } from "./utils";
+
+const baseClasses =
+ "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50";
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+ .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
+ : "[&:has([aria-selected])]:rounded-md",
+ ),
+ day: cn(
+ baseClasses,
+ "hover:bg-accent hover:text-accent-foreground", // ghost variant styles
+ "size-8 p-0 font-normal aria-selected:opacity-100",
+ ),
+ day_range_start:
+ "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
+ day_range_end:
+ "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
+ day_selected:
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
+ day_today: "bg-accent text-accent-foreground",
+ day_outside:
+ "day-outside text-muted-foreground aria-selected:text-muted-foreground",
+ day_disabled: "text-muted-foreground opacity-50",
+ day_range_middle:
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
+ day_hidden: "invisible",
+ ...classNames,
+ }}
+ components={{
+ IconLeft: ({ className, ...props }) => (
+
+ ),
+ IconRight: ({ className, ...props }) => (
+
+ ),
+ }}
+ {...props}
+ />
+ );
+}
+
+export { Calendar };
diff --git a/src/app/components/ui/card.tsx b/src/app/components/ui/card.tsx
new file mode 100644
index 0000000..bac49f9
--- /dev/null
+++ b/src/app/components/ui/card.tsx
@@ -0,0 +1,56 @@
+import {
+ Box,
+ Card as MantineCard,
+ type CardProps as MantineCardProps,
+
+ Title,
+} from "@mantine/core";
+import type React from "react";
+import { cn } from "./utils";
+
+interface CardComponentProps extends MantineCardProps {
+ // Add any specific props you had in your custom Card component
+}
+
+interface CardHeaderProps extends React.HTMLAttributes {}
+interface CardTitleProps extends React.HTMLAttributes {}
+interface CardContentProps extends React.HTMLAttributes {}
+
+const Card = ({ className, children, ...props }: CardComponentProps) => (
+
+ {children}
+
+);
+
+const CardHeader = ({ className, children, ...props }: CardHeaderProps) => (
+
+ {children}
+
+);
+
+const CardTitle = ({ className, children, ...props }: CardTitleProps) => (
+
+ {children}
+
+);
+
+const CardContent = ({ className, children, ...props }: CardContentProps) => (
+
+ {children}
+
+);
+
+export { Card, CardHeader, CardTitle, CardContent };
diff --git a/src/app/components/ui/carousel.tsx b/src/app/components/ui/carousel.tsx
new file mode 100644
index 0000000..db3ddee
--- /dev/null
+++ b/src/app/components/ui/carousel.tsx
@@ -0,0 +1,240 @@
+"use client";
+
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react";
+import { ArrowLeft, ArrowRight } from "lucide-react";
+import * as React from "react";
+import { Button } from "./button";
+import { cn } from "./utils";
+
+type CarouselApi = UseEmblaCarouselType[1];
+type UseCarouselParameters = Parameters;
+type CarouselOptions = UseCarouselParameters[0];
+type CarouselPlugin = UseCarouselParameters[1];
+
+type CarouselProps = {
+ opts?: CarouselOptions;
+ plugins?: CarouselPlugin;
+ orientation?: "horizontal" | "vertical";
+ setApi?: (api: CarouselApi) => void;
+};
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0];
+ api: ReturnType[1];
+ scrollPrev: () => void;
+ scrollNext: () => void;
+ canScrollPrev: boolean;
+ canScrollNext: boolean;
+} & CarouselProps;
+
+const CarouselContext = React.createContext(null);
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext);
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ");
+ }
+
+ return context;
+}
+
+function Carousel({
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & CarouselProps) {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins,
+ );
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) return;
+ setCanScrollPrev(api.canScrollPrev());
+ setCanScrollNext(api.canScrollNext());
+ }, []);
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev();
+ }, [api]);
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext();
+ }, [api]);
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault();
+ scrollPrev();
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault();
+ scrollNext();
+ }
+ },
+ [scrollPrev, scrollNext],
+ );
+
+ React.useEffect(() => {
+ if (!api || !setApi) return;
+ setApi(api);
+ }, [api, setApi]);
+
+ React.useEffect(() => {
+ if (!api) return;
+ onSelect(api);
+ api.on("reInit", onSelect);
+ api.on("select", onSelect);
+
+ return () => {
+ api?.off("select", onSelect);
+ };
+ }, [api, onSelect]);
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
+ const { carouselRef, orientation } = useCarousel();
+
+ return (
+
+ );
+}
+
+function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
+ const { orientation } = useCarousel();
+
+ return (
+
+ );
+}
+
+function CarouselPrevious({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
+
+ return (
+
+ );
+}
+
+function CarouselNext({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
+
+ return (
+
+ );
+}
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+};
diff --git a/src/app/components/ui/chart.tsx b/src/app/components/ui/chart.tsx
new file mode 100644
index 0000000..6c0b8a6
--- /dev/null
+++ b/src/app/components/ui/chart.tsx
@@ -0,0 +1,353 @@
+"use client";
+
+import * as React from "react";
+import * as RechartsPrimitive from "recharts";
+
+import { cn } from "./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 (
+