feat: implement app directory routing, authentication flow, and dynamic reporting module
This commit is contained in:
@@ -12,9 +12,15 @@ Fondasi utama skema database:
|
|||||||
|
|
||||||
table User:
|
table User:
|
||||||
|
|
||||||
- id String @id
|
- id String @id @default(uuid())
|
||||||
- name String @unique
|
- username String @unique
|
||||||
|
- name String
|
||||||
|
- password String
|
||||||
- photo 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")
|
- orders Order[] @relation("CreatedOrders")
|
||||||
- purchases Submission[]
|
- purchases Submission[]
|
||||||
|
|
||||||
@@ -74,15 +80,12 @@ table SubmissionItem:
|
|||||||
|
|
||||||
# 2. CORE AUTHENTICATION FLOW
|
# 2. CORE AUTHENTICATION FLOW
|
||||||
|
|
||||||
- DILARANG menggunakan NextAuth/library auth eksternal.
|
- DILARANG menggunakan NextAuth/library auth eksternal. Buat custom authentication menggunakan JWT (atau hash-based token) yang disimpan di HTTP-only cookies atau localStorage.
|
||||||
- Setiap kali web diakses, jalankan pengecekan:
|
- Fitur Token: Tanpa waktu expired (no expiration) dan mendukung multi-device login.
|
||||||
1. Cek `localStorage` untuk `user_id`.
|
- Terdapat halaman `/login` dan `/register` untuk pengguna.
|
||||||
2. Jika belum ada, generate UUID baru, simpan ke `localStorage`.
|
- Default role saat registrasi adalah `user`.
|
||||||
3. Lakukan verifikasi ke database/server action apakah `user_id` tersebut sudah terdaftar di tabel `User`.
|
- Harus ada seeder untuk membuat akun superadmin default: Username: `superadmin` (atau spesifik), Password: `titipin123!`, Role: `superadmin`.
|
||||||
4. Jika belum terdaftar, wajib munculkan Modal Profile yang tidak bisa ditutup.
|
- Middleware/Route Protection: Hanya user yang sudah login yang bisa mengakses halaman utama aplikasi. Halaman manajemen user hanya bisa diakses oleh role `superadmin`.
|
||||||
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.
|
|
||||||
|
|
||||||
# 3. MENU NAVIGATION ORDER
|
# 3. MENU NAVIGATION ORDER
|
||||||
|
|
||||||
@@ -92,7 +95,8 @@ Urutan navigasi menu utama pada aplikasi adalah sebagai berikut:
|
|||||||
2. Jasa Order Saya
|
2. Jasa Order Saya
|
||||||
3. Pesanan Saya
|
3. Pesanan Saya
|
||||||
4. Laporan
|
4. Laporan
|
||||||
5. Profile
|
5. User Management (Hanya Superadmin)
|
||||||
|
6. Profile
|
||||||
|
|
||||||
# 4. PAGE SPECIFICATIONS & STEP-BY-STEP FLOW
|
# 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.
|
- Visualisasi berupa Bar Chart (Grafik) dan Data Grid (Tabel) yang diurutkan secara proporsional.
|
||||||
- Mendukung filter Bulan dan Tahun berbasis server-side.
|
- 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.
|
- 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
|
# 6. WORKFLOW EXECUTION
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# TitipIn - Sistem Titip Pesanan
|
# 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)
|
## Kebutuhan Sistem (Prerequisites)
|
||||||
Sebelum menjalankan aplikasi ini, pastikan Anda telah menginstal perangkat lunak berikut:
|
Sebelum menjalankan aplikasi ini, pastikan Anda telah menginstal perangkat lunak berikut:
|
||||||
- **Node.js**: Versi 18.x atau lebih baru (direkomendasikan versi LTS terbaru).
|
- **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).
|
- **PostgreSQL**: Database server berjalan (versi 13 atau lebih baru direkomendasikan).
|
||||||
|
|
||||||
## Instalasi
|
## Instalasi
|
||||||
@@ -13,12 +13,12 @@ Sebelum menjalankan aplikasi ini, pastikan Anda telah menginstal perangkat lunak
|
|||||||
2. Buka terminal/command prompt.
|
2. Buka terminal/command prompt.
|
||||||
3. Jalankan perintah instalasi dependensi:
|
3. Jalankan perintah instalasi dependensi:
|
||||||
```bash
|
```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
|
## 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
|
```env
|
||||||
DATABASE_HOST="172.10.10.2"
|
DATABASE_HOST="172.10.10.2"
|
||||||
@@ -27,26 +27,33 @@ DATABASE_USER="postgres"
|
|||||||
DATABASE_PASSWORD="eigen3m!"
|
DATABASE_PASSWORD="eigen3m!"
|
||||||
DATABASE_NAME="testing"
|
DATABASE_NAME="testing"
|
||||||
DATABASE_URL="postgresql://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}?schema=public"
|
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
|
## 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.
|
1. **Generate Prisma Client**: Membangun tipe TypeScript dan library client.
|
||||||
```bash
|
```bash
|
||||||
npx prisma generate
|
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
|
```bash
|
||||||
npx prisma db push
|
npx prisma db push
|
||||||
```
|
```
|
||||||
|
3. **Seed Database**: Mengisi database dengan akun Superadmin bawaan.
|
||||||
|
```bash
|
||||||
|
npx prisma db seed
|
||||||
|
```
|
||||||
|
|
||||||
## Menjalankan Aplikasi
|
## Menjalankan Aplikasi
|
||||||
Setelah database siap, Anda bisa menjalankan aplikasi di tahap pengembangan lokal (development mode):
|
Setelah database siap, Anda bisa menjalankan aplikasi di tahap pengembangan lokal (development mode):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Aplikasi secara bawaan dapat diakses melalui browser dengan URL:
|
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:
|
### Alur Singkat Aplikasi:
|
||||||
- Akses pertama kali, sistem akan _generate_ UUID unik untuk *device* Anda dan menyimpannya di `localStorage`.
|
- **Autentikasi:** Akses aplikasi harus melalui halaman login. Jika belum memiliki akun, pengguna dapat mendaftar (register) yang otomatis akan memberikan *role* sebagai `user`.
|
||||||
- Aplikasi akan menampilkan Modal Wajib untuk mendaftarkan **Nama** dan opsional **Foto Profil**.
|
- **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:
|
- Anda dapat mulai:
|
||||||
- Membuat Jasa Titip di menu **Jasa Order Saya**.
|
- Membuat Jasa Titip di menu **Jasa Order Saya**.
|
||||||
- Mengikuti Jasa Titip teman di menu **Open Order**.
|
- Mengikuti Jasa Titip teman di menu **Open Order**.
|
||||||
- Mengelola pembayaran & melihat rincian di halaman detail.
|
- Mengelola pembayaran & melihat rincian di halaman detail.
|
||||||
|
- Memantau laporan omzet & pengeluaran di menu **Laporan**.
|
||||||
|
|||||||
+9
-1
@@ -13,9 +13,11 @@
|
|||||||
"@base-ui/react": "^1.7.0",
|
"@base-ui/react": "^1.7.0",
|
||||||
"@hookform/resolvers": "^5.9.1",
|
"@hookform/resolvers": "^5.9.1",
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.4.0",
|
||||||
|
"jose": "^6.2.10",
|
||||||
"lucide-react": "^1.34.0",
|
"lucide-react": "^1.34.0",
|
||||||
"next": "16.3.3",
|
"next": "16.3.3",
|
||||||
"prisma": "^5.22.0",
|
"prisma": "^5.22.0",
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
@@ -39,6 +42,11 @@
|
|||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.3.3",
|
"eslint-config-next": "16.3.3",
|
||||||
"tailwindcss": "^4",
|
"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
@@ -11,9 +11,15 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id
|
id String @id @default(uuid())
|
||||||
name String @unique
|
username String @unique
|
||||||
|
name String
|
||||||
|
password String
|
||||||
photo 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")
|
orders Order[] @relation("CreatedOrders")
|
||||||
purchases Submission[]
|
purchases Submission[]
|
||||||
}
|
}
|
||||||
@@ -28,13 +34,17 @@ model Order {
|
|||||||
creator User @relation("CreatedOrders", fields: [creator_id], references: [id])
|
creator User @relation("CreatedOrders", fields: [creator_id], references: [id])
|
||||||
available_items AvailableItem[]
|
available_items AvailableItem[]
|
||||||
submissions Submission[]
|
submissions Submission[]
|
||||||
|
created_at DateTime @default(now())
|
||||||
|
updated_at DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model AvailableItem {
|
model AvailableItem {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
order_id String
|
order_id String
|
||||||
name String
|
name String
|
||||||
order Order @relation(fields: [order_id], references: [id], onDelete: Cascade)
|
order Order @relation(fields: [order_id], references: [id], onDelete: Cascade)
|
||||||
|
created_at DateTime @default(now())
|
||||||
|
updated_at DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model Submission {
|
model Submission {
|
||||||
@@ -46,6 +56,8 @@ model Submission {
|
|||||||
order Order @relation(fields: [order_id], references: [id], onDelete: Cascade)
|
order Order @relation(fields: [order_id], references: [id], onDelete: Cascade)
|
||||||
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
||||||
items SubmissionItem[]
|
items SubmissionItem[]
|
||||||
|
created_at DateTime @default(now())
|
||||||
|
updated_at DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model SubmissionItem {
|
model SubmissionItem {
|
||||||
@@ -55,4 +67,6 @@ model SubmissionItem {
|
|||||||
qty Int @default(1)
|
qty Int @default(1)
|
||||||
is_custom Boolean @default(false)
|
is_custom Boolean @default(false)
|
||||||
submission Submission @relation(fields: [submission_id], references: [id], onDelete: Cascade)
|
submission Submission @relation(fields: [submission_id], references: [id], onDelete: Cascade)
|
||||||
|
created_at DateTime @default(now())
|
||||||
|
updated_at DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
@@ -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 { useEffect, useState } from 'react'
|
||||||
import { useParams, useRouter } from 'next/navigation'
|
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 { Card, CardContent } from '@/components/ui/card'
|
||||||
import { Button, buttonVariants } from '@/components/ui/button'
|
import { Button, buttonVariants } from '@/components/ui/button'
|
||||||
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
|
||||||
@@ -47,9 +47,14 @@ export default function OrderDetailPage() {
|
|||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = localStorage.getItem('user_id')
|
const init = async () => {
|
||||||
setUserId(id)
|
const user = await getSessionUser()
|
||||||
if (orderId) loadOrder()
|
if (user?.id) {
|
||||||
|
setUserId(user.id)
|
||||||
|
}
|
||||||
|
if (orderId) loadOrder()
|
||||||
|
}
|
||||||
|
init()
|
||||||
}, [orderId])
|
}, [orderId])
|
||||||
|
|
||||||
const loadOrder = async () => {
|
const loadOrder = async () => {
|
||||||
@@ -476,13 +481,13 @@ function SubmissionRow({
|
|||||||
isClosed: boolean
|
isClosed: boolean
|
||||||
onUpdate: () => void
|
onUpdate: () => void
|
||||||
}) {
|
}) {
|
||||||
const [bill, setBill] = useState(sub.bill || '')
|
const [bill, setBill] = useState(sub.bill ?? '')
|
||||||
const [status, setStatus] = useState(sub.payment_status || 'BELUM_BAYAR')
|
const [status, setStatus] = useState(sub.payment_status || 'BELUM_BAYAR')
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
await updateSubmissionPayment(sub.id, bill ? parseInt(bill) : null, status)
|
await updateSubmissionPayment(sub.id, bill !== '' ? parseInt(bill as string) : null, status)
|
||||||
onUpdate()
|
onUpdate()
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
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 { Button, buttonVariants } from '@/components/ui/button'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
@@ -46,11 +46,14 @@ export default function MyOrdersPage() {
|
|||||||
const ITEMS_PER_PAGE = 5
|
const ITEMS_PER_PAGE = 5
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = localStorage.getItem('user_id')
|
const init = async () => {
|
||||||
if (id) {
|
const user = await getSessionUser()
|
||||||
setUserId(id)
|
if (user?.id) {
|
||||||
loadOrders(id)
|
setUserId(user.id)
|
||||||
|
loadOrders(user.id)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
init()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const loadOrders = async (id: string) => {
|
const loadOrders = async (id: string) => {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
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 { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'
|
||||||
import { Button, buttonVariants } from '@/components/ui/button'
|
import { Button, buttonVariants } from '@/components/ui/button'
|
||||||
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
|
||||||
@@ -38,11 +38,14 @@ export default function MyPurchasesPage() {
|
|||||||
const ITEMS_PER_PAGE = 5
|
const ITEMS_PER_PAGE = 5
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = localStorage.getItem('user_id')
|
const init = async () => {
|
||||||
if (id) {
|
const user = await getSessionUser()
|
||||||
setUserId(id)
|
if (user?.id) {
|
||||||
loadPurchases(id)
|
setUserId(user.id)
|
||||||
|
loadPurchases(user.id)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
init()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const loadPurchases = async (id: string) => {
|
const loadPurchases = async (id: string) => {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAvailableOrders } from './actions'
|
import { getAvailableOrders } from '@/app/actions'
|
||||||
import { OrderCard } from '@/components/OrderCard'
|
import { OrderCard } from '@/components/OrderCard'
|
||||||
import { Sparkles, Plus, Store } from 'lucide-react'
|
import { Sparkles, Plus, Store } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
@@ -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'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
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 { Card } from '@/components/ui/card'
|
||||||
import { Loader2, TrendingUp, TrendingDown, Wallet, ArrowRightLeft, Calendar as CalendarIcon } from 'lucide-react'
|
import { Loader2, TrendingUp, TrendingDown, Wallet, ArrowRightLeft, Calendar as CalendarIcon } from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -51,7 +51,7 @@ function getIntervalFromFilter(type: 'WEEK' | 'MONTH', value: string): { start:
|
|||||||
|
|
||||||
export default function ReportsPage() {
|
export default function ReportsPage() {
|
||||||
const [activeTab, setActiveTab] = useState<'SUBMITTOR' | 'CREATOR'>('SUBMITTOR')
|
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 [filterType, setFilterType] = useState<'WEEK' | 'MONTH'>('WEEK')
|
||||||
const [filterValue, setFilterValue] = useState<string>(getWeekValue(new Date()))
|
const [filterValue, setFilterValue] = useState<string>(getWeekValue(new Date()))
|
||||||
@@ -62,10 +62,12 @@ export default function ReportsPage() {
|
|||||||
const [creatorData, setCreatorData] = useState<any[]>([])
|
const [creatorData, setCreatorData] = useState<any[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
// Initial load: get userId
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = localStorage.getItem('user_id')
|
const init = async () => {
|
||||||
if (id) setUserId(id)
|
const user = await getSessionUser()
|
||||||
|
if (user?.id) setUserId(user.id)
|
||||||
|
}
|
||||||
|
init()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Re-fetch every time userId, filterType, or filterValue changes
|
// Re-fetch every time userId, filterType, or filterValue changes
|
||||||
@@ -261,7 +263,7 @@ export default function ReportsPage() {
|
|||||||
|
|
||||||
{/* Sub-Tabs */}
|
{/* 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">
|
<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
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
onClick={() => setCreatorTab(tab)}
|
onClick={() => setCreatorTab(tab)}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
@@ -3,38 +3,204 @@
|
|||||||
import prisma from '@/lib/prisma'
|
import prisma from '@/lib/prisma'
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from 'next/cache'
|
||||||
|
|
||||||
// === USER ACTIONS ===
|
import { hashPassword, verifyPassword, encrypt, decrypt } from '@/lib/auth'
|
||||||
export async function checkUser(id: string) {
|
import { cookies } from 'next/headers'
|
||||||
return await prisma.user.findUnique({ where: { id } })
|
|
||||||
|
// === 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 {
|
try {
|
||||||
|
const hashedPassword = await hashPassword(password)
|
||||||
const user = await prisma.user.create({
|
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) {
|
} 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.' }
|
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 {
|
try {
|
||||||
const user = await prisma.user.update({
|
const user = await prisma.user.update({
|
||||||
where: { id },
|
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('/')
|
||||||
revalidatePath('/profile')
|
revalidatePath('/profile')
|
||||||
return { success: true, user }
|
return { success: true, user: sessionData }
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.code === 'P2002') return { success: false, error: 'Nama sudah digunakan.' }
|
|
||||||
return { success: false, error: 'Gagal memperbarui profil.' }
|
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) ===
|
// === ORDER ACTIONS (DASHBOARD & CREATE) ===
|
||||||
export async function getAvailableOrders() {
|
export async function getAvailableOrders() {
|
||||||
const startOfDay = new Date()
|
const startOfDay = new Date()
|
||||||
@@ -91,6 +257,7 @@ export async function createOrder(data: {
|
|||||||
revalidatePath('/my-orders')
|
revalidatePath('/my-orders')
|
||||||
return { success: true, order }
|
return { success: true, order }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Create Order Error:", error)
|
||||||
return { success: false, error: 'Gagal membuat order.' }
|
return { success: false, error: 'Gagal membuat order.' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,7 +392,7 @@ export async function updateSubmissionPayment(submission_id: string, bill: numbe
|
|||||||
revalidatePath(`/reports`)
|
revalidatePath(`/reports`)
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { success: false, error: 'Gagal menyimpan tagihan.' }
|
console.error(e); return { success: false, error: 'Gagal menyimpan tagihan.' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,3 +116,6 @@
|
|||||||
font-family: var(--font-roboto), var(--font-open-sans), -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Segoe UI", Roboto, "Helvetica Neue", "Open Sans", system-ui, sans-serif;
|
font-family: var(--font-roboto), var(--font-open-sans), -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Segoe UI", Roboto, "Helvetica Neue", "Open Sans", system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|||||||
+2
-7
@@ -1,8 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Roboto, Open_Sans } from "next/font/google";
|
import { Roboto, Open_Sans } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { AuthProvider } from "@/components/AuthProvider";
|
|
||||||
import { AppLayout } from "@/components/AppLayout";
|
|
||||||
|
|
||||||
const roboto = Roboto({
|
const roboto = Roboto({
|
||||||
weight: ["300", "400", "500", "700", "900"],
|
weight: ["300", "400", "500", "700", "900"],
|
||||||
@@ -37,11 +36,7 @@ export default function RootLayout({
|
|||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
className="min-h-full flex flex-col bg-[#F4F6FB] dark:bg-[#0B0F19] font-sans antialiased"
|
className="min-h-full flex flex-col bg-[#F4F6FB] dark:bg-[#0B0F19] font-sans antialiased"
|
||||||
>
|
>
|
||||||
<AuthProvider>
|
{children}
|
||||||
<AppLayout>
|
|
||||||
{children}
|
|
||||||
</AppLayout>
|
|
||||||
</AuthProvider>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -54,6 +54,13 @@ export function Header({ onMenuClick }: { onMenuClick: () => void }) {
|
|||||||
badge: 'Laporan',
|
badge: 'Laporan',
|
||||||
Icon: BarChart2
|
Icon: BarChart2
|
||||||
}
|
}
|
||||||
|
case '/users':
|
||||||
|
return {
|
||||||
|
title: 'Manajemen Pengguna',
|
||||||
|
subtitle: 'Kelola data pengguna, role, dan status aktif.',
|
||||||
|
badge: 'Superadmin',
|
||||||
|
Icon: User
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
if (pathname.startsWith('/my-orders/')) {
|
if (pathname.startsWith('/my-orders/')) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
|
|||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Input } from '@/components/ui/input'
|
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 { PlusCircle, MinusCircle, User, CheckCircle2, ShoppingBag, Sparkles, Users } from 'lucide-react'
|
||||||
|
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
@@ -113,11 +113,14 @@ function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: () => voi
|
|||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = localStorage.getItem('user_id')
|
const init = async () => {
|
||||||
if (id) {
|
const user = await getSessionUser()
|
||||||
setUserId(id)
|
if (user?.id) {
|
||||||
loadExistingSubmission(id)
|
setUserId(user.id)
|
||||||
|
loadExistingSubmission(user.id)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
init()
|
||||||
}, [order.id])
|
}, [order.id])
|
||||||
|
|
||||||
const loadExistingSubmission = async (uid: string) => {
|
const loadExistingSubmission = async (uid: string) => {
|
||||||
|
|||||||
+76
-10
@@ -4,6 +4,12 @@ import { useEffect, useState } from 'react'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Store,
|
Store,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
@@ -15,7 +21,6 @@ import {
|
|||||||
Info,
|
Info,
|
||||||
BarChart2
|
BarChart2
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { checkUser } from '@/app/actions'
|
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ name: 'Open Order', href: '/', icon: Store, desc: 'Daftar PO live hari ini' },
|
{ 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 }) {
|
export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () => void }) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const [userName, setUserName] = useState<string>('Pengguna')
|
const [userName, setUserName] = useState<string>('Pengguna')
|
||||||
|
const [userNameAccount, setUserNameAccount] = useState<string>('Pengguna')
|
||||||
const [userPhoto, setUserPhoto] = useState<string | null>(null)
|
const [userPhoto, setUserPhoto] = useState<string | null>(null)
|
||||||
|
const [userRole, setUserRole] = useState<string | null>(null)
|
||||||
const [userId, setUserId] = useState<string | null>(null)
|
const [userId, setUserId] = useState<string | null>(null)
|
||||||
|
const [isLogoutOpen, setIsLogoutOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = localStorage.getItem('user_id')
|
import('@/app/actions').then(({ getSessionUser }) => {
|
||||||
if (id) {
|
getSessionUser().then((u) => {
|
||||||
setUserId(id)
|
|
||||||
checkUser(id).then((u) => {
|
|
||||||
if (u) {
|
if (u) {
|
||||||
|
setUserId(u.id)
|
||||||
setUserName(u.name)
|
setUserName(u.name)
|
||||||
|
setUserNameAccount(u.username)
|
||||||
|
setUserRole(u.role)
|
||||||
if (u.photo) setUserPhoto(u.photo)
|
if (u.photo) setUserPhoto(u.photo)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
}, [pathname])
|
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Mobile Backdrop */}
|
{/* Mobile Backdrop */}
|
||||||
@@ -106,9 +127,16 @@ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () =>
|
|||||||
</span>
|
</span>
|
||||||
<ArrowUpRight className="w-3.5 h-3.5 text-slate-400 opacity-0 group-hover:opacity-100 transition-opacity" />
|
<ArrowUpRight className="w-3.5 h-3.5 text-slate-400 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] font-mono text-slate-400 truncate">
|
<div className="flex items-center gap-1.5 mt-0.5">
|
||||||
{userId ? `ID: ${userId.slice(0, 10)}...` : 'Aktif'}
|
{userRole === 'superadmin' && (
|
||||||
</span>
|
<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>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@@ -118,7 +146,7 @@ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () =>
|
|||||||
Navigasi Utama
|
Navigasi Utama
|
||||||
</div>
|
</div>
|
||||||
<nav className="space-y-1">
|
<nav className="space-y-1">
|
||||||
{menuItems.map((item) => {
|
{finalMenuItems.map((item) => {
|
||||||
const Icon = item.icon
|
const Icon = item.icon
|
||||||
const isActive = pathname === item.href
|
const isActive = pathname === item.href
|
||||||
return (
|
return (
|
||||||
@@ -153,6 +181,44 @@ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () =>
|
|||||||
</Link>
|
</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>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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).*)'],
|
||||||
|
}
|
||||||
@@ -273,6 +273,13 @@
|
|||||||
reselect "^5.2.0"
|
reselect "^5.2.0"
|
||||||
use-sync-external-store "^1.6.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":
|
"@date-fns/tz@^1.4.1":
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@date-fns/tz/-/tz-1.5.0.tgz#e9e79b7583f0b1322c53db884a0112551095e3f3"
|
resolved "https://registry.yarnpkg.com/@date-fns/tz/-/tz-1.5.0.tgz#e9e79b7583f0b1322c53db884a0112551095e3f3"
|
||||||
@@ -349,6 +356,136 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.4.0"
|
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":
|
"@eslint-community/eslint-utils@4.9.1":
|
||||||
version "4.9.1"
|
version "4.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595"
|
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/gen-mapping" "^0.3.5"
|
||||||
"@jridgewell/trace-mapping" "^0.3.24"
|
"@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"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
|
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
|
||||||
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
|
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"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz#f4c663e862f06dc98ca4d453862c46902789a18d"
|
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz#f4c663e862f06dc98ca4d453862c46902789a18d"
|
||||||
integrity sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==
|
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":
|
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28":
|
||||||
version "0.3.31"
|
version "0.3.31"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
|
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
|
||||||
@@ -1001,6 +1146,26 @@
|
|||||||
minimatch "^10.0.1"
|
minimatch "^10.0.1"
|
||||||
path-browserify "^1.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":
|
"@tybys/wasm-util@^0.10.2", "@tybys/wasm-util@^0.10.3":
|
||||||
version "0.10.3"
|
version "0.10.3"
|
||||||
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz#015cba9e9dd47ce14d03d2a8c5d547bfb169665d"
|
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.3.tgz#015cba9e9dd47ce14d03d2a8c5d547bfb169665d"
|
||||||
@@ -1008,6 +1173,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.4.0"
|
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":
|
"@types/d3-array@^3.0.3":
|
||||||
version "3.2.2"
|
version "3.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.2.tgz#e02151464d02d4a1b44646d0fcdb93faf88fde8c"
|
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"
|
"@typescript-eslint/types" "8.68.0"
|
||||||
eslint-visitor-keys "^5.0.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":
|
"@unrs/resolver-binding-android-arm-eabi@1.12.2":
|
||||||
version "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"
|
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"
|
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
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"
|
version "8.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.18.0.tgz#4faf01b2d6d326bfeed97aea1f52220b5f4c1940"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.18.0.tgz#4faf01b2d6d326bfeed97aea1f52220b5f4c1940"
|
||||||
integrity sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==
|
integrity sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==
|
||||||
@@ -1392,6 +1671,11 @@ ansi-styles@^4.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color-convert "^2.0.1"
|
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:
|
argparse@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
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"
|
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz#26078c7a4b08299656ea7ddceaebec955dc44303"
|
||||||
integrity sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==
|
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:
|
body-parser@^2.2.1:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.3.0.tgz#6d8662f4d8c336028b8ac9aa24251b0ca64ba437"
|
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"
|
js-yaml "^4.1.0"
|
||||||
parse-json "^5.2.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:
|
cross-spawn@^7.0.3, cross-spawn@^7.0.5, cross-spawn@^7.0.6:
|
||||||
version "7.0.6"
|
version "7.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
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"
|
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad"
|
||||||
integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==
|
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:
|
diff@^8.0.2:
|
||||||
version "8.0.4"
|
version "8.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/diff/-/diff-8.0.4.tgz#4f5baf3188b9b2431117b962eb20ba330fadf696"
|
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"
|
resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.51.0.tgz#f1f0fe1944e6405ef5e4d19a92b91c1b65e8b657"
|
||||||
integrity sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==
|
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:
|
escalade@^3.2.0:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
|
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"
|
jsonfile "^6.0.1"
|
||||||
universalify "^2.0.0"
|
universalify "^2.0.0"
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3, fsevents@~2.3.3:
|
||||||
version "2.3.3"
|
version "2.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
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"
|
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.7.0.tgz#974228f2f4ca2bc21885a1797b45fea68e950c64"
|
||||||
integrity sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==
|
integrity sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==
|
||||||
|
|
||||||
jose@^6.1.3:
|
jose@^6.1.3, jose@^6.2.10:
|
||||||
version "6.2.10"
|
version "6.2.10"
|
||||||
resolved "https://registry.yarnpkg.com/jose/-/jose-6.2.10.tgz#b70436c920c4b3f97314c28a8b8612c15b1d9e7f"
|
resolved "https://registry.yarnpkg.com/jose/-/jose-6.2.10.tgz#b70436c920c4b3f97314c28a8b8612c15b1d9e7f"
|
||||||
integrity sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==
|
integrity sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==
|
||||||
@@ -3581,6 +3912,11 @@ magic-string@^0.30.21:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@jridgewell/sourcemap-codec" "^1.5.5"
|
"@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:
|
math-intrinsics@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
|
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"
|
"@ts-morph/common" "~0.27.0"
|
||||||
code-block-writer "^13.0.3"
|
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:
|
tsconfig-paths@^3.15.0:
|
||||||
version "3.15.0"
|
version "3.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4"
|
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"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
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:
|
tw-animate-css@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/tw-animate-css/-/tw-animate-css-1.4.0.tgz#b4a06f68244cba39428aa47e65e6e4c0babc21ee"
|
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/typescript-estree" "8.68.0"
|
||||||
"@typescript-eslint/utils" "8.68.0"
|
"@typescript-eslint/utils" "8.68.0"
|
||||||
|
|
||||||
typescript@^5:
|
typescript@^7.0.2:
|
||||||
version "5.9.3"
|
version "7.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-7.0.2.tgz#9ec773d7954a8c182c17cc5bbd575aa28bc51582"
|
||||||
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
|
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:
|
unbox-primitive@^1.1.0:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-14.0.2.tgz#d5ae03e4db0881c87271f8b0b9bd7312d02c799a"
|
||||||
integrity sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==
|
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:
|
validate-npm-package-name@^7.0.1:
|
||||||
version "7.0.2"
|
version "7.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz#e57c3d721a4c8bbff454a246e7f7da811559ea0d"
|
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"
|
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
|
||||||
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
|
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:
|
yocto-queue@^0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
|
|||||||
Reference in New Issue
Block a user