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:
Yoga Pangestu 2026-07-14 09:56:51 +07:00
parent 152e994961
commit 0416e9191f
7 changed files with 737 additions and 107 deletions

View 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
View 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>
);
}

View File

@ -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;
} }
} }

View File

@ -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>

View 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>
);
}

View 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
View 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"
}
}