Initial commit from Create Next App

This commit is contained in:
danialasadi 2026-06-05 14:33:34 +03:30
commit 41879b49c7
23 changed files with 8476 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

5
AGENTS.md Normal file
View File

@ -0,0 +1,5 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

1
CLAUDE.md Normal file
View File

@ -0,0 +1 @@
@AGENTS.md

36
README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

18
eslint.config.mjs Normal file
View File

@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

7
next.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

6651
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "reactapp",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"next": "16.2.7",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.7",
"tailwindcss": "^4",
"typescript": "^5"
}
}

7
postcss.config.mjs Normal file
View File

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

1
public/file.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
public/window.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

244
src/app/globals.css Normal file
View File

@ -0,0 +1,244 @@
@import "tailwindcss";
@layer base {
:root {
--sidebar-width: 260px;
--header-height: 64px;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--border: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--border: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-gray-200 dark:border-gray-700;
}
body {
@apply bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-100;
}
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply bg-gray-300 dark:bg-gray-600 rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-gray-400 dark:bg-gray-500;
}
/* Animation for page transitions */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.page-enter {
animation: fadeIn 0.2s ease-out;
}
/* Card hover effect */
.card-hover {
@apply transition-all duration-200;
}
.card-hover:hover {
@apply shadow-lg -translate-y-0.5;
}
/* Sidebar link styles */
.sidebar-link {
@apply flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200;
}
.sidebar-link:hover {
@apply bg-gray-100 dark:bg-gray-800;
}
.sidebar-link.active {
@apply bg-primary/10 text-primary dark:bg-primary/20;
}
/* Table styles */
.table-container {
@apply overflow-x-auto rounded-xl border border-gray-200 dark:border-gray-700;
}
.table-container table {
@apply w-full text-sm;
}
.table-container th {
@apply px-4 py-3 text-left font-semibold text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700 whitespace-nowrap;
}
.table-container td {
@apply px-4 py-3 border-b border-gray-100 dark:border-gray-800;
}
.table-container tbody tr {
@apply transition-colors duration-150;
}
.table-container tbody tr:hover {
@apply bg-gray-50 dark:bg-gray-800/30;
}
/* Stats card */
.stat-card {
@apply bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 p-6 card-hover;
}
/* Button styles */
.btn {
@apply inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-primary {
@apply btn bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500;
}
.btn-secondary {
@apply btn bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-500 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700;
}
.btn-danger {
@apply btn bg-red-600 text-white hover:bg-red-700 focus:ring-red-500;
}
.btn-success {
@apply btn bg-green-600 text-white hover:bg-green-700 focus:ring-green-500;
}
.btn-warning {
@apply btn bg-yellow-500 text-white hover:bg-yellow-600 focus:ring-yellow-500;
}
.btn-outline {
@apply btn border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800;
}
.btn-sm {
@apply px-3 py-1.5 text-xs;
}
.btn-lg {
@apply px-6 py-3 text-base;
}
/* Form styles */
.form-input {
@apply w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2.5 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200;
}
.form-label {
@apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5;
}
.form-select {
@apply form-input appearance-none cursor-pointer;
}
.form-textarea {
@apply form-input resize-none min-h-[100px];
}
/* Badge */
.badge {
@apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium;
}
/* Card */
.card {
@apply bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700;
}
.card-header {
@apply px-6 py-4 border-b border-gray-200 dark:border-gray-700;
}
.card-body {
@apply p-6;
}
.card-footer {
@apply px-6 py-4 border-t border-gray-200 dark:border-gray-700;
}
/* Loading spinner */
.spinner {
@apply animate-spin h-5 w-5 border-2 border-blue-600 border-t-transparent rounded-full;
}
.spinner-sm {
@apply h-4 w-4 border;
}
.spinner-lg {
@apply h-8 w-8 border-3;
}
/* Modal overlay */
.modal-overlay {
@apply fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4;
}
.modal-content {
@apply bg-white dark:bg-gray-900 rounded-xl shadow-2xl max-w-lg w-full max-h-[90vh] overflow-y-auto;
}
/* Empty state */
.empty-state {
@apply flex flex-col items-center justify-center py-16 text-center;
}
.empty-state-icon {
@apply text-5xl mb-4 opacity-50;
}
.empty-state-text {
@apply text-gray-500 dark:text-gray-400 text-lg font-medium;
}
.empty-state-description {
@apply text-gray-400 dark:text-gray-500 text-sm mt-1;
}
/* Grid layout helpers */
.grid-cards {
@apply grid gap-6;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}

33
src/app/layout.tsx Normal file
View File

@ -0,0 +1,33 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
</html>
);
}

65
src/app/page.tsx Normal file
View File

@ -0,0 +1,65 @@
import Image from "next/image";
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
);
}

230
src/components/Sidebar.tsx Normal file
View File

@ -0,0 +1,230 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '@/lib/utils';
import { getUserRole } from '@/lib/api';
import { useState, useEffect } from 'react';
interface NavItem {
label: string;
href: string;
icon: string;
roles?: string[];
children?: { label: string; href: string }[];
}
const navItems: NavItem[] = [
{
label: 'Dashboard',
href: '/',
icon: '📊',
},
{
label: 'Benefactors',
href: '/admin/benefactors',
icon: '👥',
roles: ['super-admin', 'admin'],
},
{
label: 'Agents',
href: '/admin/agents',
icon: '🤝',
roles: ['super-admin', 'admin'],
},
{
label: 'Kind Boxes',
href: '/admin/kind-boxes',
icon: '📦',
roles: ['super-admin', 'admin'],
children: [
{ label: 'All Boxes', href: '/admin/kind-boxes' },
{ label: 'Create', href: '/admin/kind-boxes/create' },
],
},
{
label: 'Kind Box Requests',
href: '/admin/kind-box-requests',
icon: '📋',
roles: ['super-admin', 'admin'],
children: [
{ label: 'All Requests', href: '/admin/kind-box-requests' },
{ label: 'Create', href: '/admin/kind-box-requests/create' },
],
},
{
label: 'Refer Times',
href: '/admin/refer-times',
icon: '⏰',
roles: ['super-admin', 'admin'],
},
{
label: 'My Kind Boxes',
href: '/benefactor/kind-boxes',
icon: '📦',
roles: ['benefactor'],
},
{
label: 'My Requests',
href: '/benefactor/kind-box-requests',
icon: '📋',
roles: ['benefactor'],
},
{
label: 'My Addresses',
href: '/benefactor/addresses',
icon: '📍',
roles: ['benefactor'],
},
{
label: 'Refer Times',
href: '/benefactor/refer-times',
icon: '⏰',
roles: ['benefactor'],
},
{
label: 'Agent Dashboard',
href: '/agent/dashboard',
icon: '🎯',
roles: ['agent'],
},
{
label: 'Return Awaiting',
href: '/agent/kind-boxes',
icon: '📥',
roles: ['agent'],
},
{
label: 'Delivery Awaiting',
href: '/agent/kind-box-requests',
icon: '📤',
roles: ['agent'],
},
{
label: 'Products',
href: '/products',
icon: '🛍️',
},
{
label: 'Cart',
href: '/cart',
icon: '🛒',
},
{
label: 'Orders',
href: '/orders',
icon: '📦',
},
{
label: 'Campaigns',
href: '/campaigns',
icon: '🎯',
},
{
label: 'Wallet',
href: '/wallet',
icon: '💳',
},
{
label: 'Patients',
href: '/patients',
icon: '🏥',
roles: ['super-admin', 'admin'],
},
{
label: 'Drivers',
href: '/drivers',
icon: '🚗',
roles: ['super-admin', 'admin'],
},
{
label: 'Staff',
href: '/staff',
icon: '👔',
roles: ['super-admin', 'admin'],
},
{
label: 'Sales Reports',
href: '/sales-reports',
icon: '📈',
roles: ['super-admin', 'admin'],
},
{
label: 'Gamification',
href: '/gamification',
icon: '🏆',
roles: ['super-admin', 'admin'],
},
];
export default function Sidebar() {
const pathname = usePathname();
const [role, setRole] = useState<string | null>(null);
const [collapsed, setCollapsed] = useState(false);
useEffect(() => {
setRole(getUserRole());
}, []);
const filteredItems = navItems.filter((item) => {
if (!item.roles || item.roles.length === 0) return true;
return role && item.roles.includes(role);
});
const isActive = (href: string) => {
if (href === '/') return pathname === '/';
return pathname.startsWith(href);
};
return (
<aside
className={cn(
'fixed left-0 top-0 h-screen bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700 z-30 transition-all duration-300 flex flex-col',
collapsed ? 'w-[60px]' : 'w-[260px]'
)}
>
{/* Logo */}
<div className="flex items-center gap-3 px-4 h-16 border-b border-gray-200 dark:border-gray-700">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-bold text-sm shrink-0">
N
</div>
{!collapsed && (
<span className="font-bold text-lg bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
Niki
</span>
)}
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto p-3 space-y-1">
{filteredItems.map((item) => {
const active = isActive(item.href);
return (
<div key={item.href}>
<Link
href={item.href}
className={cn(
'sidebar-link',
active && 'active',
collapsed && 'justify-center px-2'
)}
title={collapsed ? item.label : undefined}
>
<span className="text-lg shrink-0">{item.icon}</span>
{!collapsed && <span>{item.label}</span>}
</Link>
</div>
);
})}
</nav>
{/* Collapse toggle */}
<button
onClick={() => setCollapsed(!collapsed)}
className="p-3 border-t border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors flex items-center justify-center text-gray-400"
>
<span className="text-sm">{collapsed ? '→' : '←'}</span>
</button>
</aside>
);
}

503
src/lib/api.ts Normal file
View File

@ -0,0 +1,503 @@
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
interface ApiOptions {
method?: HttpMethod;
body?: unknown;
params?: Record<string, string | number | undefined>;
headers?: Record<string, string>;
isFormData?: boolean;
}
async function request<T>(endpoint: string, options: ApiOptions = {}): Promise<T> {
const { method = 'GET', body, params, headers = {}, isFormData = false } = options;
let url = `${API_BASE_URL}${endpoint}`;
if (params) {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
searchParams.append(key, String(value));
}
});
const queryString = searchParams.toString();
if (queryString) {
url += `?${queryString}`;
}
}
const token = getToken();
const authHeaders: Record<string, string> = {};
if (token) {
authHeaders['Authorization'] = `Bearer ${token}`;
}
const fetchOptions: RequestInit = {
method,
headers: {
...authHeaders,
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
...headers,
},
};
if (body) {
fetchOptions.body = isFormData ? (body as FormData) : JSON.stringify(body);
}
const response = await fetch(url, fetchOptions);
if (response.status === 401) {
// Try refresh token
const refreshed = await tryRefreshToken();
if (refreshed) {
authHeaders['Authorization'] = `Bearer ${getToken()}`;
fetchOptions.headers = {
...authHeaders,
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
...headers,
};
const retryResponse = await fetch(url, fetchOptions);
return retryResponse.json();
}
throw new Error('Unauthorized');
}
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Request failed' }));
throw new Error(error.message || `HTTP ${response.status}`);
}
return response.json();
}
// ==================== Auth Token Management ====================
export function getToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('niki_token');
}
export function getRefreshToken(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('niki_refresh_token');
}
export function setTokens(token: string, refreshToken: string): void {
localStorage.setItem('niki_token', token);
localStorage.setItem('niki_refresh_token', refreshToken);
}
export function clearTokens(): void {
localStorage.removeItem('niki_token');
localStorage.removeItem('niki_refresh_token');
localStorage.removeItem('niki_user');
localStorage.removeItem('niki_role');
}
export function setUser(user: unknown, role: string): void {
localStorage.setItem('niki_user', JSON.stringify(user));
localStorage.setItem('niki_role', role);
}
export function getUser(): unknown {
if (typeof window === 'undefined') return null;
const user = localStorage.getItem('niki_user');
return user ? JSON.parse(user) : null;
}
export function getUserRole(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem('niki_role');
}
async function tryRefreshToken(): Promise<boolean> {
const refreshToken = getRefreshToken();
if (!refreshToken) return false;
try {
const response = await fetch(`${API_BASE_URL}/benefactors/refresh-access`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (response.ok) {
const data = await response.json();
setTokens(data.token, data.refreshToken);
return true;
}
} catch {
// Refresh failed
}
return false;
}
// ==================== Auth API ====================
export const authApi = {
// Benefactor auth
benefactorSendOtp: (data: { phoneNumber: string }) =>
request<{ message: string }>('/benefactors/send-otp', { method: 'POST', body: data }),
benefactorLoginRegister: (data: { phoneNumber: string; code: string }) =>
request<{ token: string; refreshToken: string; user: import('./types').Benefactor }>(
'/benefactors/login-register',
{ method: 'POST', body: data }
),
benefactorRefreshAccess: (data: { refreshToken: string }) =>
request<{ token: string; refreshToken: string }>('/benefactors/refresh-access', {
method: 'POST',
body: data,
}),
// Admin auth
adminLogin: (data: { phoneNumber: string; password: string }) =>
request<{ token: string; refreshToken: string; user: import('./types').Admin }>('/admins/login-by-phone', {
method: 'POST',
body: data,
}),
adminRegister: (data: {
firstName: string;
lastName: string;
phoneNumber: string;
password: string;
email?: string;
}) =>
request<{ token: string; refreshToken: string; user: import('./types').Admin }>('/admins/register', {
method: 'POST',
body: data,
}),
adminRefreshAccess: (data: { refreshToken: string }) =>
request<{ token: string; refreshToken: string }>('/admins/refresh-access', {
method: 'POST',
body: data,
}),
adminProfile: () => request<import('./types').Admin>('/admins/profile'),
};
// ==================== Benefactor API ====================
export const benefactorApi = {
getAll: (params?: import('./types').PaginationParams) =>
request<import('./types').PaginatedResponse<import('./types').Benefactor>>('/admins/benefactors', {
params: params as Record<string, string | number | undefined>,
}),
getById: (id: number) =>
request<import('./types').Benefactor>(`/admins/benefactors/${id}`),
update: (id: number, data: import('./types').BenefactorUpdateRequest) =>
request<import('./types').Benefactor>(`/admins/benefactors/${id}`, {
method: 'PUT',
body: data,
}),
updateStatus: (id: number, status: import('./types').BenefactorStatus) =>
request<void>(`/admins/benefactors/${id}/status`, { method: 'PUT', body: { status } }),
// Self-service
getProfile: () => request<import('./types').Benefactor>('/benefactors/profile'),
updateProfile: (data: import('./types').BenefactorUpdateRequest) =>
request<import('./types').Benefactor>('/benefactors/profile', { method: 'PUT', body: data }),
};
// ==================== KindBox API ====================
export const kindBoxApi = {
// Admin endpoints
getAll: (params?: import('./types').PaginationParams) =>
request<import('./types').PaginatedResponse<import('./types').KindBox>>('/admins/kindboxes', {
params: params as Record<string, string | number | undefined>,
}),
getById: (id: number) =>
request<import('./types').KindBox>(`/admins/kindboxes/${id}`),
update: (id: number, data: Partial<import('./types').KindBox>) =>
request<import('./types').KindBox>(`/admins/kindboxes/update/${id}`, {
method: 'PUT',
body: data,
}),
enumerate: (id: number) =>
request<void>(`/admins/kindboxes/${id}/enumerate`, { method: 'PATCH' }),
assignReceiverAgent: (id: number, agentId: number) =>
request<void>(`/admins/kindboxes/${id}/assign-receiver-agent`, {
method: 'PATCH',
body: { receiverAgentID: agentId },
}),
// Benefactor endpoints
getMyKindBoxes: (params?: import('./types').PaginationParams) =>
request<import('./types').PaginatedResponse<import('./types').KindBox>>('/benefactors/kindboxes', {
params: params as Record<string, string | number | undefined>,
}),
getMyKindBox: (id: number) =>
request<import('./types').KindBox>(`/benefactors/kindboxes/${id}`),
registerEmptyingRequest: (id: number) =>
request<void>(`/benefactors/kindboxes/${id}/emptying-requests`, { method: 'PATCH' }),
// Agent endpoints
getReturnAwaiting: (params?: import('./types').PaginationParams) =>
request<import('./types').PaginatedResponse<import('./types').KindBox>>('/agents/kindboxes', {
params: params as Record<string, string | number | undefined>,
}),
getReturnAwaitingById: (id: number) =>
request<import('./types').KindBox>(`/agents/kindboxes/${id}`),
returnKindBox: (id: number) =>
request<void>(`/agents/kindboxes/${id}/return`, { method: 'PATCH' }),
};
// ==================== KindBoxReq API ====================
export const kindBoxReqApi = {
// Admin endpoints
getAll: (params?: import('./types').PaginationParams) =>
request<import('./types').PaginatedResponse<import('./types').KindBoxReq>>('/admins/kindboxreqs', {
params: params as Record<string, string | number | undefined>,
}),
getById: (id: number) =>
request<import('./types').KindBoxReq>(`/admins/kindboxreqs/${id}`),
create: (data: {
benefactorId: number;
kindBoxType: import('./types').KindBoxType;
countRequested: number;
description?: string;
deliverReferTimeId: number;
deliverReferDate: string;
deliverAddressId: number;
}) =>
request<import('./types').KindBoxReq>('/admins/kindboxreqs', { method: 'POST', body: data }),
update: (id: number, data: Partial<import('./types').KindBoxReq>) =>
request<import('./types').KindBoxReq>(`/admins/kindboxreqs/${id}`, {
method: 'PUT',
body: data,
}),
accept: (id: number) =>
request<void>(`/admins/kindboxreqs/${id}/accept-kind-box-req`, { method: 'PATCH' }),
reject: (id: number) =>
request<void>(`/admins/kindboxreqs/${id}/reject-kind-box-req`, { method: 'PATCH' }),
assignSenderAgent: (id: number, agentId: number) =>
request<void>(`/admins/kindboxreqs/${id}/assign-sender-agent`, {
method: 'PATCH',
body: { senderAgentID: agentId },
}),
// Benefactor endpoints
getMyRequests: (params?: import('./types').PaginationParams) =>
request<import('./types').PaginatedResponse<import('./types').KindBoxReq>>('/benefactors/kindboxreqs', {
params: params as Record<string, string | number | undefined>,
}),
getMyRequest: (id: number) =>
request<import('./types').KindBoxReq>(`/benefactors/kindboxreqs/${id}`),
createMyRequest: (data: {
kindBoxType: import('./types').KindBoxType;
countRequested: number;
description?: string;
deliverReferTimeId: number;
deliverReferDate: string;
deliverAddressId: number;
}) =>
request<import('./types').KindBoxReq>('/benefactors/kindboxreqs', { method: 'POST', body: data }),
updateMyRequest: (id: number, data: Partial<import('./types').KindBoxReq>) =>
request<import('./types').KindBoxReq>(`/benefactors/kindboxreqs/${id}`, {
method: 'PUT',
body: data,
}),
deleteMyRequest: (id: number) =>
request<void>(`/benefactors/kindboxreqs/${id}`, { method: 'DELETE' }),
// Agent endpoints
getDeliveryAwaiting: (params?: import('./types').PaginationParams) =>
request<import('./types').PaginatedResponse<import('./types').KindBoxReq>>(
'/agents/kindboxreqs/awaiting-delivery',
{ params: params as Record<string, string | number | undefined> }
),
getDeliveryAwaitingById: (id: number) =>
request<import('./types').KindBoxReq>(`/agents/kindboxreqs/awaiting-delivery/${id}`),
deliverKindBoxReq: (id: number) =>
request<void>(`/agents/kindboxreqs/${id}/deliver-kind-box-req`, { method: 'PATCH' }),
};
// ==================== Address API ====================
export const addressApi = {
getProvinces: () =>
request<import('./types').Province[]>('/benefactors/addresses/provinces'),
getCitiesByProvince: (provinceId: number) =>
request<import('./types').City[]>(`/benefactors/addresses/cities?provinceId=${provinceId}`),
getAll: (params?: import('./types').PaginationParams) =>
request<import('./types').PaginatedResponse<import('./types').AddressAggregated>>(
'/benefactors/addresses',
{ params: params as Record<string, string | number | undefined> }
),
getById: (id: number) =>
request<import('./types').AddressAggregated>(`/benefactors/addresses/${id}`),
create: (data: {
name: string;
address: string;
postalCode: string;
cityId: number;
provinceId: number;
lat?: number;
lon?: number;
}) =>
request<import('./types').Address>('/benefactors/addresses', { method: 'POST', body: data }),
update: (id: number, data: Partial<import('./types').Address>) =>
request<import('./types').Address>(`/benefactors/addresses/${id}`, {
method: 'PUT',
body: data,
}),
delete: (id: number) =>
request<void>(`/benefactors/addresses/${id}`, { method: 'DELETE' }),
};
// ==================== Refer Time API ====================
export const referTimeApi = {
getAll: (params?: import('./types').PaginationParams) =>
request<import('./types').PaginatedResponse<import('./types').ReferTime>>('/benefactors/refer-times', {
params: params as Record<string, string | number | undefined>,
}),
getById: (id: number) =>
request<import('./types').ReferTime>(`/benefactors/refer-times/${id}`),
// Admin
adminGetAll: (params?: import('./types').PaginationParams) =>
request<import('./types').PaginatedResponse<import('./types').ReferTime>>('/admins/refer-times', {
params: params as Record<string, string | number | undefined>,
}),
};
// ==================== Agent API ====================
export const agentApi = {
getAll: (params?: import('./types').PaginationParams) =>
request<import('./types').Admin[]>('/admins/agents', {
params: params as Record<string, string | number | undefined>,
}),
getById: (id: number) =>
request<import('./types').Admin>(`/admins/agents/${id}`),
};
// ==================== Product API ====================
export const productApi = {
getAll: (params?: import('./types').PaginationParams) =>
request<import('./types').PaginatedResponse<import('./types').Product>>('/products', {
params: params as Record<string, string | number | undefined>,
}),
getById: (id: number) =>
request<import('./types').Product>(`/products/${id}`),
};
// ==================== Campaign API ====================
export const campaignApi = {
getAll: (params?: import('./types').PaginationParams) =>
request<import('./types').PaginatedResponse<import('./types').Campaign>>('/campaigns', {
params: params as Record<string, string | number | undefined>,
}),
getById: (id: number) =>
request<import('./types').Campaign>(`/campaigns/${id}`),
create: (data: Partial<import('./types').Campaign>) =>
request<import('./types').Campaign>('/campaigns', { method: 'POST', body: data }),
};
// ==================== Order API ====================
export const orderApi = {
getAll: (params?: import('./types').PaginationParams) =>
request<import('./types').PaginatedResponse<import('./types').Order>>('/orders', {
params: params as Record<string, string | number | undefined>,
}),
getById: (id: number) =>
request<import('./types').Order>(`/orders/${id}`),
create: (data: {
items: { productId: number; quantity: number }[];
addressId: number;
shippingId: number;
paymentMethod: import('./types').PaymentMethodType;
}) =>
request<import('./types').Order>('/orders', { method: 'POST', body: data }),
};
// ==================== Wallet API ====================
export const walletApi = {
getWallet: () =>
request<import('./types').Wallet>('/wallet'),
getTransactions: (params?: import('./types').PaginationParams) =>
request<import('./types').PaginatedResponse<import('./types').Transaction>>('/wallet/transactions', {
params: params as Record<string, string | number | undefined>,
}),
};
// ==================== Payment API ====================
export const paymentApi = {
initiate: (data: {
amount: number;
payableType: import('./types').PayableType;
payableId: number;
methodId: number;
gatewayId: number;
currency: import('./types').Currency;
}) =>
request<{ paymentUrl: string; paymentId: number }>('/payments/initiate', {
method: 'POST',
body: data,
}),
verify: (paymentId: number) =>
request<import('./types').Payment>(`/payments/${paymentId}/verify`, { method: 'POST' }),
};
// ==================== Dashboard API ====================
export const dashboardApi = {
getStats: () =>
request<import('./types').DashboardStats>('/dashboard/stats'),
};
// Export the API base URL for use in other contexts
export { API_BASE_URL };
export default request;

447
src/lib/types.ts Normal file
View File

@ -0,0 +1,447 @@
// ==================== Entity Types ====================
export type Gender = 'male' | 'female';
export type UserRole = 'benefactor';
export type AdminRole = 'super-admin' | 'admin' | 'agent';
export type AdminStatus = 'active' | 'inactive';
export type BenefactorStatus = 'active' | 'inactive';
export type ReferTimeStatus = 'active' | 'inactive';
export type KindBoxType = 'on-table' | 'cylindrical' | 'stand-up';
export type KindBoxStatus =
| 'delivered'
| 'ready-to-return'
| 'assigned-receiver-agent'
| 'returned'
| 'enumerated';
export type KindBoxReqStatus =
| 'pending'
| 'accepted'
| 'assigned-sender-agent'
| 'rejected'
| 'delivered';
export type CartStatus = 'active' | 'expired' | 'checked_out';
export type CampaignStatus = 'draft' | 'active' | 'completed' | 'paused' | 'cancelled';
export type ProcessStatus =
| 'waiting-to-pay'
| 'processing'
| 'accepted'
| 'preparing'
| 'prepared'
| 'given-to-post'
| 'delivered'
| 'cancelled'
| 'system-cancellation';
export type PaymentStatus = 'paid' | 'unpaid';
export type PaymentTransactionStatus = 'Pending' | 'Success' | 'Failed' | 'Cancelled';
export type Currency = 'IRR' | 'USD';
export type WalletStatus = 'active' | 'frozen' | 'closed';
export type TransactionType = 'deposit' | 'withdraw' | 'refund' | 'donate';
export type NotificationType = 'email' | 'sms' | 'push';
export type PatientSex = 'unknown' | 'male' | 'female' | 'other';
export type PatientCaseStatus = 'open' | 'close' | 'inProgress';
export type PatientReferralSource = 'hospital' | 'community' | 'other';
export type DriverRole = 'driver';
// ==================== Entity Interfaces ====================
export interface Benefactor {
id: number;
firstName: string;
lastName: string;
phoneNumber: string;
description: string;
email: string;
gender: Gender;
birthDate: string;
role: UserRole;
status: BenefactorStatus;
}
export interface Admin {
id: number;
firstName: string;
lastName: string;
password?: string;
phoneNumber: string;
role: AdminRole;
description: string;
email: string;
gender: Gender;
status: AdminStatus;
}
export interface Address {
id: number;
postalCode: string;
address: string;
name: string;
lat: number;
lon: number;
cityId: number;
provinceId: number;
benefactorId: number;
}
export interface AddressAggregated {
address: Address;
province: Province;
city: City;
}
export interface Province {
id: number;
name: string;
}
export interface City {
id: number;
name: string;
provinceId: number;
}
export interface ReferTime {
id: number;
duration: string;
status: ReferTimeStatus;
}
export interface KindBox {
id: number;
kindBoxReqId: number;
benefactorId: number;
kindBoxType: KindBoxType;
amount: number;
serialNumber: string;
status: KindBoxStatus;
deliverReferTimeId: number;
deliverReferDate: string;
deliverAddressId: number;
senderAgentId: number;
deliveredAt: string;
returnReferTimeId: number;
returnReferDate: string;
returnAddressId: number;
receiverAgentId: number;
returnedAt: string;
}
export interface KindBoxReq {
id: number;
benefactorId: number;
kindBoxType: KindBoxType;
countRequested: number;
countAccepted: number;
description: string;
status: KindBoxReqStatus;
deliverReferTimeId: number;
deliverReferDate: string;
deliverAddressId: number;
senderAgentId: number;
deliveredAt: string;
}
export interface Product {
id: number;
name: string;
slug: string;
description: string;
price: number;
stock: number;
isActive: boolean;
features: string;
createdAt: string;
images?: ProductImage[];
}
export interface ProductImage {
id: number;
productId: number;
imagePath: string;
isPrimary: boolean;
}
export interface Cart {
id: number;
userId: number;
items: CartItem[];
status: CartStatus;
totalPrice: number;
expireAt: string;
createdAt: string;
updatedAt: string;
}
export interface CartItem {
id: number;
cartId: number;
productId: number;
userId: number;
quantity: number;
price: number;
name: string;
addedAt: string;
updatedAt: string;
}
export interface Order {
id: number;
userId: number;
totalAmount: number;
totalDiscount: number;
shippingId: number;
paymentMethod: PaymentMethodType;
processStatus: ProcessStatus;
paymentStatus: PaymentStatus;
addressId: number;
createdAt: string;
updatedAt: string;
items?: OrderItem[];
shipping?: Shipping;
}
export type PaymentMethodType = 'online' | 'wallet' | 'cart';
export interface OrderItem {
id: number;
productId: number;
price: number;
quantity: number;
priceWithDiscount: number;
orderId: number;
createdAt: string;
}
export interface Shipping {
id: number;
name: string;
price: number;
isActive: boolean;
}
export interface Payment {
id: number;
userId: number;
methodId: number;
gatewayId: number;
payableType: PayableType;
payableId: number;
totalAmount: number;
paidAmount: number;
currency: Currency;
status: PaymentTransactionStatus;
description: string;
createdAt: string;
updatedAt: string;
paidAt: string | null;
}
export type PayableType = 'Donate' | 'Order' | 'WaletCharge';
export interface PaymentGateway {
id: number;
name: string;
code: string;
isActive: boolean;
config: Record<string, unknown>;
createdAt: string;
updatedAt: string;
}
export interface PaymentMethod {
id: number;
name: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
export interface Wallet {
id: number;
userId: number;
balance: string;
currency: Currency;
status: WalletStatus;
updatedAt: string;
createdAt: string;
}
export interface Transaction {
id: number;
userId: number;
amount: string;
currency: Currency;
actionType: TransactionType;
timestamp: string;
idempotencyKey: string;
createdAt: string;
}
export interface Campaign {
id: number;
title: string;
description: string;
link: string;
slogan: string;
goalAmount: number;
raisedAmount: number;
status: CampaignStatus;
createdAt: string;
deadlineAt: string | null;
creatorId: number;
}
export interface Donation {
id: number;
campaignId: number;
userId?: number;
sourceType: string;
sourceName: string;
referralCode: string;
link: string;
clicks: number;
conversions: number;
donationsTotal: number;
createdAt: string;
}
export interface Driver {
id: number;
firstName: string;
lastName: string;
phoneNumber: string;
nationalCode: string;
licenseNumber: string;
birthDate: string;
}
export interface Patient {
id: number;
firstName: string;
lastName: string;
dateOfBirth: string;
sex: PatientSex;
phone: string;
address: PatientAddress;
caseStatus: PatientCaseStatus;
referralSource: PatientReferralSource;
assignedStaffId: number;
startDate: string;
endDate: string;
}
export interface PatientAddress {
id: number;
postalCode: string;
address: string;
name: string;
lat: number;
lon: number;
cityId: number;
provinceId: number;
}
export interface Staff {
id: number;
firstName: string;
lastName: string;
phoneNumber: string;
email: string;
role: string;
isActive: boolean;
createdAt: string;
}
export interface Notification {
id: number;
type: NotificationType;
recipient: string;
body: string;
status: string;
}
export interface Permission {
title: string;
description?: string;
}
export interface Authenticable {
id: number;
role: string;
}
// ==================== API Types ====================
export interface ApiResponse<T> {
data: T;
message?: string;
success: boolean;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface LoginRequest {
phoneNumber: string;
password?: string;
}
export interface LoginResponse {
token: string;
refreshToken: string;
user: Benefactor | Admin;
}
export interface SendOtpRequest {
phoneNumber: string;
}
export interface VerifyOtpRequest {
phoneNumber: string;
code: string;
}
export interface RegisterRequest {
firstName: string;
lastName: string;
phoneNumber: string;
email?: string;
gender: Gender;
password?: string;
}
export interface BenefactorUpdateRequest {
firstName?: string;
lastName?: string;
description?: string;
email?: string;
gender?: Gender;
birthDate?: string;
}
export interface PaginationParams {
page?: number;
limit?: number;
sort?: string;
order?: 'asc' | 'desc';
search?: string;
}
export interface DashboardStats {
totalBenefactors: number;
totalKindBoxes: number;
totalKindBoxRequests: number;
totalCampaigns: number;
totalDonations: number;
totalOrders: number;
totalRevenue: number;
activeBenefactors: number;
pendingRequests: number;
returnedKindBoxes: number;
}

123
src/lib/utils.ts Normal file
View File

@ -0,0 +1,123 @@
export function formatCurrency(amount: number, currency: string = 'IRR'): string {
if (currency === 'IRR') {
return `${amount.toLocaleString('fa-IR')} ریال`;
}
return `$${amount.toFixed(2)}`;
}
export function formatDate(dateString: string): string {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleDateString('fa-IR', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
export function formatDateTime(dateString: string): string {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleDateString('fa-IR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
export function getStatusColor(status: string): string {
const colorMap: Record<string, string> = {
active: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
inactive: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400',
pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400',
accepted: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400',
rejected: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
delivered: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
'assigned-sender-agent': 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400',
'assigned-receiver-agent': 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-400',
'ready-to-return': 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400',
returned: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400',
enumerated: 'bg-teal-100 text-teal-800 dark:bg-teal-900/30 dark:text-teal-400',
draft: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400',
completed: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
paused: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400',
cancelled: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
paid: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
unpaid: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
'waiting-to-pay': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400',
processing: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400',
preparing: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400',
prepared: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-400',
'given-to-post': 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-400',
'system-cancellation': 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
Success: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
Failed: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
frozen: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400',
closed: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400',
};
return colorMap[status] || 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
}
export function getStatusLabel(status: string): string {
const labelMap: Record<string, string> = {
'on-table': 'On Table',
cylindrical: 'Cylindrical',
'stand-up': 'Stand Up',
'assigned-sender-agent': 'Assigned Sender',
'assigned-receiver-agent': 'Assigned Receiver',
'ready-to-return': 'Ready to Return',
'given-to-post': 'Given to Post',
'system-cancellation': 'System Cancellation',
'waiting-to-pay': 'Waiting to Pay',
};
return labelMap[status] || status.charAt(0).toUpperCase() + status.slice(1).replace(/-/g, ' ');
}
export function getKindBoxTypeIcon(type: string): string {
const icons: Record<string, string> = {
'on-table': '📦',
cylindrical: '🥫',
'stand-up': '🗄️',
};
return icons[type] || '📦';
}
export function cn(...classes: (string | undefined | null | false)[]): string {
return classes.filter(Boolean).join(' ');
}
export function truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
}
export function generatePagination(currentPage: number, totalPages: number): (number | '...')[] {
if (totalPages <= 7) {
return Array.from({ length: totalPages }, (_, i) => i + 1);
}
const pages: (number | '...')[] = [];
pages.push(1);
if (currentPage > 3) {
pages.push('...');
}
const start = Math.max(2, currentPage - 1);
const end = Math.min(totalPages - 1, currentPage + 1);
for (let i = start; i <= end; i++) {
pages.push(i);
}
if (currentPage < totalPages - 2) {
pages.push('...');
}
pages.push(totalPages);
return pages;
}

34
tsconfig.json Normal file
View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}