style: apply consistent code formatting across project files

This commit is contained in:
Firman Ramdhani
2026-09-16 13:14:02 +07:00
parent d5791e759e
commit 51d05653c6
48 changed files with 3585 additions and 2575 deletions
+110 -52
View File
@@ -28,7 +28,7 @@ export default function BalancesPage() {
setLoading(true)
const [subRes, creRes] = await Promise.all([
getBalancesAsSubmittor(id),
getBalancesAsCreator(id)
getBalancesAsCreator(id),
])
setSubmittorBalances(subRes)
setCreatorBalances(creRes)
@@ -36,29 +36,33 @@ export default function BalancesPage() {
}
const formatRupiah = (n: number) =>
new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(n)
new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
maximumFractionDigits: 0,
}).format(n)
return (
<div className="space-y-6 animate-in fade-in duration-500 pb-12">
<div className="animate-in fade-in space-y-6 pb-12 duration-500">
<div>
<span className="text-xs font-semibold text-slate-400">Keuangan / Saldo</span>
<h2 className="text-xl font-black text-slate-900 dark:text-white flex items-center gap-2 tracking-tight">
<Wallet className="w-6 h-6 text-[#1B2CC1]" />
<h2 className="flex items-center gap-2 text-xl font-black tracking-tight text-slate-900 dark:text-white">
<Wallet className="h-6 w-6 text-[#1B2CC1]" />
Buku Saldo
</h2>
<p className="text-sm text-slate-500 mt-1">
<p className="mt-1 text-sm text-slate-500">
Pantau riwayat saldo lebih atau kurang dari transaksi pesanan.
</p>
</div>
<div className="flex bg-slate-100 dark:bg-slate-900 p-1 rounded-xl w-full max-w-sm border border-slate-200 dark:border-slate-800">
<div className="flex w-full max-w-sm rounded-xl border border-slate-200 bg-slate-100 p-1 dark:border-slate-800 dark:bg-slate-900">
<button
onClick={() => setActiveTab('SUBMITTOR')}
className={cn(
"flex-1 text-xs font-bold py-2 rounded-lg transition-all",
activeTab === 'SUBMITTOR'
? "bg-white dark:bg-slate-800 shadow-sm text-slate-900 dark:text-white"
: "text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
'flex-1 rounded-lg py-2 text-xs font-bold transition-all',
activeTab === 'SUBMITTOR'
? 'bg-white text-slate-900 shadow-sm dark:bg-slate-800 dark:text-white'
: 'text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'
)}
>
Saldo Saya (Penitip)
@@ -66,10 +70,10 @@ export default function BalancesPage() {
<button
onClick={() => setActiveTab('CREATOR')}
className={cn(
"flex-1 text-xs font-bold py-2 rounded-lg transition-all",
activeTab === 'CREATOR'
? "bg-white dark:bg-slate-800 shadow-sm text-slate-900 dark:text-white"
: "text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
'flex-1 rounded-lg py-2 text-xs font-bold transition-all',
activeTab === 'CREATOR'
? 'bg-white text-slate-900 shadow-sm dark:bg-slate-800 dark:text-white'
: 'text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'
)}
>
Saldo Orang (Kreator)
@@ -77,45 +81,71 @@ export default function BalancesPage() {
</div>
{loading ? (
<div className="flex flex-col items-center justify-center min-h-[30vh] gap-3">
<Loader2 className="animate-spin text-[#1B2CC1] w-8 h-8" />
<span className="text-xs text-slate-500 font-semibold">Memuat saldo...</span>
<div className="flex min-h-[30vh] flex-col items-center justify-center gap-3">
<Loader2 className="h-8 w-8 animate-spin text-[#1B2CC1]" />
<span className="text-xs font-semibold text-slate-500">Memuat saldo...</span>
</div>
) : activeTab === 'SUBMITTOR' ? (
<div className="space-y-4">
<div className="bg-blue-50 dark:bg-blue-950/30 p-4 rounded-xl border border-blue-100 dark:border-blue-900/50 mb-6">
<h3 className="font-bold text-blue-900 dark:text-blue-100 text-sm mb-1">Saldo Anda di Kreator Lain</h3>
<p className="text-xs text-blue-700 dark:text-blue-300 leading-relaxed">
Jika saldo <strong>positif</strong> (hijau), Anda memiliki deposit yang bisa digunakan untuk pesanan berikutnya di kreator tersebut. Jika <strong>negatif</strong> (merah), Anda berhutang.
<div className="mb-6 rounded-xl border border-blue-100 bg-blue-50 p-4 dark:border-blue-900/50 dark:bg-blue-950/30">
<h3 className="mb-1 text-sm font-bold text-blue-900 dark:text-blue-100">
Saldo Anda di Kreator Lain
</h3>
<p className="text-xs leading-relaxed text-blue-700 dark:text-blue-300">
Jika saldo <strong>positif</strong> (hijau), Anda memiliki deposit yang bisa digunakan
untuk pesanan berikutnya di kreator tersebut. Jika <strong>negatif</strong> (merah),
Anda berhutang.
</p>
</div>
{submittorBalances.length === 0 ? (
<div className="text-center p-12 rounded-3xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm">
<p className="text-xs text-slate-500 font-medium">Belum ada catatan saldo.</p>
<div className="rounded-3xl border-2 border-dashed border-slate-200 bg-white p-12 text-center shadow-sm dark:border-slate-800 dark:bg-slate-900">
<p className="text-xs font-medium text-slate-500">Belum ada catatan saldo.</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
{submittorBalances.map((b, i) => (
<Card key={i} className="rounded-2xl border-slate-200/90 dark:border-slate-800 overflow-hidden hover:border-[#1B2CC1]/30 transition-all shadow-sm">
<div className="p-4 flex items-center gap-3 bg-slate-50/50 dark:bg-slate-800/40 border-b border-slate-100 dark:border-slate-800">
<Card
key={i}
className="overflow-hidden rounded-2xl border-slate-200/90 shadow-sm transition-all hover:border-[#1B2CC1]/30 dark:border-slate-800"
>
<div className="flex items-center gap-3 border-b border-slate-100 bg-slate-50/50 p-4 dark:border-slate-800 dark:bg-slate-800/40">
{b.creator.photo ? (
<img src={b.creator.photo} alt={b.creator.name} className="w-10 h-10 rounded-full object-cover ring-2 ring-white dark:ring-slate-900 shadow-sm" />
<img
src={b.creator.photo}
alt={b.creator.name}
className="h-10 w-10 rounded-full object-cover shadow-sm ring-2 ring-white dark:ring-slate-900"
/>
) : (
<div className="w-10 h-10 rounded-full bg-[#1B2CC1]/10 text-[#1B2CC1] flex items-center justify-center font-bold text-sm ring-2 ring-white dark:ring-slate-900 shadow-sm">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-[#1B2CC1]/10 text-sm font-bold text-[#1B2CC1] shadow-sm ring-2 ring-white dark:ring-slate-900">
{b.creator.name.charAt(0).toUpperCase()}
</div>
)}
<div>
<p className="font-bold text-slate-900 dark:text-white text-sm">{b.creator.name}</p>
<p className="text-[10px] font-semibold text-slate-500 uppercase tracking-wider">Kreator</p>
<p className="text-sm font-bold text-slate-900 dark:text-white">
{b.creator.name}
</p>
<p className="text-[10px] font-semibold tracking-wider text-slate-500 uppercase">
Kreator
</p>
</div>
</div>
<CardContent className="p-4 bg-white dark:bg-slate-900">
<CardContent className="bg-white p-4 dark:bg-slate-900">
<div className="flex flex-col gap-1">
<span className="text-[11px] font-bold text-slate-400 uppercase tracking-wider">Total Saldo</span>
<div className={cn("text-xl font-black flex items-center gap-1.5", b.amount > 0 ? "text-emerald-600" : "text-rose-600")}>
{b.amount > 0 ? <ArrowUpRight className="w-5 h-5" /> : <ArrowDownRight className="w-5 h-5" />}
<span className="text-[11px] font-bold tracking-wider text-slate-400 uppercase">
Total Saldo
</span>
<div
className={cn(
'flex items-center gap-1.5 text-xl font-black',
b.amount > 0 ? 'text-emerald-600' : 'text-rose-600'
)}
>
{b.amount > 0 ? (
<ArrowUpRight className="h-5 w-5" />
) : (
<ArrowDownRight className="h-5 w-5" />
)}
{formatRupiah(b.amount)}
</div>
</div>
@@ -127,40 +157,68 @@ export default function BalancesPage() {
</div>
) : (
<div className="space-y-4">
<div className="bg-amber-50 dark:bg-amber-950/30 p-4 rounded-xl border border-amber-100 dark:border-amber-900/50 mb-6">
<h3 className="font-bold text-amber-900 dark:text-amber-100 text-sm mb-1">Saldo Orang Lain di Anda</h3>
<p className="text-xs text-amber-700 dark:text-amber-300 leading-relaxed">
Jika saldo <strong>positif</strong> (merah bagi Anda), artinya Anda memegang uang lebih milik penitip (Hutang Anda ke mereka). Jika <strong>negatif</strong> (hijau), mereka berhutang ke Anda.
<div className="mb-6 rounded-xl border border-amber-100 bg-amber-50 p-4 dark:border-amber-900/50 dark:bg-amber-950/30">
<h3 className="mb-1 text-sm font-bold text-amber-900 dark:text-amber-100">
Saldo Orang Lain di Anda
</h3>
<p className="text-xs leading-relaxed text-amber-700 dark:text-amber-300">
Jika saldo <strong>positif</strong> (merah bagi Anda), artinya Anda memegang uang
lebih milik penitip (Hutang Anda ke mereka). Jika <strong>negatif</strong> (hijau),
mereka berhutang ke Anda.
</p>
</div>
{creatorBalances.length === 0 ? (
<div className="text-center p-12 rounded-3xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm">
<p className="text-xs text-slate-500 font-medium">Belum ada penitip yang memiliki catatan saldo dengan Anda.</p>
<div className="rounded-3xl border-2 border-dashed border-slate-200 bg-white p-12 text-center shadow-sm dark:border-slate-800 dark:bg-slate-900">
<p className="text-xs font-medium text-slate-500">
Belum ada penitip yang memiliki catatan saldo dengan Anda.
</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
{creatorBalances.map((b, i) => (
<Card key={i} className="rounded-2xl border-slate-200/90 dark:border-slate-800 overflow-hidden hover:border-[#1B2CC1]/30 transition-all shadow-sm">
<div className="p-4 flex items-center gap-3 bg-slate-50/50 dark:bg-slate-800/40 border-b border-slate-100 dark:border-slate-800">
<Card
key={i}
className="overflow-hidden rounded-2xl border-slate-200/90 shadow-sm transition-all hover:border-[#1B2CC1]/30 dark:border-slate-800"
>
<div className="flex items-center gap-3 border-b border-slate-100 bg-slate-50/50 p-4 dark:border-slate-800 dark:bg-slate-800/40">
{b.user.photo ? (
<img src={b.user.photo} alt={b.user.name} className="w-10 h-10 rounded-full object-cover ring-2 ring-white dark:ring-slate-900 shadow-sm" />
<img
src={b.user.photo}
alt={b.user.name}
className="h-10 w-10 rounded-full object-cover shadow-sm ring-2 ring-white dark:ring-slate-900"
/>
) : (
<div className="w-10 h-10 rounded-full bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300 flex items-center justify-center font-bold text-sm ring-2 ring-white dark:ring-slate-900 shadow-sm">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-slate-200 text-sm font-bold text-slate-600 shadow-sm ring-2 ring-white dark:bg-slate-700 dark:text-slate-300 dark:ring-slate-900">
{b.user.name.charAt(0).toUpperCase()}
</div>
)}
<div>
<p className="font-bold text-slate-900 dark:text-white text-sm">{b.user.name}</p>
<p className="text-[10px] font-semibold text-slate-500 uppercase tracking-wider">Penitip</p>
<p className="text-sm font-bold text-slate-900 dark:text-white">
{b.user.name}
</p>
<p className="text-[10px] font-semibold tracking-wider text-slate-500 uppercase">
Penitip
</p>
</div>
</div>
<CardContent className="p-4 bg-white dark:bg-slate-900">
<CardContent className="bg-white p-4 dark:bg-slate-900">
<div className="flex flex-col gap-1">
<span className="text-[11px] font-bold text-slate-400 uppercase tracking-wider">Saldo Penitip</span>
<span className="text-[11px] font-bold tracking-wider text-slate-400 uppercase">
Saldo Penitip
</span>
{/* From Creator perspective: positive balance of submittor is bad for creator (creator holds their money) */}
<div className={cn("text-xl font-black flex items-center gap-1.5", b.amount > 0 ? "text-rose-600" : "text-emerald-600")}>
{b.amount > 0 ? <ArrowDownRight className="w-5 h-5" /> : <ArrowUpRight className="w-5 h-5" />}
<div
className={cn(
'flex items-center gap-1.5 text-xl font-black',
b.amount > 0 ? 'text-rose-600' : 'text-emerald-600'
)}
>
{b.amount > 0 ? (
<ArrowDownRight className="h-5 w-5" />
) : (
<ArrowUpRight className="h-5 w-5" />
)}
{formatRupiah(b.amount)}
</div>
</div>
+2 -6
View File
@@ -1,9 +1,5 @@
import { AppLayout } from "@/components/AppLayout";
import { AppLayout } from '@/components/AppLayout'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<AppLayout>
{children}
</AppLayout>
);
return <AppLayout>{children}</AppLayout>
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -16,18 +16,18 @@ export function PublicOrderActions({ order }: { order: any }) {
}
return (
<div className="flex flex-row justify-center items-center gap-2 mt-5">
<div className="mt-5 flex flex-row items-center justify-center gap-2">
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className="h-10 px-5 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold text-sm shadow-md shadow-[#1B2CC1]/20 gap-2 flex items-center justify-center transition-all cursor-pointer">
<ShoppingBag className="w-4 h-4" />
<DialogTrigger className="flex h-10 cursor-pointer items-center justify-center gap-2 rounded-xl bg-[#1B2CC1] px-5 text-sm font-bold text-white shadow-md shadow-[#1B2CC1]/20 transition-all hover:bg-[#15229E]">
<ShoppingBag className="h-4 w-4" />
Titip Sekarang
</DialogTrigger>
{open && <OrderFormModal order={order} onSuccess={() => setOpen(false)} />}
</Dialog>
<ShareButton
<ShareButton
orderId={order.id}
className="h-10 px-4 rounded-xl border-slate-200 dark:border-slate-700 font-bold text-sm gap-2 hover:bg-slate-50 dark:hover:bg-slate-800 shadow-sm text-slate-700 dark:text-slate-200"
className="h-10 gap-2 rounded-xl border-slate-200 px-4 text-sm font-bold text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
showText={true}
/>
</div>
+99 -62
View File
@@ -10,10 +10,14 @@ import { cn } from '@/lib/utils'
export const dynamic = 'force-dynamic'
export default async function PublicOrderDetailPage({ params }: { params: Promise<{ id: string }> }) {
export default async function PublicOrderDetailPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const session = await getSessionUser()
if (!session) {
redirect(`/login?callbackUrl=/order/${id}`)
}
@@ -25,48 +29,59 @@ export default async function PublicOrderDetailPage({ params }: { params: Promis
}
return (
<div className="space-y-6 max-w-4xl mx-auto pb-20 animate-in fade-in duration-500">
<div className="animate-in fade-in mx-auto max-w-4xl space-y-6 pb-20 duration-500">
<div className="flex items-center gap-3">
<Link href="/" className="p-2 -ml-2 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-500 transition-colors">
<ChevronLeft className="w-5 h-5" />
<Link
href="/"
className="-ml-2 rounded-xl p-2 text-slate-500 transition-colors hover:bg-slate-100 dark:hover:bg-slate-800"
>
<ChevronLeft className="h-5 w-5" />
</Link>
<h1 className="text-xl font-black text-slate-900 dark:text-white tracking-tight">Detail PO</h1>
<h1 className="text-xl font-black tracking-tight text-slate-900 dark:text-white">
Detail PO
</h1>
</div>
{/* Hero Card */}
<div className="relative overflow-hidden rounded-3xl bg-white dark:bg-slate-900 shadow-sm border border-slate-200/60 dark:border-slate-800">
<div className="absolute top-0 left-0 w-full h-24 bg-gradient-to-r from-[#1B2CC1] to-[#121E85]" />
<div className="relative pt-10 px-5 pb-5 sm:px-6 sm:pb-6 flex flex-col items-center text-center">
<div className="w-20 h-20 rounded-2xl bg-white dark:bg-slate-900 shadow-md border-4 border-white dark:border-slate-900 flex items-center justify-center mb-3">
<div className="relative overflow-hidden rounded-3xl border border-slate-200/60 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="absolute top-0 left-0 h-24 w-full bg-gradient-to-r from-[#1B2CC1] to-[#121E85]" />
<div className="relative flex flex-col items-center px-5 pt-10 pb-5 text-center sm:px-6 sm:pb-6">
<div className="mb-3 flex h-20 w-20 items-center justify-center rounded-2xl border-4 border-white bg-white shadow-md dark:border-slate-900 dark:bg-slate-900">
{order.creator.photo ? (
<img src={order.creator.photo} alt={order.creator.name} className="w-full h-full object-cover rounded-xl" />
<img
src={order.creator.photo}
alt={order.creator.name}
className="h-full w-full rounded-xl object-cover"
/>
) : (
<Store className="w-8 h-8 text-[#1B2CC1]" />
<Store className="h-8 w-8 text-[#1B2CC1]" />
)}
</div>
<h2 className="text-xl sm:text-2xl font-black text-slate-900 dark:text-white mb-2 leading-tight">
<h2 className="mb-2 text-xl leading-tight font-black text-slate-900 sm:text-2xl dark:text-white">
{order.title}
</h2>
<div className="flex flex-wrap justify-center items-center gap-3 text-sm font-semibold text-slate-600 dark:text-slate-400">
<span className="flex items-center gap-1.5 bg-slate-100 dark:bg-slate-800 px-3 py-1 rounded-full">
<User className="w-4 h-4 text-[#1B2CC1]" />
<div className="flex flex-wrap items-center justify-center gap-3 text-sm font-semibold text-slate-600 dark:text-slate-400">
<span className="flex items-center gap-1.5 rounded-full bg-slate-100 px-3 py-1 dark:bg-slate-800">
<User className="h-4 w-4 text-[#1B2CC1]" />
{order.creator.name}
</span>
<span className={cn(
"flex items-center gap-1.5 px-3 py-1 rounded-full border",
order.status === 'OPEN'
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-400 border-emerald-100 dark:border-emerald-900/50"
: "bg-rose-50 text-rose-700 dark:bg-rose-950/30 dark:text-rose-400 border-rose-100 dark:border-rose-900/50"
)}>
<CheckCircle2 className="w-4 h-4" />
<span
className={cn(
'flex items-center gap-1.5 rounded-full border px-3 py-1',
order.status === 'OPEN'
? 'border-emerald-100 bg-emerald-50 text-emerald-700 dark:border-emerald-900/50 dark:bg-emerald-950/30 dark:text-emerald-400'
: 'border-rose-100 bg-rose-50 text-rose-700 dark:border-rose-900/50 dark:bg-rose-950/30 dark:text-rose-400'
)}
>
<CheckCircle2 className="h-4 w-4" />
{order.status}
</span>
{order.allow_custom && (
<span className="flex items-center gap-1.5 bg-blue-50 text-blue-700 dark:bg-blue-950/30 dark:text-blue-400 px-3 py-1 rounded-full border border-blue-100 dark:border-blue-900/50">
<Sparkles className="w-4 h-4" />
<span className="flex items-center gap-1.5 rounded-full border border-blue-100 bg-blue-50 px-3 py-1 text-blue-700 dark:border-blue-900/50 dark:bg-blue-950/30 dark:text-blue-400">
<Sparkles className="h-4 w-4" />
Custom Item
</span>
)}
@@ -74,46 +89,54 @@ export default async function PublicOrderDetailPage({ params }: { params: Promis
<PublicOrderActions order={order} />
</div>
<div className="bg-slate-50 dark:bg-slate-800/50 border-t border-slate-100 dark:border-slate-800 p-4 sm:p-5 flex flex-col sm:flex-row divide-y sm:divide-y-0 sm:divide-x divide-slate-200 dark:divide-slate-700">
<div className="flex-1 py-3 sm:py-0 sm:px-4 text-center">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">Tanggal PO</p>
<div className="flex flex-col divide-y divide-slate-200 border-t border-slate-100 bg-slate-50 p-4 sm:flex-row sm:divide-x sm:divide-y-0 sm:p-5 dark:divide-slate-700 dark:border-slate-800 dark:bg-slate-800/50">
<div className="flex-1 py-3 text-center sm:px-4 sm:py-0">
<p className="mb-0.5 text-[10px] font-bold tracking-wider text-slate-400 uppercase">
Tanggal PO
</p>
<p className="text-base font-black text-slate-800 dark:text-slate-200">
{format(new Date(order.date), 'dd MMM yyyy', { locale: idLocale })}
</p>
</div>
<div className="flex-1 py-3 sm:py-0 sm:px-4 text-center">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">Total Orang</p>
<div className="flex-1 py-3 text-center sm:px-4 sm:py-0">
<p className="mb-0.5 text-[10px] font-bold tracking-wider text-slate-400 uppercase">
Total Orang
</p>
<p className="text-base font-black text-slate-800 dark:text-slate-200">
{order.submissions.length} <span className="text-xs font-semibold text-slate-500">menitip</span>
{order.submissions.length}{' '}
<span className="text-xs font-semibold text-slate-500">menitip</span>
</p>
</div>
<div className="flex-1 py-3 sm:py-0 sm:px-4 text-center">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">Menu Tersedia</p>
<div className="flex-1 py-3 text-center sm:px-4 sm:py-0">
<p className="mb-0.5 text-[10px] font-bold tracking-wider text-slate-400 uppercase">
Menu Tersedia
</p>
<p className="text-base font-black text-slate-800 dark:text-slate-200">
{order.available_items.length} <span className="text-xs font-semibold text-slate-500">item</span>
{order.available_items.length}{' '}
<span className="text-xs font-semibold text-slate-500">item</span>
</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
<div className="grid grid-cols-1 items-start gap-6 lg:grid-cols-3">
{/* Left 2 Cols: Daftar Penitip & Description */}
<div className="lg:col-span-2 space-y-6">
<div className="space-y-6 lg:col-span-2">
{order.description && (
<div className="bg-white dark:bg-slate-900 rounded-3xl border border-slate-200/60 dark:border-slate-800 shadow-sm overflow-hidden p-5 sm:p-6">
<h3 className="font-bold text-slate-900 dark:text-white mb-3">Deskripsi</h3>
<div className="text-slate-600 dark:text-slate-300 text-sm leading-relaxed whitespace-pre-wrap">
<div className="overflow-hidden rounded-3xl border border-slate-200/60 bg-white p-5 shadow-sm sm:p-6 dark:border-slate-800 dark:bg-slate-900">
<h3 className="mb-3 font-bold text-slate-900 dark:text-white">Deskripsi</h3>
<div className="text-sm leading-relaxed whitespace-pre-wrap text-slate-600 dark:text-slate-300">
{order.description}
</div>
</div>
)}
<div className="bg-white dark:bg-slate-900 rounded-3xl border border-slate-200/60 dark:border-slate-800 shadow-sm overflow-hidden">
<div className="p-5 sm:p-6 border-b border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/30 flex items-center justify-between">
<div className="overflow-hidden rounded-3xl border border-slate-200/60 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="flex items-center justify-between border-b border-slate-100 bg-slate-50/50 p-5 sm:p-6 dark:border-slate-800 dark:bg-slate-800/30">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-xl bg-[#1B2CC1]/10 text-[#1B2CC1] flex items-center justify-center">
<Users className="w-4 h-4" />
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-[#1B2CC1]/10 text-[#1B2CC1]">
<Users className="h-4 w-4" />
</div>
<h3 className="font-bold text-slate-900 dark:text-white">Daftar Penitip</h3>
</div>
@@ -122,28 +145,40 @@ export default async function PublicOrderDetailPage({ params }: { params: Promis
<div className="divide-y divide-slate-100 dark:divide-slate-800">
{order.submissions.length > 0 ? (
order.submissions.map((sub: any) => (
<div key={sub.id} className="p-4 sm:p-6 flex items-start gap-4 hover:bg-slate-50/50 dark:hover:bg-slate-800/20 transition-colors">
<div
key={sub.id}
className="flex items-start gap-4 p-4 transition-colors hover:bg-slate-50/50 sm:p-6 dark:hover:bg-slate-800/20"
>
{sub.user.photo ? (
<img src={sub.user.photo} alt={sub.user.name} className="w-10 h-10 rounded-full object-cover shrink-0" />
<img
src={sub.user.photo}
alt={sub.user.name}
className="h-10 w-10 shrink-0 rounded-full object-cover"
/>
) : (
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-500 flex items-center justify-center font-bold text-sm shrink-0">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-slate-100 text-sm font-bold text-slate-500 dark:bg-slate-800">
{sub.user.name.charAt(0).toUpperCase()}
</div>
)}
<div className="flex-1 min-w-0">
<h4 className="font-bold text-slate-800 dark:text-slate-200 text-sm mb-2">{sub.user.name}</h4>
<div className="min-w-0 flex-1">
<h4 className="mb-2 text-sm font-bold text-slate-800 dark:text-slate-200">
{sub.user.name}
</h4>
<div className="flex flex-wrap gap-1.5">
{sub.items.map((item: any) => (
<span
key={item.id}
className={`inline-flex items-center px-2 py-0.5 rounded-md text-[11px] font-medium border ${
item.is_custom
? 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-800'
: 'bg-slate-100 text-slate-700 border-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:border-slate-700'
<span
key={item.id}
className={`inline-flex items-center rounded-md border px-2 py-0.5 text-[11px] font-medium ${
item.is_custom
? 'border-purple-200 bg-purple-50 text-purple-700 dark:border-purple-800 dark:bg-purple-900/30 dark:text-purple-300'
: 'border-slate-200 bg-slate-100 text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300'
}`}
>
{item.qty}x {item.name} {item.note && <span className="italic ml-0.5 opacity-70">({item.note})</span>}
{item.qty}x {item.name}{' '}
{item.note && (
<span className="ml-0.5 italic opacity-70">({item.note})</span>
)}
</span>
))}
</div>
@@ -152,8 +187,10 @@ export default async function PublicOrderDetailPage({ params }: { params: Promis
))
) : (
<div className="p-12 text-center">
<p className="text-slate-500 font-medium">Belum ada yang menitip.</p>
<p className="text-xs text-slate-400 mt-1">Jadilah yang pertama untuk menitip pesanan!</p>
<p className="font-medium text-slate-500">Belum ada yang menitip.</p>
<p className="mt-1 text-xs text-slate-400">
Jadilah yang pertama untuk menitip pesanan!
</p>
</div>
)}
</div>
+35 -31
View File
@@ -11,37 +11,38 @@ export default async function Dashboard() {
const orders = await getAvailableOrders()
return (
<div className="space-y-8 animate-in fade-in duration-500">
<div className="animate-in fade-in space-y-8 duration-500">
{/* Banner / Hero Section */}
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-r from-[#1B2CC1] via-[#2135E0] to-[#121E85] p-6 sm:p-8 text-white shadow-xl shadow-[#1B2CC1]/15">
<div className="absolute right-0 top-0 -mt-10 -mr-10 h-64 w-64 rounded-full bg-white/10 blur-3xl pointer-events-none" />
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-r from-[#1B2CC1] via-[#2135E0] to-[#121E85] p-6 text-white shadow-xl shadow-[#1B2CC1]/15 sm:p-8">
<div className="pointer-events-none absolute top-0 right-0 -mt-10 -mr-10 h-64 w-64 rounded-full bg-white/10 blur-3xl" />
<div className="relative z-10 max-w-2xl space-y-3">
<div className="inline-flex items-center gap-2 rounded-full bg-white/15 px-3 py-1 text-xs font-bold text-blue-100 backdrop-blur-md border border-white/20">
<div className="inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/15 px-3 py-1 text-xs font-bold text-blue-100 backdrop-blur-md">
<Sparkles className="h-3.5 w-3.5" />
<span>Sistem Titip & Jastip Cepat</span>
</div>
<h2 className="text-2xl sm:text-3xl font-black tracking-tight leading-tight">
<h2 className="text-2xl leading-tight font-black tracking-tight sm:text-3xl">
Titip Makanan & Minuman Bareng Teman Kantor
</h2>
<p className="text-sm text-blue-100/90 leading-relaxed max-w-xl">
Pilih PO yang sedang buka hari ini, tentukan menu favoritmu, dan biarkan pembuat PO mengurus tagihan secara praktis.
<p className="max-w-xl text-sm leading-relaxed text-blue-100/90">
Pilih PO yang sedang buka hari ini, tentukan menu favoritmu, dan biarkan pembuat PO
mengurus tagihan secara praktis.
</p>
<div className="pt-2 flex flex-wrap gap-3">
<Link
<div className="flex flex-wrap gap-3 pt-2">
<Link
href="/my-orders"
className={cn(
buttonVariants(),
"rounded-xl bg-white text-[#1B2CC1] hover:bg-blue-50 font-bold shadow-md h-10 px-5 gap-2"
'h-10 gap-2 rounded-xl bg-white px-5 font-bold text-[#1B2CC1] shadow-md hover:bg-blue-50'
)}
>
<Plus className="w-4 h-4" />
<Plus className="h-4 w-4" />
Buka Jasa PO Baru
</Link>
<Link
<Link
href="/my-purchases"
className={cn(
buttonVariants({ variant: "outline" }),
"rounded-xl bg-white/10 hover:bg-white/20 text-white border-white/20 font-semibold h-10 px-4"
buttonVariants({ variant: 'outline' }),
'h-10 rounded-xl border-white/20 bg-white/10 px-4 font-semibold text-white hover:bg-white/20'
)}
>
Lihat Titipan Saya
@@ -51,46 +52,49 @@ export default async function Dashboard() {
</div>
{/* Main Grid Header */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
<div>
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full bg-[#1B2CC1] animate-pulse" />
<h2 className="text-xl font-black text-slate-900 dark:text-white tracking-tight">
<div className="h-2.5 w-2.5 animate-pulse rounded-full bg-[#1B2CC1]" />
<h2 className="text-xl font-black tracking-tight text-slate-900 dark:text-white">
Daftar PO Terbuka Hari Ini
</h2>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
<p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
Pesanan dengan status OPEN yang bisa Anda ikuti hari ini.
</p>
</div>
<span className="text-xs font-bold text-[#1B2CC1] bg-[#1B2CC1]/10 px-3 py-1.5 rounded-xl">
<span className="rounded-xl bg-[#1B2CC1]/10 px-3 py-1.5 text-xs font-bold text-[#1B2CC1]">
Total: {orders.length} PO Aktif
</span>
</div>
{orders.length === 0 ? (
<div className="flex flex-col items-center justify-center p-12 sm:p-16 text-center rounded-3xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm">
<div className="w-16 h-16 bg-[#1B2CC1]/10 rounded-2xl flex items-center justify-center mb-4 text-[#1B2CC1]">
<Store className="w-8 h-8" />
<div className="flex flex-col items-center justify-center rounded-3xl border-2 border-dashed border-slate-200 bg-white p-12 text-center shadow-sm sm:p-16 dark:border-slate-800 dark:bg-slate-900">
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-[#1B2CC1]/10 text-[#1B2CC1]">
<Store className="h-8 w-8" />
</div>
<h3 className="text-lg font-bold text-slate-800 dark:text-slate-200 mb-1">Belum Ada PO yang Dibuka Hari Ini</h3>
<p className="text-xs text-slate-500 max-w-md mb-6 leading-relaxed">
Mau jajan atau beli sesuatu? Jadilah orang pertama yang membuka jasa titip pesanan untuk teman-teman Anda!
<h3 className="mb-1 text-lg font-bold text-slate-800 dark:text-slate-200">
Belum Ada PO yang Dibuka Hari Ini
</h3>
<p className="mb-6 max-w-md text-xs leading-relaxed text-slate-500">
Mau jajan atau beli sesuatu? Jadilah orang pertama yang membuka jasa titip pesanan untuk
teman-teman Anda!
</p>
<Link
<Link
href="/my-orders"
className={cn(
buttonVariants(),
"bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold rounded-xl shadow-md shadow-[#1B2CC1]/25 h-10 px-5 gap-2"
'h-10 gap-2 rounded-xl bg-[#1B2CC1] px-5 font-bold text-white shadow-md shadow-[#1B2CC1]/25 hover:bg-[#15229E]'
)}
>
<Plus className="w-4 h-4" /> Buka PO Sekarang
<Plus className="h-4 w-4" /> Buka PO Sekarang
</Link>
</div>
) : (
<div className="flex flex-col h-full space-y-4">
<div className="flex flex-col space-y-4 max-h-[calc(100vh-260px)] overflow-y-auto pr-2 scrollbar-thin">
<div className="flex h-full flex-col space-y-4">
<div className="flex max-h-[calc(100vh-260px)] scrollbar-thin flex-col space-y-4 overflow-y-auto pr-2">
{orders.map((order: any) => (
<OrderCard key={order.id} order={order} />
))}
+180 -77
View File
@@ -6,20 +6,34 @@ import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/componen
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Loader2, Save, CheckCircle, ShieldCheck, UserCircle, KeyRound, LockKeyhole } from 'lucide-react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import {
Loader2,
Save,
CheckCircle,
ShieldCheck,
UserCircle,
KeyRound,
LockKeyhole,
} from 'lucide-react'
export default function ProfilePage() {
const [user, setUser] = useState<any>(null)
const [name, setName] = useState('')
const [photo, setPhoto] = useState('')
const [loading, setLoading] = useState(true)
// Profile state
const [savingProfile, setSavingProfile] = useState(false)
const [profileError, setProfileError] = useState('')
const [profileSuccess, setProfileSuccess] = useState(false)
// Password state
const [oldPassword, setOldPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
@@ -50,13 +64,13 @@ export default function ProfilePage() {
setProfileError('Nama wajib diisi')
return
}
setProfileError('')
setProfileSuccess(false)
setSavingProfile(true)
const res = await updateProfile(user.id, name.trim(), photo.trim() || null)
if (res.success) {
setUser(res.user)
setProfileSuccess(true)
@@ -104,167 +118,256 @@ export default function ProfilePage() {
if (loading) {
return (
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-3">
<Loader2 className="w-8 h-8 animate-spin text-[#1B2CC1]" />
<span className="text-xs text-slate-500 font-semibold">Memuat profil...</span>
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-3">
<Loader2 className="h-8 w-8 animate-spin text-[#1B2CC1]" />
<span className="text-xs font-semibold text-slate-500">Memuat profil...</span>
</div>
)
}
if (!user) {
return (
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-3">
<span className="text-xs text-slate-500 font-semibold">Anda belum login.</span>
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-3">
<span className="text-xs font-semibold text-slate-500">Anda belum login.</span>
</div>
)
}
return (
<div className="space-y-6 animate-in fade-in duration-500 pb-12 max-w-5xl">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="animate-in fade-in max-w-5xl space-y-6 pb-12 duration-500">
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Left 2 Cols: Forms */}
<div className="lg:col-span-2 space-y-6">
<div className="space-y-6 lg:col-span-2">
{/* PROFILE FORM */}
<Card className="rounded-3xl border border-slate-200/90 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
<div className="p-6 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between">
<Card className="overflow-hidden rounded-3xl border border-slate-200/90 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="flex items-center justify-between border-b border-slate-100 p-6 dark:border-slate-800">
<div>
<h2 className="text-lg font-black text-slate-900 dark:text-white">Informasi Akun</h2>
<p className="text-xs text-slate-500 mt-0.5">Ubah nama tampilan dan foto profil Anda.</p>
<h2 className="text-lg font-black text-slate-900 dark:text-white">
Informasi Akun
</h2>
<p className="mt-0.5 text-xs text-slate-500">
Ubah nama tampilan dan foto profil Anda.
</p>
</div>
<span className="text-[11px] font-bold text-[#1B2CC1] bg-[#1B2CC1]/10 px-2.5 py-1 rounded-full uppercase tracking-wider">
<span className="rounded-full bg-[#1B2CC1]/10 px-2.5 py-1 text-[11px] font-bold tracking-wider text-[#1B2CC1] uppercase">
{user.role}
</span>
</div>
<form onSubmit={handleSaveProfile}>
<CardContent className="p-6 space-y-6">
<CardContent className="space-y-6 p-6">
<div className="space-y-2">
<Label className="text-xs font-bold uppercase tracking-wider text-slate-700 dark:text-slate-300">Foto Profil</Label>
<div className="flex flex-col sm:flex-row items-center gap-5 p-5 rounded-2xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/30">
<Label className="text-xs font-bold tracking-wider text-slate-700 uppercase dark:text-slate-300">
Foto Profil
</Label>
<div className="flex flex-col items-center gap-5 rounded-2xl border-2 border-dashed border-slate-200 bg-slate-50/50 p-5 sm:flex-row dark:border-slate-800 dark:bg-slate-800/30">
{photo ? (
<img src={photo} alt={name} className="w-20 h-20 rounded-2xl object-cover ring-4 ring-white dark:ring-slate-700 shadow-md shrink-0" />
<img
src={photo}
alt={name}
className="h-20 w-20 shrink-0 rounded-2xl object-cover shadow-md ring-4 ring-white dark:ring-slate-700"
/>
) : (
<div className="w-20 h-20 rounded-2xl bg-[#1B2CC1]/10 text-[#1B2CC1] flex items-center justify-center font-bold text-2xl ring-4 ring-white dark:ring-slate-700 shadow-sm shrink-0">
{name ? name.charAt(0).toUpperCase() : <UserCircle className="w-10 h-10" />}
<div className="flex h-20 w-20 shrink-0 items-center justify-center rounded-2xl bg-[#1B2CC1]/10 text-2xl font-bold text-[#1B2CC1] shadow-sm ring-4 ring-white dark:ring-slate-700">
{name ? name.charAt(0).toUpperCase() : <UserCircle className="h-10 w-10" />}
</div>
)}
<div className="space-y-1.5 flex-1 w-full text-center sm:text-left">
<p className="text-xs font-bold text-slate-800 dark:text-slate-200">Pratinjau Avatar</p>
<p className="text-[11px] text-slate-400">Masukkan tautan URL foto gambar di bawah untuk memperbarui gambar profil.</p>
<div className="w-full flex-1 space-y-1.5 text-center sm:text-left">
<p className="text-xs font-bold text-slate-800 dark:text-slate-200">
Pratinjau Avatar
</p>
<p className="text-[11px] text-slate-400">
Masukkan tautan URL foto gambar di bawah untuk memperbarui gambar profil.
</p>
</div>
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="prof-name" className="text-xs font-bold uppercase tracking-wider text-slate-700 dark:text-slate-300">
<Label
htmlFor="prof-name"
className="text-xs font-bold tracking-wider text-slate-700 uppercase dark:text-slate-300"
>
Nama Tampilan <span className="text-red-500">*</span>
</Label>
<Input id="prof-name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Contoh: Budi Santoso" className="h-11 rounded-xl" />
<Input
id="prof-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Contoh: Budi Santoso"
className="h-11 rounded-xl"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="prof-photo" className="text-xs font-bold uppercase tracking-wider text-slate-700 dark:text-slate-300">
<Label
htmlFor="prof-photo"
className="text-xs font-bold tracking-wider text-slate-700 uppercase dark:text-slate-300"
>
URL Foto Profil
</Label>
<Input id="prof-photo" value={photo} onChange={(e) => setPhoto(e.target.value)} placeholder="https://..." className="h-11 rounded-xl" />
<Input
id="prof-photo"
value={photo}
onChange={(e) => setPhoto(e.target.value)}
placeholder="https://..."
className="h-11 rounded-xl"
/>
</div>
{profileError && (
<div className="p-3 bg-red-50 text-red-600 rounded-xl text-xs font-semibold">{profileError}</div>
<div className="rounded-xl bg-red-50 p-3 text-xs font-semibold text-red-600">
{profileError}
</div>
)}
{profileSuccess && (
<div className="p-3 bg-emerald-50 text-emerald-700 rounded-xl text-xs font-bold flex items-center gap-2">
<CheckCircle className="w-4 h-4" /><span>Profil berhasil diperbarui!</span>
<div className="flex items-center gap-2 rounded-xl bg-emerald-50 p-3 text-xs font-bold text-emerald-700">
<CheckCircle className="h-4 w-4" />
<span>Profil berhasil diperbarui!</span>
</div>
)}
</CardContent>
<div className="p-6 bg-slate-50/60 border-t border-slate-100 flex justify-end">
<Button type="submit" disabled={savingProfile} className="h-11 px-6 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold">
{savingProfile ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Save className="w-4 h-4 mr-2" />}
<div className="flex justify-end border-t border-slate-100 bg-slate-50/60 p-6">
<Button
type="submit"
disabled={savingProfile}
className="h-11 rounded-xl bg-[#1B2CC1] px-6 font-bold text-white hover:bg-[#15229E]"
>
{savingProfile ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Save className="mr-2 h-4 w-4" />
)}
Simpan Profil
</Button>
</div>
</form>
</Card>
</div>
{/* Right 1 Col: Account Details */}
<div className="space-y-6">
<Card className="rounded-3xl border border-slate-200/90 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm p-6 space-y-4">
<div className="flex items-center gap-2 text-slate-800 dark:text-slate-200 font-bold text-sm mb-0">
<ShieldCheck className="w-4 h-4 text-[#1B2CC1]" />
<Card className="space-y-4 rounded-3xl border border-slate-200/90 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="mb-0 flex items-center gap-2 text-sm font-bold text-slate-800 dark:text-slate-200">
<ShieldCheck className="h-4 w-4 text-[#1B2CC1]" />
<span>Detail Akun</span>
</div>
<div className="space-y-2 pt-2 border-t border-slate-100 dark:border-slate-800">
<div className="space-y-2 border-t border-slate-100 pt-2 dark:border-slate-800">
<div className="space-y-1">
<span className="text-[10px] uppercase font-bold text-slate-400 block">Username</span>
<div className="font-semibold text-slate-800 text-sm">{user.username}</div>
<span className="block text-[10px] font-bold text-slate-400 uppercase">
Username
</span>
<div className="text-sm font-semibold text-slate-800">{user.username}</div>
</div>
<div className="space-y-1">
<span className="text-[10px] uppercase font-bold text-slate-400 block">Role Akses</span>
<div className="font-semibold text-[#1B2CC1] text-sm uppercase">{user.role}</div>
<span className="block text-[10px] font-bold text-slate-400 uppercase">
Role Akses
</span>
<div className="text-sm font-semibold text-[#1B2CC1] uppercase">{user.role}</div>
</div>
</div>
{/* PASSWORD CHANGE MODAL BUTTON */}
<div className="pt-2 border-t border-slate-100 dark:border-slate-800">
<div className="border-t border-slate-100 pt-2 dark:border-slate-800">
<Dialog open={isPasswordModalOpen} onOpenChange={setIsPasswordModalOpen}>
<DialogTrigger className="cursor-pointer inline-flex items-center justify-center whitespace-nowrap text-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#1B2CC1] disabled:pointer-events-none disabled:opacity-50 h-11 px-4 py-2 rounded-xl gap-2 text-[#1B2CC1] hover:text-[#15229E] font-bold border border-[#1B2CC1]/20 bg-[#1B2CC1]/5 hover:bg-[#1B2CC1]/10 w-full">
<KeyRound className="w-4 h-4" />
<DialogTrigger className="inline-flex h-11 w-full cursor-pointer items-center justify-center gap-2 rounded-xl border border-[#1B2CC1]/20 bg-[#1B2CC1]/5 px-4 py-2 text-sm font-bold whitespace-nowrap text-[#1B2CC1] hover:bg-[#1B2CC1]/10 hover:text-[#15229E] focus-visible:ring-1 focus-visible:ring-[#1B2CC1] focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50">
<KeyRound className="h-4 w-4" />
Ganti Password
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] rounded-3xl p-0 overflow-hidden border-slate-200/90 dark:border-slate-800 shadow-xl">
<DialogHeader className="p-6 bg-slate-50 dark:bg-slate-900/50 border-b border-slate-100 dark:border-slate-800">
<DialogTitle className="text-xl font-black text-slate-900 dark:text-white">Ganti Password</DialogTitle>
<p className="text-xs text-slate-500 mt-1">Lindungi akun Anda dengan mengubah password secara berkala.</p>
<DialogContent className="overflow-hidden rounded-3xl border-slate-200/90 p-0 shadow-xl sm:max-w-[425px] dark:border-slate-800">
<DialogHeader className="border-b border-slate-100 bg-slate-50 p-6 dark:border-slate-800 dark:bg-slate-900/50">
<DialogTitle className="text-xl font-black text-slate-900 dark:text-white">
Ganti Password
</DialogTitle>
<p className="mt-1 text-xs text-slate-500">
Lindungi akun Anda dengan mengubah password secara berkala.
</p>
</DialogHeader>
<form onSubmit={handleChangePassword}>
<div className="p-6 space-y-5 bg-white dark:bg-slate-900">
<div className="space-y-5 bg-white p-6 dark:bg-slate-900">
<div className="space-y-4">
<div className="space-y-1.5">
<Label className="text-xs font-bold uppercase tracking-wider text-slate-700">Password Lama</Label>
<Label className="text-xs font-bold tracking-wider text-slate-700 uppercase">
Password Lama
</Label>
<div className="relative">
<LockKeyhole className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input type="password" value={oldPassword} onChange={(e) => setOldPassword(e.target.value)} className="pl-9 h-11 rounded-xl" placeholder="Masukkan password lama" />
<LockKeyhole className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-slate-400" />
<Input
type="password"
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
className="h-11 rounded-xl pl-9"
placeholder="Masukkan password lama"
/>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-bold uppercase tracking-wider text-slate-700">Password Baru</Label>
<Label className="text-xs font-bold tracking-wider text-slate-700 uppercase">
Password Baru
</Label>
<div className="relative">
<KeyRound className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} className="pl-9 h-11 rounded-xl" placeholder="Minimal 6 karakter" />
<KeyRound className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-slate-400" />
<Input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="h-11 rounded-xl pl-9"
placeholder="Minimal 6 karakter"
/>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-bold uppercase tracking-wider text-slate-700">Konfirmasi Password Baru</Label>
<Label className="text-xs font-bold tracking-wider text-slate-700 uppercase">
Konfirmasi Password Baru
</Label>
<div className="relative">
<KeyRound className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} className="pl-9 h-11 rounded-xl" placeholder="Ketik ulang password baru" />
<KeyRound className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-slate-400" />
<Input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="h-11 rounded-xl pl-9"
placeholder="Ketik ulang password baru"
/>
</div>
</div>
</div>
{passwordError && (
<div className="p-3 bg-red-50 text-red-600 rounded-xl text-xs font-semibold">{passwordError}</div>
<div className="rounded-xl bg-red-50 p-3 text-xs font-semibold text-red-600">
{passwordError}
</div>
)}
{passwordSuccess && (
<div className="p-3 bg-emerald-50 text-emerald-700 rounded-xl text-xs font-bold flex items-center gap-2">
<CheckCircle className="w-4 h-4" /><span>Password berhasil diubah!</span>
<div className="flex items-center gap-2 rounded-xl bg-emerald-50 p-3 text-xs font-bold text-emerald-700">
<CheckCircle className="h-4 w-4" />
<span>Password berhasil diubah!</span>
</div>
)}
</div>
<div className="p-6 bg-slate-50/60 border-t border-slate-100 flex justify-end gap-3">
<Button type="button" variant="ghost" onClick={() => setIsPasswordModalOpen(false)} className="h-11 rounded-xl font-bold">Batal</Button>
<Button type="submit" disabled={savingPassword} className="h-11 px-6 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold">
{savingPassword ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Save className="w-4 h-4 mr-2" />}
<div className="flex justify-end gap-3 border-t border-slate-100 bg-slate-50/60 p-6">
<Button
type="button"
variant="ghost"
onClick={() => setIsPasswordModalOpen(false)}
className="h-11 rounded-xl font-bold"
>
Batal
</Button>
<Button
type="submit"
disabled={savingPassword}
className="h-11 rounded-xl bg-[#1B2CC1] px-6 font-bold text-white hover:bg-[#15229E]"
>
{savingPassword ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Save className="mr-2 h-4 w-4" />
)}
Ubah Password
</Button>
</div>
+113 -66
View File
@@ -1,18 +1,34 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { getCreatorReport, getSubmittorReport, getSessionUser, getBalancesAsCreator } from '@/app/actions'
import {
getCreatorReport,
getSubmittorReport,
getSessionUser,
getBalancesAsCreator,
} from '@/app/actions'
import { Card } from '@/components/ui/card'
import { Loader2, TrendingUp, TrendingDown, Wallet, ArrowRightLeft, Calendar as CalendarIcon } from 'lucide-react'
import {
Loader2,
TrendingUp,
TrendingDown,
Wallet,
ArrowRightLeft,
Calendar as CalendarIcon,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import ReportCharts from '@/components/ReportCharts'
import ReportDataGrid from '@/components/ReportDataGrid'
import ReportByPersonGrid from '@/components/ReportByPersonGrid'
import {
getISOWeek, getYear,
startOfISOWeek, endOfISOWeek,
startOfMonth, endOfMonth,
setISOWeek, setYear as dfSetYear
getISOWeek,
getYear,
startOfISOWeek,
endOfISOWeek,
startOfMonth,
endOfMonth,
setISOWeek,
setYear as dfSetYear,
} from 'date-fns'
// ── Helpers ──────────────────────────────────────────────
@@ -28,7 +44,10 @@ function getMonthValue(date: Date) {
return `${y}-${m.toString().padStart(2, '0')}`
}
function getIntervalFromFilter(type: 'WEEK' | 'MONTH', value: string): { start: Date; end: Date } | null {
function getIntervalFromFilter(
type: 'WEEK' | 'MONTH',
value: string
): { start: Date; end: Date } | null {
if (!value) return null
if (type === 'WEEK') {
@@ -83,7 +102,7 @@ export default function ReportsPage() {
const [subRes, creRes, balRes] = await Promise.all([
getSubmittorReport(id, interval?.start, interval?.end),
getCreatorReport(id, interval?.start, interval?.end),
getBalancesAsCreator(id)
getBalancesAsCreator(id),
])
setSubmittorData(subRes)
setCreatorData(creRes)
@@ -97,10 +116,16 @@ export default function ReportsPage() {
}
const formatRupiah = (n: number) =>
new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(n)
new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
maximumFractionDigits: 0,
}).format(n)
// ── Summary calcs ──────────────────────────────────────
const getPaidAmount = (sub: any) => (sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? (Number(sub.bill) || 0) : 0)) + (sub.saldo_used || 0)
const getPaidAmount = (sub: any) =>
(sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? Number(sub.bill) || 0 : 0)) +
(sub.saldo_used || 0)
const getUnpaidAmount = (sub: any) => Math.max(0, (Number(sub.bill) || 0) - getPaidAmount(sub))
const totalPengeluaran = submittorData.reduce((acc, s) => acc + getPaidAmount(s), 0)
@@ -108,7 +133,7 @@ export default function ReportsPage() {
let totalOmzet = 0
let totalPiutang = 0
creatorData.forEach(order => {
creatorData.forEach((order) => {
order.submissions.forEach((sub: any) => {
totalOmzet += Number(sub.bill) || 0
totalPiutang += getUnpaidAmount(sub)
@@ -118,49 +143,47 @@ export default function ReportsPage() {
const filterLabel = filterType === 'WEEK' ? `Minggu ${filterValue}` : `Bulan ${filterValue}`
return (
<div className="space-y-6 animate-in fade-in duration-500 pb-12">
<div className="animate-in fade-in space-y-6 pb-12 duration-500">
{/* ── Header: Tab Switcher (left) + Filter Controls (right) ── */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-white dark:bg-slate-900 px-4 py-3 rounded-2xl border border-slate-200/80 dark:border-slate-800 shadow-sm">
<div className="flex flex-col justify-between gap-4 rounded-2xl border border-slate-200/80 bg-white px-4 py-3 shadow-sm md:flex-row md:items-center dark:border-slate-800 dark:bg-slate-900">
{/* Tab Switcher — positioned where the title was */}
<div className="flex items-center bg-slate-100 dark:bg-slate-800/80 p-1 rounded-xl text-xs font-bold">
<div className="flex items-center rounded-xl bg-slate-100 p-1 text-xs font-bold dark:bg-slate-800/80">
<button
onClick={() => setActiveTab('SUBMITTOR')}
className={cn(
'px-5 py-2 rounded-lg transition-all duration-300 flex items-center justify-center gap-2',
'flex items-center justify-center gap-2 rounded-lg px-5 py-2 transition-all duration-300',
activeTab === 'SUBMITTOR'
? 'bg-white dark:bg-slate-900 text-[#1B2CC1] dark:text-white shadow-sm font-black'
? 'bg-white font-black text-[#1B2CC1] shadow-sm dark:bg-slate-900 dark:text-white'
: 'text-slate-500 hover:text-slate-900 dark:hover:text-white'
)}
>
<TrendingDown className="w-4 h-4" /> Pengeluaran Saya
<TrendingDown className="h-4 w-4" /> Pengeluaran Saya
</button>
<button
onClick={() => setActiveTab('CREATOR')}
className={cn(
'px-5 py-2 rounded-lg transition-all duration-300 flex items-center justify-center gap-2',
'flex items-center justify-center gap-2 rounded-lg px-5 py-2 transition-all duration-300',
activeTab === 'CREATOR'
? 'bg-white dark:bg-slate-900 text-[#1B2CC1] dark:text-white shadow-sm font-black'
? 'bg-white font-black text-[#1B2CC1] shadow-sm dark:bg-slate-900 dark:text-white'
: 'text-slate-500 hover:text-slate-900 dark:hover:text-white'
)}
>
<TrendingUp className="w-4 h-4" /> Omzet (Kreator)
<TrendingUp className="h-4 w-4" /> Omzet (Kreator)
</button>
</div>
{/* Filter Controls */}
<div className="flex items-center gap-2 flex-wrap">
<div className="flex flex-wrap items-center gap-2">
{/* Type toggle */}
<div className="flex items-center bg-slate-100 dark:bg-slate-800 rounded-xl p-1 gap-1">
{(['WEEK', 'MONTH'] as const).map(t => (
<div className="flex items-center gap-1 rounded-xl bg-slate-100 p-1 dark:bg-slate-800">
{(['WEEK', 'MONTH'] as const).map((t) => (
<button
key={t}
onClick={() => handleFilterTypeChange(t)}
className={cn(
'px-3 py-1.5 rounded-lg text-xs font-bold transition-all',
'rounded-lg px-3 py-1.5 text-xs font-bold transition-all',
filterType === t
? 'bg-white dark:bg-slate-700 text-[#1B2CC1] shadow-sm'
? 'bg-white text-[#1B2CC1] shadow-sm dark:bg-slate-700'
: 'text-slate-500 hover:text-slate-800 dark:hover:text-slate-200'
)}
>
@@ -172,57 +195,67 @@ export default function ReportsPage() {
{/* Date Picker */}
<div
onClick={() => pickerRef.current?.showPicker()}
className="flex items-center gap-2 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl px-3 py-2 shadow-sm cursor-pointer hover:border-[#1B2CC1]/50 transition-colors select-none"
className="flex cursor-pointer items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 shadow-sm transition-colors select-none hover:border-[#1B2CC1]/50 dark:border-slate-700 dark:bg-slate-900"
>
<input
ref={pickerRef}
type={filterType === 'WEEK' ? 'week' : 'month'}
value={filterValue}
onChange={e => setFilterValue(e.target.value)}
className="text-sm font-bold text-slate-700 dark:text-slate-200 bg-transparent border-none outline-none cursor-pointer w-[140px] [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-inner-spin-button]:hidden"
onChange={(e) => setFilterValue(e.target.value)}
className="w-[140px] cursor-pointer border-none bg-transparent text-sm font-bold text-slate-700 outline-none dark:text-slate-200 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-inner-spin-button]:hidden"
/>
<CalendarIcon className="w-4 h-4 text-slate-400 shrink-0" />
<CalendarIcon className="h-4 w-4 shrink-0 text-slate-400" />
</div>
</div>
</div>
{/* Loading overlay */}
{loading && (
<div className="flex items-center justify-center py-16 gap-3 text-slate-400">
<Loader2 className="w-6 h-6 animate-spin text-[#1B2CC1]" />
<div className="flex items-center justify-center gap-3 py-16 text-slate-400">
<Loader2 className="h-6 w-6 animate-spin text-[#1B2CC1]" />
<span className="text-sm font-semibold">Memuat data {filterLabel}...</span>
</div>
)}
{/* ── SUBMITTOR TAB ── */}
{!loading && activeTab === 'SUBMITTOR' && (
<div className="space-y-6 animate-in slide-in-from-left-2 duration-300">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="animate-in slide-in-from-left-2 space-y-6 duration-300">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Card className="rounded-2xl border-none bg-gradient-to-br from-emerald-500 to-emerald-700 text-white shadow-lg shadow-emerald-500/20">
<div className="p-4 flex items-center gap-4">
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center backdrop-blur-sm shrink-0">
<Wallet className="w-6 h-6 text-white" />
<div className="flex items-center gap-4 p-4">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm">
<Wallet className="h-6 w-6 text-white" />
</div>
<div className="flex flex-col">
<span className="text-emerald-100 font-bold text-xs tracking-wide uppercase mb-1">Total Pengeluaran (Lunas)</span>
<h2 className="text-2xl font-black leading-none mb-1">{formatRupiah(totalPengeluaran)}</h2>
<span className="mb-1 text-xs font-bold tracking-wide text-emerald-100 uppercase">
Total Pengeluaran (Lunas)
</span>
<h2 className="mb-1 text-2xl leading-none font-black">
{formatRupiah(totalPengeluaran)}
</h2>
<p className="text-xs text-emerald-100">
Dari {submittorData.filter(s => s.payment_status === 'LUNAS').length} pesanan lunas.
Dari {submittorData.filter((s) => s.payment_status === 'LUNAS').length} pesanan
lunas.
</p>
</div>
</div>
</Card>
<Card className="rounded-2xl border-none bg-gradient-to-br from-rose-500 to-rose-700 text-white shadow-lg shadow-rose-500/20">
<div className="p-4 flex items-center gap-4">
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center backdrop-blur-sm shrink-0">
<ArrowRightLeft className="w-6 h-6 text-white" />
<div className="flex items-center gap-4 p-4">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm">
<ArrowRightLeft className="h-6 w-6 text-white" />
</div>
<div className="flex flex-col">
<span className="text-rose-100 font-bold text-xs tracking-wide uppercase mb-1">Hutang Pribadi (Belum Bayar)</span>
<h2 className="text-2xl font-black leading-none mb-1">{formatRupiah(totalHutang)}</h2>
<span className="mb-1 text-xs font-bold tracking-wide text-rose-100 uppercase">
Hutang Pribadi (Belum Bayar)
</span>
<h2 className="mb-1 text-2xl leading-none font-black">
{formatRupiah(totalHutang)}
</h2>
<p className="text-xs text-rose-100">
{submittorData.filter(s => s.payment_status !== 'LUNAS').length} tagihan masih gantung.
{submittorData.filter((s) => s.payment_status !== 'LUNAS').length} tagihan masih
gantung.
</p>
</div>
</div>
@@ -236,30 +269,42 @@ export default function ReportsPage() {
{/* ── CREATOR TAB ── */}
{!loading && activeTab === 'CREATOR' && (
<div className="space-y-6 animate-in slide-in-from-right-2 duration-300">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="animate-in slide-in-from-right-2 space-y-6 duration-300">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Card className="rounded-2xl border-none bg-gradient-to-br from-[#1B2CC1] to-[#121E85] text-white shadow-lg shadow-[#1B2CC1]/20">
<div className="p-4 flex items-center gap-4">
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center backdrop-blur-sm shrink-0">
<TrendingUp className="w-6 h-6 text-white" />
<div className="flex items-center gap-4 p-4">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm">
<TrendingUp className="h-6 w-6 text-white" />
</div>
<div className="flex flex-col">
<span className="text-blue-200 font-bold text-xs tracking-wide uppercase mb-1">Total Omzet Dikelola</span>
<h2 className="text-2xl font-black leading-none mb-1">{formatRupiah(totalOmzet)}</h2>
<p className="text-xs text-blue-200">Dari {creatorData.length} PO yang Anda buat.</p>
<span className="mb-1 text-xs font-bold tracking-wide text-blue-200 uppercase">
Total Omzet Dikelola
</span>
<h2 className="mb-1 text-2xl leading-none font-black">
{formatRupiah(totalOmzet)}
</h2>
<p className="text-xs text-blue-200">
Dari {creatorData.length} PO yang Anda buat.
</p>
</div>
</div>
</Card>
<Card className="rounded-2xl border-none bg-gradient-to-br from-amber-500 to-amber-600 text-white shadow-lg shadow-amber-500/20">
<div className="p-4 flex items-center gap-4">
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center backdrop-blur-sm shrink-0">
<ArrowRightLeft className="w-6 h-6 text-white" />
<div className="flex items-center gap-4 p-4">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm">
<ArrowRightLeft className="h-6 w-6 text-white" />
</div>
<div className="flex flex-col">
<span className="text-amber-100 font-bold text-xs tracking-wide uppercase mb-1">Total Piutang (Belum Dibayar)</span>
<h2 className="text-2xl font-black leading-none mb-1">{formatRupiah(totalPiutang)}</h2>
<p className="text-xs text-amber-100">Uang Anda yang masih tertahan di teman-teman.</p>
<span className="mb-1 text-xs font-bold tracking-wide text-amber-100 uppercase">
Total Piutang (Belum Dibayar)
</span>
<h2 className="mb-1 text-2xl leading-none font-black">
{formatRupiah(totalPiutang)}
</h2>
<p className="text-xs text-amber-100">
Uang Anda yang masih tertahan di teman-teman.
</p>
</div>
</div>
</Card>
@@ -268,15 +313,15 @@ export default function ReportsPage() {
<ReportCharts type="CREATOR" data={creatorData} />
{/* Sub-Tabs */}
<div className="flex items-center bg-slate-100 dark:bg-slate-800/80 p-1 rounded-xl text-xs font-bold w-full sm:w-auto">
{([ 'BY_PERSON','BY_ORDER',] as const).map(tab => (
<div className="flex w-full items-center rounded-xl bg-slate-100 p-1 text-xs font-bold sm:w-auto dark:bg-slate-800/80">
{(['BY_PERSON', 'BY_ORDER'] as const).map((tab) => (
<button
key={tab}
onClick={() => setCreatorTab(tab)}
className={cn(
'flex-1 sm:flex-none px-5 py-2 rounded-lg transition-all duration-200 flex items-center justify-center gap-1.5',
'flex flex-1 items-center justify-center gap-1.5 rounded-lg px-5 py-2 transition-all duration-200 sm:flex-none',
creatorTab === tab
? 'bg-white dark:bg-slate-900 text-[#1B2CC1] dark:text-white shadow-sm font-black'
? 'bg-white font-black text-[#1B2CC1] shadow-sm dark:bg-slate-900 dark:text-white'
: 'text-slate-500 hover:text-slate-900 dark:hover:text-white'
)}
>
@@ -292,7 +337,9 @@ export default function ReportsPage() {
data={creatorData}
balancesData={creatorBalances}
creatorId={userId!}
onUpdate={() => { if (userId) loadData(userId) }}
onUpdate={() => {
if (userId) loadData(userId)
}}
/>
)}
</div>
+65 -42
View File
@@ -15,7 +15,7 @@ export default function IntegrationsPage() {
const [settings, setSettings] = useState({
MATTERMOST_WEBHOOK_URL: '',
MATTERMOST_TEMPLATE: '',
WHATSAPP_TEMPLATE: ''
WHATSAPP_TEMPLATE: '',
})
useEffect(() => {
@@ -24,8 +24,12 @@ export default function IntegrationsPage() {
if (data) {
setSettings({
MATTERMOST_WEBHOOK_URL: data.MATTERMOST_WEBHOOK_URL || '',
MATTERMOST_TEMPLATE: data.MATTERMOST_TEMPLATE || 'Halo tim! Lagi ada orderan open nih untuk {title}. Mumpung belum di-checkout, yang mau ikutan nitip bisa langsung cek ke sini ya: {url}',
WHATSAPP_TEMPLATE: data.WHATSAPP_TEMPLATE || 'Halo tim! Lagi ada orderan open nih untuk {title}. Mumpung belum di-checkout, yang mau ikutan nitip bisa langsung cek ke sini ya: {url}'
MATTERMOST_TEMPLATE:
data.MATTERMOST_TEMPLATE ||
'Halo tim! Lagi ada orderan open nih untuk {title}. Mumpung belum di-checkout, yang mau ikutan nitip bisa langsung cek ke sini ya: {url}',
WHATSAPP_TEMPLATE:
data.WHATSAPP_TEMPLATE ||
'Halo tim! Lagi ada orderan open nih untuk {title}. Mumpung belum di-checkout, yang mau ikutan nitip bisa langsung cek ke sini ya: {url}',
})
}
setLoading(false)
@@ -34,7 +38,7 @@ export default function IntegrationsPage() {
}, [])
const handleChange = (key: string, value: string) => {
setSettings(prev => ({ ...prev, [key]: value }))
setSettings((prev) => ({ ...prev, [key]: value }))
}
const handleSave = async () => {
@@ -43,12 +47,12 @@ export default function IntegrationsPage() {
if (res.success) {
toast.success('Berhasil Disimpan', {
description: 'Pengaturan integrasi berhasil disimpan!',
duration: 3000
duration: 3000,
})
} else {
toast.error('Gagal', {
description: 'Gagal menyimpan pengaturan',
duration: 3000
duration: 3000,
})
}
setSaving(false)
@@ -56,73 +60,92 @@ export default function IntegrationsPage() {
if (loading) {
return (
<div className="flex justify-center items-center h-64">
<Loader2 className="w-8 h-8 animate-spin text-[#1B2CC1]" />
<div className="flex h-64 items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-[#1B2CC1]" />
</div>
)
}
return (
<div className="p-6 space-y-8">
<div className="space-y-8 p-6">
<div>
<h2 className="text-xl font-bold text-slate-900 dark:text-white">Integrasi & Sharing</h2>
<p className="text-xs text-slate-500 mt-1">Konfigurasi endpoint API dan template pesan otomatis.</p>
<p className="mt-1 text-xs text-slate-500">
Konfigurasi endpoint API dan template pesan otomatis.
</p>
</div>
<div className="space-y-6 max-w-3xl">
<div className="max-w-3xl space-y-6">
{/* Mattermost Section */}
<div className="p-5 rounded-2xl border border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 space-y-4">
<div className="flex items-center gap-2 text-indigo-600 dark:text-indigo-400 font-black">
<MessageSquare className="w-5 h-5" />
<div className="space-y-4 rounded-2xl border border-slate-200 bg-slate-50/50 p-5 dark:border-slate-800 dark:bg-slate-900/50">
<div className="flex items-center gap-2 font-black text-indigo-600 dark:text-indigo-400">
<MessageSquare className="h-5 w-5" />
<h3>Mattermost Webhook</h3>
</div>
<p className="text-xs text-slate-500">Gunakan URL Incoming Webhook dari Mattermost untuk mengirim broadcast PO baru secara otomatis.</p>
<p className="text-xs text-slate-500">
Gunakan URL Incoming Webhook dari Mattermost untuk mengirim broadcast PO baru secara
otomatis.
</p>
<div className="space-y-1.5">
<Label className="text-xs font-bold uppercase text-slate-700">Webhook URL</Label>
<Input
value={settings.MATTERMOST_WEBHOOK_URL}
onChange={e => handleChange('MATTERMOST_WEBHOOK_URL', e.target.value)}
className="h-11 rounded-xl bg-white dark:bg-slate-950"
placeholder="https://mattermost.yourdomain.com/hooks/xxx"
<Label className="text-xs font-bold text-slate-700 uppercase">Webhook URL</Label>
<Input
value={settings.MATTERMOST_WEBHOOK_URL}
onChange={(e) => handleChange('MATTERMOST_WEBHOOK_URL', e.target.value)}
className="h-11 rounded-xl bg-white dark:bg-slate-950"
placeholder="https://mattermost.yourdomain.com/hooks/xxx"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-bold uppercase text-slate-700">Pesan Template</Label>
<Textarea
value={settings.MATTERMOST_TEMPLATE}
onChange={e => handleChange('MATTERMOST_TEMPLATE', e.target.value)}
className="rounded-xl min-h-[100px] bg-white dark:bg-slate-950"
placeholder="Template pesan..."
<Label className="text-xs font-bold text-slate-700 uppercase">Pesan Template</Label>
<Textarea
value={settings.MATTERMOST_TEMPLATE}
onChange={(e) => handleChange('MATTERMOST_TEMPLATE', e.target.value)}
className="min-h-[100px] rounded-xl bg-white dark:bg-slate-950"
placeholder="Template pesan..."
/>
<p className="text-[10px] text-slate-500 flex items-center gap-1"><AlertCircle className="w-3 h-3"/> Gunakan variabel: {'{title}'}, {'{url}'}</p>
<p className="flex items-center gap-1 text-[10px] text-slate-500">
<AlertCircle className="h-3 w-3" /> Gunakan variabel: {'{title}'}, {'{url}'}
</p>
</div>
</div>
{/* WhatsApp Section */}
<div className="p-5 rounded-2xl border border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 space-y-4">
<div className="flex items-center gap-2 text-emerald-600 dark:text-emerald-400 font-black">
<MessageCircle className="w-5 h-5" />
<div className="space-y-4 rounded-2xl border border-slate-200 bg-slate-50/50 p-5 dark:border-slate-800 dark:bg-slate-900/50">
<div className="flex items-center gap-2 font-black text-emerald-600 dark:text-emerald-400">
<MessageCircle className="h-5 w-5" />
<h3>WhatsApp Share Template</h3>
</div>
<p className="text-xs text-slate-500">Atur template teks default saat user menekan tombol share ke WhatsApp.</p>
<p className="text-xs text-slate-500">
Atur template teks default saat user menekan tombol share ke WhatsApp.
</p>
<div className="space-y-1.5">
<Label className="text-xs font-bold uppercase text-slate-700">Pesan Template</Label>
<Textarea
value={settings.WHATSAPP_TEMPLATE}
onChange={e => handleChange('WHATSAPP_TEMPLATE', e.target.value)}
className="rounded-xl min-h-[100px] bg-white dark:bg-slate-950"
placeholder="Template pesan..."
<Label className="text-xs font-bold text-slate-700 uppercase">Pesan Template</Label>
<Textarea
value={settings.WHATSAPP_TEMPLATE}
onChange={(e) => handleChange('WHATSAPP_TEMPLATE', e.target.value)}
className="min-h-[100px] rounded-xl bg-white dark:bg-slate-950"
placeholder="Template pesan..."
/>
<p className="text-[10px] text-slate-500 flex items-center gap-1"><AlertCircle className="w-3 h-3"/> Gunakan variabel: {'{title}'}, {'{url}'}</p>
<p className="flex items-center gap-1 text-[10px] text-slate-500">
<AlertCircle className="h-3 w-3" /> Gunakan variabel: {'{title}'}, {'{url}'}
</p>
</div>
</div>
<div className="pt-2">
<Button onClick={handleSave} disabled={saving} className="bg-[#1B2CC1] hover:bg-[#15229E] text-white rounded-xl h-11 px-8 font-bold shadow-md w-full sm:w-auto">
{saving ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Save className="w-4 h-4 mr-2" />}
<Button
onClick={handleSave}
disabled={saving}
className="h-11 w-full rounded-xl bg-[#1B2CC1] px-8 font-bold text-white shadow-md hover:bg-[#15229E] sm:w-auto"
>
{saving ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Save className="mr-2 h-4 w-4" />
)}
Simpan Konfigurasi
</Button>
</div>
+9 -9
View File
@@ -14,10 +14,10 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
]
return (
<div className="space-y-6 animate-in fade-in duration-500 pb-12">
<div className="animate-in fade-in space-y-6 pb-12 duration-500">
{/* Settings Navigation Tabs */}
<div className="flex flex-col md:flex-row items-start md:items-center gap-4 bg-white dark:bg-slate-900 p-4 rounded-2xl border border-slate-200/80 dark:border-slate-800 shadow-sm overflow-x-auto">
<div className="flex items-center bg-slate-100 dark:bg-slate-800/80 p-1 rounded-xl text-xs font-bold w-full md:w-auto">
<div className="flex flex-col items-start gap-4 overflow-x-auto rounded-2xl border border-slate-200/80 bg-white p-4 shadow-sm md:flex-row md:items-center dark:border-slate-800 dark:bg-slate-900">
<div className="flex w-full items-center rounded-xl bg-slate-100 p-1 text-xs font-bold md:w-auto dark:bg-slate-800/80">
{tabs.map((tab) => {
const isActive = pathname === tab.href
const Icon = tab.icon
@@ -26,13 +26,13 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
key={tab.href}
href={tab.href}
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-lg transition-all duration-200 whitespace-nowrap",
isActive
? "bg-white dark:bg-slate-900 text-[#1B2CC1] dark:text-blue-400 shadow-sm font-black"
: "text-slate-500 hover:text-slate-900 dark:hover:text-white"
'flex items-center gap-2 rounded-lg px-4 py-2 whitespace-nowrap transition-all duration-200',
isActive
? 'bg-white font-black text-[#1B2CC1] shadow-sm dark:bg-slate-900 dark:text-blue-400'
: 'text-slate-500 hover:text-slate-900 dark:hover:text-white'
)}
>
<Icon className="w-4 h-4" />
<Icon className="h-4 w-4" />
{tab.name}
</Link>
)
@@ -41,7 +41,7 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
</div>
{/* Settings Content */}
<div className="bg-white dark:bg-slate-900 rounded-2xl border border-slate-200/80 dark:border-slate-800 shadow-sm overflow-hidden">
<div className="overflow-hidden rounded-2xl border border-slate-200/80 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
{children}
</div>
</div>
+249 -151
View File
@@ -1,14 +1,26 @@
'use client'
import { useEffect, useState } from 'react'
import { getUsers, createUserByAdmin, toggleUserActive, deleteUser, resetUserPassword } from '@/app/actions'
import {
getUsers,
createUserByAdmin,
toggleUserActive,
deleteUser,
resetUserPassword,
} from '@/app/actions'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Loader2, Plus, KeyRound, Ban, Trash2, CheckCircle2, ShieldCheck, User } from 'lucide-react'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
export default function UsersPage() {
const [users, setUsers] = useState<any[]>([])
@@ -28,7 +40,7 @@ export default function UsersPage() {
const [password, setPassword] = useState('')
const [role, setRole] = useState('user')
const [newPass, setNewPass] = useState('')
const [actionLoading, setActionLoading] = useState(false)
useEffect(() => {
@@ -47,7 +59,7 @@ export default function UsersPage() {
const handleCreateUser = async (e: React.FormEvent) => {
e.preventDefault()
if (!name || !username || !password) return alert('Lengkapi semua field')
setActionLoading(true)
const res = await createUserByAdmin(name, username, password, role)
if (res.success) {
@@ -66,7 +78,7 @@ export default function UsersPage() {
const handleResetPassword = async (e: React.FormEvent) => {
e.preventDefault()
if (!newPass || newPass.length < 6) return alert('Password minimal 6 karakter')
setActionLoading(true)
const res = await resetUserPassword(selectedUser.id, newPass)
if (res.success) {
@@ -80,7 +92,8 @@ export default function UsersPage() {
}
const handleToggleActive = async (id: string, currentStatus: boolean) => {
if (!confirm(`Yakin ingin ${currentStatus ? 'menonaktifkan' : 'mengaktifkan'} user ini?`)) return
if (!confirm(`Yakin ingin ${currentStatus ? 'menonaktifkan' : 'mengaktifkan'} user ini?`))
return
const res = await toggleUserActive(id, !currentStatus)
if (res.success) {
loadUsers(page)
@@ -101,187 +114,251 @@ export default function UsersPage() {
return (
<div className="p-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
<div className="mb-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
<div>
<h2 className="text-xl font-bold text-slate-900 dark:text-white">Manajemen Pengguna</h2>
<p className="text-xs text-slate-500 mt-1">Kelola data seluruh pengguna TitipIn.</p>
<p className="mt-1 text-xs text-slate-500">Kelola data seluruh pengguna TitipIn.</p>
</div>
<Button onClick={() => setIsCreateOpen(true)} className="bg-[#1B2CC1] hover:bg-[#15229E] text-white rounded-xl h-10 px-4 shadow-sm text-xs font-bold">
<Plus className="w-4 h-4 mr-1.5" /> Tambah User
<Button
onClick={() => setIsCreateOpen(true)}
className="h-10 rounded-xl bg-[#1B2CC1] px-4 text-xs font-bold text-white shadow-sm hover:bg-[#15229E]"
>
<Plus className="mr-1.5 h-4 w-4" /> Tambah User
</Button>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm text-left">
<thead className="bg-slate-50/50 dark:bg-slate-800/50 border-b border-slate-100 dark:border-slate-800 text-xs uppercase font-bold text-slate-500">
<table className="w-full text-left text-sm">
<thead className="border-b border-slate-100 bg-slate-50/50 text-xs font-bold text-slate-500 uppercase dark:border-slate-800 dark:bg-slate-800/50">
<tr>
<th className="px-6 py-4">Pengguna</th>
<th className="px-6 py-4">Role</th>
<th className="px-6 py-4">Status</th>
<th className="px-6 py-4">Terdaftar</th>
<th className="px-6 py-4 text-right">Aksi</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
{loading ? (
<tr>
<th className="px-6 py-4">Pengguna</th>
<th className="px-6 py-4">Role</th>
<th className="px-6 py-4">Status</th>
<th className="px-6 py-4">Terdaftar</th>
<th className="px-6 py-4 text-right">Aksi</th>
<td colSpan={5} className="px-6 py-12 text-center">
<Loader2 className="mx-auto h-6 w-6 animate-spin text-[#1B2CC1]" />
</td>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
{loading ? (
<tr>
<td colSpan={5} className="px-6 py-12 text-center">
<Loader2 className="w-6 h-6 animate-spin mx-auto text-[#1B2CC1]" />
) : users.length === 0 ? (
<tr>
<td colSpan={5} className="px-6 py-12 text-center text-slate-500">
Belum ada pengguna lain.
</td>
</tr>
) : (
users.map((u) => (
<tr
key={u.id}
className="transition-colors hover:bg-slate-50/50 dark:hover:bg-slate-800/20"
>
<td className="px-6 py-4">
<div className="flex items-center gap-3">
{u.photo ? (
<img
src={u.photo}
alt={u.name}
className="h-9 w-9 shrink-0 rounded-full object-cover"
/>
) : (
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-blue-100 text-xs font-bold text-blue-700 uppercase">
{u.name.charAt(0)}
</div>
)}
<div>
<div className="font-bold text-slate-900 dark:text-slate-100">{u.name}</div>
<div className="text-xs text-slate-500">@{u.username}</div>
</div>
</div>
</td>
<td className="px-6 py-4">
{u.role === 'superadmin' ? (
<span className="inline-flex items-center gap-1 rounded-md bg-purple-100 px-2 py-1 text-[10px] font-bold text-purple-700 uppercase">
<ShieldCheck className="h-3 w-3" /> Superadmin
</span>
) : (
<span className="inline-flex items-center gap-1 rounded-md bg-slate-100 px-2 py-1 text-[10px] font-bold text-slate-700 uppercase">
<User className="h-3 w-3" /> User
</span>
)}
</td>
<td className="px-6 py-4">
{u.is_active ? (
<span className="inline-flex items-center gap-1 rounded-md bg-emerald-100 px-2 py-1 text-[10px] font-bold text-emerald-700 uppercase">
<CheckCircle2 className="h-3 w-3" /> Aktif
</span>
) : (
<span className="inline-flex items-center gap-1 rounded-md bg-red-100 px-2 py-1 text-[10px] font-bold text-red-700 uppercase">
<Ban className="h-3 w-3" /> Nonaktif
</span>
)}
</td>
<td className="px-6 py-4 text-xs text-slate-500">
{new Date(u.created_at).toLocaleDateString('id-ID')}
</td>
<td className="px-6 py-4 text-right">
<div className="flex justify-end gap-2">
<Button
size="sm"
variant="ghost"
onClick={() => {
setSelectedUser(u)
setIsPassOpen(true)
}}
className="h-8 rounded-lg bg-slate-100 px-2 font-medium text-slate-700 hover:bg-slate-200 hover:text-slate-900"
title="Ubah Password"
>
<KeyRound className="h-4 w-4" />
</Button>
{u.role !== 'superadmin' && (
<>
<Button
size="sm"
variant="ghost"
onClick={() => handleToggleActive(u.id, u.is_active)}
className={`h-8 rounded-lg px-2 font-medium ${u.is_active ? 'bg-amber-100 text-amber-700 hover:bg-amber-200 hover:text-amber-900' : 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 hover:text-emerald-900'}`}
title={u.is_active ? 'Nonaktifkan' : 'Aktifkan'}
>
{u.is_active ? (
<Ban className="h-4 w-4" />
) : (
<CheckCircle2 className="h-4 w-4" />
)}
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleDelete(u.id)}
className="h-8 rounded-lg bg-rose-100 px-2 font-medium text-rose-700 hover:bg-rose-200 hover:text-rose-900"
title="Hapus"
>
<Trash2 className="h-4 w-4" />
</Button>
</>
)}
</div>
</td>
</tr>
) : users.length === 0 ? (
<tr>
<td colSpan={5} className="px-6 py-12 text-center text-slate-500">Belum ada pengguna lain.</td>
</tr>
) : (
users.map(u => (
<tr key={u.id} className="hover:bg-slate-50/50 dark:hover:bg-slate-800/20 transition-colors">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
{u.photo ? (
<img src={u.photo} alt={u.name} className="w-9 h-9 rounded-full object-cover shrink-0" />
) : (
<div className="w-9 h-9 rounded-full bg-blue-100 text-blue-700 flex items-center justify-center font-bold text-xs uppercase shrink-0">
{u.name.charAt(0)}
</div>
)}
<div>
<div className="font-bold text-slate-900 dark:text-slate-100">{u.name}</div>
<div className="text-xs text-slate-500">@{u.username}</div>
</div>
</div>
</td>
<td className="px-6 py-4">
{u.role === 'superadmin' ? (
<span className="inline-flex items-center gap-1 text-[10px] font-bold px-2 py-1 rounded-md bg-purple-100 text-purple-700 uppercase">
<ShieldCheck className="w-3 h-3" /> Superadmin
</span>
) : (
<span className="inline-flex items-center gap-1 text-[10px] font-bold px-2 py-1 rounded-md bg-slate-100 text-slate-700 uppercase">
<User className="w-3 h-3" /> User
</span>
)}
</td>
<td className="px-6 py-4">
{u.is_active ? (
<span className="inline-flex items-center gap-1 text-[10px] font-bold px-2 py-1 rounded-md bg-emerald-100 text-emerald-700 uppercase">
<CheckCircle2 className="w-3 h-3" /> Aktif
</span>
) : (
<span className="inline-flex items-center gap-1 text-[10px] font-bold px-2 py-1 rounded-md bg-red-100 text-red-700 uppercase">
<Ban className="w-3 h-3" /> Nonaktif
</span>
)}
</td>
<td className="px-6 py-4 text-slate-500 text-xs">
{new Date(u.created_at).toLocaleDateString('id-ID')}
</td>
<td className="px-6 py-4 text-right">
<div className="flex justify-end gap-2">
<Button
size="sm"
variant="ghost"
onClick={() => {
setSelectedUser(u)
setIsPassOpen(true)
}}
className="h-8 px-2 rounded-lg bg-slate-100 text-slate-700 hover:bg-slate-200 hover:text-slate-900 font-medium"
title="Ubah Password"
>
<KeyRound className="w-4 h-4" />
</Button>
))
)}
</tbody>
</table>
</div>
{u.role !== 'superadmin' && (
<>
<Button
size="sm"
variant="ghost"
onClick={() => handleToggleActive(u.id, u.is_active)}
className={`h-8 px-2 rounded-lg font-medium ${u.is_active ? 'bg-amber-100 text-amber-700 hover:bg-amber-200 hover:text-amber-900' : 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 hover:text-emerald-900'}`}
title={u.is_active ? 'Nonaktifkan' : 'Aktifkan'}
>
{u.is_active ? <Ban className="w-4 h-4" /> : <CheckCircle2 className="w-4 h-4" />}
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleDelete(u.id)}
className="h-8 px-2 rounded-lg bg-rose-100 text-rose-700 hover:bg-rose-200 hover:text-rose-900 font-medium"
title="Hapus"
>
<Trash2 className="w-4 h-4" />
</Button>
</>
)}
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{totalPages > 1 && (
<div className="p-4 border-t border-slate-100 dark:border-slate-800 flex justify-between items-center bg-slate-50/50">
<span className="text-xs text-slate-500">Halaman {page} dari {totalPages}</span>
<div className="flex gap-2">
<Button disabled={page === 1} onClick={() => setPage(p => p - 1)} variant="outline" size="sm" className="h-8 rounded-lg text-xs">Sebelumnya</Button>
<Button disabled={page === totalPages} onClick={() => setPage(p => p + 1)} variant="outline" size="sm" className="h-8 rounded-lg text-xs">Selanjutnya</Button>
</div>
{totalPages > 1 && (
<div className="flex items-center justify-between border-t border-slate-100 bg-slate-50/50 p-4 dark:border-slate-800">
<span className="text-xs text-slate-500">
Halaman {page} dari {totalPages}
</span>
<div className="flex gap-2">
<Button
disabled={page === 1}
onClick={() => setPage((p) => p - 1)}
variant="outline"
size="sm"
className="h-8 rounded-lg text-xs"
>
Sebelumnya
</Button>
<Button
disabled={page === totalPages}
onClick={() => setPage((p) => p + 1)}
variant="outline"
size="sm"
className="h-8 rounded-lg text-xs"
>
Selanjutnya
</Button>
</div>
)}
</div>
)}
{/* CREATE MODAL */}
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogContent className="sm:max-w-md rounded-3xl p-0 overflow-hidden border-0">
<div className="px-6 pt-6 pb-4 border-b border-slate-100">
<DialogContent className="overflow-hidden rounded-3xl border-0 p-0 sm:max-w-md">
<div className="border-b border-slate-100 px-6 pt-6 pb-4">
<DialogTitle className="text-xl font-black">Tambah User Baru</DialogTitle>
</div>
<form onSubmit={handleCreateUser} className="px-6 py-4 space-y-4">
<form onSubmit={handleCreateUser} className="space-y-4 px-6 py-4">
<div className="space-y-1.5">
<Label className="text-xs font-bold uppercase text-slate-700">Nama Lengkap</Label>
<Input required value={name} onChange={e => setName(e.target.value)} className="h-11 rounded-xl" placeholder="Masukkan nama lengkap" />
<Label className="text-xs font-bold text-slate-700 uppercase">Nama Lengkap</Label>
<Input
required
value={name}
onChange={(e) => setName(e.target.value)}
className="h-11 rounded-xl"
placeholder="Masukkan nama lengkap"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-bold uppercase text-slate-700">Username</Label>
<Input required value={username} onChange={e => setUsername(e.target.value)} className="h-11 rounded-xl" placeholder="Masukkan username unik" />
<Label className="text-xs font-bold text-slate-700 uppercase">Username</Label>
<Input
required
value={username}
onChange={(e) => setUsername(e.target.value)}
className="h-11 rounded-xl"
placeholder="Masukkan username unik"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-bold uppercase text-slate-700">Password</Label>
<Input required type="password" value={password} onChange={e => setPassword(e.target.value)} className="h-11 rounded-xl" placeholder="Buat kata sandi (min. 6 karakter)" />
<Label className="text-xs font-bold text-slate-700 uppercase">Password</Label>
<Input
required
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="h-11 rounded-xl"
placeholder="Buat kata sandi (min. 6 karakter)"
/>
</div>
<div className="space-y-2">
<Label className="text-xs font-bold uppercase text-slate-700">Role Akses</Label>
<Label className="text-xs font-bold text-slate-700 uppercase">Role Akses</Label>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => setRole('user')}
className={`h-11 rounded-xl border flex items-center justify-center gap-2 text-sm font-bold transition-all ${
className={`flex h-11 items-center justify-center gap-2 rounded-xl border text-sm font-bold transition-all ${
role === 'user'
? 'border-[#1B2CC1] bg-[#1B2CC1]/5 text-[#1B2CC1] ring-1 ring-[#1B2CC1]/20'
: 'border-slate-200 text-slate-500 hover:bg-slate-50'
}`}
>
<User className="w-4 h-4" /> User Biasa
<User className="h-4 w-4" /> User Biasa
</button>
<button
type="button"
onClick={() => setRole('superadmin')}
className={`h-11 rounded-xl border flex items-center justify-center gap-2 text-sm font-bold transition-all ${
className={`flex h-11 items-center justify-center gap-2 rounded-xl border text-sm font-bold transition-all ${
role === 'superadmin'
? 'border-purple-500 bg-purple-50 text-purple-700 ring-1 ring-purple-500/20'
: 'border-slate-200 text-slate-500 hover:bg-slate-50'
}`}
>
<ShieldCheck className="w-4 h-4" /> Superadmin
<ShieldCheck className="h-4 w-4" /> Superadmin
</button>
</div>
</div>
<div className="pt-4 flex gap-3">
<Button type="button" variant="outline" onClick={() => setIsCreateOpen(false)} className="flex-1 h-11 rounded-xl">Batal</Button>
<Button type="submit" disabled={actionLoading} className="flex-1 h-11 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white">
{actionLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Simpan User'}
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setIsCreateOpen(false)}
className="h-11 flex-1 rounded-xl"
>
Batal
</Button>
<Button
type="submit"
disabled={actionLoading}
className="h-11 flex-1 rounded-xl bg-[#1B2CC1] text-white hover:bg-[#15229E]"
>
{actionLoading ? <Loader2 className="h-5 w-5 animate-spin" /> : 'Simpan User'}
</Button>
</div>
</form>
@@ -290,20 +367,41 @@ export default function UsersPage() {
{/* RESET PASSWORD MODAL */}
<Dialog open={isPassOpen} onOpenChange={setIsPassOpen}>
<DialogContent className="sm:max-w-md rounded-3xl p-0 overflow-hidden border-0">
<div className="px-6 pt-6 pb-4 border-b border-slate-100">
<DialogContent className="overflow-hidden rounded-3xl border-0 p-0 sm:max-w-md">
<div className="border-b border-slate-100 px-6 pt-6 pb-4">
<DialogTitle className="text-xl font-black">Ubah Password</DialogTitle>
<p className="text-xs text-slate-500 mt-1">Ubah password untuk user <span className="font-bold text-slate-900">@{selectedUser?.username}</span></p>
<p className="mt-1 text-xs text-slate-500">
Ubah password untuk user{' '}
<span className="font-bold text-slate-900">@{selectedUser?.username}</span>
</p>
</div>
<form onSubmit={handleResetPassword} className="px-6 py-4 space-y-4">
<form onSubmit={handleResetPassword} className="space-y-4 px-6 py-4">
<div className="space-y-1.5">
<Label className="text-xs font-bold uppercase text-slate-700">Password Baru</Label>
<Input required type="password" value={newPass} onChange={e => setNewPass(e.target.value)} className="h-11 rounded-xl" placeholder="Minimal 6 karakter" />
<Label className="text-xs font-bold text-slate-700 uppercase">Password Baru</Label>
<Input
required
type="password"
value={newPass}
onChange={(e) => setNewPass(e.target.value)}
className="h-11 rounded-xl"
placeholder="Minimal 6 karakter"
/>
</div>
<div className="pt-4 flex gap-3">
<Button type="button" variant="outline" onClick={() => setIsPassOpen(false)} className="flex-1 h-11 rounded-xl">Batal</Button>
<Button type="submit" disabled={actionLoading} className="flex-1 h-11 rounded-xl bg-slate-900 hover:bg-slate-800 text-white">
{actionLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Simpan Password'}
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setIsPassOpen(false)}
className="h-11 flex-1 rounded-xl"
>
Batal
</Button>
<Button
type="submit"
disabled={actionLoading}
className="h-11 flex-1 rounded-xl bg-slate-900 text-white hover:bg-slate-800"
>
{actionLoading ? <Loader2 className="h-5 w-5 animate-spin" /> : 'Simpan Password'}
</Button>
</div>
</form>
+1 -5
View File
@@ -1,7 +1,3 @@
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-[#F4F6FB] dark:bg-[#0B0F19]">
{children}
</div>
);
return <div className="min-h-screen bg-[#F4F6FB] dark:bg-[#0B0F19]">{children}</div>
}
+64 -46
View File
@@ -41,121 +41,139 @@ export default function LoginPage() {
}
return (
<div className="min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-950 p-4 lg:p-6 relative overflow-hidden">
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-slate-50 p-4 lg:p-6 dark:bg-slate-950">
{/* Elegant Ambient Background */}
<div className="absolute inset-0 z-0 pointer-events-none">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)]"></div>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 h-[400px] w-[600px] rounded-full bg-[#1B2CC1] opacity-10 dark:opacity-20 blur-[120px]"></div>
<div className="pointer-events-none absolute inset-0 z-0">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)] bg-[size:24px_24px]"></div>
<div className="absolute top-1/2 left-1/2 h-[400px] w-[600px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#1B2CC1] opacity-10 blur-[120px] dark:opacity-20"></div>
</div>
<div className="w-full max-w-5xl bg-white dark:bg-slate-900 lg:rounded-[2.5rem] rounded-3xl shadow-2xl shadow-[#1B2CC1]/10 flex overflow-hidden border border-slate-100 dark:border-slate-800 relative z-10">
<div className="relative z-10 flex w-full max-w-5xl overflow-hidden rounded-3xl border border-slate-100 bg-white shadow-2xl shadow-[#1B2CC1]/10 lg:rounded-[2.5rem] dark:border-slate-800 dark:bg-slate-900">
{/* Left Side: Branding (Hidden on Mobile) */}
<div className="hidden lg:flex w-1/2 bg-gradient-to-br from-[#1B2CC1] to-[#121E85] p-12 flex-col justify-between relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-full pointer-events-none">
<div className="absolute -top-[20%] -left-[10%] w-[60%] h-[60%] rounded-full bg-white/10 blur-3xl" />
<div className="absolute top-[60%] -right-[10%] w-[70%] h-[70%] rounded-full bg-blue-400/20 blur-3xl" />
<div className="relative hidden w-1/2 flex-col justify-between overflow-hidden bg-gradient-to-br from-[#1B2CC1] to-[#121E85] p-12 lg:flex">
<div className="pointer-events-none absolute top-0 left-0 h-full w-full">
<div className="absolute -top-[20%] -left-[10%] h-[60%] w-[60%] rounded-full bg-white/10 blur-3xl" />
<div className="absolute top-[60%] -right-[10%] h-[70%] w-[70%] rounded-full bg-blue-400/20 blur-3xl" />
</div>
<div className="relative z-10">
<div className="inline-flex items-center justify-center p-3 bg-white/10 rounded-2xl backdrop-blur-md border border-white/20 mb-8 shadow-sm">
<LogIn className="w-8 h-8 text-white" />
<div className="mb-8 inline-flex items-center justify-center rounded-2xl border border-white/20 bg-white/10 p-3 shadow-sm backdrop-blur-md">
<LogIn className="h-8 w-8 text-white" />
</div>
<h1 className="text-4xl font-black text-white leading-tight">
<h1 className="text-4xl leading-tight font-black text-white">
Selamat Datang <br /> di TitipIn
</h1>
<p className="text-blue-100 mt-4 text-base max-w-sm leading-relaxed">
Platform modern dan terpercaya untuk mengelola pesanan jasa titip Anda dengan rapi, efisien, dan transparan.
<p className="mt-4 max-w-sm text-base leading-relaxed text-blue-100">
Platform modern dan terpercaya untuk mengelola pesanan jasa titip Anda dengan rapi,
efisien, dan transparan.
</p>
</div>
<div className="relative z-10 flex items-center gap-4 bg-white/10 p-4 rounded-2xl backdrop-blur-sm border border-white/10 w-fit">
<div className="relative z-10 flex w-fit items-center gap-4 rounded-2xl border border-white/10 bg-white/10 p-4 backdrop-blur-sm">
<div className="flex -space-x-3">
<div className="w-10 h-10 rounded-full bg-blue-200 border-2 border-[#121E85] flex items-center justify-center text-[10px] font-bold text-blue-700">A</div>
<div className="w-10 h-10 rounded-full bg-purple-200 border-2 border-[#121E85] flex items-center justify-center text-[10px] font-bold text-purple-700">B</div>
<div className="w-10 h-10 rounded-full bg-amber-200 border-2 border-[#121E85] flex items-center justify-center text-[10px] font-bold text-amber-700">C</div>
<div className="flex h-10 w-10 items-center justify-center rounded-full border-2 border-[#121E85] bg-blue-200 text-[10px] font-bold text-blue-700">
A
</div>
<div className="flex h-10 w-10 items-center justify-center rounded-full border-2 border-[#121E85] bg-purple-200 text-[10px] font-bold text-purple-700">
B
</div>
<div className="flex h-10 w-10 items-center justify-center rounded-full border-2 border-[#121E85] bg-amber-200 text-[10px] font-bold text-amber-700">
C
</div>
</div>
<p className="text-xs text-blue-100 font-medium">Bergabung dengan ribuan<br/>pengguna lainnya.</p>
<p className="text-xs font-medium text-blue-100">
Bergabung dengan ribuan
<br />
pengguna lainnya.
</p>
</div>
</div>
{/* Right Side: Form */}
<div className="w-full lg:w-1/2 p-8 lg:p-14 flex flex-col justify-center">
<div className="lg:hidden flex flex-col items-center mb-8">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-[#1B2CC1] to-[#121E85] flex items-center justify-center text-white shadow-lg shadow-[#1B2CC1]/25 mb-4">
<LogIn className="w-8 h-8" />
<div className="flex w-full flex-col justify-center p-8 lg:w-1/2 lg:p-14">
<div className="mb-8 flex flex-col items-center lg:hidden">
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-[#1B2CC1] to-[#121E85] text-white shadow-lg shadow-[#1B2CC1]/25">
<LogIn className="h-8 w-8" />
</div>
<h1 className="text-2xl font-black text-slate-900 dark:text-white">TitipIn</h1>
</div>
<div className="mb-8 text-center lg:text-left">
<h2 className="text-2xl font-black text-slate-900 dark:text-white hidden lg:block mb-2">Masuk ke Akun</h2>
<p className="text-sm text-slate-500">Silakan masukkan kredensial Anda untuk melanjutkan</p>
<h2 className="mb-2 hidden text-2xl font-black text-slate-900 lg:block dark:text-white">
Masuk ke Akun
</h2>
<p className="text-sm text-slate-500">
Silakan masukkan kredensial Anda untuk melanjutkan
</p>
</div>
{error && (
<div className="bg-rose-50 text-rose-600 p-3 rounded-xl text-sm font-medium mb-6 border border-rose-100 text-center">
<div className="mb-6 rounded-xl border border-rose-100 bg-rose-50 p-3 text-center text-sm font-medium text-rose-600">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5">
<div className="space-y-2">
<label className="text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider">Username</label>
<label className="text-xs font-bold tracking-wider text-slate-700 uppercase dark:text-slate-300">
Username
</label>
<div className="relative">
<User className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
<User className="absolute top-1/2 left-3.5 h-4 w-4 -translate-y-1/2 text-slate-400" />
<Input
name="username"
className="pl-10 h-12 bg-slate-50 dark:bg-slate-800/50 rounded-xl border-slate-200 focus-visible:ring-[#1B2CC1]"
className="h-12 rounded-xl border-slate-200 bg-slate-50 pl-10 focus-visible:ring-[#1B2CC1] dark:bg-slate-800/50"
placeholder="Masukkan username"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider">Password</label>
<label className="text-xs font-bold tracking-wider text-slate-700 uppercase dark:text-slate-300">
Password
</label>
<div className="relative flex items-center">
<KeyRound className="absolute left-3.5 w-4 h-4 text-slate-400" />
<Input
<KeyRound className="absolute left-3.5 h-4 w-4 text-slate-400" />
<Input
name="password"
type={showPassword ? 'text' : 'password'}
className="pl-10 pr-12 h-12 bg-slate-50 dark:bg-slate-800/50 rounded-xl border-slate-200 focus-visible:ring-[#1B2CC1]"
className="h-12 rounded-xl border-slate-200 bg-slate-50 pr-12 pl-10 focus-visible:ring-[#1B2CC1] dark:bg-slate-800/50"
placeholder="Masukkan password"
/>
<button
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors cursor-pointer"
className="absolute right-3.5 cursor-pointer text-slate-400 transition-colors hover:text-slate-600 dark:hover:text-slate-200"
aria-label={showPassword ? 'Sembunyikan password' : 'Tampilkan password'}
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
<Button
type="submit"
<Button
type="submit"
disabled={loading}
className="w-full h-12 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold text-base shadow-md shadow-[#1B2CC1]/20 transition-all active:scale-[0.98] mt-2 cursor-pointer"
className="mt-2 h-12 w-full cursor-pointer rounded-xl bg-[#1B2CC1] text-base font-bold text-white shadow-md shadow-[#1B2CC1]/20 transition-all hover:bg-[#15229E] active:scale-[0.98]"
>
{loading ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Masuk Sekarang'}
{loading ? <Loader2 className="h-5 w-5 animate-spin" /> : 'Masuk Sekarang'}
</Button>
</form>
<p className="text-center text-sm text-slate-500 mt-8">
<p className="mt-8 text-center text-sm text-slate-500">
Belum punya akun?{' '}
<button
<button
onClick={() => {
const search = window.location.search
router.push(`/register${search}`)
}}
className="font-bold text-[#1B2CC1] hover:underline cursor-pointer"
className="cursor-pointer font-bold text-[#1B2CC1] hover:underline"
>
Daftar di sini
</button>
</p>
<p className="text-center text-xs text-slate-400 mt-12 font-medium">
<p className="mt-12 text-center text-xs font-medium text-slate-400">
&copy; {new Date().getFullYear()} TitipIn - Sistem Titip Pesanan
</p>
</div>
+74 -55
View File
@@ -56,154 +56,173 @@ export default function RegisterPage() {
}
return (
<div className="min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-950 p-4 lg:p-6 relative overflow-hidden">
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-slate-50 p-4 lg:p-6 dark:bg-slate-950">
{/* Elegant Ambient Background */}
<div className="absolute inset-0 z-0 pointer-events-none">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)]"></div>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 h-[400px] w-[600px] rounded-full bg-[#121E85] opacity-10 dark:opacity-20 blur-[120px]"></div>
<div className="pointer-events-none absolute inset-0 z-0">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)] bg-[size:24px_24px]"></div>
<div className="absolute top-1/2 left-1/2 h-[400px] w-[600px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#121E85] opacity-10 blur-[120px] dark:opacity-20"></div>
</div>
<div className="w-full max-w-5xl bg-white dark:bg-slate-900 lg:rounded-[2.5rem] rounded-3xl shadow-2xl shadow-[#1B2CC1]/10 flex overflow-hidden border border-slate-100 dark:border-slate-800 relative z-10">
<div className="relative z-10 flex w-full max-w-5xl overflow-hidden rounded-3xl border border-slate-100 bg-white shadow-2xl shadow-[#1B2CC1]/10 lg:rounded-[2.5rem] dark:border-slate-800 dark:bg-slate-900">
{/* Left Side: Branding (Hidden on Mobile) */}
<div className="hidden lg:flex w-1/2 bg-gradient-to-br from-[#121E85] to-[#1B2CC1] p-12 flex-col justify-between relative overflow-hidden">
<div className="absolute top-0 left-0 w-full h-full pointer-events-none">
<div className="absolute -top-[20%] -left-[10%] w-[60%] h-[60%] rounded-full bg-white/10 blur-3xl" />
<div className="absolute top-[60%] -right-[10%] w-[70%] h-[70%] rounded-full bg-indigo-400/20 blur-3xl" />
<div className="relative hidden w-1/2 flex-col justify-between overflow-hidden bg-gradient-to-br from-[#121E85] to-[#1B2CC1] p-12 lg:flex">
<div className="pointer-events-none absolute top-0 left-0 h-full w-full">
<div className="absolute -top-[20%] -left-[10%] h-[60%] w-[60%] rounded-full bg-white/10 blur-3xl" />
<div className="absolute top-[60%] -right-[10%] h-[70%] w-[70%] rounded-full bg-indigo-400/20 blur-3xl" />
</div>
<div className="relative z-10">
<div className="inline-flex items-center justify-center p-3 bg-white/10 rounded-2xl backdrop-blur-md border border-white/20 mb-8 shadow-sm">
<UserPlus className="w-8 h-8 text-white" />
<div className="mb-8 inline-flex items-center justify-center rounded-2xl border border-white/20 bg-white/10 p-3 shadow-sm backdrop-blur-md">
<UserPlus className="h-8 w-8 text-white" />
</div>
<h1 className="text-4xl font-black text-white leading-tight">
<h1 className="text-4xl leading-tight font-black text-white">
Mulai Perjalanan <br /> Anda di TitipIn
</h1>
<p className="text-indigo-100 mt-4 text-base max-w-sm leading-relaxed">
<p className="mt-4 max-w-sm text-base leading-relaxed text-indigo-100">
Buat akun secara gratis dan nikmati kemudahan mengelola PO jasa titip tanpa ribet.
</p>
</div>
<div className="relative z-10 flex items-center gap-4 bg-white/10 p-4 rounded-2xl backdrop-blur-sm border border-white/10 w-fit">
<div className="relative z-10 flex w-fit items-center gap-4 rounded-2xl border border-white/10 bg-white/10 p-4 backdrop-blur-sm">
<div className="flex -space-x-3">
<div className="w-10 h-10 rounded-full bg-emerald-200 border-2 border-[#121E85] flex items-center justify-center text-[10px] font-bold text-emerald-700"></div>
<div className="w-10 h-10 rounded-full bg-cyan-200 border-2 border-[#121E85] flex items-center justify-center text-[10px] font-bold text-cyan-700"></div>
<div className="w-10 h-10 rounded-full bg-rose-200 border-2 border-[#121E85] flex items-center justify-center text-[10px] font-bold text-rose-700"></div>
<div className="flex h-10 w-10 items-center justify-center rounded-full border-2 border-[#121E85] bg-emerald-200 text-[10px] font-bold text-emerald-700">
</div>
<div className="flex h-10 w-10 items-center justify-center rounded-full border-2 border-[#121E85] bg-cyan-200 text-[10px] font-bold text-cyan-700">
</div>
<div className="flex h-10 w-10 items-center justify-center rounded-full border-2 border-[#121E85] bg-rose-200 text-[10px] font-bold text-rose-700">
</div>
</div>
<p className="text-xs text-indigo-100 font-medium">Aman, Cepat, dan<br/>Mudah digunakan.</p>
<p className="text-xs font-medium text-indigo-100">
Aman, Cepat, dan
<br />
Mudah digunakan.
</p>
</div>
</div>
{/* Right Side: Form */}
<div className="w-full lg:w-1/2 p-8 lg:p-14 flex flex-col justify-center">
<div className="lg:hidden flex flex-col items-center mb-8">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-[#1B2CC1] to-[#121E85] flex items-center justify-center text-white shadow-lg shadow-[#1B2CC1]/25 mb-4">
<UserPlus className="w-8 h-8" />
<div className="flex w-full flex-col justify-center p-8 lg:w-1/2 lg:p-14">
<div className="mb-8 flex flex-col items-center lg:hidden">
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-[#1B2CC1] to-[#121E85] text-white shadow-lg shadow-[#1B2CC1]/25">
<UserPlus className="h-8 w-8" />
</div>
<h1 className="text-2xl font-black text-slate-900 dark:text-white">Daftar TitipIn</h1>
</div>
<div className="mb-8 text-center lg:text-left">
<h2 className="text-2xl font-black text-slate-900 dark:text-white hidden lg:block mb-2">Buat Akun Baru</h2>
<h2 className="mb-2 hidden text-2xl font-black text-slate-900 lg:block dark:text-white">
Buat Akun Baru
</h2>
<p className="text-sm text-slate-500">Lengkapi form di bawah untuk membuat akun baru</p>
</div>
{error && (
<div className="bg-rose-50 text-rose-600 p-3 rounded-xl text-sm font-medium mb-6 border border-rose-100 text-center">
<div className="mb-6 rounded-xl border border-rose-100 bg-rose-50 p-3 text-center text-sm font-medium text-rose-600">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label className="text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider">Nama Lengkap</label>
<label className="text-xs font-bold tracking-wider text-slate-700 uppercase dark:text-slate-300">
Nama Lengkap
</label>
<div className="relative">
<BadgeCheck className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
<BadgeCheck className="absolute top-1/2 left-3.5 h-4 w-4 -translate-y-1/2 text-slate-400" />
<Input
name="name"
className="pl-10 h-12 bg-slate-50 dark:bg-slate-800/50 rounded-xl border-slate-200 focus-visible:ring-[#1B2CC1]"
className="h-12 rounded-xl border-slate-200 bg-slate-50 pl-10 focus-visible:ring-[#1B2CC1] dark:bg-slate-800/50"
placeholder="Masukkan nama lengkap"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider">Username</label>
<label className="text-xs font-bold tracking-wider text-slate-700 uppercase dark:text-slate-300">
Username
</label>
<div className="relative">
<User className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
<User className="absolute top-1/2 left-3.5 h-4 w-4 -translate-y-1/2 text-slate-400" />
<Input
name="username"
className="pl-10 h-12 bg-slate-50 dark:bg-slate-800/50 rounded-xl border-slate-200 focus-visible:ring-[#1B2CC1]"
className="h-12 rounded-xl border-slate-200 bg-slate-50 pl-10 focus-visible:ring-[#1B2CC1] dark:bg-slate-800/50"
placeholder="Buat username unik"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider">Password</label>
<label className="text-xs font-bold tracking-wider text-slate-700 uppercase dark:text-slate-300">
Password
</label>
<div className="relative flex items-center">
<KeyRound className="absolute left-3.5 w-4 h-4 text-slate-400" />
<Input
<KeyRound className="absolute left-3.5 h-4 w-4 text-slate-400" />
<Input
name="password"
type={showPassword ? 'text' : 'password'}
className="pl-10 pr-12 h-12 bg-slate-50 dark:bg-slate-800/50 rounded-xl border-slate-200 focus-visible:ring-[#1B2CC1]"
className="h-12 rounded-xl border-slate-200 bg-slate-50 pr-12 pl-10 focus-visible:ring-[#1B2CC1] dark:bg-slate-800/50"
placeholder="Minimal 6 karakter"
/>
<button
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors cursor-pointer"
className="absolute right-3.5 cursor-pointer text-slate-400 transition-colors hover:text-slate-600 dark:hover:text-slate-200"
aria-label={showPassword ? 'Sembunyikan password' : 'Tampilkan password'}
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider">Konfirmasi Password</label>
<label className="text-xs font-bold tracking-wider text-slate-700 uppercase dark:text-slate-300">
Konfirmasi Password
</label>
<div className="relative flex items-center">
<KeyRound className="absolute left-3.5 w-4 h-4 text-slate-400" />
<Input
<KeyRound className="absolute left-3.5 h-4 w-4 text-slate-400" />
<Input
name="confirm"
type={showConfirm ? 'text' : 'password'}
className="pl-10 pr-12 h-12 bg-slate-50 dark:bg-slate-800/50 rounded-xl border-slate-200 focus-visible:ring-[#1B2CC1]"
className="h-12 rounded-xl border-slate-200 bg-slate-50 pr-12 pl-10 focus-visible:ring-[#1B2CC1] dark:bg-slate-800/50"
placeholder="Ketik ulang password"
/>
<button
<button
type="button"
onClick={() => setShowConfirm(!showConfirm)}
className="absolute right-3.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors cursor-pointer"
className="absolute right-3.5 cursor-pointer text-slate-400 transition-colors hover:text-slate-600 dark:hover:text-slate-200"
aria-label={showConfirm ? 'Sembunyikan password' : 'Tampilkan password'}
>
{showConfirm ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
{showConfirm ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
<Button
type="submit"
<Button
type="submit"
disabled={loading}
className="w-full h-12 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold text-base shadow-md shadow-[#1B2CC1]/20 transition-all active:scale-[0.98] mt-2 cursor-pointer"
className="mt-2 h-12 w-full cursor-pointer rounded-xl bg-[#1B2CC1] text-base font-bold text-white shadow-md shadow-[#1B2CC1]/20 transition-all hover:bg-[#15229E] active:scale-[0.98]"
>
{loading ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Buat Akun'}
{loading ? <Loader2 className="h-5 w-5 animate-spin" /> : 'Buat Akun'}
</Button>
</form>
<p className="text-center text-sm text-slate-500 mt-8">
<p className="mt-8 text-center text-sm text-slate-500">
Sudah punya akun?{' '}
<button
<button
onClick={() => {
const search = window.location.search
router.push(`/login${search}`)
}}
className="font-bold text-[#1B2CC1] hover:underline cursor-pointer"
className="cursor-pointer font-bold text-[#1B2CC1] hover:underline"
>
Masuk di sini
</button>
</p>
<p className="text-center text-xs text-slate-400 mt-12 font-medium">
<p className="mt-12 text-center text-xs font-medium text-slate-400">
&copy; {new Date().getFullYear()} TitipIn - Sistem Titip Pesanan
</p>
</div>
+243 -191
View File
@@ -21,21 +21,21 @@ export async function loginUser(username: string, password: string) {
username: user.username,
name: user.name,
role: user.role,
photo: user.photo
photo: user.photo,
}
const sessionToken = await encrypt(sessionData);
(await cookies()).set('session', sessionToken, {
const sessionToken = await encrypt(sessionData)
;(await cookies()).set('session', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/'
path: '/',
})
return { success: true, user: sessionData }
} catch (e) {
console.error("Login Error:", e)
console.error('Login Error:', e)
return { success: false, error: 'Terjadi kesalahan saat login.' }
}
}
@@ -44,7 +44,7 @@ export async function registerUser(name: string, username: string, password: str
try {
const hashedPassword = await hashPassword(password)
const user = await prisma.user.create({
data: { name, username, password: hashedPassword }
data: { name, username, password: hashedPassword },
})
const sessionData = {
@@ -52,28 +52,28 @@ export async function registerUser(name: string, username: string, password: str
username: user.username,
name: user.name,
role: user.role,
photo: user.photo
photo: user.photo,
}
const sessionToken = await encrypt(sessionData);
(await cookies()).set('session', sessionToken, {
const sessionToken = await encrypt(sessionData)
;(await cookies()).set('session', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/'
path: '/',
})
return { success: true, user: sessionData }
} catch (error: any) {
console.error("Register Error:", error)
console.error('Register Error:', error)
if (error.code === 'P2002') return { success: false, error: 'Username sudah digunakan.' }
return { success: false, error: 'Terjadi kesalahan.' }
}
}
export async function logoutUser() {
(await cookies()).delete('session')
;(await cookies()).delete('session')
return { success: true }
}
@@ -87,7 +87,7 @@ export async function updateProfile(id: string, name: string, photo?: string | n
try {
const user = await prisma.user.update({
where: { id },
data: { name, photo: photo === undefined ? undefined : (photo || null) }
data: { name, photo: photo === undefined ? undefined : photo || null },
})
// Update session cookie with new data
@@ -96,14 +96,14 @@ export async function updateProfile(id: string, name: string, photo?: string | n
username: user.username,
name: user.name,
role: user.role,
photo: user.photo
photo: user.photo,
}
const sessionToken = await encrypt(sessionData);
(await cookies()).set('session', sessionToken, {
const sessionToken = await encrypt(sessionData)
;(await cookies()).set('session', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/'
path: '/',
})
revalidatePath('/')
@@ -118,16 +118,16 @@ export async function changePassword(id: string, oldPass: string, newPass: strin
try {
const user = await prisma.user.findUnique({ where: { id } })
if (!user) return { success: false, error: 'User tidak ditemukan.' }
const isMatch = await verifyPassword(oldPass, user.password)
if (!isMatch) return { success: false, error: 'Password lama salah.' }
const hashed = await hashPassword(newPass)
await prisma.user.update({
where: { id },
data: { password: hashed }
data: { password: hashed },
})
return { success: true }
} catch (e) {
return { success: false, error: 'Gagal mengganti password.' }
@@ -139,16 +139,21 @@ export async function getUsers(page = 1, limit = 10) {
const skip = (page - 1) * limit
const [users, total] = await Promise.all([
prisma.user.findMany({ skip, take: limit, orderBy: { created_at: 'desc' } }),
prisma.user.count()
prisma.user.count(),
])
return { users, total, totalPages: Math.ceil(total / limit) }
}
export async function createUserByAdmin(name: string, username: string, password: string, role: string) {
export async function createUserByAdmin(
name: string,
username: string,
password: string,
role: string
) {
try {
const hashedPassword = await hashPassword(password)
await prisma.user.create({
data: { name, username, password: hashedPassword, role }
data: { name, username, password: hashedPassword, role },
})
revalidatePath('/users')
return { success: true }
@@ -161,11 +166,12 @@ export async function createUserByAdmin(name: string, username: string, password
export async function toggleUserActive(id: string, is_active: boolean) {
try {
const user = await prisma.user.findUnique({ where: { id } })
if (user?.role === 'superadmin') return { success: false, error: 'Data Superadmin tidak bisa dinonaktifkan.' }
if (user?.role === 'superadmin')
return { success: false, error: 'Data Superadmin tidak bisa dinonaktifkan.' }
await prisma.user.update({
where: { id },
data: { is_active }
data: { is_active },
})
revalidatePath('/users')
return { success: true }
@@ -177,7 +183,8 @@ export async function toggleUserActive(id: string, is_active: boolean) {
export async function deleteUser(id: string) {
try {
const user = await prisma.user.findUnique({ where: { id } })
if (user?.role === 'superadmin') return { success: false, error: 'Data Superadmin tidak bisa dihapus.' }
if (user?.role === 'superadmin')
return { success: false, error: 'Data Superadmin tidak bisa dihapus.' }
await prisma.user.delete({ where: { id } })
revalidatePath('/users')
@@ -192,7 +199,7 @@ export async function resetUserPassword(id: string, newPass: string) {
const hashed = await hashPassword(newPass)
await prisma.user.update({
where: { id },
data: { password: hashed }
data: { password: hashed },
})
revalidatePath('/users')
return { success: true }
@@ -205,21 +212,21 @@ export async function resetUserPassword(id: string, newPass: string) {
export async function getAvailableOrders() {
const startOfDay = new Date()
startOfDay.setHours(0, 0, 0, 0)
const endOfDay = new Date()
endOfDay.setHours(23, 59, 59, 999)
return await prisma.order.findMany({
where: {
status: 'OPEN',
date: { gte: startOfDay, lte: endOfDay }
date: { gte: startOfDay, lte: endOfDay },
},
include: {
creator: true,
available_items: true,
submissions: { select: { user_id: true } }
submissions: { select: { user_id: true } },
},
orderBy: { date: 'desc' }
orderBy: { date: 'desc' },
})
}
@@ -228,19 +235,19 @@ export async function getMyOrders(creator_id: string) {
where: { creator_id },
include: {
available_items: true,
submissions: { select: { id: true, payment_status: true } }
submissions: { select: { id: true, payment_status: true } },
},
orderBy: { date: 'desc' }
orderBy: { date: 'desc' },
})
}
export async function createOrder(data: {
creator_id: string;
title: string;
description?: string;
date: Date;
allow_custom: boolean;
available_items: string[];
creator_id: string
title: string
description?: string
date: Date
allow_custom: boolean
available_items: string[]
}) {
try {
const order = await prisma.order.create({
@@ -252,14 +259,14 @@ export async function createOrder(data: {
creator_id: data.creator_id,
status: 'DRAFT',
available_items: {
create: data.available_items.map(name => ({ name }))
}
}
create: data.available_items.map((name) => ({ name })),
},
},
})
revalidatePath('/my-orders')
return { success: true, order }
} catch (error) {
console.error("Create Order Error:", error)
console.error('Create Order Error:', error)
return { success: false, error: 'Gagal membuat order.' }
}
}
@@ -268,10 +275,10 @@ export async function duplicateOrder(order_id: string) {
try {
const oldOrder = await prisma.order.findUnique({
where: { id: order_id },
include: { available_items: true }
include: { available_items: true },
})
if (!oldOrder) return { success: false, error: 'Order tidak ditemukan' }
const newOrder = await prisma.order.create({
data: {
title: oldOrder.title + ' (Copy)',
@@ -281,9 +288,12 @@ export async function duplicateOrder(order_id: string) {
creator_id: oldOrder.creator_id,
status: 'DRAFT',
available_items: {
create: oldOrder.available_items.map(ai => ({ name: ai.name, is_sold_out: ai.is_sold_out }))
}
}
create: oldOrder.available_items.map((ai) => ({
name: ai.name,
is_sold_out: ai.is_sold_out,
})),
},
},
})
revalidatePath('/my-orders')
return { success: true, order: newOrder }
@@ -292,33 +302,38 @@ export async function duplicateOrder(order_id: string) {
}
}
export async function updateOrder(order_id: string, data: {
title: string;
description?: string;
allow_custom: boolean;
available_items: { name: string; is_sold_out: boolean }[];
}) {
export async function updateOrder(
order_id: string,
data: {
title: string
description?: string
allow_custom: boolean
available_items: { name: string; is_sold_out: boolean }[]
}
) {
try {
const order = await prisma.order.findUnique({
where: { id: order_id }
where: { id: order_id },
})
if (!order) return { success: false, error: 'Order tidak ditemukan' }
if (order.status === 'CLOSE') {
return { success: false, error: 'Order dengan status CLOSE tidak dapat diedit.' }
}
// Check if order date is today
const orderDate = new Date(order.date)
const today = new Date()
if (orderDate.getDate() !== today.getDate() ||
orderDate.getMonth() !== today.getMonth() ||
orderDate.getFullYear() !== today.getFullYear()) {
if (
orderDate.getDate() !== today.getDate() ||
orderDate.getMonth() !== today.getMonth() ||
orderDate.getFullYear() !== today.getFullYear()
) {
return { success: false, error: 'Hanya pesanan hari ini yang dapat diedit.' }
}
// Delete existing available items and insert updated ones
await prisma.availableItem.deleteMany({
where: { order_id }
where: { order_id },
})
const updatedOrder = await prisma.order.update({
@@ -328,9 +343,12 @@ export async function updateOrder(order_id: string, data: {
description: data.description || null,
allow_custom: data.allow_custom,
available_items: {
create: data.available_items.map(item => ({ name: item.name, is_sold_out: item.is_sold_out }))
}
}
create: data.available_items.map((item) => ({
name: item.name,
is_sold_out: item.is_sold_out,
})),
},
},
})
revalidatePath('/my-orders')
@@ -338,7 +356,7 @@ export async function updateOrder(order_id: string, data: {
revalidatePath('/')
return { success: true, order: updatedOrder }
} catch (error: any) {
console.error("Update Order Error:", error)
console.error('Update Order Error:', error)
return { success: false, error: 'Gagal memperbarui order: ' + error?.message }
}
}
@@ -347,7 +365,7 @@ export async function updateOrderStatus(order_id: string, status: string) {
try {
await prisma.order.update({
where: { id: order_id },
data: { status }
data: { status },
})
revalidatePath('/my-orders')
revalidatePath('/')
@@ -360,15 +378,18 @@ export async function updateOrderStatus(order_id: string, status: string) {
export async function deleteOrder(order_id: string) {
try {
const order = await prisma.order.findUnique({
where: { id: order_id }
where: { id: order_id },
})
if (!order) return { success: false, error: 'Order tidak ditemukan.' }
if (order.status === 'OPEN') {
return { success: false, error: 'Order berstatus OPEN tidak dapat dihapus. Silakan tutup (CLOSE) terlebih dahulu.' }
return {
success: false,
error: 'Order berstatus OPEN tidak dapat dihapus. Silakan tutup (CLOSE) terlebih dahulu.',
}
}
await prisma.order.delete({
where: { id: order_id }
where: { id: order_id },
})
revalidatePath('/my-orders')
@@ -389,18 +410,24 @@ export async function getOrderDetail(order_id: string) {
submissions: {
include: {
user: true,
items: true
}
}
}
items: true,
},
},
},
})
}
export async function updateSubmissionPayment(submission_id: string, bill: number | null, payment_status: string, paid_amount: number | null = null, saldo_used: number = 0) {
export async function updateSubmissionPayment(
submission_id: string,
bill: number | null,
payment_status: string,
paid_amount: number | null = null,
saldo_used: number = 0
) {
try {
await prisma.submission.update({
where: { id: submission_id },
data: { bill, payment_status, paid_amount, saldo_used }
data: { bill, payment_status, paid_amount, saldo_used },
})
revalidatePath(`/my-orders`)
revalidatePath(`/my-purchases`)
@@ -408,27 +435,40 @@ export async function updateSubmissionPayment(submission_id: string, bill: numbe
revalidatePath(`/balances`)
return { success: true }
} catch (e) {
console.error(e); return { success: false, error: 'Gagal menyimpan tagihan.' }
console.error(e)
return { success: false, error: 'Gagal menyimpan tagihan.' }
}
}
export async function updateBulkSubmissionPayment(submission_ids: string[], payment_status: string, use_balance: boolean = false) {
export async function updateBulkSubmissionPayment(
submission_ids: string[],
payment_status: string,
use_balance: boolean = false
) {
try {
if (payment_status === 'LUNAS') {
const submissions = await prisma.submission.findMany({ where: { id: { in: submission_ids } } })
const ops = submissions.map(sub => prisma.submission.update({
where: { id: sub.id },
data: {
payment_status,
paid_amount: use_balance ? (sub.paid_amount || 0) : (sub.paid_amount != null ? sub.paid_amount : sub.bill),
saldo_used: use_balance ? Math.max(0, (sub.bill || 0) - (sub.paid_amount || 0)) : 0
}
}))
const submissions = await prisma.submission.findMany({
where: { id: { in: submission_ids } },
})
const ops = submissions.map((sub) =>
prisma.submission.update({
where: { id: sub.id },
data: {
payment_status,
paid_amount: use_balance
? sub.paid_amount || 0
: sub.paid_amount != null
? sub.paid_amount
: sub.bill,
saldo_used: use_balance ? Math.max(0, (sub.bill || 0) - (sub.paid_amount || 0)) : 0,
},
})
)
await prisma.$transaction(ops)
} else {
await prisma.submission.updateMany({
where: { id: { in: submission_ids } },
data: { payment_status }
data: { payment_status },
})
}
revalidatePath(`/my-orders`)
@@ -443,25 +483,25 @@ export async function updateBulkSubmissionPayment(submission_ids: string[], paym
}
export async function processBulkPayment(data: {
submission_ids: string[];
cash_amount: number;
use_balance: boolean;
creator_id: string;
user_id: string;
submission_ids: string[]
cash_amount: number
use_balance: boolean
creator_id: string
user_id: string
}) {
try {
let currentBalance = 0
if (data.use_balance) {
const balanceData = await getBalancesAsCreator(data.creator_id)
const userBalance = balanceData.find(b => b.user.id === data.user_id)
const userBalance = balanceData.find((b) => b.user.id === data.user_id)
if (userBalance) currentBalance = userBalance.amount
}
const submissions = await prisma.submission.findMany({
where: { id: { in: data.submission_ids } },
include: { order: { select: { date: true } } }
include: { order: { select: { date: true } } },
})
// Sort by order date ascending (oldest first)
submissions.sort((a, b) => new Date(a.order.date).getTime() - new Date(b.order.date).getTime())
@@ -474,16 +514,16 @@ export async function processBulkPayment(data: {
for (let i = 0; i < submissions.length; i++) {
const sub = submissions[i]
const isLast = i === submissions.length - 1
const subBill = sub.bill || 0
const prevPaid = sub.paid_amount || 0
const amountToCover = Math.max(0, subBill - prevPaid)
if (totalAvailable >= amountToCover && amountToCover > 0) {
// Fully covered -> LUNAS
let balanceToUse = Math.min(remainingBalance, amountToCover)
remainingBalance -= balanceToUse
let cashToUse = amountToCover - balanceToUse
remainingCash -= cashToUse
totalAvailable -= amountToCover
@@ -494,47 +534,55 @@ export async function processBulkPayment(data: {
remainingCash = 0
}
ops.push(prisma.submission.update({
where: { id: sub.id },
data: { payment_status: 'LUNAS', paid_amount: finalPaid }
}))
ops.push(
prisma.submission.update({
where: { id: sub.id },
data: { payment_status: 'LUNAS', paid_amount: finalPaid },
})
)
} else if (totalAvailable >= amountToCover && amountToCover === 0) {
// It's already fully paid somehow, just mark LUNAS. Give excess cash if last.
let finalPaid = prevPaid
if (isLast && remainingCash > 0) {
finalPaid += remainingCash
remainingCash = 0
finalPaid += remainingCash
remainingCash = 0
}
ops.push(prisma.submission.update({
where: { id: sub.id },
data: { payment_status: 'LUNAS', paid_amount: finalPaid }
}))
ops.push(
prisma.submission.update({
where: { id: sub.id },
data: { payment_status: 'LUNAS', paid_amount: finalPaid },
})
)
} else if (totalAvailable > 0) {
// Partially covered -> BELUM_BAYAR. Only use cash.
let finalPaid = prevPaid + remainingCash
remainingCash = 0
totalAvailable = remainingBalance // only balance left, which can't be used
ops.push(prisma.submission.update({
where: { id: sub.id },
data: { payment_status: 'BELUM_BAYAR', paid_amount: finalPaid }
}))
ops.push(
prisma.submission.update({
where: { id: sub.id },
data: { payment_status: 'BELUM_BAYAR', paid_amount: finalPaid },
})
)
} else {
// totalAvailable == 0. No more money. Just leave it as is, or update to BELUM_BAYAR.
ops.push(prisma.submission.update({
where: { id: sub.id },
data: { payment_status: 'BELUM_BAYAR' }
}))
ops.push(
prisma.submission.update({
where: { id: sub.id },
data: { payment_status: 'BELUM_BAYAR' },
})
)
}
}
await prisma.$transaction(ops)
revalidatePath(`/my-orders`)
revalidatePath(`/my-purchases`)
revalidatePath(`/reports`)
revalidatePath(`/balances`)
return { success: true }
} catch (e) {
console.error(e)
@@ -546,7 +594,7 @@ export async function processBulkPayment(data: {
export async function getUserSubmission(order_id: string, user_id: string) {
return await prisma.submission.findFirst({
where: { order_id, user_id },
include: { items: true }
include: { items: true },
})
}
@@ -557,21 +605,21 @@ export async function getMyPurchases(user_id: string) {
order: {
include: {
creator: true,
available_items: true
}
available_items: true,
},
},
items: true
items: true,
},
orderBy: {
order: { date: 'desc' }
}
order: { date: 'desc' },
},
})
}
export async function submitOrder(data: {
order_id: string;
user_id: string;
items: Array<{ name: string; qty: number; is_custom: boolean; note?: string }>;
order_id: string
user_id: string
items: Array<{ name: string; qty: number; is_custom: boolean; note?: string }>
}) {
try {
if (data.items.length === 0) {
@@ -580,17 +628,18 @@ export async function submitOrder(data: {
const order = await prisma.order.findUnique({ where: { id: data.order_id } })
if (!order) return { success: false, error: 'Pesanan tidak ditemukan.' }
if (order.status !== 'OPEN') return { success: false, error: 'Pesanan sudah ditutup atau masih draft.' }
if (order.status !== 'OPEN')
return { success: false, error: 'Pesanan sudah ditutup atau masih draft.' }
const itemsToCreate = data.items.map(item => ({
const itemsToCreate = data.items.map((item) => ({
name: item.name,
qty: item.qty,
is_custom: item.is_custom,
note: item.note || null
note: item.note || null,
}))
const existing = await prisma.submission.findFirst({
where: { order_id: data.order_id, user_id: data.user_id }
where: { order_id: data.order_id, user_id: data.user_id },
})
if (existing) {
@@ -599,9 +648,9 @@ export async function submitOrder(data: {
where: { id: existing.id },
data: {
items: {
create: itemsToCreate
}
}
create: itemsToCreate,
},
},
})
} else {
await prisma.submission.create({
@@ -609,16 +658,16 @@ export async function submitOrder(data: {
order_id: data.order_id,
user_id: data.user_id,
items: {
create: itemsToCreate
}
}
create: itemsToCreate,
},
},
})
}
revalidatePath('/')
revalidatePath('/my-purchases')
revalidatePath(`/my-orders/${data.order_id}`)
return { success: true }
} catch (error) {
console.error(error)
@@ -633,18 +682,18 @@ export async function getSubmittorReport(user_id: string, startDate?: Date, endD
user_id,
order: {
status: 'CLOSE',
...(startDate && endDate ? { date: { gte: startDate, lte: endDate } } : {})
}
...(startDate && endDate ? { date: { gte: startDate, lte: endDate } } : {}),
},
},
include: {
order: {
include: { creator: true }
include: { creator: true },
},
items: true
items: true,
},
orderBy: {
order: { date: 'desc' }
}
order: { date: 'desc' },
},
})
}
@@ -653,17 +702,17 @@ export async function getCreatorReport(creator_id: string, startDate?: Date, end
where: {
creator_id,
status: 'CLOSE',
...(startDate && endDate ? { date: { gte: startDate, lte: endDate } } : {})
...(startDate && endDate ? { date: { gte: startDate, lte: endDate } } : {}),
},
include: {
submissions: {
include: {
user: true,
items: true
}
}
items: true,
},
},
},
orderBy: { date: 'desc' }
orderBy: { date: 'desc' },
})
}
@@ -671,25 +720,25 @@ export async function getCreatorReport(creator_id: string, startDate?: Date, end
export async function getBalancesAsCreator(creator_id: string) {
const submissions = await prisma.submission.findMany({
where: {
order: { creator_id }
order: { creator_id },
},
include: {
user: { select: { id: true, name: true, photo: true } }
}
user: { select: { id: true, name: true, photo: true } },
},
})
const balanceMap = new Map<string, { user: any, amount: number }>()
submissions.forEach(sub => {
let diff = 0;
const balanceMap = new Map<string, { user: any; amount: number }>()
submissions.forEach((sub) => {
let diff = 0
if (sub.payment_status === 'LUNAS') {
if (sub.bill != null) {
diff = (sub.paid_amount || 0) - sub.bill;
diff = (sub.paid_amount || 0) - sub.bill
}
} else {
diff = -(sub.saldo_used || 0);
diff = -(sub.saldo_used || 0)
}
if (diff !== 0) {
if (!balanceMap.has(sub.user.id)) {
balanceMap.set(sub.user.id, { user: sub.user, amount: diff })
@@ -699,33 +748,33 @@ export async function getBalancesAsCreator(creator_id: string) {
}
})
return Array.from(balanceMap.values()).filter(b => b.amount !== 0)
return Array.from(balanceMap.values()).filter((b) => b.amount !== 0)
}
export async function getBalancesAsSubmittor(user_id: string) {
const submissions = await prisma.submission.findMany({
where: {
user_id
user_id,
},
include: {
order: {
include: {
creator: { select: { id: true, name: true, photo: true } }
}
}
}
creator: { select: { id: true, name: true, photo: true } },
},
},
},
})
const balanceMap = new Map<string, { creator: any, amount: number }>()
submissions.forEach(sub => {
let diff = 0;
const balanceMap = new Map<string, { creator: any; amount: number }>()
submissions.forEach((sub) => {
let diff = 0
if (sub.payment_status === 'LUNAS') {
if (sub.bill != null) {
diff = (sub.paid_amount || 0) - sub.bill;
diff = (sub.paid_amount || 0) - sub.bill
}
} else {
diff = -(sub.saldo_used || 0);
diff = -(sub.saldo_used || 0)
}
if (diff !== 0) {
@@ -738,26 +787,27 @@ export async function getBalancesAsSubmittor(user_id: string) {
}
})
return Array.from(balanceMap.values()).filter(b => b.amount !== 0)
return Array.from(balanceMap.values()).filter((b) => b.amount !== 0)
}
export async function deleteSubmission(submission_id: string, user_id: string) {
try {
const submission = await prisma.submission.findUnique({
where: { id: submission_id },
include: { order: true }
});
include: { order: true },
})
if (!submission) return { success: false, error: 'Pesanan tidak ditemukan' };
if (submission.user_id !== user_id) return { success: false, error: 'Tidak ada akses' };
if (submission.order.status !== 'OPEN') return { success: false, error: 'Hanya bisa membatalkan PO yang OPEN' };
if (!submission) return { success: false, error: 'Pesanan tidak ditemukan' }
if (submission.user_id !== user_id) return { success: false, error: 'Tidak ada akses' }
if (submission.order.status !== 'OPEN')
return { success: false, error: 'Hanya bisa membatalkan PO yang OPEN' }
await prisma.submission.delete({
where: { id: submission_id }
});
return { success: true };
where: { id: submission_id },
})
return { success: true }
} catch (error) {
return { success: false, error: 'Gagal membatalkan pesanan' };
return { success: false, error: 'Gagal membatalkan pesanan' }
}
}
@@ -766,7 +816,7 @@ export async function deleteSubmission(submission_id: string, user_id: string) {
export async function getSettings() {
const settings = await prisma.setting.findMany()
const obj: Record<string, string> = {}
settings.forEach(s => {
settings.forEach((s) => {
obj[s.key] = s.value
})
return obj
@@ -778,7 +828,7 @@ export async function updateSettings(settings: Record<string, string>) {
await prisma.setting.upsert({
where: { key },
update: { value: settings[key] },
create: { key, value: settings[key] }
create: { key, value: settings[key] },
})
}
return { success: true }
@@ -793,22 +843,24 @@ export async function updateSettings(settings: Record<string, string>) {
export async function broadcastToMattermost(orderId: string, orderUrl: string) {
try {
const order = await prisma.order.findUnique({
where: { id: orderId }
where: { id: orderId },
})
if (!order) return { success: false, error: 'Order not found' }
const settings = await getSettings()
const webhookUrl = settings.MATTERMOST_WEBHOOK_URL
let template = settings.MATTERMOST_TEMPLATE || 'Halo tim! Lagi ada orderan open nih untuk {title}. Mumpung belum di-checkout, yang mau ikutan nitip bisa langsung cek ke sini ya: {url}'
let template =
settings.MATTERMOST_TEMPLATE ||
'Halo tim! Lagi ada orderan open nih untuk {title}. Mumpung belum di-checkout, yang mau ikutan nitip bisa langsung cek ke sini ya: {url}'
if (!webhookUrl) return { success: false, error: 'Webhook URL belum di-setting' }
const message = template.replace('{title}', order.title).replace('{url}', orderUrl)
const res = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: message })
body: JSON.stringify({ text: message }),
})
if (!res.ok) {
+86 -56
View File
@@ -1,15 +1,21 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import 'tailwindcss';
@import 'tw-animate-css';
@import 'shadcn/tailwind.css';
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-roboto), var(--font-open-sans), -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Segoe UI", Roboto, "Helvetica Neue", "Open Sans", system-ui, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--font-heading: var(--font-roboto), -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif;
--font-sans:
var(--font-roboto), var(--font-open-sans), -apple-system, BlinkMacSystemFont, 'SF Pro Text',
'SF Pro Display', 'Segoe UI', Roboto, 'Helvetica Neue', 'Open Sans', system-ui, sans-serif;
--font-mono:
ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono',
'Courier New', monospace;
--font-heading:
var(--font-roboto), -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto,
sans-serif;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -44,62 +50,62 @@
}
:root {
--background: #F4F6FB;
--foreground: #0F172A;
--card: #FFFFFF;
--card-foreground: #0F172A;
--popover: #FFFFFF;
--popover-foreground: #0F172A;
--primary: #1B2CC1;
--primary-foreground: #FFFFFF;
--secondary: #EEF1FC;
--secondary-foreground: #1B2CC1;
--muted: #F1F5F9;
--muted-foreground: #64748B;
--accent: #EEF2FF;
--accent-foreground: #1B2CC1;
--destructive: #EF4444;
--border: #E2E8F0;
--input: #E2E8F0;
--ring: #1B2CC1;
--background: #f4f6fb;
--foreground: #0f172a;
--card: #ffffff;
--card-foreground: #0f172a;
--popover: #ffffff;
--popover-foreground: #0f172a;
--primary: #1b2cc1;
--primary-foreground: #ffffff;
--secondary: #eef1fc;
--secondary-foreground: #1b2cc1;
--muted: #f1f5f9;
--muted-foreground: #64748b;
--accent: #eef2ff;
--accent-foreground: #1b2cc1;
--destructive: #ef4444;
--border: #e2e8f0;
--input: #e2e8f0;
--ring: #1b2cc1;
--radius: 0.75rem;
--sidebar: #FFFFFF;
--sidebar: #ffffff;
--sidebar-foreground: #334155;
--sidebar-primary: #1B2CC1;
--sidebar-primary-foreground: #FFFFFF;
--sidebar-accent: #F1F5F9;
--sidebar-accent-foreground: #0F172A;
--sidebar-border: #E2E8F0;
--sidebar-ring: #1B2CC1;
--sidebar-primary: #1b2cc1;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #f1f5f9;
--sidebar-accent-foreground: #0f172a;
--sidebar-border: #e2e8f0;
--sidebar-ring: #1b2cc1;
}
.dark {
--background: #0B0F19;
--foreground: #F8FAFC;
--background: #0b0f19;
--foreground: #f8fafc;
--card: #111827;
--card-foreground: #F8FAFC;
--card-foreground: #f8fafc;
--popover: #111827;
--popover-foreground: #F8FAFC;
--primary: #3B82F6;
--primary-foreground: #FFFFFF;
--secondary: #1E293B;
--secondary-foreground: #93C5FD;
--muted: #1E293B;
--muted-foreground: #94A3B8;
--accent: #1E293B;
--accent-foreground: #93C5FD;
--destructive: #EF4444;
--border: #1F2937;
--input: #1F2937;
--ring: #3B82F6;
--popover-foreground: #f8fafc;
--primary: #3b82f6;
--primary-foreground: #ffffff;
--secondary: #1e293b;
--secondary-foreground: #93c5fd;
--muted: #1e293b;
--muted-foreground: #94a3b8;
--accent: #1e293b;
--accent-foreground: #93c5fd;
--destructive: #ef4444;
--border: #1f2937;
--input: #1f2937;
--ring: #3b82f6;
--sidebar: #111827;
--sidebar-foreground: #CBD5E1;
--sidebar-primary: #3B82F6;
--sidebar-primary-foreground: #FFFFFF;
--sidebar-accent: #1E293B;
--sidebar-accent-foreground: #F8FAFC;
--sidebar-border: #1F2937;
--sidebar-ring: #3B82F6;
--sidebar-foreground: #cbd5e1;
--sidebar-primary: #3b82f6;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #1e293b;
--sidebar-accent-foreground: #f8fafc;
--sidebar-border: #1f2937;
--sidebar-ring: #3b82f6;
}
@layer base {
@@ -108,12 +114,36 @@
}
body {
@apply bg-background text-foreground;
font-family: var(--font-roboto), var(--font-open-sans), -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Segoe UI", Roboto, "Helvetica Neue", "Open Sans", system-ui, sans-serif;
font-family:
var(--font-roboto),
var(--font-open-sans),
-apple-system,
BlinkMacSystemFont,
'SF Pro Text',
'SF Pro Display',
'Segoe UI',
Roboto,
'Helvetica Neue',
'Open Sans',
system-ui,
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html {
font-family: var(--font-roboto), var(--font-open-sans), -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Segoe UI", Roboto, "Helvetica Neue", "Open Sans", system-ui, sans-serif;
font-family:
var(--font-roboto),
var(--font-open-sans),
-apple-system,
BlinkMacSystemFont,
'SF Pro Text',
'SF Pro Display',
'Segoe UI',
Roboto,
'Helvetica Neue',
'Open Sans',
system-ui,
sans-serif;
}
}
button {
+28 -30
View File
@@ -8,37 +8,35 @@ export const contentType = 'image/png'
export default function Icon() {
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(to bottom right, #1B2CC1, #121E85)',
borderRadius: '8px',
}}
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(to bottom right, #1B2CC1, #121E85)',
borderRadius: '8px',
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="white"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="white"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z" />
<path d="M20 3v4" />
<path d="M22 5h-4" />
<path d="M4 17v2" />
<path d="M5 18H3" />
</svg>
</div>
),
<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z" />
<path d="M20 3v4" />
<path d="M22 5h-4" />
<path d="M4 17v2" />
<path d="M5 18H3" />
</svg>
</div>,
{
...size,
}
+30 -32
View File
@@ -1,58 +1,57 @@
import type { Metadata, Viewport } from "next";
import { Roboto, Open_Sans } from "next/font/google";
import "./globals.css";
import { Toaster } from "@/components/ui/sonner";
import type { Metadata, Viewport } from 'next'
import { Roboto, Open_Sans } from 'next/font/google'
import './globals.css'
import { Toaster } from '@/components/ui/sonner'
const roboto = Roboto({
weight: ["300", "400", "500", "700", "900"],
subsets: ["latin"],
variable: "--font-roboto",
display: "swap",
});
weight: ['300', '400', '500', '700', '900'],
subsets: ['latin'],
variable: '--font-roboto',
display: 'swap',
})
const openSans = Open_Sans({
subsets: ["latin"],
variable: "--font-open-sans",
display: "swap",
});
subsets: ['latin'],
variable: '--font-open-sans',
display: 'swap',
})
export const viewport: Viewport = {
themeColor: "#1B2CC1",
width: "device-width",
themeColor: '#1B2CC1',
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
};
}
export const metadata: Metadata = {
title: "TitipIn - Sistem Titip Pesanan",
description: "Platform jasa titip pesanan bersama yang modern, cepat, dan transparan.",
manifest: "/manifest.json",
title: 'TitipIn - Sistem Titip Pesanan',
description: 'Platform jasa titip pesanan bersama yang modern, cepat, dan transparan.',
manifest: '/manifest.json',
appleWebApp: {
capable: true,
statusBarStyle: "black-translucent",
title: "TitipIn",
statusBarStyle: 'black-translucent',
title: 'TitipIn',
},
formatDetection: {
telephone: false,
},
icons: {
icon: [
{ url: "/icons/icon-192x192.png", sizes: "192x192", type: "image/png" },
{ url: "/icons/icon-512x512.png", sizes: "512x512", type: "image/png" },
{ url: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' },
{ url: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' },
],
apple: [
{ url: "/icons/icon-152x152.png", sizes: "152x152", type: "image/png" },
{ url: "/icons/icon-192x192.png", sizes: "192x192", type: "image/png" },
{ url: '/icons/icon-152x152.png', sizes: '152x152', type: 'image/png' },
{ url: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' },
],
},
};
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode
}>) {
return (
<html
@@ -60,14 +59,13 @@ export default function RootLayout({
suppressHydrationWarning
className={`${roboto.variable} ${openSans.variable} h-full antialiased`}
>
<body
<body
suppressHydrationWarning
className="min-h-full flex flex-col bg-[#F4F6FB] dark:bg-[#0B0F19] font-sans antialiased"
className="flex min-h-full flex-col bg-[#F4F6FB] font-sans antialiased dark:bg-[#0B0F19]"
>
{children}
<Toaster position="top-center" />
</body>
</html>
);
)
}
+19 -20
View File
@@ -4,45 +4,44 @@ import { SearchX, ArrowLeft, Home } from 'lucide-react'
export default function NotFound() {
return (
<div className="min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-950 p-4 relative overflow-hidden">
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-slate-50 p-4 dark:bg-slate-950">
{/* Background ambient effects */}
<div className="absolute inset-0 z-0 pointer-events-none">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)]"></div>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 h-[400px] w-[600px] rounded-full bg-[#1B2CC1] opacity-10 dark:opacity-20 blur-[120px]"></div>
<div className="pointer-events-none absolute inset-0 z-0">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)] bg-[size:24px_24px]"></div>
<div className="absolute top-1/2 left-1/2 h-[400px] w-[600px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#1B2CC1] opacity-10 blur-[120px] dark:opacity-20"></div>
</div>
<div className="relative z-10 w-full max-w-lg">
<div className="bg-white dark:bg-slate-900 rounded-[2rem] shadow-2xl shadow-[#1B2CC1]/10 border border-slate-100 dark:border-slate-800 p-8 sm:p-12 text-center animate-in fade-in zoom-in duration-500">
<div className="w-24 h-24 sm:w-32 sm:h-32 mx-auto bg-blue-50 dark:bg-blue-950/50 rounded-[2rem] border-4 border-white dark:border-slate-800 flex items-center justify-center mb-6 shadow-inner rotate-3">
<SearchX className="w-12 h-12 sm:w-16 sm:h-16 text-[#1B2CC1]" />
<div className="animate-in fade-in zoom-in rounded-[2rem] border border-slate-100 bg-white p-8 text-center shadow-2xl shadow-[#1B2CC1]/10 duration-500 sm:p-12 dark:border-slate-800 dark:bg-slate-900">
<div className="mx-auto mb-6 flex h-24 w-24 rotate-3 items-center justify-center rounded-[2rem] border-4 border-white bg-blue-50 shadow-inner sm:h-32 sm:w-32 dark:border-slate-800 dark:bg-blue-950/50">
<SearchX className="h-12 w-12 text-[#1B2CC1] sm:h-16 sm:w-16" />
</div>
<h1 className="text-6xl font-black text-slate-900 dark:text-white mb-2 tracking-tighter">
<h1 className="mb-2 text-6xl font-black tracking-tighter text-slate-900 dark:text-white">
4<span className="text-[#1B2CC1]">0</span>4
</h1>
<h2 className="text-xl sm:text-2xl font-bold text-slate-800 dark:text-slate-200 mb-4">
<h2 className="mb-4 text-xl font-bold text-slate-800 sm:text-2xl dark:text-slate-200">
Halaman Tidak Ditemukan
</h2>
<p className="text-sm sm:text-base text-slate-500 dark:text-slate-400 mb-8 max-w-sm mx-auto leading-relaxed">
Waduh, sepertinya halaman yang Anda cari sedang jalan-jalan atau memang tidak pernah ada. Mari kita kembali ke jalan yang benar!
<p className="mx-auto mb-8 max-w-sm text-sm leading-relaxed text-slate-500 sm:text-base dark:text-slate-400">
Waduh, sepertinya halaman yang Anda cari sedang jalan-jalan atau memang tidak pernah
ada. Mari kita kembali ke jalan yang benar!
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<div className="flex flex-col justify-center gap-3 sm:flex-row">
<Link href="/" className="w-full sm:w-auto">
<Button className="w-full h-12 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold px-6 shadow-md shadow-[#1B2CC1]/20 gap-2 transition-all">
<Home className="w-4 h-4" />
<Button className="h-12 w-full gap-2 rounded-xl bg-[#1B2CC1] px-6 font-bold text-white shadow-md shadow-[#1B2CC1]/20 transition-all hover:bg-[#15229E]">
<Home className="h-4 w-4" />
Kembali ke Beranda
</Button>
</Link>
</div>
</div>
{/* Footer info */}
<p className="text-center text-xs text-slate-400 font-medium mt-8">
<p className="mt-8 text-center text-xs font-medium text-slate-400">
TitipIn &copy; {new Date().getFullYear()} - Sistem Titip Pesanan
</p>
</div>