Update global styles and enhance onboarding page. Refactored CSS variables for a neutral color palette and improved styling consistency. Added routing functionality to the onboarding page and updated component styles for better visual coherence.
This commit is contained in:
parent
152e994961
commit
0416e9191f
20
src/app/dashboard/layout.tsx
Normal file
20
src/app/dashboard/layout.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Sidebar } from "@/components/layout/sidebar";
|
||||||
|
import { Header } from "@/components/layout/header";
|
||||||
|
|
||||||
|
export default function DashboardLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen bg-neutral-50">
|
||||||
|
<Sidebar />
|
||||||
|
<div className="flex-1 flex flex-col min-w-0">
|
||||||
|
<Header />
|
||||||
|
<main className="flex-1 overflow-y-auto p-6">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
222
src/app/dashboard/page.tsx
Normal file
222
src/app/dashboard/page.tsx
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
TrendingUp,
|
||||||
|
TrendingDown,
|
||||||
|
ShoppingCart,
|
||||||
|
Package,
|
||||||
|
Users,
|
||||||
|
DollarSign,
|
||||||
|
MoreHorizontal,
|
||||||
|
ArrowUpRight,
|
||||||
|
type LucideIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import dashboardData from "@/data/dashboard.json";
|
||||||
|
|
||||||
|
const ICON_MAP: Record<string, LucideIcon> = {
|
||||||
|
DollarSign,
|
||||||
|
ShoppingCart,
|
||||||
|
Package,
|
||||||
|
Users,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DashboardPage() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* KPI Cards */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||||
|
{dashboardData.kpiCards.map((card) => {
|
||||||
|
const Icon = ICON_MAP[card.icon];
|
||||||
|
return (
|
||||||
|
<Card key={card.title} className="border-neutral-100 rounded-lg" style={{ boxShadow: '0 1px 2px rgba(0,0,0,.05), 0 4px 10px rgba(0,0,0,.04)' }}>
|
||||||
|
<CardContent className="p-5">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm text-neutral-500 font-medium">{card.title}</p>
|
||||||
|
<p className="text-2xl font-bold text-neutral-900 tracking-tight">{card.value}</p>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{card.trend === "up" ? (
|
||||||
|
<div className="flex items-center gap-1 px-2 py-0.5 rounded-full bg-green-50">
|
||||||
|
<TrendingUp className="size-3 text-green-600" />
|
||||||
|
<span className="text-xs font-medium text-green-600">{card.change}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-1 px-2 py-0.5 rounded-full bg-red-50">
|
||||||
|
<TrendingDown className="size-3 text-red-600" />
|
||||||
|
<span className="text-xs font-medium text-red-600">{card.change}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="text-xs text-neutral-400">{card.period}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-10 h-10 rounded-md bg-neutral-100 flex items-center justify-center">
|
||||||
|
{Icon && <Icon className="size-5 text-neutral-600" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Charts Row */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
||||||
|
{/* Sales Chart */}
|
||||||
|
<Card className="lg:col-span-2 border-neutral-100 rounded-lg" style={{ boxShadow: '0 1px 2px rgba(0,0,0,.05), 0 4px 10px rgba(0,0,0,.04)' }}>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
||||||
|
<CardTitle className="text-base font-semibold text-neutral-900">{dashboardData.labels.salesChart}</CardTitle>
|
||||||
|
<div className="flex items-center gap-4 text-xs">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="w-3 h-3 rounded-sm bg-neutral-700" />
|
||||||
|
<span className="text-neutral-500">{dashboardData.labels.currentYear}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="w-3 h-3 rounded-sm bg-neutral-300" />
|
||||||
|
<span className="text-neutral-500">{dashboardData.labels.previousYear}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="h-[280px] flex items-end gap-2 px-2">
|
||||||
|
{dashboardData.chart.months.map((month, i) => (
|
||||||
|
<div key={month} className="flex-1 flex flex-col items-center gap-1">
|
||||||
|
<div className="w-full flex gap-0.5 items-end justify-center" style={{ height: "240px" }}>
|
||||||
|
<div
|
||||||
|
className="flex-1 bg-neutral-300 rounded-t-sm"
|
||||||
|
style={{ height: `${dashboardData.chart.previousYear[i]}%` }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="flex-1 bg-neutral-700 rounded-t-sm"
|
||||||
|
style={{ height: `${dashboardData.chart.currentYear[i]}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-neutral-400 mt-1">{month}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Donut Chart Widget */}
|
||||||
|
<Card className="border-neutral-100 rounded-lg" style={{ boxShadow: '0 1px 2px rgba(0,0,0,.05), 0 4px 10px rgba(0,0,0,.04)' }}>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
||||||
|
<CardTitle className="text-base font-semibold text-neutral-900">{dashboardData.labels.productCategories}</CardTitle>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-neutral-400">
|
||||||
|
<MoreHorizontal className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
{/* Category List */}
|
||||||
|
<div className="flex-1 space-y-3">
|
||||||
|
{dashboardData.productCategories.map((cat) => (
|
||||||
|
<div key={cat.name} className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className={`w-3 h-3 rounded-sm ${cat.color}`} />
|
||||||
|
<span className="text-sm text-neutral-600">{cat.name}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-neutral-900">{cat.value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Donut Visual */}
|
||||||
|
<div className="relative w-28 h-28 shrink-0">
|
||||||
|
<svg viewBox="0 0 100 100" className="w-full h-full -rotate-90">
|
||||||
|
<circle cx="50" cy="50" r="35" fill="none" stroke="#E5E5E5" strokeWidth="14" />
|
||||||
|
<circle cx="50" cy="50" r="35" fill="none" stroke="#171717" strokeWidth="14" strokeDasharray="99 121" strokeDashoffset="0" />
|
||||||
|
<circle cx="50" cy="50" r="35" fill="none" stroke="#525252" strokeWidth="14" strokeDasharray="66 154" strokeDashoffset="-99" />
|
||||||
|
<circle cx="50" cy="50" r="35" fill="none" stroke="#A3A3A3" strokeWidth="14" strokeDasharray="33 187" strokeDashoffset="-165" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tables Row */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
||||||
|
{/* Recent Transactions */}
|
||||||
|
<Card className="lg:col-span-2 border-neutral-100 rounded-lg" style={{ boxShadow: '0 1px 2px rgba(0,0,0,.05), 0 4px 10px rgba(0,0,0,.04)' }}>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
||||||
|
<CardTitle className="text-base font-semibold text-neutral-900">{dashboardData.labels.recentTransactions}</CardTitle>
|
||||||
|
<Button variant="ghost" size="sm" className="text-neutral-500 hover:text-neutral-700 h-8 gap-1">
|
||||||
|
{dashboardData.labels.viewAll}
|
||||||
|
<ArrowUpRight className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-neutral-100 bg-neutral-50">
|
||||||
|
<th className="text-left text-xs font-semibold text-neutral-500 uppercase tracking-wider px-5 py-3">{dashboardData.tableHeaders.id}</th>
|
||||||
|
<th className="text-left text-xs font-semibold text-neutral-500 uppercase tracking-wider px-5 py-3">{dashboardData.tableHeaders.customer}</th>
|
||||||
|
<th className="text-left text-xs font-semibold text-neutral-500 uppercase tracking-wider px-5 py-3">{dashboardData.tableHeaders.items}</th>
|
||||||
|
<th className="text-left text-xs font-semibold text-neutral-500 uppercase tracking-wider px-5 py-3">{dashboardData.tableHeaders.total}</th>
|
||||||
|
<th className="text-left text-xs font-semibold text-neutral-500 uppercase tracking-wider px-5 py-3">{dashboardData.tableHeaders.status}</th>
|
||||||
|
<th className="text-left text-xs font-semibold text-neutral-500 uppercase tracking-wider px-5 py-3">{dashboardData.tableHeaders.time}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{dashboardData.recentTransactions.map((trx) => (
|
||||||
|
<tr key={trx.id} className="border-b border-neutral-100 last:border-0 hover:bg-neutral-50 transition-colors">
|
||||||
|
<td className="px-5 py-3.5 text-sm font-medium text-neutral-900">{trx.id}</td>
|
||||||
|
<td className="px-5 py-3.5 text-sm text-neutral-700">{trx.customer}</td>
|
||||||
|
<td className="px-5 py-3.5 text-sm text-neutral-500">{trx.items} item</td>
|
||||||
|
<td className="px-5 py-3.5 text-sm font-medium text-neutral-900">{trx.total}</td>
|
||||||
|
<td className="px-5 py-3.5">
|
||||||
|
<span className={`inline-flex px-2 py-1 rounded-full text-xs font-medium ${
|
||||||
|
trx.status === "Selesai"
|
||||||
|
? "bg-green-50 text-green-700"
|
||||||
|
: trx.status === "Pending"
|
||||||
|
? "bg-yellow-50 text-yellow-700"
|
||||||
|
: "bg-red-50 text-red-700"
|
||||||
|
}`}>
|
||||||
|
{trx.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-5 py-3.5 text-sm text-neutral-500">{trx.time}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Top Products */}
|
||||||
|
<Card className="border-neutral-100 rounded-lg" style={{ boxShadow: '0 1px 2px rgba(0,0,0,.05), 0 4px 10px rgba(0,0,0,.04)' }}>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
||||||
|
<CardTitle className="text-base font-semibold text-neutral-900">{dashboardData.labels.topProducts}</CardTitle>
|
||||||
|
<Button variant="ghost" size="sm" className="text-neutral-500 hover:text-neutral-700 h-8 gap-1">
|
||||||
|
{dashboardData.labels.viewAll}
|
||||||
|
<ArrowUpRight className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="divide-y divide-neutral-100">
|
||||||
|
{dashboardData.topProducts.map((product, i) => (
|
||||||
|
<div key={product.name} className="flex items-center gap-4 px-5 py-3.5 hover:bg-neutral-50 transition-colors">
|
||||||
|
<div className="w-8 h-8 rounded-md bg-neutral-100 flex items-center justify-center shrink-0">
|
||||||
|
<span className="text-xs font-semibold text-neutral-500">{i + 1}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-neutral-900 truncate">{product.name}</p>
|
||||||
|
<p className="text-xs text-neutral-500">{product.sold} terjual</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium text-neutral-900">{product.revenue}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -5,39 +5,62 @@
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--card: oklch(1 0 0);
|
/* Neutral Palette */
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--neutral-50: #FAFAFA;
|
||||||
--popover: oklch(1 0 0);
|
--neutral-100: #F5F5F5;
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--neutral-200: #E5E5E5;
|
||||||
--primary: oklch(0.205 0 0);
|
--neutral-300: #D4D4D4;
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--neutral-400: #A3A3A3;
|
||||||
--secondary: oklch(0.97 0 0);
|
--neutral-500: #737373;
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--neutral-600: #525252;
|
||||||
--muted: oklch(0.97 0 0);
|
--neutral-700: #404040;
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
--neutral-800: #262626;
|
||||||
--accent: oklch(0.97 0 0);
|
--neutral-900: #171717;
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
/* Semantic Colors */
|
||||||
--destructive-foreground: #fafafa;
|
--success: #22C55E;
|
||||||
--border: oklch(0.922 0 0);
|
--warning: #F59E0B;
|
||||||
--input: oklch(0.922 0 0);
|
--error: #EF4444;
|
||||||
--ring: oklch(0.708 0 0);
|
--info: #3B82F6;
|
||||||
--radius: 0.625rem;
|
|
||||||
--background: oklch(1 0 0);
|
/* shadcn Tokens */
|
||||||
--foreground: oklch(0.145 0 0);
|
--card: #FFFFFF;
|
||||||
--chart-1: oklch(0.87 0 0);
|
--card-foreground: #171717;
|
||||||
--chart-2: oklch(0.556 0 0);
|
--popover: #FFFFFF;
|
||||||
--chart-3: oklch(0.439 0 0);
|
--popover-foreground: #171717;
|
||||||
--chart-4: oklch(0.371 0 0);
|
--primary: #171717;
|
||||||
--chart-5: oklch(0.269 0 0);
|
--primary-foreground: #FAFAFA;
|
||||||
--sidebar: oklch(0.985 0 0);
|
--secondary: #F5F5F5;
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
--secondary-foreground: #171717;
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
--muted: #F5F5F5;
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--muted-foreground: #737373;
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
--accent: #F5F5F5;
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--accent-foreground: #171717;
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
--destructive: #EF4444;
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
--destructive-foreground: #FAFAFA;
|
||||||
|
--border: #E5E5E5;
|
||||||
|
--input: #D4D4D4;
|
||||||
|
--ring: #525252;
|
||||||
|
--radius: 12px;
|
||||||
|
--background: #FAFAFA;
|
||||||
|
--foreground: #171717;
|
||||||
|
|
||||||
|
/* Chart Colors */
|
||||||
|
--chart-1: #404040;
|
||||||
|
--chart-2: #A3A3A3;
|
||||||
|
--chart-3: #D4D4D4;
|
||||||
|
--chart-4: #525252;
|
||||||
|
--chart-5: #737373;
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
--sidebar: #FFFFFF;
|
||||||
|
--sidebar-foreground: #171717;
|
||||||
|
--sidebar-primary: #171717;
|
||||||
|
--sidebar-primary-foreground: #FAFAFA;
|
||||||
|
--sidebar-accent: #F5F5F5;
|
||||||
|
--sidebar-accent-foreground: #171717;
|
||||||
|
--sidebar-border: #E5E5E5;
|
||||||
|
--sidebar-ring: #525252;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@ -77,17 +100,17 @@
|
|||||||
--color-chart-1: var(--chart-1);
|
--color-chart-1: var(--chart-1);
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
--color-popover: var(--popover);
|
--color-popover: var(--popover);
|
||||||
--radius-sm: calc(var(--radius) * 0.6);
|
--radius-sm: 8px;
|
||||||
--radius-md: calc(var(--radius) * 0.8);
|
--radius-md: 12px;
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: 16px;
|
||||||
--radius-xl: calc(var(--radius) * 1.4);
|
--radius-xl: 20px;
|
||||||
--radius-2xl: calc(var(--radius) * 1.8);
|
--radius-2xl: 24px;
|
||||||
--radius-3xl: calc(var(--radius) * 2.2);
|
--radius-3xl: 28px;
|
||||||
--radius-4xl: calc(var(--radius) * 2.6);
|
--radius-4xl: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-poppins), var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
font-family: var(--font-inter), var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
@ -103,51 +126,51 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: var(--border);
|
background: var(--neutral-300);
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--muted-foreground);
|
background: var(--neutral-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: var(--border) transparent;
|
scrollbar-color: var(--neutral-300) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
--background: #171717;
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: #FAFAFA;
|
||||||
--card: oklch(0.205 0 0);
|
--card: #2D2D2D;
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: #FAFAFA;
|
||||||
--popover: oklch(0.205 0 0);
|
--popover: #262626;
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: #FAFAFA;
|
||||||
--primary: oklch(0.922 0 0);
|
--primary: #FAFAFA;
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
--primary-foreground: #171717;
|
||||||
--secondary: oklch(0.269 0 0);
|
--secondary: #262626;
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: #FAFAFA;
|
||||||
--muted: oklch(0.269 0 0);
|
--muted: #262626;
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
--muted-foreground: #A3A3A3;
|
||||||
--accent: oklch(0.269 0 0);
|
--accent: #262626;
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: #FAFAFA;
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: #EF4444;
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: #404040;
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: #404040;
|
||||||
--ring: oklch(0.556 0 0);
|
--ring: #737373;
|
||||||
--chart-1: oklch(0.87 0 0);
|
--chart-1: #D4D4D4;
|
||||||
--chart-2: oklch(0.556 0 0);
|
--chart-2: #737373;
|
||||||
--chart-3: oklch(0.439 0 0);
|
--chart-3: #525252;
|
||||||
--chart-4: oklch(0.371 0 0);
|
--chart-4: #A3A3A3;
|
||||||
--chart-5: oklch(0.269 0 0);
|
--chart-5: #404040;
|
||||||
--sidebar: oklch(0.205 0 0);
|
--sidebar: #262626;
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--sidebar-foreground: #FAFAFA;
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--sidebar-primary: #FAFAFA;
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: #171717;
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
--sidebar-accent: #404040;
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar-accent-foreground: #FAFAFA;
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-border: #404040;
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
--sidebar-ring: #737373;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@ -160,4 +183,4 @@ body {
|
|||||||
html {
|
html {
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
@ -20,6 +21,7 @@ import { ArrowRightIcon, CheckCircle, ChevronRightIcon } from "lucide-react";
|
|||||||
import onboardingData from "@/data/onboarding.json";
|
import onboardingData from "@/data/onboarding.json";
|
||||||
|
|
||||||
export default function OnboardingPage() {
|
export default function OnboardingPage() {
|
||||||
|
const router = useRouter();
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const [selectedBusiness, setSelectedBusiness] = useState<string | null>(null);
|
const [selectedBusiness, setSelectedBusiness] = useState<string | null>(null);
|
||||||
const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
|
const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
|
||||||
@ -47,19 +49,20 @@ export default function OnboardingPage() {
|
|||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
console.log("Onboarding complete:", { selectedBusiness, form, selectedPlan });
|
console.log("Onboarding complete:", { selectedBusiness, form, selectedPlan });
|
||||||
|
router.push("/dashboard");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen lg:h-screen bg-background flex flex-col lg:flex-row lg:overflow-hidden">
|
<div className="min-h-screen lg:h-screen bg-neutral-50 flex flex-col lg:flex-row lg:overflow-hidden">
|
||||||
{/* Left Panel */}
|
{/* Left Panel */}
|
||||||
<div className="w-full lg:w-[60%] flex flex-col px-5 py-6 sm:px-6 sm:py-10 lg:px-20 lg:py-14 lg:h-full lg:overflow-hidden">
|
<div className="w-full lg:w-[60%] flex flex-col px-5 py-6 sm:px-6 sm:py-10 lg:px-20 lg:py-14 lg:h-full lg:overflow-hidden">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="mb-6 lg:mb-10 shrink-0">
|
<div className="mb-6 lg:mb-10 shrink-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
|
<div className="w-8 h-8 rounded-md bg-neutral-900 flex items-center justify-center">
|
||||||
<span className="text-primary-foreground font-bold text-sm">P</span>
|
<span className="text-white font-bold text-sm">P</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xl font-semibold tracking-tight text-foreground">
|
<span className="text-xl font-semibold tracking-tight text-neutral-900">
|
||||||
Profitra
|
Profitra
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -71,11 +74,11 @@ export default function OnboardingPage() {
|
|||||||
{[1, 2, 3].map((s) => (
|
{[1, 2, 3].map((s) => (
|
||||||
<div key={s} className="flex items-center gap-1.5">
|
<div key={s} className="flex items-center gap-1.5">
|
||||||
<div
|
<div
|
||||||
className={`h-1.5 rounded-full transition-all ${step === s ? "w-6 bg-primary" : "w-2 bg-muted-foreground/30"
|
className={`h-1.5 rounded-full transition-all ${step === s ? "w-6 bg-neutral-900" : "w-2 bg-neutral-300"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{step === s && (
|
{step === s && (
|
||||||
<span className="text-[11px] font-medium text-foreground whitespace-nowrap">
|
<span className="text-[11px] font-medium text-neutral-900 whitespace-nowrap">
|
||||||
{onboardingData.stepLabels[String(s) as keyof typeof onboardingData.stepLabels]}
|
{onboardingData.stepLabels[String(s) as keyof typeof onboardingData.stepLabels]}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -111,7 +114,7 @@ export default function OnboardingPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 mt-6 lg:mt-10 pt-6 border-t border-border shrink-0">
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 mt-6 lg:mt-10 pt-6 border-t border-neutral-100 shrink-0">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
size="lg"
|
||||||
@ -145,15 +148,15 @@ export default function OnboardingPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{step === 1 && !selectedBusiness && (
|
{step === 1 && !selectedBusiness && (
|
||||||
<p className="text-sm text-muted-foreground mt-3 shrink-0">
|
<p className="text-sm text-neutral-500 mt-3 shrink-0">
|
||||||
Silakan pilih jenis bisnis terlebih dahulu.
|
Silakan pilih jenis bisnis terlebih dahulu.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Panel */}
|
{/* Right Panel */}
|
||||||
<div className="hidden lg:flex w-[40%] relative border-l border-border overflow-hidden p-3">
|
<div className="hidden lg:flex w-[40%] relative border-l border-neutral-100 overflow-hidden p-3">
|
||||||
<div className="relative w-full h-full rounded-xl overflow-hidden">
|
<div className="relative w-full h-full rounded-lg overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src={onboardingData.rightImages[String(step) as keyof typeof onboardingData.rightImages]}
|
src={onboardingData.rightImages[String(step) as keyof typeof onboardingData.rightImages]}
|
||||||
alt="Onboarding illustration"
|
alt="Onboarding illustration"
|
||||||
@ -188,10 +191,10 @@ function StepOne({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-semibold tracking-tight text-foreground mb-2">
|
<h2 className="text-2xl font-semibold tracking-tight text-neutral-900 mb-2">
|
||||||
Pilih Jenis Bisnis Anda
|
Pilih Jenis Bisnis Anda
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground text-sm mb-8 leading-relaxed">
|
<p className="text-neutral-500 text-sm mb-8 leading-relaxed">
|
||||||
Profitra akan menyesuaikan fitur dan pengaturan awal berdasarkan jenis
|
Profitra akan menyesuaikan fitur dan pengaturan awal berdasarkan jenis
|
||||||
usaha yang Anda jalankan.
|
usaha yang Anda jalankan.
|
||||||
</p>
|
</p>
|
||||||
@ -201,9 +204,9 @@ function StepOne({
|
|||||||
<button
|
<button
|
||||||
key={type.label}
|
key={type.label}
|
||||||
onClick={() => onSelect(type.label)}
|
onClick={() => onSelect(type.label)}
|
||||||
className={`group relative aspect-4/3 rounded-lg overflow-hidden text-left transition-all duration-150 ${selected === type.label
|
className={`group relative aspect-4/3 rounded-md overflow-hidden text-left transition-all duration-150 ${selected === type.label
|
||||||
? "ring-2 ring-primary ring-offset-2 shadow-md"
|
? "ring-2 ring-neutral-900 ring-offset-2 shadow-md"
|
||||||
: "ring-1 ring-border hover:ring-foreground/30 hover:shadow-sm"
|
: "ring-1 ring-neutral-200 hover:ring-neutral-400 hover:shadow-sm"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
@ -220,8 +223,8 @@ function StepOne({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{selected === type.label && (
|
{selected === type.label && (
|
||||||
<div className="absolute top-2 right-2 w-5 h-5 rounded-full bg-primary flex items-center justify-center">
|
<div className="absolute top-2 right-2 w-5 h-5 rounded-full bg-neutral-900 flex items-center justify-center">
|
||||||
<CheckCircle className="size-3 text-primary-foreground" strokeWidth={3} />
|
<CheckCircle className="size-3 text-white" strokeWidth={3} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@ -252,10 +255,10 @@ function StepTwo({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-semibold tracking-tight text-foreground mb-2">
|
<h2 className="text-2xl font-semibold tracking-tight text-neutral-900 mb-2">
|
||||||
Lengkapi Informasi Bisnis
|
Lengkapi Informasi Bisnis
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground text-sm mb-8 leading-relaxed">
|
<p className="text-neutral-500 text-sm mb-8 leading-relaxed">
|
||||||
Masukkan informasi dasar mengenai usaha Anda.
|
Masukkan informasi dasar mengenai usaha Anda.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -363,10 +366,10 @@ function StepThree({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-semibold tracking-tight text-foreground mb-2">
|
<h2 className="text-2xl font-semibold tracking-tight text-neutral-900 mb-2">
|
||||||
Pilih Paket Profitra
|
Pilih Paket Profitra
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground text-sm mb-8 leading-relaxed">
|
<p className="text-neutral-500 text-sm mb-8 leading-relaxed">
|
||||||
Mulai gratis dan upgrade kapan saja sesuai perkembangan bisnis Anda.
|
Mulai gratis dan upgrade kapan saja sesuai perkembangan bisnis Anda.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -375,31 +378,32 @@ function StepThree({
|
|||||||
<Card
|
<Card
|
||||||
key={plan.name}
|
key={plan.name}
|
||||||
onClick={() => onSelect(plan.name)}
|
onClick={() => onSelect(plan.name)}
|
||||||
className={`relative cursor-pointer transition-all duration-150 ${selected === plan.name
|
className={`relative cursor-pointer transition-all duration-150 rounded-lg ${selected === plan.name
|
||||||
? "border-primary bg-primary/5 shadow-sm ring-1 ring-primary"
|
? "border-neutral-900 bg-neutral-50 shadow-sm ring-1 ring-neutral-900"
|
||||||
: "hover:border-foreground/20 hover:bg-accent/50"
|
: "border-neutral-100 hover:border-neutral-300 hover:bg-neutral-50"
|
||||||
}`}
|
}`}
|
||||||
|
style={{ boxShadow: selected === plan.name ? '0 1px 2px rgba(0,0,0,.05), 0 4px 10px rgba(0,0,0,.04)' : undefined }}
|
||||||
>
|
>
|
||||||
{plan.popular && (
|
{plan.popular && (
|
||||||
<Badge className="absolute -top-2.5 left-4">
|
<Badge className="absolute -top-2.5 left-4 bg-neutral-900 text-white">
|
||||||
Paling Populer
|
Paling Populer
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{plan.name}</CardTitle>
|
<CardTitle className="text-neutral-900">{plan.name}</CardTitle>
|
||||||
<div className="flex items-baseline gap-1">
|
<div className="flex items-baseline gap-1">
|
||||||
<span className="text-xl font-bold text-foreground">
|
<span className="text-xl font-bold text-neutral-900">
|
||||||
{plan.price}
|
{plan.price}
|
||||||
</span>
|
</span>
|
||||||
{plan.priceSub && (
|
{plan.priceSub && (
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-neutral-500">
|
||||||
{plan.priceSub}
|
{plan.priceSub}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{plan.description && (
|
{plan.description && (
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-neutral-500 mt-1">
|
||||||
{plan.description}
|
{plan.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -410,10 +414,10 @@ function StepThree({
|
|||||||
{plan.features.map((feature) => (
|
{plan.features.map((feature) => (
|
||||||
<li
|
<li
|
||||||
key={feature}
|
key={feature}
|
||||||
className="flex items-center gap-2 text-sm text-muted-foreground"
|
className="flex items-center gap-2 text-sm text-neutral-600"
|
||||||
>
|
>
|
||||||
<CheckCircle
|
<CheckCircle
|
||||||
className="size-3.5 text-primary shrink-0"
|
className="size-3.5 text-neutral-900 shrink-0"
|
||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
/>
|
/>
|
||||||
<span>{feature}</span>
|
<span>{feature}</span>
|
||||||
|
|||||||
158
src/components/layout/header.tsx
Normal file
158
src/components/layout/header.tsx
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
LayoutDashboard,
|
||||||
|
ShoppingCart,
|
||||||
|
Package,
|
||||||
|
Users,
|
||||||
|
BarChart3,
|
||||||
|
Settings,
|
||||||
|
LogOut,
|
||||||
|
Bell,
|
||||||
|
Calendar,
|
||||||
|
Search,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Sheet, SheetContent, SheetTrigger, SheetTitle } from "@/components/ui/sheet";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
|
const NAV_ITEMS = [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
href: "/dashboard",
|
||||||
|
icon: LayoutDashboard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "POS",
|
||||||
|
href: "/dashboard/pos",
|
||||||
|
icon: ShoppingCart,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Produk",
|
||||||
|
href: "/dashboard/products",
|
||||||
|
icon: Package,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Pelanggan",
|
||||||
|
href: "/dashboard/customers",
|
||||||
|
icon: Users,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Laporan",
|
||||||
|
href: "/dashboard/reports",
|
||||||
|
icon: BarChart3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Pengaturan",
|
||||||
|
href: "/dashboard/settings",
|
||||||
|
icon: Settings,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="flex items-center justify-between h-[72px] px-6 border-b border-neutral-100 bg-white shrink-0">
|
||||||
|
{/* Left: Mobile Menu + Page Title */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{/* Mobile Menu */}
|
||||||
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="xl:hidden text-neutral-600">
|
||||||
|
<Menu className="size-5" />
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent side="left" className="w-[260px] p-0 bg-white">
|
||||||
|
<div className="flex items-center gap-3 px-5 h-[72px] border-b border-neutral-100">
|
||||||
|
<div className="w-9 h-9 rounded-md bg-neutral-900 flex items-center justify-center">
|
||||||
|
<span className="text-white font-bold text-sm">P</span>
|
||||||
|
</div>
|
||||||
|
<SheetTitle className="text-lg font-semibold tracking-tight text-neutral-900">
|
||||||
|
Profitra
|
||||||
|
</SheetTitle>
|
||||||
|
</div>
|
||||||
|
<nav className="py-4 px-3 space-y-1">
|
||||||
|
{NAV_ITEMS.map((item) => {
|
||||||
|
const isActive = pathname === item.href;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 px-3 h-10 rounded-md text-sm transition-colors",
|
||||||
|
isActive
|
||||||
|
? "bg-neutral-100 text-neutral-900 font-semibold"
|
||||||
|
: "text-neutral-500 hover:bg-neutral-50 hover:text-neutral-700"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<item.icon className="size-5 shrink-0" />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
<div className="border-t border-neutral-100 p-3">
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="flex items-center gap-3 px-3 h-10 rounded-md text-sm text-neutral-500 hover:bg-neutral-50 hover:text-neutral-700 transition-colors"
|
||||||
|
>
|
||||||
|
<LogOut className="size-5 shrink-0" />
|
||||||
|
<span>Keluar</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
|
||||||
|
{/* Page Title */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-lg font-semibold text-neutral-900">
|
||||||
|
{NAV_ITEMS.find((item) => item.href === pathname)?.label || "Dashboard"}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Search, Date, Notifications, Profile */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Search - Hidden on mobile */}
|
||||||
|
<div className="hidden md:flex items-center relative">
|
||||||
|
<Search className="absolute left-3 size-4 text-neutral-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Cari..."
|
||||||
|
className="w-56 h-10 pl-9 bg-neutral-50 border-neutral-100 rounded-md text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Date Filter */}
|
||||||
|
<Button variant="outline" size="sm" className="hidden sm:flex h-10 gap-2 border-neutral-100 text-neutral-600 rounded-md">
|
||||||
|
<Calendar className="size-4" />
|
||||||
|
<span className="text-sm">Hari Ini</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Notifications */}
|
||||||
|
<Button variant="ghost" size="icon" className="relative text-neutral-500 hover:text-neutral-700 h-10 w-10">
|
||||||
|
<Bell className="size-5" />
|
||||||
|
<span className="absolute top-2 right-2 w-2 h-2 rounded-full bg-red-500" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* User Profile */}
|
||||||
|
<div className="flex items-center gap-3 pl-3 border-l border-neutral-100">
|
||||||
|
<div className="w-9 h-9 rounded-full bg-neutral-200 flex items-center justify-center">
|
||||||
|
<span className="text-xs font-semibold text-neutral-600">YP</span>
|
||||||
|
</div>
|
||||||
|
<div className="hidden xl:block">
|
||||||
|
<p className="text-sm font-medium text-neutral-900">Yoga Pangestu</p>
|
||||||
|
<p className="text-xs text-neutral-500">Admin</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
123
src/components/layout/sidebar.tsx
Normal file
123
src/components/layout/sidebar.tsx
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import {
|
||||||
|
LayoutDashboard,
|
||||||
|
ShoppingCart,
|
||||||
|
Package,
|
||||||
|
Users,
|
||||||
|
BarChart3,
|
||||||
|
Settings,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
LogOut,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
const NAV_ITEMS = [
|
||||||
|
{
|
||||||
|
label: "Dashboard",
|
||||||
|
href: "/dashboard",
|
||||||
|
icon: LayoutDashboard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "POS",
|
||||||
|
href: "/dashboard/pos",
|
||||||
|
icon: ShoppingCart,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Produk",
|
||||||
|
href: "/dashboard/products",
|
||||||
|
icon: Package,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Pelanggan",
|
||||||
|
href: "/dashboard/customers",
|
||||||
|
icon: Users,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Laporan",
|
||||||
|
href: "/dashboard/reports",
|
||||||
|
icon: BarChart3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Pengaturan",
|
||||||
|
href: "/dashboard/settings",
|
||||||
|
icon: Settings,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Sidebar() {
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
className={cn(
|
||||||
|
"hidden xl:flex flex-col h-screen bg-white border-r border-neutral-100 transition-all duration-300",
|
||||||
|
collapsed ? "w-[72px]" : "w-[260px]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="flex items-center gap-3 px-5 h-[72px] border-b border-neutral-100 shrink-0">
|
||||||
|
<div className="w-9 h-9 rounded-md bg-neutral-900 flex items-center justify-center shrink-0">
|
||||||
|
<span className="text-white font-bold text-sm">P</span>
|
||||||
|
</div>
|
||||||
|
{!collapsed && (
|
||||||
|
<span className="text-lg font-semibold tracking-tight text-neutral-900">
|
||||||
|
Profitra
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="flex-1 py-4 px-3 space-y-1 overflow-y-auto">
|
||||||
|
{NAV_ITEMS.map((item) => {
|
||||||
|
const isActive = pathname === item.href;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 px-3 h-10 rounded-md text-sm transition-colors",
|
||||||
|
isActive
|
||||||
|
? "bg-neutral-100 text-neutral-900 font-semibold"
|
||||||
|
: "text-neutral-500 hover:bg-neutral-50 hover:text-neutral-700"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<item.icon className="size-5 shrink-0" />
|
||||||
|
{!collapsed && <span>{item.label}</span>}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="border-t border-neutral-100 p-3 space-y-1 shrink-0">
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="flex items-center gap-3 px-3 h-10 rounded-md text-sm text-neutral-500 hover:bg-neutral-50 hover:text-neutral-700 transition-colors"
|
||||||
|
>
|
||||||
|
<LogOut className="size-5 shrink-0" />
|
||||||
|
{!collapsed && <span>Keluar</span>}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
className="w-full justify-center text-neutral-400 hover:text-neutral-600 h-10"
|
||||||
|
>
|
||||||
|
{collapsed ? (
|
||||||
|
<ChevronRight className="size-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronLeft className="size-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
src/data/dashboard.json
Normal file
80
src/data/dashboard.json
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"kpiCards": [
|
||||||
|
{
|
||||||
|
"title": "Total Penjualan",
|
||||||
|
"value": "Rp 12.450.000",
|
||||||
|
"change": "+12.5%",
|
||||||
|
"trend": "up",
|
||||||
|
"icon": "DollarSign",
|
||||||
|
"period": "vs bulan lalu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Total Transaksi",
|
||||||
|
"value": "1.248",
|
||||||
|
"change": "+8.2%",
|
||||||
|
"trend": "up",
|
||||||
|
"icon": "ShoppingCart",
|
||||||
|
"period": "vs bulan lalu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Produk Terjual",
|
||||||
|
"value": "3.456",
|
||||||
|
"change": "-2.4%",
|
||||||
|
"trend": "down",
|
||||||
|
"icon": "Package",
|
||||||
|
"period": "vs bulan lalu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Total Pelanggan",
|
||||||
|
"value": "567",
|
||||||
|
"change": "+24.3%",
|
||||||
|
"trend": "up",
|
||||||
|
"icon": "Users",
|
||||||
|
"period": "vs bulan lalu"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"recentTransactions": [
|
||||||
|
{ "id": "TRX-001", "customer": "Budi Santoso", "items": 3, "total": "Rp 125.000", "status": "Selesai", "time": "10:30" },
|
||||||
|
{ "id": "TRX-002", "customer": "Siti Rahayu", "items": 1, "total": "Rp 45.000", "status": "Selesai", "time": "10:15" },
|
||||||
|
{ "id": "TRX-003", "customer": "Ahmad Hidayat", "items": 5, "total": "Rp 287.500", "status": "Pending", "time": "09:45" },
|
||||||
|
{ "id": "TRX-004", "customer": "Dewi Lestari", "items": 2, "total": "Rp 98.000", "status": "Selesai", "time": "09:30" },
|
||||||
|
{ "id": "TRX-005", "customer": "Rudi Hermawan", "items": 4, "total": "Rp 175.000", "status": "Selesai", "time": "09:00" },
|
||||||
|
{ "id": "TRX-006", "customer": "Maya Putri", "items": 2, "total": "Rp 67.000", "status": "Dibatalkan", "time": "08:45" }
|
||||||
|
],
|
||||||
|
"topProducts": [
|
||||||
|
{ "name": "Kopi Arabica 250gr", "sold": 145, "revenue": "Rp 2.175.000" },
|
||||||
|
{ "name": "Roti Tawar Gandum", "sold": 132, "revenue": "Rp 792.000" },
|
||||||
|
{ "name": "Susu Fresh 1L", "sold": 98, "revenue": "Rp 686.000" },
|
||||||
|
{ "name": "Mie Instan Pack", "sold": 87, "revenue": "Rp 435.000" },
|
||||||
|
{ "name": "Air Mineral 600ml", "sold": 76, "revenue": "Rp 152.000" }
|
||||||
|
],
|
||||||
|
"productCategories": [
|
||||||
|
{ "name": "Makanan", "value": "45%", "color": "bg-neutral-900" },
|
||||||
|
{ "name": "Minuman", "value": "30%", "color": "bg-neutral-600" },
|
||||||
|
{ "name": "Snack", "value": "15%", "color": "bg-neutral-400" },
|
||||||
|
{ "name": "Lainnya", "value": "10%", "color": "bg-neutral-200" }
|
||||||
|
],
|
||||||
|
"chart": {
|
||||||
|
"months": ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"],
|
||||||
|
"currentYear": [45, 52, 48, 65, 58, 72, 68, 75, 82, 78, 85, 90],
|
||||||
|
"previousYear": [35, 42, 38, 55, 48, 62, 58, 65, 72, 68, 75, 80]
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"salesChart": "Grafik Penjualan",
|
||||||
|
"currentYear": "Tahun Ini",
|
||||||
|
"previousYear": "Tahun Lalu",
|
||||||
|
"productCategories": "Kategori Produk",
|
||||||
|
"recentTransactions": "Transaksi Terakhir",
|
||||||
|
"topProducts": "Produk Terlaris",
|
||||||
|
"viewAll": "Lihat Semua",
|
||||||
|
"vsLastMonth": "vs bulan lalu"
|
||||||
|
},
|
||||||
|
"tableHeaders": {
|
||||||
|
"id": "ID",
|
||||||
|
"customer": "Pelanggan",
|
||||||
|
"items": "Item",
|
||||||
|
"total": "Total",
|
||||||
|
"status": "Status",
|
||||||
|
"time": "Waktu"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user