Initial commit from Create Next App
This commit is contained in:
commit
41879b49c7
|
|
@ -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
|
||||
|
|
@ -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 -->
|
||||
|
|
@ -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.
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
Loading…
Reference in New Issue