feat: implement app directory routing, authentication flow, and dynamic reporting module

This commit is contained in:
Firman Ramdhani
2026-08-28 15:50:01 +07:00
parent 3e68d12ce4
commit 84e6599b23
28 changed files with 1869 additions and 478 deletions
+30 -14
View File
@@ -12,9 +12,15 @@ Fondasi utama skema database:
table User:
- id String @id
- name String @unique
- id String @id @default(uuid())
- username String @unique
- name String
- password String
- photo String?
- role String @default("user") // "user" or "superadmin"
- is_active Boolean @default(true)
- created_at DateTime @default(now())
- updated_at DateTime @updatedAt
- orders Order[] @relation("CreatedOrders")
- purchases Submission[]
@@ -74,15 +80,12 @@ table SubmissionItem:
# 2. CORE AUTHENTICATION FLOW
- DILARANG menggunakan NextAuth/library auth eksternal.
- Setiap kali web diakses, jalankan pengecekan:
1. Cek `localStorage` untuk `user_id`.
2. Jika belum ada, generate UUID baru, simpan ke `localStorage`.
3. Lakukan verifikasi ke database/server action apakah `user_id` tersebut sudah terdaftar di tabel `User`.
4. Jika belum terdaftar, wajib munculkan Modal Profile yang tidak bisa ditutup.
5. Modal meminta input "Nama" (Wajib) dan "Foto Profile" (Opsional).
6. Validasi: Nama harus UNIK di tabel User. Setelah valid, simpan ke database menggunakan `user_id` sebagai `id`.
7. Jika `user_id` sudah terdaftar, lewati modal dan muat data user secara mulus.
- DILARANG menggunakan NextAuth/library auth eksternal. Buat custom authentication menggunakan JWT (atau hash-based token) yang disimpan di HTTP-only cookies atau localStorage.
- Fitur Token: Tanpa waktu expired (no expiration) dan mendukung multi-device login.
- Terdapat halaman `/login` dan `/register` untuk pengguna.
- Default role saat registrasi adalah `user`.
- Harus ada seeder untuk membuat akun superadmin default: Username: `superadmin` (atau spesifik), Password: `titipin123!`, Role: `superadmin`.
- Middleware/Route Protection: Hanya user yang sudah login yang bisa mengakses halaman utama aplikasi. Halaman manajemen user hanya bisa diakses oleh role `superadmin`.
# 3. MENU NAVIGATION ORDER
@@ -92,7 +95,8 @@ Urutan navigasi menu utama pada aplikasi adalah sebagai berikut:
2. Jasa Order Saya
3. Pesanan Saya
4. Laporan
5. Profile
5. User Management (Hanya Superadmin)
6. Profile
# 4. PAGE SPECIFICATIONS & STEP-BY-STEP FLOW
@@ -148,10 +152,22 @@ Urutan navigasi menu utama pada aplikasi adalah sebagai berikut:
- Visualisasi berupa Bar Chart (Grafik) dan Data Grid (Tabel) yang diurutkan secara proporsional.
- Mendukung filter Bulan dan Tahun berbasis server-side.
## Step 6: Halaman Profile (/profile)
## Step 6: Halaman User Management (/users) - KHUSUS SUPERADMIN
- Hanya muncul jika current user memiliki role `superadmin`.
- Menampilkan tabel daftar user dengan pagination (default 10 per halaman).
- Fitur Superadmin:
- Membuat data user baru.
- Mengedit password user lain.
- Activate dan Deactivate user.
- Menghapus user.
- Proteksi Data Superadmin: Data superadmin tidak bisa dihapus atau di-deactivate (terkunci), hanya bisa ganti password.
## Step 7: Halaman Profile (/profile)
- Form untuk mengedit Nama dan Foto Profile milik current user.
- Validasi nama unik tetap berlaku saat proses update.
- Terdapat form Ganti Password yang mewajibkan input: Password Lama, Password Baru, dan Konfirmasi Password Baru.
- Validasi nama dan verifikasi password lama tetap berlaku saat proses update.
# 6. WORKFLOW EXECUTION
+19 -11
View File
@@ -1,11 +1,11 @@
# TitipIn - Sistem Titip Pesanan
Aplikasi web modern berbasis Next.js untuk mempermudah sistem "jasa titip" (jastip) atau pesanan bersama. Aplikasi ini berjalan dengan arsitektur serverless (Server Actions) dan menggunakan sistem autentikasi stateless (berbasis UUID di LocalStorage).
Aplikasi web modern berbasis Next.js untuk mempermudah sistem "jasa titip" (jastip) atau pesanan bersama. Aplikasi ini berjalan dengan arsitektur serverless (Server Actions) dan menggunakan sistem autentikasi **stateless berbasis JWT yang disimpan secara aman di HTTP-only Cookies**. Dilengkapi dengan Role-Based Access Control (RBAC) untuk manajemen akses pengguna dan Superadmin.
## Kebutuhan Sistem (Prerequisites)
Sebelum menjalankan aplikasi ini, pastikan Anda telah menginstal perangkat lunak berikut:
- **Node.js**: Versi 18.x atau lebih baru (direkomendasikan versi LTS terbaru).
- **npm**: (biasanya sudah termasuk saat instalasi Node.js).
- **Yarn / npm**: (Disarankan menggunakan Yarn).
- **PostgreSQL**: Database server berjalan (versi 13 atau lebih baru direkomendasikan).
## Instalasi
@@ -13,12 +13,12 @@ Sebelum menjalankan aplikasi ini, pastikan Anda telah menginstal perangkat lunak
2. Buka terminal/command prompt.
3. Jalankan perintah instalasi dependensi:
```bash
npm install
yarn install
```
*Catatan: Pastikan dependensi Prisma terinstal pada versi stabil (contoh: `@prisma/client@5`).*
*(atau `npm install` jika tidak menggunakan yarn).*
## Pengaturan Environment
Buat sebuah file bernama `.env` di root folder proyek Anda. Konfigurasi kredensial database PostgreSQL Anda seperti format berikut:
Buat sebuah file bernama `.env` di root folder proyek Anda. Konfigurasi kredensial database PostgreSQL Anda beserta kunci rahasia JWT seperti format berikut:
```env
DATABASE_HOST="172.10.10.2"
@@ -27,26 +27,33 @@ DATABASE_USER="postgres"
DATABASE_PASSWORD="eigen3m!"
DATABASE_NAME="testing"
DATABASE_URL="postgresql://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}?schema=public"
# Kunci rahasia untuk enkripsi sesi JWT
JWT_SECRET="isi-dengan-kunci-rahasia-acak-disini"
```
*(Sesuaikan IP, port, dan kredensial lain jika server database Anda berbeda).*
*(Sesuaikan IP, port, dan kredensial database jika server database Anda berbeda).*
## Eksekusi Migrasi Database
Aplikasi menggunakan **Prisma ORM** untuk berinteraksi dengan database. Jalankan perintah terminal berikut secara berurutan untuk menyinkronkan skema dan mengaktifkan client:
Aplikasi menggunakan **Prisma ORM** untuk berinteraksi dengan database. Jalankan perintah terminal berikut secara berurutan untuk menyinkronkan skema dan mengisi (seed) data awal:
1. **Generate Prisma Client**: Membangun tipe TypeScript dan library client.
```bash
npx prisma generate
```
2. **Push Skema ke Database**: Membuat struktur tabel-tabel di database (User, Order, Submission, dll.) sesuai dengan `prisma/schema.prisma`.
2. **Push Skema ke Database**: Membuat struktur tabel-tabel di database sesuai dengan `prisma/schema.prisma`.
```bash
npx prisma db push
```
3. **Seed Database**: Mengisi database dengan akun Superadmin bawaan.
```bash
npx prisma db seed
```
## Menjalankan Aplikasi
Setelah database siap, Anda bisa menjalankan aplikasi di tahap pengembangan lokal (development mode):
```bash
npm run dev
yarn dev
```
Aplikasi secara bawaan dapat diakses melalui browser dengan URL:
@@ -55,9 +62,10 @@ Aplikasi secara bawaan dapat diakses melalui browser dengan URL:
---
### Alur Singkat Aplikasi:
- Akses pertama kali, sistem akan _generate_ UUID unik untuk *device* Anda dan menyimpannya di `localStorage`.
- Aplikasi akan menampilkan Modal Wajib untuk mendaftarkan **Nama** dan opsional **Foto Profil**.
- **Autentikasi:** Akses aplikasi harus melalui halaman login. Jika belum memiliki akun, pengguna dapat mendaftar (register) yang otomatis akan memberikan *role* sebagai `user`.
- **Akun Superadmin:** Secara default, sistem telah menyiapkan akun *Superadmin* (Username: `superadmin`, Password: `titipin123!`). Superadmin memiliki akses eksklusif ke halaman **User Management** untuk mengelola, menonaktifkan, atau menghapus pengguna lain.
- Anda dapat mulai:
- Membuat Jasa Titip di menu **Jasa Order Saya**.
- Mengikuti Jasa Titip teman di menu **Open Order**.
- Mengelola pembayaran & melihat rincian di halaman detail.
- Memantau laporan omzet & pengeluaran di menu **Laporan**.
+10 -2
View File
@@ -13,9 +13,11 @@
"@base-ui/react": "^1.7.0",
"@hookform/resolvers": "^5.9.1",
"@prisma/client": "^5.22.0",
"bcryptjs": "^3.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.4.0",
"jose": "^6.2.10",
"lucide-react": "^1.34.0",
"next": "16.3.3",
"prisma": "^5.22.0",
@@ -32,6 +34,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/bcryptjs": "^3.0.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -39,6 +42,11 @@
"eslint": "^9",
"eslint-config-next": "16.3.3",
"tailwindcss": "^4",
"typescript": "^5"
"ts-node": "^10.9.2",
"tsx": "^4.23.12",
"typescript": "^7.0.2"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
}
}
}
+18 -4
View File
@@ -11,9 +11,15 @@ datasource db {
}
model User {
id String @id
name String @unique
id String @id @default(uuid())
username String @unique
name String
password String
photo String?
role String @default("user")
is_active Boolean @default(true)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
orders Order[] @relation("CreatedOrders")
purchases Submission[]
}
@@ -28,13 +34,17 @@ model Order {
creator User @relation("CreatedOrders", fields: [creator_id], references: [id])
available_items AvailableItem[]
submissions Submission[]
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}
model AvailableItem {
id String @id @default(uuid())
order_id String
name String
order Order @relation(fields: [order_id], references: [id], onDelete: Cascade)
name String
order Order @relation(fields: [order_id], references: [id], onDelete: Cascade)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}
model Submission {
@@ -46,6 +56,8 @@ model Submission {
order Order @relation(fields: [order_id], references: [id], onDelete: Cascade)
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
items SubmissionItem[]
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}
model SubmissionItem {
@@ -55,4 +67,6 @@ model SubmissionItem {
qty Int @default(1)
is_custom Boolean @default(false)
submission Submission @relation(fields: [submission_id], references: [id], onDelete: Cascade)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}
+35
View File
@@ -0,0 +1,35 @@
import { PrismaClient } from '@prisma/client'
import bcrypt from 'bcryptjs'
const prisma = new PrismaClient()
async function main() {
const superadminUsername = 'superadmin'
const superadminPassword = 'titipin123!'
const hashedPassword = await bcrypt.hash(superadminPassword, 10)
const superadmin = await prisma.user.upsert({
where: { username: superadminUsername },
update: {},
create: {
username: superadminUsername,
name: 'Super Admin',
password: hashedPassword,
role: 'superadmin',
is_active: true,
},
})
console.log({ superadmin })
}
main()
.then(async () => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
+9
View File
@@ -0,0 +1,9 @@
import { AppLayout } from "@/components/AppLayout";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<AppLayout>
{children}
</AppLayout>
);
}
@@ -2,7 +2,7 @@
import { useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { getOrderDetail, updateSubmissionPayment, updateOrderStatus, updateOrder, deleteOrder } from '@/app/actions'
import { getOrderDetail, updateSubmissionPayment, updateOrderStatus, updateOrder, deleteOrder, getSessionUser } from '@/app/actions'
import { Card, CardContent } from '@/components/ui/card'
import { Button, buttonVariants } from '@/components/ui/button'
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
@@ -47,9 +47,14 @@ export default function OrderDetailPage() {
const [searchQuery, setSearchQuery] = useState('')
useEffect(() => {
const id = localStorage.getItem('user_id')
setUserId(id)
if (orderId) loadOrder()
const init = async () => {
const user = await getSessionUser()
if (user?.id) {
setUserId(user.id)
}
if (orderId) loadOrder()
}
init()
}, [orderId])
const loadOrder = async () => {
@@ -476,13 +481,13 @@ function SubmissionRow({
isClosed: boolean
onUpdate: () => void
}) {
const [bill, setBill] = useState(sub.bill || '')
const [bill, setBill] = useState(sub.bill ?? '')
const [status, setStatus] = useState(sub.payment_status || 'BELUM_BAYAR')
const [saving, setSaving] = useState(false)
const handleSave = async () => {
setSaving(true)
await updateSubmissionPayment(sub.id, bill ? parseInt(bill) : null, status)
await updateSubmissionPayment(sub.id, bill !== '' ? parseInt(bill as string) : null, status)
onUpdate()
setSaving(false)
}
@@ -1,7 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
import { getMyOrders, createOrder, updateOrder, duplicateOrder, updateOrderStatus, deleteOrder } from '@/app/actions'
import { getMyOrders, createOrder, updateOrder, duplicateOrder, updateOrderStatus, deleteOrder, getSessionUser } from '@/app/actions'
import { Button, buttonVariants } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
@@ -46,11 +46,14 @@ export default function MyOrdersPage() {
const ITEMS_PER_PAGE = 5
useEffect(() => {
const id = localStorage.getItem('user_id')
if (id) {
setUserId(id)
loadOrders(id)
const init = async () => {
const user = await getSessionUser()
if (user?.id) {
setUserId(user.id)
loadOrders(user.id)
}
}
init()
}, [])
const loadOrders = async (id: string) => {
@@ -1,7 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
import { getMyPurchases, submitOrder, getUserSubmission } from '@/app/actions'
import { getMyPurchases, submitOrder, getUserSubmission, getSessionUser } from '@/app/actions'
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'
import { Button, buttonVariants } from '@/components/ui/button'
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
@@ -38,11 +38,14 @@ export default function MyPurchasesPage() {
const ITEMS_PER_PAGE = 5
useEffect(() => {
const id = localStorage.getItem('user_id')
if (id) {
setUserId(id)
loadPurchases(id)
const init = async () => {
const user = await getSessionUser()
if (user?.id) {
setUserId(user.id)
loadPurchases(user.id)
}
}
init()
}, [])
const loadPurchases = async (id: string) => {
+1 -1
View File
@@ -1,4 +1,4 @@
import { getAvailableOrders } from './actions'
import { getAvailableOrders } from '@/app/actions'
import { OrderCard } from '@/components/OrderCard'
import { Sparkles, Plus, Store } from 'lucide-react'
import Link from 'next/link'
+280
View File
@@ -0,0 +1,280 @@
'use client'
import { useEffect, useState } from 'react'
import { getSessionUser, updateProfile, changePassword } from '@/app/actions'
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'
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'
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('')
const [confirmPassword, setConfirmPassword] = useState('')
const [savingPassword, setSavingPassword] = useState(false)
const [passwordError, setPasswordError] = useState('')
const [passwordSuccess, setPasswordSuccess] = useState(false)
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false)
useEffect(() => {
loadUser()
}, [])
const loadUser = async () => {
setLoading(true)
const sessionUser = await getSessionUser()
if (sessionUser) {
setUser(sessionUser)
setName(sessionUser.name)
setPhoto(sessionUser.photo || '')
}
setLoading(false)
}
const handleSaveProfile = async (e: React.FormEvent) => {
e.preventDefault()
if (!name.trim()) {
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)
setTimeout(() => setProfileSuccess(false), 3000)
} else {
setProfileError(res.error || 'Terjadi kesalahan')
}
setSavingProfile(false)
}
const handleChangePassword = async (e: React.FormEvent) => {
e.preventDefault()
setPasswordError('')
setPasswordSuccess(false)
if (!oldPassword || !newPassword || !confirmPassword) {
setPasswordError('Semua field wajib diisi')
return
}
if (newPassword !== confirmPassword) {
setPasswordError('Konfirmasi password tidak cocok')
return
}
if (newPassword.length < 6) {
setPasswordError('Password baru minimal 6 karakter')
return
}
setSavingPassword(true)
const res = await changePassword(user.id, oldPassword, newPassword)
if (res.success) {
setPasswordSuccess(true)
setOldPassword('')
setNewPassword('')
setConfirmPassword('')
setTimeout(() => {
setPasswordSuccess(false)
setIsPasswordModalOpen(false)
}, 2000)
} else {
setPasswordError(res.error || 'Gagal mengubah password')
}
setSavingPassword(false)
}
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>
)
}
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>
)
}
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">
{/* Left 2 Cols: Forms */}
<div className="lg:col-span-2 space-y-6">
{/* 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">
<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>
</div>
<span className="text-[11px] font-bold text-[#1B2CC1] bg-[#1B2CC1]/10 px-2.5 py-1 rounded-full uppercase tracking-wider">
{user.role}
</span>
</div>
<form onSubmit={handleSaveProfile}>
<CardContent className="p-6 space-y-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">
{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" />
) : (
<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>
)}
<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>
</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">
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" />
</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">
URL Foto Profil
</Label>
<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>
)}
{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>
)}
</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" />}
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]" />
<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-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>
</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>
</div>
</div>
{/* PASSWORD CHANGE MODAL BUTTON */}
<div className="pt-2 border-t border-slate-100 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" />
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>
</DialogHeader>
<form onSubmit={handleChangePassword}>
<div className="p-6 space-y-5 bg-white 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>
<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" />
</div>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-bold uppercase tracking-wider text-slate-700">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" />
</div>
</div>
<div className="space-y-1.5">
<Label className="text-xs font-bold uppercase tracking-wider text-slate-700">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" />
</div>
</div>
</div>
{passwordError && (
<div className="p-3 bg-red-50 text-red-600 rounded-xl text-xs font-semibold">{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>
)}
</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" />}
Ubah Password
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
</Card>
</div>
</div>
</div>
)
}
@@ -1,7 +1,7 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { getCreatorReport, getSubmittorReport } from '@/app/actions'
import { getCreatorReport, getSubmittorReport, getSessionUser } from '@/app/actions'
import { Card } from '@/components/ui/card'
import { Loader2, TrendingUp, TrendingDown, Wallet, ArrowRightLeft, Calendar as CalendarIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
@@ -51,7 +51,7 @@ function getIntervalFromFilter(type: 'WEEK' | 'MONTH', value: string): { start:
export default function ReportsPage() {
const [activeTab, setActiveTab] = useState<'SUBMITTOR' | 'CREATOR'>('SUBMITTOR')
const [creatorTab, setCreatorTab] = useState<'BY_ORDER' | 'BY_PERSON'>('BY_ORDER')
const [creatorTab, setCreatorTab] = useState<'BY_ORDER' | 'BY_PERSON'>('BY_PERSON')
const [filterType, setFilterType] = useState<'WEEK' | 'MONTH'>('WEEK')
const [filterValue, setFilterValue] = useState<string>(getWeekValue(new Date()))
@@ -62,10 +62,12 @@ export default function ReportsPage() {
const [creatorData, setCreatorData] = useState<any[]>([])
const [loading, setLoading] = useState(true)
// Initial load: get userId
useEffect(() => {
const id = localStorage.getItem('user_id')
if (id) setUserId(id)
const init = async () => {
const user = await getSessionUser()
if (user?.id) setUserId(user.id)
}
init()
}, [])
// Re-fetch every time userId, filterType, or filterValue changes
@@ -261,7 +263,7 @@ export default function ReportsPage() {
{/* 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_ORDER', 'BY_PERSON'] as const).map(tab => (
{([ 'BY_PERSON','BY_ORDER',] as const).map(tab => (
<button
key={tab}
onClick={() => setCreatorTab(tab)}
+316
View File
@@ -0,0 +1,316 @@
'use client'
import { useEffect, useState } from 'react'
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'
export default function UsersPage() {
const [users, setUsers] = useState<any[]>([])
const [total, setTotal] = useState(0)
const [totalPages, setTotalPages] = useState(1)
const [page, setPage] = useState(1)
const [loading, setLoading] = useState(true)
// Modals
const [isCreateOpen, setIsCreateOpen] = useState(false)
const [isPassOpen, setIsPassOpen] = useState(false)
const [selectedUser, setSelectedUser] = useState<any>(null)
// Forms
const [name, setName] = useState('')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [role, setRole] = useState('user')
const [newPass, setNewPass] = useState('')
const [actionLoading, setActionLoading] = useState(false)
useEffect(() => {
loadUsers(page)
}, [page])
const loadUsers = async (p: number) => {
setLoading(true)
const res = await getUsers(p, 10)
setUsers(res.users)
setTotal(res.total)
setTotalPages(res.totalPages)
setLoading(false)
}
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) {
setIsCreateOpen(false)
setName('')
setUsername('')
setPassword('')
setRole('user')
loadUsers(page)
} else {
alert(res.error)
}
setActionLoading(false)
}
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) {
setIsPassOpen(false)
setNewPass('')
alert('Password berhasil diubah')
} else {
alert(res.error)
}
setActionLoading(false)
}
const handleToggleActive = async (id: string, currentStatus: boolean) => {
if (!confirm(`Yakin ingin ${currentStatus ? 'menonaktifkan' : 'mengaktifkan'} user ini?`)) return
const res = await toggleUserActive(id, !currentStatus)
if (res.success) {
loadUsers(page)
} else {
alert(res.error)
}
}
const handleDelete = async (id: string) => {
if (!confirm('Yakin ingin menghapus user ini secara permanen?')) return
const res = await deleteUser(id)
if (res.success) {
loadUsers(page)
} else {
alert(res.error)
}
}
return (
<div className="space-y-6 animate-in fade-in duration-500 pb-12">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-black text-slate-900 dark:text-white">Manajemen Pengguna</h1>
<p className="text-sm text-slate-500 mt-1">Kelola data seluruh pengguna TitipIn.</p>
</div>
<Button onClick={() => setIsCreateOpen(true)} className="bg-[#1B2CC1] hover:bg-[#15229E] text-white rounded-xl h-11 px-5 shadow-md">
<Plus className="w-4 h-4 mr-2" /> Tambah User
</Button>
</div>
<Card className="rounded-3xl border border-slate-200/90 dark:border-slate-800 bg-white dark:bg-slate-900 overflow-hidden shadow-sm">
<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">
<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>
<td colSpan={5} className="px-6 py-12 text-center">
<Loader2 className="w-6 h-6 animate-spin mx-auto text-[#1B2CC1]" />
</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>
{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>
</div>
)}
</Card>
{/* 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">
<DialogTitle className="text-xl font-black">Tambah User Baru</DialogTitle>
</div>
<form onSubmit={handleCreateUser} className="px-6 py-4 space-y-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" />
</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" />
</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)" />
</div>
<div className="space-y-2">
<Label className="text-xs font-bold uppercase text-slate-700">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 ${
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
</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 ${
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
</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'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
{/* 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">
<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>
</div>
<form onSubmit={handleResetPassword} className="px-6 py-4 space-y-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" />
</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'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
)
}
+7
View File
@@ -0,0 +1,7 @@
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-[#F4F6FB] dark:bg-[#0B0F19]">
{children}
</div>
);
}
+153
View File
@@ -0,0 +1,153 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { loginUser } from '@/app/actions'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { LogIn, KeyRound, User, Loader2, Eye, EyeOff } from 'lucide-react'
export default function LoginPage() {
const router = useRouter()
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [showPassword, setShowPassword] = useState(false)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setLoading(true)
setError('')
const formData = new FormData(e.currentTarget)
const username = (formData.get('username') as string).trim()
const password = formData.get('password') as string
if (!username || !password) {
setError('Username dan Password wajib diisi')
setLoading(false)
return
}
const res = await loginUser(username, password)
if (res.success) {
window.location.href = '/' // Force hard reload to update context/middleware
} else {
setError(res.error || 'Gagal login')
setLoading(false)
}
}
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">
{/* 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>
<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">
{/* 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>
<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>
<h1 className="text-4xl font-black text-white leading-tight">
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>
</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="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>
<p className="text-xs text-blue-100 font-medium">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>
<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>
</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">
{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>
<div className="relative">
<User className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 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]"
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>
<div className="relative flex items-center">
<KeyRound className="absolute left-3.5 w-4 h-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]"
placeholder="Masukkan password"
/>
<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"
aria-label={showPassword ? 'Sembunyikan password' : 'Tampilkan password'}
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<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"
>
{loading ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Masuk Sekarang'}
</Button>
</form>
<p className="text-center text-sm text-slate-500 mt-8">
Belum punya akun?{' '}
<Link href="/register" className="font-bold text-[#1B2CC1] hover:underline">
Daftar di sini
</Link>
</p>
</div>
</div>
</div>
)
}
+201
View File
@@ -0,0 +1,201 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { registerUser } from '@/app/actions'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { UserPlus, KeyRound, User, Loader2, BadgeCheck, Eye, EyeOff } from 'lucide-react'
export default function RegisterPage() {
const router = useRouter()
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [showPassword, setShowPassword] = useState(false)
const [showConfirm, setShowConfirm] = useState(false)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setLoading(true)
setError('')
const formData = new FormData(e.currentTarget)
const name = formData.get('name') as string
const username = (formData.get('username') as string).trim()
const password = formData.get('password') as string
const confirm = formData.get('confirm') as string
if (!name || !username || !password || !confirm) {
setError('Semua field wajib diisi')
setLoading(false)
return
}
if (password !== confirm) {
setError('Password dan Konfirmasi Password tidak cocok')
setLoading(false)
return
}
if (password.length < 6) {
setError('Password minimal 6 karakter')
setLoading(false)
return
}
const res = await registerUser(name, username, password)
if (res.success) {
window.location.href = '/' // Force hard reload to update context/middleware
} else {
setError(res.error || 'Gagal mendaftar')
setLoading(false)
}
}
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">
{/* 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>
<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">
{/* 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>
<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>
<h1 className="text-4xl font-black text-white leading-tight">
Mulai Perjalanan <br /> Anda di TitipIn
</h1>
<p className="text-indigo-100 mt-4 text-base max-w-sm leading-relaxed">
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="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>
<p className="text-xs text-indigo-100 font-medium">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>
<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>
<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">
{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>
<div className="relative">
<BadgeCheck className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 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]"
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>
<div className="relative">
<User className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 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]"
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>
<div className="relative flex items-center">
<KeyRound className="absolute left-3.5 w-4 h-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]"
placeholder="Minimal 6 karakter"
/>
<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"
aria-label={showPassword ? 'Sembunyikan password' : 'Tampilkan password'}
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-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>
<div className="relative flex items-center">
<KeyRound className="absolute left-3.5 w-4 h-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]"
placeholder="Ketik ulang password"
/>
<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"
aria-label={showConfirm ? 'Sembunyikan password' : 'Tampilkan password'}
>
{showConfirm ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<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"
>
{loading ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Buat Akun'}
</Button>
</form>
<p className="text-center text-sm text-slate-500 mt-8">
Sudah punya akun?{' '}
<Link href="/login" className="font-bold text-[#1B2CC1] hover:underline">
Masuk di sini
</Link>
</p>
</div>
</div>
</div>
)
}
+179 -12
View File
@@ -3,38 +3,204 @@
import prisma from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
// === USER ACTIONS ===
export async function checkUser(id: string) {
return await prisma.user.findUnique({ where: { id } })
import { hashPassword, verifyPassword, encrypt, decrypt } from '@/lib/auth'
import { cookies } from 'next/headers'
// === AUTHENTICATION ACTIONS ===
export async function loginUser(username: string, password: string) {
try {
const user = await prisma.user.findUnique({ where: { username } })
if (!user) return { success: false, error: 'Username tidak ditemukan.' }
if (!user.is_active) return { success: false, error: 'Akun Anda dinonaktifkan.' }
const isMatch = await verifyPassword(password, user.password)
if (!isMatch) return { success: false, error: 'Password salah.' }
const sessionData = {
id: user.id,
username: user.username,
name: user.name,
role: user.role,
photo: user.photo
}
const sessionToken = await encrypt(sessionData);
(await cookies()).set('session', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/'
})
return { success: true, user: sessionData }
} catch (e) {
console.error("Login Error:", e)
return { success: false, error: 'Terjadi kesalahan saat login.' }
}
}
export async function registerUser(id: string, name: string, photo?: string) {
export async function registerUser(name: string, username: string, password: string) {
try {
const hashedPassword = await hashPassword(password)
const user = await prisma.user.create({
data: { id, name, photo }
data: { name, username, password: hashedPassword }
})
return { success: true, user }
const sessionData = {
id: user.id,
username: user.username,
name: user.name,
role: user.role,
photo: user.photo
}
const sessionToken = await encrypt(sessionData);
(await cookies()).set('session', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/'
})
return { success: true, user: sessionData }
} catch (error: any) {
if (error.code === 'P2002') return { success: false, error: 'Nama sudah digunakan.' }
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 updateProfile(id: string, name: string, photo?: string) {
export async function logoutUser() {
(await cookies()).delete('session')
return { success: true }
}
export async function getSessionUser() {
const cookie = (await cookies()).get('session')?.value
if (!cookie) return null
return await decrypt(cookie)
}
export async function updateProfile(id: string, name: string, photo?: string | null) {
try {
const user = await prisma.user.update({
where: { id },
data: { name, photo }
data: { name, photo: photo === undefined ? undefined : (photo || null) }
})
// Update session cookie with new data
const sessionData = {
id: user.id,
username: user.username,
name: user.name,
role: user.role,
photo: user.photo
}
const sessionToken = await encrypt(sessionData);
(await cookies()).set('session', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/'
})
revalidatePath('/')
revalidatePath('/profile')
return { success: true, user }
return { success: true, user: sessionData }
} catch (error: any) {
if (error.code === 'P2002') return { success: false, error: 'Nama sudah digunakan.' }
return { success: false, error: 'Gagal memperbarui profil.' }
}
}
export async function changePassword(id: string, oldPass: string, newPass: string) {
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 }
})
return { success: true }
} catch (e) {
return { success: false, error: 'Gagal mengganti password.' }
}
}
// === USER MANAGEMENT (SUPERADMIN) ===
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()
])
return { users, total, totalPages: Math.ceil(total / limit) }
}
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 }
})
revalidatePath('/users')
return { success: true }
} catch (e: any) {
if (e.code === 'P2002') return { success: false, error: 'Username sudah digunakan.' }
return { success: false, error: 'Gagal membuat user.' }
}
}
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.' }
await prisma.user.update({
where: { id },
data: { is_active }
})
revalidatePath('/users')
return { success: true }
} catch (e) {
return { success: false, error: 'Gagal mengupdate status user.' }
}
}
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.' }
await prisma.user.delete({ where: { id } })
revalidatePath('/users')
return { success: true }
} catch (e) {
return { success: false, error: 'Gagal menghapus user.' }
}
}
export async function resetUserPassword(id: string, newPass: string) {
try {
const hashed = await hashPassword(newPass)
await prisma.user.update({
where: { id },
data: { password: hashed }
})
revalidatePath('/users')
return { success: true }
} catch (e) {
return { success: false, error: 'Gagal mereset password.' }
}
}
// === ORDER ACTIONS (DASHBOARD & CREATE) ===
export async function getAvailableOrders() {
const startOfDay = new Date()
@@ -91,6 +257,7 @@ export async function createOrder(data: {
revalidatePath('/my-orders')
return { success: true, order }
} catch (error) {
console.error("Create Order Error:", error)
return { success: false, error: 'Gagal membuat order.' }
}
}
@@ -225,7 +392,7 @@ export async function updateSubmissionPayment(submission_id: string, bill: numbe
revalidatePath(`/reports`)
return { success: true }
} catch (e) {
return { success: false, error: 'Gagal menyimpan tagihan.' }
console.error(e); return { success: false, error: 'Gagal menyimpan tagihan.' }
}
}
+4 -1
View File
@@ -115,4 +115,7 @@
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;
}
}
}
button {
cursor: pointer;
}
+2 -7
View File
@@ -1,8 +1,7 @@
import type { Metadata } from "next";
import { Roboto, Open_Sans } from "next/font/google";
import "./globals.css";
import { AuthProvider } from "@/components/AuthProvider";
import { AppLayout } from "@/components/AppLayout";
const roboto = Roboto({
weight: ["300", "400", "500", "700", "900"],
@@ -37,11 +36,7 @@ export default function RootLayout({
suppressHydrationWarning
className="min-h-full flex flex-col bg-[#F4F6FB] dark:bg-[#0B0F19] font-sans antialiased"
>
<AuthProvider>
<AppLayout>
{children}
</AppLayout>
</AuthProvider>
{children}
</body>
</html>
);
-222
View File
@@ -1,222 +0,0 @@
'use client'
import { useEffect, useState } from 'react'
import { checkUser, updateProfile } from '@/app/actions'
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Loader2, UserCircle, Save, CheckCircle, Upload, ShieldCheck, Sparkles, Image as ImageIcon } from 'lucide-react'
export default function ProfilePage() {
const [userId, setUserId] = useState<string | null>(null)
const [name, setName] = useState('')
const [photo, setPhoto] = useState('')
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState(false)
useEffect(() => {
const id = localStorage.getItem('user_id')
if (id) {
setUserId(id)
loadProfile(id)
}
}, [])
const loadProfile = async (id: string) => {
setLoading(true)
const user = await checkUser(id)
if (user) {
setName(user.name)
setPhoto(user.photo || '')
}
setLoading(false)
}
const handleSave = async (e: React.FormEvent) => {
e.preventDefault()
if (!name.trim()) {
setError('Nama wajib diisi')
return
}
setError('')
setSuccess(false)
setSaving(true)
const res = await updateProfile(userId!, name.trim(), photo.trim() || undefined)
if (res.success) {
setSuccess(true)
setTimeout(() => setSuccess(false), 3000)
} else {
setError(res.error || 'Terjadi kesalahan')
}
setSaving(false)
}
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>
)
}
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">
{/* Left 2 Cols: Main Profile Form */}
<div className="lg:col-span-2 space-y-6">
<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">
<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 avatar yang terlihat oleh teman-teman.
</p>
</div>
<span className="text-[11px] font-bold text-[#1B2CC1] bg-[#1B2CC1]/10 px-2.5 py-1 rounded-full">
Stateless Auth
</span>
</div>
<form onSubmit={handleSave}>
<CardContent className="p-6 space-y-6">
{/* Photo Preview & Dashed Container */}
<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">
{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"
/>
) : (
<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>
)}
<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>
</div>
</div>
{/* Form Inputs */}
<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">
Nama Layar (Wajib Unik) <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 border-slate-200 dark:border-slate-800 focus-visible:ring-4 focus-visible:ring-[#1B2CC1]/15 focus-visible:border-[#1B2CC1] font-semibold"
/>
<p className="text-[11px] text-slate-400">Nama ini akan tercantum di setiap PO yang Anda buat atau ikuti.</p>
</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">
URL Foto Profil
</Label>
<Input
id="prof-photo"
value={photo}
onChange={(e) => setPhoto(e.target.value)}
placeholder="https://images.unsplash.com/photo-..."
className="h-11 rounded-xl border-slate-200 dark:border-slate-800 focus-visible:ring-4 focus-visible:ring-[#1B2CC1]/15 focus-visible:border-[#1B2CC1] text-xs font-mono"
/>
</div>
{error && (
<div className="p-3.5 bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 rounded-xl text-xs text-red-600 dark:text-red-400 font-semibold">
{error}
</div>
)}
{success && (
<div className="p-3.5 bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-200 dark:border-emerald-900 rounded-xl text-xs text-emerald-700 dark:text-emerald-400 font-bold flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-emerald-600" />
<span>Profil Anda berhasil diperbarui!</span>
</div>
)}
</CardContent>
{/* Card Footer with Dedicated Container & Padding */}
<div className="p-6 bg-slate-50/60 dark:bg-slate-800/40 border-t border-slate-100 dark:border-slate-800 flex justify-end items-center">
<Button
type="submit"
disabled={saving}
className="w-full sm:w-auto h-11 px-6 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold shadow-md shadow-[#1B2CC1]/20 gap-2 cursor-pointer"
>
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
<span>{saving ? 'Menyimpan...' : 'Simpan Perubahan'}</span>
</Button>
</div>
</form>
</Card>
</div>
{/* Right 1 Col: Device & ID 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">
<ShieldCheck className="w-4 h-4 text-[#1B2CC1]" />
<span>Identitas Stateless</span>
</div>
<p className="text-xs text-slate-500 leading-relaxed">
Akun Anda tersimpan secara lokal pada peramban ini menggunakan Device ID yang unik di bawah ini.
</p>
<div className="space-y-1.5 pt-2 border-t border-slate-100 dark:border-slate-800">
<div className="flex items-center justify-between">
<span className="text-[10px] uppercase font-bold text-slate-400 block">Unique User UUID</span>
<button
type="button"
onClick={() => {
if (userId) {
navigator.clipboard.writeText(userId)
}
}}
className="text-[10px] font-bold text-[#1B2CC1] hover:underline"
>
Salin ID
</button>
</div>
<div className="p-3 rounded-xl bg-slate-50 dark:bg-slate-800/60 border border-slate-200/80 dark:border-slate-700 text-xs font-mono text-slate-700 dark:text-slate-300 break-all select-all font-semibold">
{userId}
</div>
</div>
<div className="p-4 rounded-2xl bg-blue-50/60 dark:bg-blue-950/30 border border-blue-100 dark:border-blue-900/40 text-xs text-[#1B2CC1] dark:text-blue-300 space-y-1">
<div className="flex items-center gap-1.5 font-bold">
<Sparkles className="w-3.5 h-3.5" />
<span>Tips Penggunaan</span>
</div>
<p className="text-[11px] opacity-90 leading-tight">
Jangan bersihkan data peramban (localStorage) jika Anda ingin tetap login dengan ID yang sama.
</p>
</div>
</Card>
</div>
</div>
</div>
)
}
-157
View File
@@ -1,157 +0,0 @@
'use client'
import { useEffect, useState } from 'react'
import { v4 as uuidv4 } from 'uuid'
import { checkUser, registerUser } from '@/app/actions'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
import { Loader2, Sparkles, User, Image as ImageIcon } from 'lucide-react'
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [isVerified, setIsVerified] = useState(false)
const [showModal, setShowModal] = useState(false)
const [userId, setUserId] = useState<string | null>(null)
const [name, setName] = useState('')
const [photo, setPhoto] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
useEffect(() => {
async function initAuth() {
let id = localStorage.getItem('user_id')
if (!id) {
id = uuidv4()
localStorage.setItem('user_id', id)
}
setUserId(id)
const user = await checkUser(id)
if (user) {
setIsVerified(true)
} else {
setShowModal(true)
}
setLoading(false)
}
initAuth()
}, [])
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault()
if (!name.trim()) {
setError('Nama wajib diisi')
return
}
setError('')
setSaving(true)
const res = await registerUser(userId!, name.trim(), photo.trim() || undefined)
if (res.success) {
setShowModal(false)
setIsVerified(true)
} else {
setError(res.error || 'Terjadi kesalahan')
}
setSaving(false)
}
const handleOpenChange = (open: boolean) => {
if (!open && !isVerified) {
setShowModal(true)
}
}
if (loading) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-[#F4F6FB] dark:bg-[#0B0F19] gap-4">
<div className="w-12 h-12 rounded-2xl bg-[#1B2CC1] flex items-center justify-center text-white shadow-xl shadow-[#1B2CC1]/30 animate-pulse">
<Sparkles className="w-6 h-6" />
</div>
<div className="flex items-center gap-2 text-sm font-semibold text-slate-500">
<Loader2 className="h-4 w-4 animate-spin text-[#1B2CC1]" />
<span>Memuat sesi Anda...</span>
</div>
</div>
)
}
return (
<>
{isVerified && children}
<Dialog open={showModal} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-[440px] p-0 overflow-hidden rounded-3xl border-slate-200/90 dark:border-slate-800 shadow-2xl [&>button]:hidden">
<div className="bg-gradient-to-br from-[#1B2CC1] to-[#121E85] p-6 text-white text-center relative overflow-hidden">
<div className="absolute -right-8 -bottom-8 w-32 h-32 bg-white/10 rounded-full blur-2xl pointer-events-none" />
<div className="w-14 h-14 rounded-2xl bg-white/15 backdrop-blur-md flex items-center justify-center mx-auto mb-3 text-white border border-white/20 shadow-inner">
<Sparkles className="w-7 h-7" />
</div>
<DialogTitle className="text-2xl font-black tracking-tight text-white">
Selamat Datang di TitipIn
</DialogTitle>
<DialogDescription className="text-blue-100/90 text-xs mt-1.5 max-w-xs mx-auto">
Daftarkan nama Anda untuk mulai membuka atau menitip pesanan bersama teman.
</DialogDescription>
</div>
<form onSubmit={handleRegister} className="p-6 space-y-4 bg-white dark:bg-slate-900">
<div className="space-y-1.5">
<Label htmlFor="reg-name" className="text-xs font-bold text-slate-700 dark:text-slate-300">
Nama Tampilan <span className="text-red-500">*</span>
</Label>
<div className="relative">
<Input
id="reg-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Contoh: Budi Santoso"
className="rounded-xl h-11 border-slate-200 dark:border-slate-800 focus-visible:ring-4 focus-visible:ring-[#1B2CC1]/15 focus-visible:border-[#1B2CC1]"
autoComplete="off"
/>
</div>
<p className="text-[11px] text-slate-400">Nama harus unik agar pembuat PO mudah mengenali Anda.</p>
</div>
<div className="space-y-1.5">
<Label htmlFor="reg-photo" className="text-xs font-bold text-slate-700 dark:text-slate-300">
URL Foto Profil (Opsional)
</Label>
<Input
id="reg-photo"
value={photo}
onChange={(e) => setPhoto(e.target.value)}
placeholder="https://images.unsplash.com/..."
className="rounded-xl h-11 border-slate-200 dark:border-slate-800 focus-visible:ring-4 focus-visible:ring-[#1B2CC1]/15 focus-visible:border-[#1B2CC1]"
autoComplete="off"
/>
</div>
{error && (
<div className="p-3 bg-red-50 dark:bg-red-950/40 border border-red-200 dark:border-red-900/60 rounded-xl text-xs text-red-600 dark:text-red-400 font-semibold">
{error}
</div>
)}
<Button
type="submit"
disabled={saving}
className="w-full h-11 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold shadow-md shadow-[#1B2CC1]/25 transition-all mt-2"
>
{saving ? (
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
<span>Menyimpan Profil...</span>
</div>
) : (
'Mulai Gunakan TitipIn'
)}
</Button>
</form>
</DialogContent>
</Dialog>
</>
)
}
+7
View File
@@ -54,6 +54,13 @@ export function Header({ onMenuClick }: { onMenuClick: () => void }) {
badge: 'Laporan',
Icon: BarChart2
}
case '/users':
return {
title: 'Manajemen Pengguna',
subtitle: 'Kelola data pengguna, role, dan status aktif.',
badge: 'Superadmin',
Icon: User
}
default:
if (pathname.startsWith('/my-orders/')) {
return {
+8 -5
View File
@@ -7,7 +7,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { submitOrder, getUserSubmission } from '@/app/actions'
import { submitOrder, getUserSubmission, getSessionUser } from '@/app/actions'
import { PlusCircle, MinusCircle, User, CheckCircle2, ShoppingBag, Sparkles, Users } from 'lucide-react'
import { format } from 'date-fns'
@@ -113,11 +113,14 @@ function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: () => voi
const [error, setError] = useState('')
useEffect(() => {
const id = localStorage.getItem('user_id')
if (id) {
setUserId(id)
loadExistingSubmission(id)
const init = async () => {
const user = await getSessionUser()
if (user?.id) {
setUserId(user.id)
loadExistingSubmission(user.id)
}
}
init()
}, [order.id])
const loadExistingSubmission = async (uid: string) => {
+76 -10
View File
@@ -4,6 +4,12 @@ import { useEffect, useState } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { cn } from '@/lib/utils'
import {
Dialog,
DialogContent,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import {
Store,
ClipboardList,
@@ -15,7 +21,6 @@ import {
Info,
BarChart2
} from 'lucide-react'
import { checkUser } from '@/app/actions'
const menuItems = [
{ name: 'Open Order', href: '/', icon: Store, desc: 'Daftar PO live hari ini' },
@@ -27,22 +32,38 @@ const menuItems = [
export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () => void }) {
const pathname = usePathname()
const [userName, setUserName] = useState<string>('Pengguna')
const [userNameAccount, setUserNameAccount] = useState<string>('Pengguna')
const [userPhoto, setUserPhoto] = useState<string | null>(null)
const [userRole, setUserRole] = useState<string | null>(null)
const [userId, setUserId] = useState<string | null>(null)
const [isLogoutOpen, setIsLogoutOpen] = useState(false)
useEffect(() => {
const id = localStorage.getItem('user_id')
if (id) {
setUserId(id)
checkUser(id).then((u) => {
import('@/app/actions').then(({ getSessionUser }) => {
getSessionUser().then((u) => {
if (u) {
setUserId(u.id)
setUserName(u.name)
setUserNameAccount(u.username)
setUserRole(u.role)
if (u.photo) setUserPhoto(u.photo)
}
})
}
})
}, [pathname])
const handleLogout = async () => {
const { logoutUser } = await import('@/app/actions')
await logoutUser()
window.location.href = '/login'
}
// Dynamic Menu Items based on role
const finalMenuItems = [...menuItems]
if (userRole === 'superadmin') {
finalMenuItems.push({ name: 'User Management', href: '/users', icon: UserCircle, desc: 'Kelola pengguna' })
}
return (
<>
{/* Mobile Backdrop */}
@@ -106,9 +127,16 @@ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () =>
</span>
<ArrowUpRight className="w-3.5 h-3.5 text-slate-400 opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
<span className="text-[10px] font-mono text-slate-400 truncate">
{userId ? `ID: ${userId.slice(0, 10)}...` : 'Aktif'}
</span>
<div className="flex items-center gap-1.5 mt-0.5">
{userRole === 'superadmin' && (
<span className="px-1.5 py-[2px] rounded-md bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 text-[8px] font-black uppercase tracking-wider">
Admin
</span>
)}
<span className="text-[10px] font-medium text-slate-500 dark:text-slate-400 truncate">
{userNameAccount ? `@${userNameAccount}` : 'Aktif'}
</span>
</div>
</div>
</Link>
@@ -118,7 +146,7 @@ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () =>
Navigasi Utama
</div>
<nav className="space-y-1">
{menuItems.map((item) => {
{finalMenuItems.map((item) => {
const Icon = item.icon
const isActive = pathname === item.href
return (
@@ -153,6 +181,44 @@ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () =>
</Link>
)
})}
{/* Logout Button */}
{userId && (
<>
<button
onClick={() => setIsLogoutOpen(true)}
className="w-full flex items-center gap-3 px-3.5 py-3 rounded-2xl text-sm font-semibold transition-all duration-200 group relative text-rose-600 dark:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-950/30 cursor-pointer"
>
<div className="w-8 h-8 rounded-xl flex items-center justify-center transition-colors bg-rose-100 dark:bg-rose-900/40 text-rose-500 group-hover:bg-rose-200 dark:group-hover:bg-rose-800/60">
<X className="w-4 h-4" />
</div>
<div className="flex flex-col min-w-0 text-left">
<span className="leading-tight">Logout</span>
<span className="text-[10px] font-normal truncate mt-0.5 text-rose-400/80">Keluar dari akun</span>
</div>
</button>
<Dialog open={isLogoutOpen} onOpenChange={setIsLogoutOpen}>
<DialogContent className="sm:max-w-xs rounded-3xl p-0 overflow-hidden border-0">
<div className="px-6 pt-6 pb-2 text-center">
<div className="w-12 h-12 rounded-full bg-rose-100 text-rose-600 flex items-center justify-center mx-auto mb-3">
<X className="w-6 h-6" />
</div>
<DialogTitle className="text-xl font-black text-slate-800">Konfirmasi Logout</DialogTitle>
<p className="text-xs text-slate-500 mt-2">
Apakah Anda yakin ingin keluar dari akun ini?
</p>
</div>
<div className="p-4 flex gap-3 bg-slate-50">
<Button type="button" variant="outline" onClick={() => setIsLogoutOpen(false)} className="flex-1 h-11 rounded-xl">Batal</Button>
<Button type="button" onClick={handleLogout} className="flex-1 h-11 rounded-xl bg-rose-600 hover:bg-rose-700 text-white font-bold cursor-pointer">
Ya, Logout
</Button>
</div>
</DialogContent>
</Dialog>
</>
)}
</nav>
</div>
</div>
+1 -1
View File
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"cursor-pointer group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
+39
View File
@@ -0,0 +1,39 @@
import { SignJWT, jwtVerify } from 'jose'
import bcrypt from 'bcryptjs'
import { cookies } from 'next/headers'
const secretKey = process.env.JWT_SECRET
const key = new TextEncoder().encode(secretKey)
export async function hashPassword(password: string) {
return await bcrypt.hash(password, 10)
}
export async function verifyPassword(password: string, hash: string) {
return await bcrypt.compare(password, hash)
}
export async function encrypt(payload: any) {
return await new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.sign(key) // No expiration as requested
}
export async function decrypt(input: string): Promise<any> {
try {
const { payload } = await jwtVerify(input, key, {
algorithms: ['HS256'],
})
return payload
} catch (error) {
return null
}
}
export async function getSession() {
const cookieStore = await cookies()
const session = cookieStore.get('session')?.value
if (!session) return null
return await decrypt(session)
}
+35
View File
@@ -0,0 +1,35 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { decrypt } from '@/lib/auth'
const protectedRoutes = ['/', '/my-orders', '/my-purchases', '/reports', '/profile', '/users']
const publicRoutes = ['/login', '/register']
export async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname
// Also protect dynamic routes like /my-orders/[id]
const isProtectedRoute = protectedRoutes.includes(path) || path.startsWith('/my-orders/')
const isPublicRoute = publicRoutes.includes(path)
const cookie = request.cookies.get('session')?.value
const session = cookie ? await decrypt(cookie) : null
if (isProtectedRoute && !session) {
return NextResponse.redirect(new URL('/login', request.nextUrl))
}
if (isPublicRoute && session) {
return NextResponse.redirect(new URL('/', request.nextUrl))
}
if (path.startsWith('/users') && session?.role !== 'superadmin') {
return NextResponse.redirect(new URL('/', request.nextUrl))
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|icon.tsx|icon).*)'],
}
+404 -9
View File
@@ -273,6 +273,13 @@
reselect "^5.2.0"
use-sync-external-store "^1.6.0"
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
dependencies:
"@jridgewell/trace-mapping" "0.3.9"
"@date-fns/tz@^1.4.1":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@date-fns/tz/-/tz-1.5.0.tgz#e9e79b7583f0b1322c53db884a0112551095e3f3"
@@ -349,6 +356,136 @@
dependencies:
tslib "^2.4.0"
"@esbuild/aix-ppc64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz#bf6e10303bcf2e7c686975fa52f937ec2728d8bc"
integrity sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==
"@esbuild/android-arm64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz#0c6246bc8d2c4d172aac2db3fb1190d72bd65504"
integrity sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==
"@esbuild/android-arm@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.28.2.tgz#2d84ece6a4e2684d92be26ee13d42757d831c381"
integrity sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==
"@esbuild/android-x64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.28.2.tgz#fc38d4d6358d8dc1cf53f09f7589fe436eb64801"
integrity sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==
"@esbuild/darwin-arm64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz#f83afeeac1d7dac01c7a2fd012b3e451a0591fcc"
integrity sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==
"@esbuild/darwin-x64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz#510147c055a795588dbbe14fd6b1b8ad0a2f30de"
integrity sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==
"@esbuild/freebsd-arm64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz#093b9200ecf0b115ba4e5e248a7485c9c5f8bd5e"
integrity sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==
"@esbuild/freebsd-x64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz#0be22b6df925d213e841ea87123af5df80b0faf7"
integrity sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==
"@esbuild/linux-arm64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz#1bdbc651cda9ba9995c53ed9c71ceaa65094762d"
integrity sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==
"@esbuild/linux-arm@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz#beb12ad72b84f72d28488cc1b8ee9f7eb141d753"
integrity sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==
"@esbuild/linux-ia32@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz#b81f9d55529b45c206a46a138214b1aa6879696b"
integrity sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==
"@esbuild/linux-loong64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz#598667241a04c99b76ed6ef940ac50038c419f98"
integrity sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==
"@esbuild/linux-mips64el@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz#1c51eb9cea903f53d97b5af3b1841db70f5596ca"
integrity sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==
"@esbuild/linux-ppc64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz#63dd61f17ceb31a81227f413feac8a71bc2c51f2"
integrity sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==
"@esbuild/linux-riscv64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz#3763b08fde5cf25ab1facb8e7752edfe45fbfc27"
integrity sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==
"@esbuild/linux-s390x@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz#1a137ff293a82906eb3176385bd7e8e0e5cfb7cb"
integrity sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==
"@esbuild/linux-x64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz#268b36211c146ca54f8fe12c578a8d6ef8979485"
integrity sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==
"@esbuild/netbsd-arm64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz#22571ad951d62bb6accc82d8d1fad5c8c1ac0ba1"
integrity sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==
"@esbuild/netbsd-x64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz#42fcc57297eb0a0ca3f5fc475291f4c1a3f7c0de"
integrity sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==
"@esbuild/openbsd-arm64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz#9eb32af104ac3dacf4edca01f596664aab0c73ef"
integrity sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==
"@esbuild/openbsd-x64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz#febed2402d6088225e91f20fb4ce2522ad0a4efd"
integrity sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==
"@esbuild/openharmony-arm64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz#85641c3d466428bfbccea5f21c26836663fef5ce"
integrity sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==
"@esbuild/sunos-x64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz#a736f9d8962481045fc4c3e54f5479f22c870fb4"
integrity sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==
"@esbuild/win32-arm64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz#ee5ab40fad186201b652a33f8a5eb149e9e42532"
integrity sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==
"@esbuild/win32-ia32@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz#c40d28a6d99a127da6711f2afd74b11cb63b06a7"
integrity sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==
"@esbuild/win32-x64@0.28.2":
version "0.28.2"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz#b21affb804cc167c133d95f45b3a1dc1323b9a87"
integrity sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==
"@eslint-community/eslint-utils@4.9.1":
version "4.9.1"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595"
@@ -671,16 +808,24 @@
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@jridgewell/resolve-uri@^3.1.0":
"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5":
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz#f4c663e862f06dc98ca4d453862c46902789a18d"
integrity sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==
"@jridgewell/trace-mapping@0.3.9":
version "0.3.9"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
dependencies:
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28":
version "0.3.31"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
@@ -1001,6 +1146,26 @@
minimatch "^10.0.1"
path-browserify "^1.0.1"
"@tsconfig/node10@^1.0.7":
version "1.0.13"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.13.tgz#56105a9a8c786e8f15e35746879cf2d52275485b"
integrity sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==
"@tsconfig/node12@^1.0.7":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
"@tsconfig/node14@^1.0.0":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
"@tsconfig/node16@^1.0.2":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
"@tybys/wasm-util@^0.10.2", "@tybys/wasm-util@^0.10.3":
version "0.10.3"
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz#015cba9e9dd47ce14d03d2a8c5d547bfb169665d"
@@ -1008,6 +1173,13 @@
dependencies:
tslib "^2.4.0"
"@types/bcryptjs@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-3.0.0.tgz#d7be11653aa82cf17ffee4f3925f1f80cfc1add2"
integrity sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==
dependencies:
bcryptjs "*"
"@types/d3-array@^3.0.3":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.2.tgz#e02151464d02d4a1b44646d0fcdb93faf88fde8c"
@@ -1204,6 +1376,106 @@
"@typescript-eslint/types" "8.68.0"
eslint-visitor-keys "^5.0.0"
"@typescript/typescript-aix-ppc64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz#cdc7ce81d60f1e09034960ddfb1fb880d7a776b6"
integrity sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==
"@typescript/typescript-darwin-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz#a55fdfcfa58df58d27db2237cde6a5c1e35a7235"
integrity sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==
"@typescript/typescript-darwin-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz#38d1c9172800a91d707bec64d2a370a016634db4"
integrity sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==
"@typescript/typescript-freebsd-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz#f1ff8810030b35d2b5be0db6a2dc650460ea94fa"
integrity sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==
"@typescript/typescript-freebsd-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz#3d86b03f353c5b1ba95162eb6ce35533bfc294bd"
integrity sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==
"@typescript/typescript-linux-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz#d9334d96d6dac6ff85da9c865588948de939e91f"
integrity sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==
"@typescript/typescript-linux-arm@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz#ad94b41e1aee2a4dcc6a298c7b67c43345fde32e"
integrity sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==
"@typescript/typescript-linux-loong64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz#2965aee4fc873360139d893daafe6397a29138ad"
integrity sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==
"@typescript/typescript-linux-mips64el@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz#1a887a311bed3a833f80bfd4a9ed37c271936cf0"
integrity sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==
"@typescript/typescript-linux-ppc64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz#8b63c9b2f445b393eb4e43ec21da225dade3577d"
integrity sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==
"@typescript/typescript-linux-riscv64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz#b6e8a35c289b3ea97a92a41d461aaeed0d3b36e1"
integrity sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==
"@typescript/typescript-linux-s390x@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz#2ef96693be4861f6d17965427e5b009cbbed1a3e"
integrity sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==
"@typescript/typescript-linux-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz#73269cb0baba50aea0ca060445a6b88e583f1ce2"
integrity sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==
"@typescript/typescript-netbsd-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz#3a3649f97fafa210b4e6e3798c15e06605c8a901"
integrity sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==
"@typescript/typescript-netbsd-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz#47ec59491a40c470d2807dc4d2b825528fd979ab"
integrity sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==
"@typescript/typescript-openbsd-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz#796be8da0bd989d8a3fb96f2801e38a8365b4baf"
integrity sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==
"@typescript/typescript-openbsd-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz#d37fe2a729eb942c076c454ee7f1815faf7d560f"
integrity sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==
"@typescript/typescript-sunos-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz#aba8d3464c3565a7044789baba96916bd4ab2c88"
integrity sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==
"@typescript/typescript-win32-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz#b9de50a17196383f62620b5f9d0a2f34ad3b60d7"
integrity sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==
"@typescript/typescript-win32-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz#cf3b7b0d6ce5635daca4c8e01c189cdcde47ec3c"
integrity sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==
"@unrs/resolver-binding-android-arm-eabi@1.12.2":
version "1.12.2"
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz#98a9fee62c01f209747a4ab5855f1ced38a6d03a"
@@ -1331,7 +1603,14 @@ acorn-jsx@^5.3.2:
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn@^8.15.0:
acorn-walk@^8.1.1:
version "8.3.5"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.5.tgz#8a6b8ca8fc5b34685af15dabb44118663c296496"
integrity sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==
dependencies:
acorn "^8.11.0"
acorn@^8.11.0, acorn@^8.15.0, acorn@^8.4.1:
version "8.18.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.18.0.tgz#4faf01b2d6d326bfeed97aea1f52220b5f4c1940"
integrity sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==
@@ -1392,6 +1671,11 @@ ansi-styles@^4.1.0:
dependencies:
color-convert "^2.0.1"
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
@@ -1547,6 +1831,11 @@ baseline-browser-mapping@^2.11.12, baseline-browser-mapping@^2.9.19:
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz#26078c7a4b08299656ea7ddceaebec955dc44303"
integrity sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==
bcryptjs@*, bcryptjs@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-3.0.3.tgz#4b93d6a398c48bfc9f32ee65d301174a8a8ea56f"
integrity sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==
body-parser@^2.2.1:
version "2.3.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.3.0.tgz#6d8662f4d8c336028b8ac9aa24251b0ca64ba437"
@@ -1781,6 +2070,11 @@ cosmiconfig@^9.0.0:
js-yaml "^4.1.0"
parse-json "^5.2.0"
create-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
cross-spawn@^7.0.3, cross-spawn@^7.0.5, cross-spawn@^7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
@@ -2000,6 +2294,11 @@ detect-libc@^2.0.3, detect-libc@^2.1.2:
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad"
integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==
diff@^4.0.1:
version "4.0.4"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.4.tgz#7a6dbfda325f25f07517e9b518f897c08332e07d"
integrity sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==
diff@^8.0.2:
version "8.0.4"
resolved "https://registry.yarnpkg.com/diff/-/diff-8.0.4.tgz#4f5baf3188b9b2431117b962eb20ba330fadf696"
@@ -2229,6 +2528,38 @@ es-toolkit@^1.39.3:
resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.51.0.tgz#f1f0fe1944e6405ef5e4d19a92b91c1b65e8b657"
integrity sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==
esbuild@~0.28.0:
version "0.28.2"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.28.2.tgz#0f43bd1bad955b72d24e2261e3abe5957ccf0816"
integrity sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==
optionalDependencies:
"@esbuild/aix-ppc64" "0.28.2"
"@esbuild/android-arm" "0.28.2"
"@esbuild/android-arm64" "0.28.2"
"@esbuild/android-x64" "0.28.2"
"@esbuild/darwin-arm64" "0.28.2"
"@esbuild/darwin-x64" "0.28.2"
"@esbuild/freebsd-arm64" "0.28.2"
"@esbuild/freebsd-x64" "0.28.2"
"@esbuild/linux-arm" "0.28.2"
"@esbuild/linux-arm64" "0.28.2"
"@esbuild/linux-ia32" "0.28.2"
"@esbuild/linux-loong64" "0.28.2"
"@esbuild/linux-mips64el" "0.28.2"
"@esbuild/linux-ppc64" "0.28.2"
"@esbuild/linux-riscv64" "0.28.2"
"@esbuild/linux-s390x" "0.28.2"
"@esbuild/linux-x64" "0.28.2"
"@esbuild/netbsd-arm64" "0.28.2"
"@esbuild/netbsd-x64" "0.28.2"
"@esbuild/openbsd-arm64" "0.28.2"
"@esbuild/openbsd-x64" "0.28.2"
"@esbuild/openharmony-arm64" "0.28.2"
"@esbuild/sunos-x64" "0.28.2"
"@esbuild/win32-arm64" "0.28.2"
"@esbuild/win32-ia32" "0.28.2"
"@esbuild/win32-x64" "0.28.2"
escalade@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
@@ -2708,7 +3039,7 @@ fs-extra@^11.3.1:
jsonfile "^6.0.1"
universalify "^2.0.0"
fsevents@2.3.3:
fsevents@2.3.3, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
@@ -3323,7 +3654,7 @@ jiti@^2.7.0:
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.7.0.tgz#974228f2f4ca2bc21885a1797b45fea68e950c64"
integrity sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==
jose@^6.1.3:
jose@^6.1.3, jose@^6.2.10:
version "6.2.10"
resolved "https://registry.yarnpkg.com/jose/-/jose-6.2.10.tgz#b70436c920c4b3f97314c28a8b8612c15b1d9e7f"
integrity sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==
@@ -3581,6 +3912,11 @@ magic-string@^0.30.21:
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.5"
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
@@ -4809,6 +5145,25 @@ ts-morph@^26.0.0:
"@ts-morph/common" "~0.27.0"
code-block-writer "^13.0.3"
ts-node@^10.9.2:
version "10.9.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==
dependencies:
"@cspotcode/source-map-support" "^0.8.0"
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.2"
acorn "^8.4.1"
acorn-walk "^8.1.1"
arg "^4.1.0"
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
tsconfig-paths@^3.15.0:
version "3.15.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4"
@@ -4833,6 +5188,15 @@ tslib@^2.0.1, tslib@^2.4.0, tslib@^2.8.0, tslib@^2.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
tsx@^4.23.12:
version "4.23.12"
resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.23.12.tgz#3a4919591cd9b9e00011b75e596c8ab8db23c09c"
integrity sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==
dependencies:
esbuild "~0.28.0"
optionalDependencies:
fsevents "~2.3.3"
tw-animate-css@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/tw-animate-css/-/tw-animate-css-1.4.0.tgz#b4a06f68244cba39428aa47e65e6e4c0babc21ee"
@@ -4909,10 +5273,31 @@ typescript-eslint@^8.46.0:
"@typescript-eslint/typescript-estree" "8.68.0"
"@typescript-eslint/utils" "8.68.0"
typescript@^5:
version "5.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
typescript@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-7.0.2.tgz#9ec773d7954a8c182c17cc5bbd575aa28bc51582"
integrity sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==
optionalDependencies:
"@typescript/typescript-aix-ppc64" "7.0.2"
"@typescript/typescript-darwin-arm64" "7.0.2"
"@typescript/typescript-darwin-x64" "7.0.2"
"@typescript/typescript-freebsd-arm64" "7.0.2"
"@typescript/typescript-freebsd-x64" "7.0.2"
"@typescript/typescript-linux-arm" "7.0.2"
"@typescript/typescript-linux-arm64" "7.0.2"
"@typescript/typescript-linux-loong64" "7.0.2"
"@typescript/typescript-linux-mips64el" "7.0.2"
"@typescript/typescript-linux-ppc64" "7.0.2"
"@typescript/typescript-linux-riscv64" "7.0.2"
"@typescript/typescript-linux-s390x" "7.0.2"
"@typescript/typescript-linux-x64" "7.0.2"
"@typescript/typescript-netbsd-arm64" "7.0.2"
"@typescript/typescript-netbsd-x64" "7.0.2"
"@typescript/typescript-openbsd-arm64" "7.0.2"
"@typescript/typescript-openbsd-x64" "7.0.2"
"@typescript/typescript-sunos-x64" "7.0.2"
"@typescript/typescript-win32-arm64" "7.0.2"
"@typescript/typescript-win32-x64" "7.0.2"
unbox-primitive@^1.1.0:
version "1.1.0"
@@ -5009,6 +5394,11 @@ uuid@^14.0.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-14.0.2.tgz#d5ae03e4db0881c87271f8b0b9bd7312d02c799a"
integrity sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
validate-npm-package-name@^7.0.1:
version "7.0.2"
resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz#e57c3d721a4c8bbff454a246e7f7da811559ea0d"
@@ -5129,6 +5519,11 @@ yallist@^3.0.2:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"