This commit is contained in:
mohan 2025-12-11 16:13:47 +05:30
commit 26996e1c77
105 changed files with 21522 additions and 0 deletions

31
.env.development Normal file
View File

@ -0,0 +1,31 @@
ENV_BASE_URL='/'
ENV_COMMON_BASE_URL='http://192.168.1.16:3000/'
# ENV_API_URL='http://192.168.1.25:8001'
# ENV_API_URL='http://api.pozo.co.in/Retailapi_UnderTest'
# ENV_API_URL_COMMON='http://api.pozo.co.in/Pozocommonapi_UnderTest'
# ENV_API_URL='http://192.168.1.37:8010'
# ENV_API_URL_COMMON='http://192.168.1.37'
ENV_API_URL='http://192.168.1.37:8012'
ENV_API_URL_COMMON='http://192.168.1.37:8013'
ENV_API_URL_TOKEN='http://192.168.1.37:8001'
# ENV_API_URL='https://api.pozo.app/pozo-retail-api'
# ENV_API_URL_COMMON='https://api.pozo.app/pozo-common-api'
# ENV_API_URL_TOKEN='https://api.pozo.app/JwtToken'
# ENV_API_URL='https://www.pozo.dev/pozo-retail-api'
# ENV_API_URL_COMMON='https://www.pozo.dev/pozo-common-api'
# ENV_API_URL_TOKEN='https://www.pozo.dev/JwtToken'
ENV_IMAGE_UPLOAD_API_URL="https://api.pozo.app/pozo-common-image-api/"
ENV_SECRET_KEY="3a939a2eed2cca4b64cd47f51b23b135b2b9b765273f695d47682ea803c8429d"
ENV_URL_SECRET_KEY="f2ef79b8b2d4fc1cc569726a5a753552257b4ff2872d6a71cf3119bce3f07c1b"
ENV_MAIN_BASE_URL='http://192.168.1.22:3015/'
ENV_SIGNALR_SERVER_URL='https://www.pozo.dev/'
ENV_IFSC_API_URL = "https://ifsc.razorpay.com"
ENV_PAYMENT_DEVICE_URL='https://pozo.dev/PaymentDevice/PaymentDevice/api/upload-transaction'
ENV_PAYMENT_STATUS_GET_URL='https://pozo.dev/PaymentDevice/PaymentDevice/api/get-transaction-status'
ENV_ANDROID_PRINTER= 'https://pozo.app/downloads/resources/POZO%20Printer.apk'
ENV_ANDROID_KIOSK= 'https://pozo.app/downloads/resources/POZO%20KIOSK.apk'
ENV_ANDROID_HANDHELD= 'https://pozo.dev/downloads/resources/Handheld.apk'
ENV_ANDROID_BILLING= 'https://pozo.dev/downloads/resources/PozoBillingApp.apk'
ENV_EMAIL_API='http://192.168.1.37:8014'
ENV_SESSION_CODE="sRhok0"

22
.env.production Normal file
View File

@ -0,0 +1,22 @@
ENV_BASE_URL='/apps/retail/'
ENV_COMMON_BASE_URL='https://pozo.dev'
ENV_API_URL='https://www.pozo.dev/pozo-retail-api'
ENV_API_URL_COMMON='https://www.pozo.dev/pozo-common-api'
ENV_API_URL_TOKEN='https://www.pozo.dev/JwtToken'
ENV_IMAGE_UPLOAD_API_URL="https://www.pozo.dev/pozo-common-image-api"
ENV_SECRET_KEY="3a939a2eed2cca4b64cd47f51b23b135b2b9b765273f695d47682ea803c8429d"
ENV_URL_SECRET_KEY="f2ef79b8b2d4fc1cc569726a5a753552257b4ff2872d6a71cf3119bce3f07c1b"
ENV_MAIN_BASE_URL='https://pozo.dev/apps/retail/'
ENV_SIGNALR_SERVER_URL='https://www.pozo.dev/'
ENV_CUSTOM_PAYMENT_URL='https://pozo.app/CustomPaymentGateway/CustomPaymentGateway'
ENV_IFSC_API_URL = "https://ifsc.razorpay.com"
ENV_PAYMENT_DEVICE_URL='https://pozo.dev/PaymentDevice/PaymentDevice/api/upload-transaction'
ENV_PAYMENT_STATUS_GET_URL='https://pozo.dev/PaymentDevice/PaymentDevice/api/get-transaction-status'
# ENV_PAYMENT_DEVICE_URL='https://www.plutuscloudserviceuat.in:8201/API/CloudBasedIntegration/V1/UploadBilledTransaction'
# ENV_PAYMENT_STATUS_GET_URL='https://www.plutuscloudserviceuat.in:8201/API/CloudBasedIntegration/V1/GetCloudBasedTxnStatus'
ENV_ANDROID_PRINTER= 'https://pozo.dev/downloads/resources/POZO%20Printer.apk'
ENV_ANDROID_KIOSK= 'https://pozo.dev/downloads/resources/POZO%20KIOSK.apk'
ENV_ANDROID_HANDHELD= 'https://pozo.dev/downloads/resources/Handheld.apk'
ENV_ANDROID_BILLING= 'https://pozo.dev/downloads/resources/PozoApp.apk'
ENV_EMAIL_API='https://www.pozo.dev/pozo-sms-email-template-api'

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

699
API_DOCUMENTATION.md Normal file
View File

@ -0,0 +1,699 @@
# Digital Ledger API Documentation
## Overview
This document outlines all the APIs required for the Digital Ledger Mobile App backend implementation. The APIs are organized by feature modules.
## Base URL
```
https://api.pozo-accounts.com/v1
```
## Authentication
All APIs require Bearer token authentication:
```
Authorization: Bearer <jwt_token>
```
---
## 1. USER MANAGEMENT APIs
### 1.1 User Profile
```http
GET /user/profile
```
**Response:**
```json
{
"success": true,
"data": {
"id": "user_123",
"name": "Ram Kumar",
"email": "ram@business.com",
"phone": "+91 98765 43210",
"avatar": "RK",
"businessName": "Ram's Store",
"role": "Business Owner",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-12-03T14:20:00Z"
}
}
```
### 1.2 Update User Profile
```http
PUT /user/profile
```
**Request Body:**
```json
{
"name": "Ram Kumar",
"email": "ram@business.com",
"phone": "+91 98765 43210",
"businessName": "Ram's Store"
}
```
---
## 2. DASHBOARD APIs
### 2.1 Dashboard Summary
```http
GET /dashboard/summary
```
**Response:**
```json
{
"success": true,
"data": {
"transactionSummary": {
"pendingPayment": {
"count": 125,
"change": "+8%",
"positive": true
},
"received": {
"count": 456,
"change": "+12%",
"positive": true
},
"paidOut": {
"count": 234,
"change": "+5%",
"positive": true
},
"completed": {
"count": 892,
"change": "+15%",
"positive": true
},
"cancelled": {
"count": 23,
"change": "-3%",
"positive": false
}
},
"financialOverview": {
"totalIncome": 328000,
"totalExpense": 231000,
"netBalance": 97000,
"monthlyData": [
{
"month": "Jan",
"income": 45000,
"expense": 32000
}
]
},
"quickStats": {
"totalCustomers": 248,
"customerGrowth": "+12%",
"activeTransactions": 89,
"pendingApproval": 23,
"netBalance": 97000,
"cashflowStatus": "positive"
}
}
}
```
---
## 3. CONTACT MANAGEMENT APIs (Customers & Suppliers)
### 3.1 Get All Contacts
```http
GET /contacts?page=1&limit=10&search=&balanceType=all&contactType=all
```
**Query Parameters:**
- `page`: Page number (default: 1)
- `limit`: Items per page (default: 10)
- `search`: Search by name/phone
- `balanceType`: all|credit|debit
- `contactType`: all|C|S (C=Customer, S=Supplier)
**Response:**
```json
{
"success": true,
"data": {
"contacts": [
{
"id": "contact_001",
"name": "Deepa",
"phone": "98755 43210",
"email": "deepa@example.com",
"address": "123 Main St",
"avatar": "D",
"balance": 800,
"balanceType": "credit",
"contactType": "C",
"status": "Active",
"lastTransaction": "Payment received",
"lastTransactionDate": "2024-12-03T14:30:00Z",
"totalTransactions": 24,
"createdAt": "2024-01-15T10:30:00Z"
}
],
"pagination": {
"currentPage": 1,
"totalPages": 5,
"totalItems": 50,
"hasNext": true,
"hasPrev": false
},
"summary": {
"customers": {
"total": 30,
"totalReceivable": 82000,
"totalPayable": 16700,
"active": 28
},
"suppliers": {
"total": 20,
"totalReceivable": 5000,
"totalPayable": 62500,
"active": 18
}
}
}
}
```
### 3.2 Create Contact
```http
POST /contacts
```
**Request Body:**
```json
{
"name": "John Doe",
"phone": "+91 98765 43210",
"email": "john@example.com",
"address": "123 Main St, City",
"contactType": "C",
"initialBalance": 0,
"balanceType": "credit"
}
```
### 3.3 Get Contact Details
```http
GET /contacts/{contactId}
```
**Response:**
```json
{
"success": true,
"data": {
"id": "contact_001",
"name": "Deepa",
"phone": "98755 43210",
"email": "deepa@example.com",
"address": "123 Main St",
"avatar": "D",
"balance": 800,
"balanceType": "credit",
"contactType": "C",
"status": "Active",
"createdAt": "2024-01-15T10:30:00Z",
"transactions": [
{
"id": "txn_001",
"date": "2024-12-02T00:00:00Z",
"type": "credit",
"amount": 500,
"note": "Invoice payment",
"paymentMethod": "UPI",
"status": "completed"
}
]
}
}
```
### 3.4 Update Contact
```http
PUT /contacts/{contactId}
```
**Request Body:**
```json
{
"name": "John Doe Updated",
"phone": "+91 98765 43210",
"email": "john.updated@example.com",
"address": "456 New St, City"
}
```
### 3.5 Delete Contact
```http
DELETE /contacts/{contactId}
```
---
## 4. TRANSACTION MANAGEMENT APIs
### 4.1 Get All Transactions
```http
GET /transactions?page=1&limit=10&type=all&status=all&dateFrom=&dateTo=
```
**Response:**
```json
{
"success": true,
"data": {
"transactions": [
{
"id": "txn_001",
"contactId": "contact_001",
"contactName": "Rajesh Kumar",
"contactAvatar": "RK",
"contactType": "C",
"category": "Payment",
"type": "Credit",
"amount": 45000,
"date": "2024-05-30T00:00:00Z",
"status": "Completed",
"paymentMethod": "UPI",
"note": "Invoice payment"
}
],
"pagination": {
"currentPage": 1,
"totalPages": 10,
"totalItems": 100
},
"summary": {
"pendingPayments": 34000,
"receivedPayments": 550870,
"statusCounts": {
"waitingPayment": 435,
"onProcess": 234,
"onDelivery": 456,
"completed": 675,
"cancelled": 675
}
}
}
}
```
### 4.2 Create Transaction
```http
POST /transactions
```
**Request Body:**
```json
{
"contactId": "contact_001",
"type": "credit",
"amount": 1000,
"note": "Payment received",
"paymentMethod": "Cash",
"date": "2024-12-03T00:00:00Z",
"category": "Payment"
}
```
### 4.3 Update Transaction
```http
PUT /transactions/{transactionId}
```
### 4.4 Delete Transaction
```http
DELETE /transactions/{transactionId}
```
---
## 5. CASHBOOK APIs
### 5.1 Get Cashbook Entries
```http
GET /cashbook?page=1&limit=10&type=all&dateFrom=&dateTo=
```
**Response:**
```json
{
"success": true,
"data": {
"entries": [
{
"id": "cb_001",
"time": "10:22 AM",
"date": "2024-12-03T00:00:00Z",
"type": "out",
"paymentType": "ONLINE",
"description": "Office Supplies Purchase",
"category": "Expenses",
"amount": 1200,
"attachments": []
}
],
"summary": {
"totalIn": 13500,
"totalOut": 1550,
"netBalance": 11950,
"totalTransactions": 4
},
"todaySummary": {
"todayIn": 5000,
"todayOut": 1200,
"netToday": 3800
}
}
}
```
### 5.2 Create Cashbook Entry
```http
POST /cashbook
```
**Request Body:**
```json
{
"type": "in",
"amount": 5000,
"description": "Sales Revenue",
"category": "Income",
"paymentType": "CASH",
"date": "2024-12-03T00:00:00Z",
"attachments": ["file_url_1", "file_url_2"]
}
```
### 5.3 Update Cashbook Entry
```http
PUT /cashbook/{entryId}
```
### 5.4 Delete Cashbook Entry
```http
DELETE /cashbook/{entryId}
```
---
## 6. EXPENSE MANAGEMENT APIs
### 6.1 Get All Expenses
```http
GET /expenses?page=1&limit=10&category=&status=&dateFrom=&dateTo=
```
**Response:**
```json
{
"success": true,
"data": {
"expenses": [
{
"id": "exp_001",
"title": "Inventory Restock",
"vendor": "Fresh Farm Wholesale",
"category": "Inventory",
"date": "2024-12-03T00:00:00Z",
"paymentType": "UPI",
"amount": 18500,
"status": "Settled",
"isFixedCommitment": false,
"attachments": []
}
],
"summary": {
"totalSpent": 65000,
"recurringExpenses": 37400,
"pendingAmount": 37650,
"totalExpenses": 5
},
"categories": ["Inventory", "Fixed Cost", "Logistics", "Utilities", "Supplies"]
}
}
```
### 6.2 Create Expense
```http
POST /expenses
```
**Request Body:**
```json
{
"title": "Office Rent",
"vendor": "Property Owner",
"category": "Fixed Cost",
"amount": 25000,
"paymentType": "Bank Transfer",
"date": "2024-12-01T00:00:00Z",
"status": "Scheduled",
"isFixedCommitment": true,
"attachments": []
}
```
### 6.3 Update Expense
```http
PUT /expenses/{expenseId}
```
### 6.4 Delete Expense
```http
DELETE /expenses/{expenseId}
```
---
## 7. LEDGER APIs
### 7.1 Get Ledger Summary
```http
GET /ledger/summary
```
**Response:**
```json
{
"success": true,
"data": {
"customers": {
"totalReceivable": 4800,
"totalPayable": 1700,
"count": 6
},
"suppliers": {
"totalReceivable": 5000,
"totalPayable": 62500,
"count": 5
},
"netBalance": -54400,
"recentActivity": [
{
"id": "act_001",
"type": "payment_received",
"description": "₹800 from Deepa",
"time": "2 hours ago",
"status": "completed"
}
]
}
}
```
---
## 8. REPORTS APIs
### 8.1 Get Cashbook Report
```http
GET /reports/cashbook?dateFrom=2024-11-01&dateTo=2024-12-31
```
**Response:**
```json
{
"success": true,
"data": {
"period": "1 Nov - 31 Dec 2024",
"summary": {
"totalIn": 125000,
"totalOut": 87000,
"netBalance": 38000,
"totalTransactions": 156
},
"chartData": [
{
"name": "Week 1",
"moneyIn": 25000,
"moneyOut": 18000
}
],
"categoryBreakdown": [
{
"category": "Income",
"amount": 125000,
"percentage": 58.9
}
]
}
}
```
### 8.2 Get Transaction Report
```http
GET /reports/transactions?dateFrom=2024-11-01&dateTo=2024-12-31
```
**Response:**
```json
{
"success": true,
"data": {
"period": "1 Nov - 31 Dec 2024",
"customers": {
"totalReceivable": 59550,
"totalReceived": 13500,
"pending": 46050,
"count": 6
},
"suppliers": {
"totalPayable": 9800,
"totalPaid": 1550,
"pending": 8250,
"count": 5
},
"chartData": [
{
"name": "Customers",
"receivable": 59550,
"received": 13500,
"pending": 46050
}
]
}
}
```
---
## 9. FILE UPLOAD APIs
### 9.1 Upload File
```http
POST /files/upload
```
**Request:** Multipart form data
**Response:**
```json
{
"success": true,
"data": {
"fileId": "file_123",
"fileName": "receipt.jpg",
"fileUrl": "https://cdn.pozo-accounts.com/files/file_123.jpg",
"fileSize": 2048,
"mimeType": "image/jpeg"
}
}
```
---
## 10. NOTIFICATION APIs
### 10.1 Get Notifications
```http
GET /notifications?page=1&limit=10&unreadOnly=false
```
**Response:**
```json
{
"success": true,
"data": {
"notifications": [
{
"id": "notif_001",
"title": "Payment Received",
"message": "₹800 received from Deepa",
"type": "payment",
"isRead": false,
"createdAt": "2024-12-03T14:30:00Z"
}
],
"pagination": {
"currentPage": 1,
"totalPages": 3,
"totalItems": 25,
"unreadCount": 5
}
}
}
```
### 10.2 Mark as Read
```http
PUT /notifications/{notificationId}/read
```
---
## Error Response Format
All APIs return errors in this format:
```json
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": {
"field": "email",
"message": "Invalid email format"
}
}
}
```
## API Summary
**Total APIs: 35+ endpoints**
- User Management: 2 APIs
- Dashboard: 1 API
- Contact Management (Customers & Suppliers): 5 APIs
- Transaction Management: 4 APIs
- Cashbook: 4 APIs
- Expense Management: 4 APIs
- Ledger: 1 API
- Reports: 2 APIs
- File Upload: 1 API
- Notifications: 2 APIs
## Key Benefits of Unified Contact API
- **Simplified Backend**: Single table/model for customers and suppliers
- **Consistent Data Structure**: Same fields and operations for both types
- **Easier Maintenance**: One set of CRUD operations instead of two
- **Flexible Filtering**: Can filter by contactType ('C'/'S') or get all contacts
- **Unified Search**: Search across both customers and suppliers simultaneously
## Common HTTP Status Codes
- `200` - Success
- `201` - Created
- `400` - Bad Request
- `401` - Unauthorized
- `403` - Forbidden
- `404` - Not Found
- `422` - Validation Error
- `500` - Internal Server Error
## Rate Limiting
- 1000 requests per hour per user
- 100 requests per minute per endpoint
## Pagination
All list APIs support pagination with these query parameters:
- `page`: Page number (default: 1)
- `limit`: Items per page (default: 10, max: 100)
## Date Format
All dates should be in ISO 8601 format: `2024-12-03T14:30:00Z`
## File Upload Constraints
- Maximum file size: 10MB
- Supported formats: JPG, PNG, PDF
- Files are automatically compressed and optimized

11
README.md Normal file
View File

@ -0,0 +1,11 @@
# Digital Ledger Mobile App
This is a code bundle for Digital Ledger Mobile App. The original project is available at https://www.figma.com/design/nkXOxoh7n8rP5OO2xJijz1/Digital-Ledger-Mobile-App.
## Running the code
Run `npm i` to install the dependencies.
Run `npm run dev` to start the development server.

20
index.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes" />
<title>PozoBook - Digital Ledger</title>
<link rel="shortcut icon" href="./public/fav-icon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

5044
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

67
package.json Normal file
View File

@ -0,0 +1,67 @@
{
"name": "Digital Ledger Mobile App",
"version": "0.1.0",
"private": true,
"dependencies": {
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-aspect-ratio": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-context-menu": "^2.2.6",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-menubar": "^1.1.6",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@reduxjs/toolkit": "^2.11.0",
"@tailwindcss/vite": "^4.1.17",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "*",
"cmdk": "^1.1.1",
"crypto-js": "^4.2.0",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.487.0",
"next-themes": "^0.4.6",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-device-detect": "^2.2.3",
"react-dom": "^18.3.1",
"react-hook-form": "^7.55.0",
"react-redux": "^9.2.0",
"react-resizable-panels": "^2.1.7",
"react-router-dom": "^7.10.0",
"recharts": "^2.15.2",
"sonner": "^2.0.3",
"tailwind-merge": "*",
"tailwindcss": "^4.1.17",
"vaul": "^1.1.2"
},
"devDependencies": {
"@types/node": "^20.10.0",
"@vitejs/plugin-react-swc": "^3.10.2",
"sass": "^1.94.2",
"vite": "^6.4.1"
},
"scripts": {
"dev": "vite",
"build": "vite build"
}
}

BIN
public.zip Normal file

Binary file not shown.

BIN
public/fav-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

8
src/App.jsx Normal file
View File

@ -0,0 +1,8 @@
import AppRoutes from './routes/AppRoutes';
export default function App() {
return (
<AppRoutes />
);
}

3
src/Attributions.md Normal file
View File

@ -0,0 +1,3 @@
This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md).
This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license).

18
src/check.jsx Normal file
View File

@ -0,0 +1,18 @@
import { useState } from 'react';
import Sidebar from './components/Sidebar';
import AppRoutes from './routes/AppRoutes';
export default function App() {
const [currentView, setCurrentView] = useState('dashboard');
return (
<div className="flex min-h-screen bg-[#F5F5F5]">
<Sidebar currentView={currentView} onNavigate={setCurrentView} />
<main className="flex-1 ml-64">
<div className="p-8">
<AppRoutes />
</div>
</main>
</div>
);
}

17
src/components/Avatar.jsx Normal file
View File

@ -0,0 +1,17 @@
const Avatar = ({ children, width, height, className, background, borderRadius }) => {
return (
<div
className={className}
style={{
width,
height,
background,
borderRadius,
}}
>
{children}
</div>
);
};
export default Avatar;

View File

@ -0,0 +1,87 @@
import { ChevronRight, Bell, HelpCircle } from 'lucide-react';
import './BreadCrumb.scss';
import { selectUser } from '../redux/slices/userSlice';
import { useSelector } from 'react-redux';
import { useNavigate, useLocation } from 'react-router-dom';
import ProfileDropdown from './ProfileDropdown';
import { useEffect, useState } from 'react';
const BreadCrumb = ({ items, showUser = false }) => {
const user = useSelector(selectUser);
const navigate = useNavigate();
const location = useLocation();
const handleNavigate = (path) => {
if (location.pathname !== path) {
navigate(path);
}
};
const [isMobile, setIsMobile] = useState(window.innerWidth < 576);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 576);
};
window.addEventListener("resize", handleResize);
// run once on mount
handleResize();
return () => window.removeEventListener("resize", handleResize);
}, []);
const activeLabel = items.find(item => item.active)?.label || '';
return (
<div className='breadcrumb'>
<div className='breadcrumb__mobile-header'>
<div className="breadcrumb__mobile-spacer"></div>
<div className="breadcrumb__mobile-title">{activeLabel}</div>
<div className="breadcrumb__actions">
<button
className="breadcrumb__action-button"
aria-label="Help"
>
<HelpCircle className="breadcrumb__action-icon" />
</button>
<button
className="breadcrumb__action-button breadcrumb__action-button--notification"
aria-label="Notifications"
>
<Bell className="breadcrumb__action-icon" />
<span className="breadcrumb__notification-badge" />
</button>
</div>
</div>
<div className='breadcrumb__divider' />
<div className="breadcrumb__content">
<nav className='breadcrumb__navigation' aria-label="Breadcrumb">
{items.map((item, index) => (
<div key={index} className="breadcrumb__nav-segment">
<div
className={`breadcrumb__item ${item.active ? 'breadcrumb__item--active' : ''}`}
onClick={() => handleNavigate(item.path)}
style={{ cursor: location.pathname !== item.path ? 'pointer' : 'default' }}
>
{item.icon}
<span className="breadcrumb__item-label">{item.label}</span>
</div>
{index < items.length - 1 && (
<ChevronRight className="breadcrumb__separator" />
)}
</div>
))}
</nav>
{showUser && <ProfileDropdown top={isMobile ? 'top-27.5' : false} />}
</div>
</div>
);
};
export default BreadCrumb;

View File

@ -0,0 +1,180 @@
.breadcrumb {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
justify-content: space-between;
margin-bottom: 1.5rem;
background: white;
border-radius: 0.75rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
border: 1px solid #E5E7EB;
width: 100%;
}
// Mobile header (hidden on desktop)
.breadcrumb__mobile-header {
display: none;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 10px 10px 0 10px;
@media (max-width: 576px) {
display: flex;
}
@media (max-width: 400px) {
.breadcrumb__mobile-spacer {
display: none !important;
}
.breadcrumb__mobile-title{
text-align: left;
}
}
}
.breadcrumb__mobile-spacer {
display: block;
width: 25%;
}
.breadcrumb__mobile-title {
width: 50%;
text-align: center;
font-size: 16px;
font-weight: 600;
color: #3b3b3b;
}
.breadcrumb__actions {
display: none;
width: 25%;
justify-content: flex-end;
@media (max-width: 576px) {
display: flex;
}
}
.breadcrumb__action-button {
padding: 0.5rem;
border-radius: 0.75rem;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
cursor: pointer;
&:hover {
background-color: #F3F4F6;
}
&--notification {
position: relative;
}
}
.breadcrumb__action-icon {
width: 1.25rem;
height: 1.25rem;
color: #6B7280;
}
.breadcrumb__notification-badge {
position: absolute;
top: 0.25rem;
right: 0.5rem;
width: 0.5rem;
height: 0.5rem;
background-color: #FF6B00;
border-radius: 50%;
}
// Divider line
.breadcrumb__divider {
display: none;
width: 100%;
border-bottom: 2px solid #d6d6d6;
@media (max-width: 576px) {
display: block;
}
}
// Main content wrapper
.breadcrumb__content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 10px;
@media (max-width: 576px) {
padding: 0 10px 10px 10px;
}
}
// Navigation container
.breadcrumb__navigation {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.breadcrumb__nav-segment {
display: flex;
align-items: center;
gap: 0.5rem;
}
// Individual breadcrumb item
.breadcrumb__item {
display: flex;
align-items: center;
gap: 0.5rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: #6B7280;
transition: all 0.2s;
svg {
width: 1rem;
height: 1rem;
display: flex;
}
&--active {
color: #FF6B00;
.breadcrumb__item-label {
font-weight: 600;
}
}
}
.breadcrumb__item-label {
display: none;
@media (min-width: 576px) {
display: block;
}
}
// Chevron separator
.breadcrumb__separator {
width: 1rem;
height: 1rem;
color: #D1D5DB;
}
// Responsive adjustments
@media (max-width: 992px) {
.breadcrumb {
overflow-x: auto;
}
}

View File

@ -0,0 +1,549 @@
import { Bell, HelpCircle, ChevronDown, TrendingUp, TrendingDown, Download, Clock, CheckCircle, XCircle, Send, ArrowDownLeft, Receipt, Users, Plus, MessageSquare, Filter, Search, Phone, ArrowUpRight, History, ChevronRight, IndianRupee } from 'lucide-react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
export default function Dashboard({ user }) {
// Transaction Status Cards (Original Design)
const statusCards = [
{
label: 'Pending Payment',
count: 125,
change: '+8%',
positive: true,
color: 'bg-orange-50',
iconBg: 'bg-orange-100',
iconColor: 'text-[#FF6B00]',
icon: Clock,
borderColor: 'border-orange-200'
},
{
label: 'Received',
count: 456,
change: '+12%',
positive: true,
color: 'bg-green-50',
iconBg: 'bg-green-100',
iconColor: 'text-green-600',
icon: ArrowDownLeft,
borderColor: 'border-green-200'
},
{
label: 'Paid Out',
count: 234,
change: '+5%',
positive: true,
color: 'bg-blue-50',
iconBg: 'bg-blue-100',
iconColor: 'text-blue-600',
icon: Send,
borderColor: 'border-blue-200'
},
{
label: 'Completed',
count: 892,
change: '+15%',
positive: true,
color: 'bg-teal-50',
iconBg: 'bg-teal-100',
iconColor: 'text-teal-600',
icon: CheckCircle,
borderColor: 'border-teal-200'
},
{
label: 'Cancelled',
count: 23,
change: '-3%',
positive: false,
color: 'bg-red-50',
iconBg: 'bg-red-100',
iconColor: 'text-red-600',
icon: XCircle,
borderColor: 'border-red-200'
},
];
// Customer Data
const customers = [
{
id: 1,
name: 'Deepa',
phone: '98755 43210',
avatar: 'D',
balance: 800,
type: 'credit',
lastTransaction: 'Payment received',
lastDate: 'Today, 2:30 PM',
color: 'from-green-400 to-green-500',
bgColor: 'bg-green-50',
textColor: 'text-green-600'
},
{
id: 2,
name: 'Rakesh',
phone: '98765 12345',
avatar: 'R',
balance: 500,
type: 'debit',
lastTransaction: 'Outstanding payment',
lastDate: 'Yesterday, 5:00 PM',
color: 'from-red-400 to-red-500',
bgColor: 'bg-red-50',
textColor: 'text-red-600'
},
{
id: 3,
name: 'Sunil',
phone: '98755 67890',
avatar: 'S',
balance: 1200,
type: 'debit',
lastTransaction: 'Bought supplies',
lastDate: '2 days ago',
color: 'from-orange-400 to-orange-500',
bgColor: 'bg-orange-50',
textColor: 'text-orange-600'
},
{
id: 4,
name: 'Priya',
phone: '98755 11111',
avatar: 'P',
balance: 2500,
type: 'credit',
lastTransaction: 'Invoice paid',
lastDate: '3 days ago',
color: 'from-blue-400 to-blue-500',
bgColor: 'bg-blue-50',
textColor: 'text-blue-600'
},
{
id: 5,
name: 'Karthik',
phone: '98755 22222',
avatar: 'K',
balance: 1500,
type: 'credit',
lastTransaction: 'Payment sent',
lastDate: '4 days ago',
color: 'from-purple-400 to-purple-500',
bgColor: 'bg-purple-50',
textColor: 'text-purple-600'
},
{
id: 6,
name: 'Anjali',
phone: '98755 33333',
avatar: 'A',
balance: 350,
type: 'debit',
lastTransaction: 'Pending payment',
lastDate: '5 days ago',
color: 'from-pink-400 to-pink-500',
bgColor: 'bg-pink-50',
textColor: 'text-pink-600'
},
];
const totalReceivable = customers.filter(c => c.type === 'credit').reduce((sum, c) => sum + c.balance, 0);
const totalPayable = customers.filter(c => c.type === 'debit').reduce((sum, c) => sum + c.balance, 0);
const netBalance = totalReceivable - totalPayable;
const chartData = [
{ month: 'Jan', income: 45000, expense: 32000 },
{ month: 'Feb', income: 52000, expense: 38000 },
{ month: 'Mar', income: 48000, expense: 35000 },
{ month: 'Apr', income: 61000, expense: 42000 },
{ month: 'May', income: 55000, expense: 39000 },
{ month: 'Jun', income: 67000, expense: 45000 },
];
return (
<div>
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-1">
Morning, {user.name}! 👋
</h1>
<p className="text-gray-500 text-sm">Here's your business overview</p>
</div>
<div className="flex items-center gap-3">
<button className="p-2.5 hover:bg-gray-100 rounded-xl transition-all">
<HelpCircle className="w-5 h-5 text-gray-600" />
</button>
<button className="p-2.5 hover:bg-gray-100 rounded-xl transition-all relative">
<Bell className="w-5 h-5 text-gray-600" />
<span className="absolute top-2 right-2 w-2 h-2 bg-[#FF6B00] rounded-full"></span>
</button>
<button className="flex items-center gap-2.5 hover:bg-gray-100 rounded-xl px-3 py-2 transition-all">
<div className="w-9 h-9 bg-gradient-to-br from-[#FF6B00] to-[#FF8534] rounded-full flex items-center justify-center text-white text-sm font-semibold shadow-lg">
{user.avatar}
</div>
<span className="text-sm font-medium text-gray-900">{user.name}</span>
<ChevronDown className="w-4 h-4 text-gray-600" />
</button>
</div>
</div>
{/* Transaction Summary Cards (Original Design) */}
<div>
<h2 className="text-xl font-bold text-gray-900 mb-5">Transaction Summary</h2>
<div className="grid grid-cols-5 gap-5 mb-8">
{statusCards.map((card, index) => {
const IconComponent = card.icon;
return (
<div
key={index}
className={`${card.color} rounded-2xl p-6 border ${card.borderColor} hover:shadow-lg transition-all duration-200 cursor-pointer`}
>
<div className="flex items-start justify-between mb-4">
<div className={`w-12 h-12 ${card.iconBg} rounded-xl flex items-center justify-center`}>
<IconComponent className={`w-6 h-6 ${card.iconColor}`} strokeWidth={2.5} />
</div>
</div>
<div className="mb-2 text-sm font-medium text-gray-600">{card.label}</div>
<div className="flex items-end justify-between mb-3">
<span className="text-3xl font-bold text-gray-900">{card.count}</span>
<div className={`flex items-center gap-1 text-sm font-semibold ${card.positive ? 'text-green-600' : 'text-red-600'}`}>
{card.positive ? (
<TrendingUp className="w-4 h-4" strokeWidth={2.5} />
) : (
<TrendingDown className="w-4 h-4" strokeWidth={2.5} />
)}
<span>{card.change}</span>
</div>
</div>
<button className="text-xs text-gray-500 hover:text-[#FF6B00] font-medium flex items-center gap-1.5 transition-colors">
<Download className="w-3.5 h-3.5" />
Download report
</button>
</div>
);
})}
</div>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-3 gap-4 mb-6">
{/* Green Book - You Will Get */}
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-2xl p-6 text-white shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -mr-16 -mt-16"></div>
<div className="relative">
<div className="flex items-center gap-2 mb-2">
<ArrowDownLeft className="w-5 h-5" />
<span className="text-sm font-medium opacity-90">You Will Get</span>
</div>
<div className="text-4xl font-bold mb-1">{totalReceivable.toLocaleString()}</div>
<div className="text-sm opacity-80">{customers.filter(c => c.type === 'credit').length} customers</div>
</div>
</div>
{/* Red Book - You Will Give */}
<div className="bg-gradient-to-br from-red-500 to-red-600 rounded-2xl p-6 text-white shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -mr-16 -mt-16"></div>
<div className="relative">
<div className="flex items-center gap-2 mb-2">
<ArrowUpRight className="w-5 h-5" />
<span className="text-sm font-medium opacity-90">You Will Give</span>
</div>
<div className="text-4xl font-bold mb-1">{totalPayable.toLocaleString()}</div>
<div className="text-sm opacity-80">{customers.filter(c => c.type === 'debit').length} customers</div>
</div>
</div>
{/* Net Balance */}
<div className="bg-gradient-to-br from-[#FF6B00] to-[#FF8534] rounded-2xl p-6 text-white shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-white/10 rounded-full -mr-16 -mt-16"></div>
<div className="relative">
<div className="flex items-center gap-2 mb-2">
<IndianRupee className="w-5 h-5" />
<span className="text-sm font-medium opacity-90">Net Balance</span>
</div>
<div className="text-4xl font-bold mb-1">{netBalance.toLocaleString()}</div>
<div className="text-sm opacity-80 flex items-center gap-1">
<TrendingUp className="w-3.5 h-3.5" />
Overall balance
</div>
</div>
</div>
</div>
{/* Main Content - Customer Ledger + Sidebar */}
<div className="grid grid-cols-3 gap-6 mb-8">
{/* Customer List */}
<div className="col-span-2">
<div className="bg-white rounded-2xl shadow-sm border border-gray-100">
{/* Header */}
<div className="p-5 border-b border-gray-100">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-lg font-bold text-gray-900">Customer Ledger</h2>
<p className="text-xs text-gray-500 mt-0.5">Track all your transactions</p>
</div>
<div className="flex items-center gap-2">
<button className="flex items-center gap-2 px-4 py-2 bg-[#FF6B00] text-white rounded-lg text-sm font-semibold hover:bg-[#FF8534] transition-all shadow-md">
<Plus className="w-4 h-4" />
Add Entry
</button>
</div>
</div>
{/* Search & Filter */}
<div className="flex items-center gap-2">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Search customers..."
className="w-full pl-10 pr-4 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-[#FF6B00] focus:border-transparent"
/>
</div>
<button className="p-2 bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 transition-all">
<Filter className="w-4 h-4 text-gray-600" />
</button>
</div>
</div>
{/* Customer Cards */}
<div className="p-4 space-y-3 max-h-[500px] overflow-y-auto">
{customers.map((customer) => (
<div
key={customer.id}
className={`${customer.bgColor} rounded-xl p-4 border-2 ${customer.type === 'credit' ? 'border-green-200' : 'border-red-200'} hover:shadow-md transition-all cursor-pointer`}
>
<div className="flex items-start justify-between">
{/* Customer Info */}
<div className="flex items-center gap-3 flex-1">
<div className={`w-14 h-14 bg-gradient-to-br ${customer.color} rounded-full flex items-center justify-center text-white text-xl font-bold shadow-lg`}>
{customer.avatar}
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-bold text-gray-900 text-base">{customer.name}</h3>
<span className={`px-2 py-0.5 rounded-full text-xs font-semibold ${customer.type === 'credit' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
{customer.type === 'credit' ? 'GET' : 'GIVE'}
</span>
</div>
<div className="flex items-center gap-1 text-xs text-gray-500 mb-1">
<Phone className="w-3 h-3" />
{customer.phone}
</div>
<div className="flex items-center gap-1 text-xs text-gray-400">
<History className="w-3 h-3" />
{customer.lastTransaction} {customer.lastDate}
</div>
</div>
</div>
{/* Balance */}
<div className="text-right">
<div className={`text-3xl font-bold ${customer.type === 'credit' ? 'text-green-600' : 'text-red-600'} mb-1`}>
{customer.balance.toLocaleString()}
</div>
<div className={`text-xs font-semibold ${customer.textColor}`}>
{customer.type === 'credit' ? 'You\'ll Get' : 'You\'ll Give'}
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex items-center gap-2 mt-4 pt-3 border-t border-gray-200">
<button className="flex-1 flex items-center justify-center gap-1.5 py-2 bg-white border border-gray-200 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all">
<Plus className="w-4 h-4" />
Add Entry
</button>
<button className="flex-1 flex items-center justify-center gap-1.5 py-2 bg-white border border-gray-200 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all">
<MessageSquare className="w-4 h-4" />
Remind
</button>
<button className="flex items-center justify-center gap-1.5 px-3 py-2 bg-white border border-gray-200 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all">
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
</div>
</div>
{/* Sidebar - Quick Actions & Stats */}
<div className="space-y-4">
{/* Quick Actions */}
<div className="bg-white rounded-2xl p-5 shadow-sm border border-gray-100">
<h3 className="font-bold text-gray-900 mb-4 text-sm">Quick Actions</h3>
<div className="space-y-2">
<button className="w-full flex items-center gap-3 p-3 bg-gradient-to-r from-green-50 to-green-100 text-green-700 rounded-xl font-semibold text-sm hover:shadow-md transition-all">
<div className="w-8 h-8 bg-green-500 rounded-lg flex items-center justify-center">
<Plus className="w-4 h-4 text-white" />
</div>
Gave Money
</button>
<button className="w-full flex items-center gap-3 p-3 bg-gradient-to-r from-red-50 to-red-100 text-red-700 rounded-xl font-semibold text-sm hover:shadow-md transition-all">
<div className="w-8 h-8 bg-red-500 rounded-lg flex items-center justify-center">
<Plus className="w-4 h-4 text-white" />
</div>
Got Money
</button>
<button className="w-full flex items-center gap-3 p-3 bg-gradient-to-r from-blue-50 to-blue-100 text-blue-700 rounded-xl font-semibold text-sm hover:shadow-md transition-all">
<div className="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center">
<Users className="w-4 h-4 text-white" />
</div>
Add Customer
</button>
</div>
</div>
{/* Total Customers */}
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-2xl p-5 text-white shadow-lg">
<div className="flex items-center gap-2 mb-2">
<Users className="w-5 h-5" />
<span className="text-sm font-medium opacity-90">Total Customers</span>
</div>
<div className="text-3xl font-bold">{customers.length}</div>
</div>
{/* Recent Activity */}
<div className="bg-white rounded-2xl p-5 shadow-sm border border-gray-100">
<h3 className="font-bold text-gray-900 mb-4 text-sm">Recent Activity</h3>
<div className="space-y-3">
<div className="flex items-start gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full mt-1.5"></div>
<div className="flex-1">
<p className="text-xs font-medium text-gray-900">Payment received</p>
<p className="text-xs text-gray-500">800 from Deepa</p>
<p className="text-xs text-gray-400 mt-0.5">2 hours ago</p>
</div>
</div>
<div className="flex items-start gap-2">
<div className="w-2 h-2 bg-red-500 rounded-full mt-1.5"></div>
<div className="flex-1">
<p className="text-xs font-medium text-gray-900">Payment due</p>
<p className="text-xs text-gray-500">500 to Rakesh</p>
<p className="text-xs text-gray-400 mt-0.5">Yesterday</p>
</div>
</div>
<div className="flex items-start gap-2">
<div className="w-2 h-2 bg-blue-500 rounded-full mt-1.5"></div>
<div className="flex-1">
<p className="text-xs font-medium text-gray-900">Customer added</p>
<p className="text-xs text-gray-500">Priya added</p>
<p className="text-xs text-gray-400 mt-0.5">3 days ago</p>
</div>
</div>
</div>
</div>
{/* Reminders */}
<div className="bg-yellow-50 rounded-2xl p-5 border-2 border-yellow-200">
<div className="flex items-center gap-2 mb-3">
<Bell className="w-5 h-5 text-yellow-600" />
<h3 className="font-bold text-yellow-900 text-sm">Reminders</h3>
</div>
<p className="text-xs text-yellow-800 mb-2">3 pending reminders to send</p>
<button className="w-full py-2 bg-yellow-500 text-white rounded-lg text-xs font-semibold hover:bg-yellow-600 transition-all">
Send All Reminders
</button>
</div>
</div>
</div>
{/* Financial Overview Chart (Original Design) */}
<div className="bg-white rounded-2xl p-7 border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-bold text-gray-900 mb-1">Financial Overview</h2>
<p className="text-sm text-gray-500">Monthly income vs expense comparison</p>
</div>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-[#27AE60] rounded-full"></div>
<div className="text-right">
<div className="text-xs text-gray-500 font-medium">Total Income</div>
<div className="text-lg font-bold text-[#27AE60]"> 3,28,000</div>
</div>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-[#EB5757] rounded-full"></div>
<div className="text-right">
<div className="text-xs text-gray-500 font-medium">Total Expense</div>
<div className="text-lg font-bold text-[#EB5757]"> 2,31,000</div>
</div>
</div>
</div>
</div>
<ResponsiveContainer width="100%" height={320}>
<BarChart data={chartData} barGap={8}>
<CartesianGrid strokeDasharray="3 3" stroke="#F0F0F0" vertical={false} />
<XAxis
dataKey="month"
axisLine={false}
tickLine={false}
tick={{ fill: '#6B7280', fontSize: 13, fontWeight: 500 }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: '#6B7280', fontSize: 13, fontWeight: 500 }}
tickFormatter={(value) => `${value/1000}k`}
/>
<Tooltip
cursor={{ fill: '#F9FAFB' }}
contentStyle={{
borderRadius: '12px',
border: '1px solid #E5E7EB',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
fontWeight: 600
}}
/>
<Legend
wrapperStyle={{ paddingTop: '20px' }}
iconType="circle"
/>
<Bar dataKey="income" fill="#27AE60" radius={[8, 8, 0, 0]} name="Income" />
<Bar dataKey="expense" fill="#EB5757" radius={[8, 8, 0, 0]} name="Expense" />
</BarChart>
</ResponsiveContainer>
</div>
{/* Quick Stats (Original Design) */}
<div className="grid grid-cols-3 gap-5">
<div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-sm hover:shadow-md transition-all">
<div className="flex items-center justify-between mb-4">
<div className="text-sm text-gray-500 font-medium">Total Customers</div>
<div className="w-10 h-10 bg-purple-50 rounded-xl flex items-center justify-center">
<Users className="w-5 h-5 text-purple-600" />
</div>
</div>
<div className="text-3xl font-bold text-gray-900 mb-2">248</div>
<div className="flex items-center gap-1 text-xs text-green-600 font-semibold">
<TrendingUp className="w-3.5 h-3.5" />
+12% from last month
</div>
</div>
<div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-sm hover:shadow-md transition-all">
<div className="flex items-center justify-between mb-4">
<div className="text-sm text-gray-500 font-medium">Active Transactions</div>
<div className="w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center">
<Receipt className="w-5 h-5 text-blue-600" />
</div>
</div>
<div className="text-3xl font-bold text-gray-900 mb-2">89</div>
<div className="text-xs text-blue-600 font-semibold">23 pending approval</div>
</div>
<div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-sm hover:shadow-md transition-all">
<div className="flex items-center justify-between mb-4">
<div className="text-sm text-gray-500 font-medium">Net Balance</div>
<div className="w-10 h-10 bg-green-50 rounded-xl flex items-center justify-center">
<TrendingUp className="w-5 h-5 text-green-600" />
</div>
</div>
<div className="text-3xl font-bold text-gray-900 mb-2"> 97,000</div>
<div className="flex items-center gap-1 text-xs text-green-600 font-semibold">
<CheckCircle className="w-3.5 h-3.5" />
Positive cashflow
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,707 @@
import { useRef, useEffect, useState } from 'react';
import { Bell, TrendingUp, ArrowDownLeft, Users, Plus, MessageSquare, Filter, PhoneCall, MessageCircle, Search, Phone, ArrowUpRight, History, ChevronRight, IndianRupee, Package } from 'lucide-react';
import Modal from './Modal';
import './LedgerSection.scss'
import Avatar from './Avatar';
const gradientMap = {
'from-green-400 to-green-500': 'linear-gradient(135deg, #34D399, #10B981)',
'from-red-400 to-red-500': 'linear-gradient(135deg, #F87171, #EF4444)',
'from-orange-400 to-orange-500': 'linear-gradient(135deg, #FB923C, #F97316)',
'from-blue-400 to-blue-500': 'linear-gradient(135deg, #60A5FA, #3B82F6)',
'from-purple-400 to-purple-500': 'linear-gradient(135deg, #A78BFA, #8B5CF6)',
'from-pink-400 to-pink-500': 'linear-gradient(135deg, #F472B6, #EC4899)'
};
export default function LedgerSection({ customers, suppliers }) {
const customerHeaderRef = useRef(null);
const customerCardsRef = useRef(null);
const sideBarRef = useRef(null);
const [activeTab, setActiveTab] = useState('customers');
const [customerCardMaxHeight, setCustomerCardMaxHeight] = useState(500);
const [showGaveMoneyModal, setShowGaveMoneyModal] = useState(false);
const [showGotMoneyModal, setShowGotMoneyModal] = useState(false);
const [showAddCustomerModal, setShowAddCustomerModal] = useState(false);
const [showAddEntryModal, setShowAddEntryModal] = useState(false);
const [showReminderModal, setShowReminderModal] = useState(false);
const [showHistoryModal, setShowHistoryModal] = useState(false);
const [selectedCustomer, setSelectedCustomer] = useState(null);
const totalReceivable = customers.filter(c => c.type === 'credit').reduce((sum, c) => sum + c.balance, 0);
const totalPayable = customers.filter(c => c.type === 'debit').reduce((sum, c) => sum + c.balance, 0);
const supplierReceivable = suppliers.filter(s => s.type === 'credit').reduce((sum, s) => sum + s.balance, 0);
const supplierPayable = suppliers.filter(s => s.type === 'debit').reduce((sum, s) => sum + s.balance, 0);
const netBalance = (totalReceivable + supplierReceivable) - (totalPayable + supplierPayable);
const activeData = activeTab === 'customers' ? customers : suppliers;
useEffect(() => {
if (sideBarRef?.current) {
const headerHeight = customerHeaderRef.current?.getBoundingClientRect().height;
const sidebarHeight = sideBarRef.current?.getBoundingClientRect().height;
setCustomerCardMaxHeight(sidebarHeight - headerHeight);
}
}, [sideBarRef.current]);
return (
<>
<div className="khata-summary">
<div className="khata-card green">
<div className="card-content">
<div className="card-header">
<ArrowDownLeft />
<span>You Will Get</span>
</div>
<div className="amount">{totalReceivable.toLocaleString()}</div>
<div className="subtitle"><div>{customers.filter(c => c.type === 'credit').length}</div><div>Customers</div></div>
</div>
</div>
<div className="khata-card red">
<div className="card-content">
<div className="card-header">
<ArrowUpRight />
<span>You Will Give</span>
</div>
<div className="amount">{totalPayable.toLocaleString()}</div>
<div className="subtitle"><div>{customers.filter(c => c.type === 'debit').length}</div><div>Customers</div></div>
</div>
</div>
<div className="khata-card orange">
<div className="card-content">
<div className="card-header">
<IndianRupee />
<span>Net Balance</span>
</div>
<div className="amount">{netBalance.toLocaleString()}</div>
<div className="subtitle">
<TrendingUp />
<div>Overall balance</div>
</div>
</div>
</div>
</div>
<div className="main-content">
<div className="customer-list">
<div className="customer-list-card">
<div className="customer-header" ref={customerHeaderRef}>
<div className="header-top">
<div className="header-text">
<h2>{activeTab === 'customers' ? 'Customer' : 'Supplier'} Ledger</h2>
<p>Track all your {activeTab === 'customers' ? 'customer' : 'supplier'} transactions</p>
</div>
<div className="header-actions-right">
<div className="tabs">
<button
className={`tab ${activeTab === 'customers' ? 'active' : ''}`}
onClick={() => setActiveTab('customers')}
>
<Users />
Customers
</button>
<button
className={`tab ${activeTab === 'suppliers' ? 'active' : ''}`}
onClick={() => setActiveTab('suppliers')}
>
<Package />
Suppliers
</button>
</div>
<button className="add-btn" onClick={() => setShowAddCustomerModal(true)}>
<Plus />
<div>Add {activeTab === 'customers' ? 'Customer' : 'Supplier'}</div>
</button>
</div>
</div>
<div className="search-filter">
<div className="search-wrapper">
<Search />
<input type="text" placeholder={`Search ${activeTab}...`} />
</div>
<button className="filter-btn">
<Filter />
</button>
</div>
</div>
<div className="customer-cards" style={{ maxHeight: `${customerCardMaxHeight}px` }} ref={customerCardsRef}>
{activeData.map((customer) => (
<div key={customer.id} className={`customer-card ${customer.type}`}>
<div className="card-main">
<div className="customer-info">
<Avatar className="avatar" background={gradientMap[customer.color]} children={customer?.avatar} />
<div className="customer-details">
<div className="name-row">
<h3>{customer.name}</h3>
<span className={`badge ${customer.type === 'credit' ? 'get' : 'give'}`}>
{customer.type === 'credit' ? 'GET' : 'GIVE'}
</span>
</div>
<div className="phone-row">
<Phone />
{customer.phone}
</div>
<div className="transaction-row">
<History />
{customer.lastTransaction} {customer.lastDate}
</div>
</div>
</div>
<div className="balance-info">
<div className={`amount ${customer.type === 'credit' ? 'get' : 'give'}`}>
{customer.balance.toLocaleString()}
</div>
<div className={`label ${customer.type === 'credit' ? 'get' : 'give'}`}>
{customer.type === 'credit' ? "You'll Get" : "You'll Give"}
</div>
</div>
</div>
<div className="card-actions">
<button onClick={() => {
setSelectedCustomer(customer);
setShowAddEntryModal(true);
}}>
<Plus />
<div>Add Entry</div>
</button>
<button onClick={() => {
setSelectedCustomer(customer);
setShowReminderModal(true);
}}>
<MessageSquare />
<div>Remind</div>
</button>
<button onClick={() => {
setSelectedCustomer(customer);
setShowHistoryModal(true);
}}>
<ChevronRight />
</button>
</div>
</div>
))}
</div>
</div>
</div>
<div className="sidebar" ref={sideBarRef}>
<div className="quick-actions-card">
<h3>Quick Actions</h3>
<div className="actions">
<button className="gave-money" onClick={() => setShowGotMoneyModal(true)}>
<div className="icon">
<ArrowDownLeft />
</div>
<span>Payment Received</span>
</button>
<button className="got-money" onClick={() => setShowGaveMoneyModal(true)}>
<div className="icon">
<ArrowUpRight />
</div>
<span>Payment Made</span>
</button>
<button className="add-customer" onClick={() => setShowAddCustomerModal(true)}>
<div className="icon">
<Users />
</div>
<span>Add Customer</span>
</button>
</div>
</div>
<div className="total-customers-card">
<div className="card-header">
<Users />
<span>Total Customers</span>
</div>
<div className="count">{customers.length}</div>
</div>
<div className="total-customers-card" style={{ background: 'linear-gradient(to bottom right, rgb(249, 115, 22), rgb(234, 88, 12))' }}>
<div className="card-header">
<Package />
<span>Total Suppliers</span>
</div>
<div className="count">{suppliers.length}</div>
</div>
<div className="recent-activity-card">
<h3>Recent Activity</h3>
<div className="activities">
<div className="activity-item">
<div className="dot green"></div>
<div className="activity-content">
<p className="title">Payment received</p>
<p className="description">800 from Deepa</p>
<p className="time">2 hours ago</p>
</div>
</div>
<div className="activity-item">
<div className="dot red"></div>
<div className="activity-content">
<p className="title">Payment due</p>
<p className="description">500 to Rakesh</p>
<p className="time">Yesterday</p>
</div>
</div>
<div className="activity-item">
<div className="dot blue"></div>
<div className="activity-content">
<p className="title">Customer added</p>
<p className="description">Priya added</p>
<p className="time">3 days ago</p>
</div>
</div>
</div>
</div>
<div className="reminders-card">
<div className="reminders-header">
<Bell />
<h3>Reminders</h3>
</div>
<p>3 pending reminders to send</p>
<button>Send All Reminders</button>
</div>
</div>
</div>
{/* Modals */}
{/* Payment Made Modal */}
<Modal
isOpen={showGaveMoneyModal}
onClose={() => setShowGaveMoneyModal(false)}
title={<h3>Payment Made</h3>}
submitText="Save Transaction"
submitClassName="submit-btn made-btn"
onSubmit={() => console.log('Payment Made')}
>
<div className="form-group">
<label>Select Customer/Supplier</label>
<select onChange={(e) => setSelectedCustomer([...customers, ...suppliers].find(c => c.id === parseInt(e.target.value)))}>
<option value="">Choose...</option>
{[...customers, ...suppliers].map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div>
{selectedCustomer && (
<div className="form-group">
<label>Current Balance</label>
<div style={{
padding: '1rem',
backgroundColor: selectedCustomer.type === 'credit' ? '#F0FDF4' : '#FEF2F2',
borderRadius: '0.75rem',
border: `2px solid ${selectedCustomer.type === 'credit' ? '#BBF7D0' : '#FECACA'}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<span style={{ fontSize: '0.875rem', fontWeight: 600, color: '#6B7280' }}>
{selectedCustomer.type === 'credit' ? "You'll Get" : "You'll Give"}
</span>
<span style={{ fontSize: '1.5rem', fontWeight: 700, color: selectedCustomer.type === 'credit' ? '#059669' : '#DC2626' }}>
{selectedCustomer.balance.toLocaleString()}
</span>
</div>
</div>
)}
<div className="form-group">
<label>Amount ()</label>
<input type="number" placeholder="Enter amount" />
</div>
<div className="form-group">
<label>Note</label>
<textarea placeholder="Add a note (optional)" rows="3"></textarea>
</div>
<div className="form-group">
<label>Date</label>
<input type="date" defaultValue={new Date().toISOString().split('T')[0]} />
</div>
</Modal>
{/* Payment Received Modal */}
<Modal
isOpen={showGotMoneyModal}
onClose={() => setShowGotMoneyModal(false)}
title={<h3>Payment Received</h3>}
submitText="Save Transaction"
submitClassName="submit-btn received-btn"
onSubmit={() => console.log('Payment Received')}
>
<div className="form-group">
<label>Select Customer/Supplier</label>
<select onChange={(e) => setSelectedCustomer([...customers, ...suppliers].find(c => c.id === parseInt(e.target.value)))}>
<option value="">Choose...</option>
{[...customers, ...suppliers].map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div>
{selectedCustomer && (
<div className="form-group">
<label>Current Balance</label>
<div style={{
padding: '1rem',
backgroundColor: selectedCustomer.type === 'credit' ? '#F0FDF4' : '#FEF2F2',
borderRadius: '0.75rem',
border: `2px solid ${selectedCustomer.type === 'credit' ? '#BBF7D0' : '#FECACA'}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<span style={{ fontSize: '0.875rem', fontWeight: 600, color: '#6B7280' }}>
{selectedCustomer.type === 'credit' ? "You'll Get" : "You'll Give"}
</span>
<span style={{ fontSize: '1.5rem', fontWeight: 700, color: selectedCustomer.type === 'credit' ? '#059669' : '#DC2626' }}>
{selectedCustomer.balance.toLocaleString()}
</span>
</div>
</div>
)}
<div className="form-group">
<label>Amount ()</label>
<input type="number" placeholder="Enter amount" />
</div>
<div className="form-group">
<label>Note</label>
<textarea placeholder="Add a note (optional)" rows="3"></textarea>
</div>
<div className="form-group">
<label>Date</label>
<input type="date" defaultValue={new Date().toISOString().split('T')[0]} />
</div>
</Modal>
{/* Add Customer/Supplier Modal */}
<Modal
isOpen={showAddCustomerModal}
onClose={() => setShowAddCustomerModal(false)}
title={<h3>Add New {activeTab === 'customers' ? 'Customer' : 'Supplier'}</h3>}
submitText={`Add ${activeTab === 'customers' ? 'Customer' : 'Supplier'}`}
submitClassName="submit-btn add-btn"
onSubmit={() => console.log('Add Customer')}
>
<div className="form-group">
<label>Name</label>
<input type="text" placeholder="Enter name" />
</div>
<div className="form-group">
<label>Phone Number</label>
<input type="tel" placeholder="98755 43210" />
</div>
<div className="form-group">
<label>Opening Balance ()</label>
<input type="number" placeholder="0" />
</div>
<div className="form-group">
<label>Balance Type</label>
<select>
<option value="credit">You'll Get (Credit)</option>
<option value="debit">You'll Give (Debit)</option>
</select>
</div>
<div className="form-group">
<label>Address (Optional)</label>
<textarea placeholder="Enter address" rows="2"></textarea>
</div>
</Modal>
{/* Add Customer/Supplier Entry Modal */}
<Modal
isOpen={showAddEntryModal}
onClose={() => setShowAddEntryModal(false)}
title={<h3>Add Entry for {selectedCustomer?.name}</h3>}
submitText="Save Entry"
submitClassName="submit-btn received-btn"
onSubmit={() => console.log('Add Entry')}
>
{selectedCustomer && (
<div className="form-group">
<label>Current Balance</label>
<div style={{
padding: '1rem',
backgroundColor: selectedCustomer.type === 'credit' ? '#F0FDF4' : '#FEF2F2',
borderRadius: '0.75rem',
border: `2px solid ${selectedCustomer.type === 'credit' ? '#BBF7D0' : '#FECACA'}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<span style={{ fontSize: '0.875rem', fontWeight: 600, color: '#6B7280' }}>
{selectedCustomer.type === 'credit' ? "You'll Get" : "You'll Give"}
</span>
<span style={{ fontSize: '1.5rem', fontWeight: 700, color: selectedCustomer.type === 'credit' ? '#059669' : '#DC2626' }}>
{selectedCustomer.balance.toLocaleString()}
</span>
</div>
</div>
)}
<div className="form-group">
<label>Transaction Type</label>
<select>
<option value="credit">Payment Received (You'll Get)</option>
<option value="debit">Payment Made (You'll Give)</option>
</select>
</div>
<div className="form-group">
<label>Amount ()</label>
<input type="number" placeholder="Enter amount" />
</div>
<div className="form-group">
<label>Description</label>
<textarea placeholder="Add description (optional)" rows="3"></textarea>
</div>
<div className="form-group">
<label>Payment Method</label>
<select>
<option value="cash">Cash</option>
<option value="upi">UPI</option>
<option value="bank">Bank Transfer</option>
<option value="card">Card</option>
</select>
</div>
<div className="form-group">
<label>Date</label>
<input type="date" defaultValue={new Date().toISOString().split('T')[0]} />
</div>
</Modal>
{/* Reminder Modal */}
<Modal
isOpen={showReminderModal}
onClose={() => setShowReminderModal(false)}
title={<h3>Send Reminder to {selectedCustomer?.name}</h3>}
submitText="Send Reminder"
submitClassName="submit-btn made-btn"
onSubmit={() => console.log('Send Reminder')}
>
<div className="form-group">
<label>Reminder Type</label>
<select>
<option value="payment">Payment Reminder</option>
<option value="followup">Follow-up</option>
<option value="custom">Custom Message</option>
</select>
</div>
<div className="form-group">
<label>Message</label>
<textarea
placeholder={`Hi ${selectedCustomer?.name}, this is a friendly reminder about your pending payment of ₹${selectedCustomer?.balance}. Please settle at your earliest convenience. Thank you!`}
rows="5"
defaultValue={`Hi ${selectedCustomer?.name}, this is a friendly reminder about your pending payment of ₹${selectedCustomer?.balance}. Please settle at your earliest convenience. Thank you!`}
></textarea>
</div>
<div className="form-group">
<label>Send Via</label>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<input type="checkbox" defaultChecked />
<span>WhatsApp</span>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<input type="checkbox" />
<span>SMS</span>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<input type="checkbox" />
<span>Email</span>
</label>
</div>
</div>
<div className="form-group">
<label>Schedule (Optional)</label>
<input type="datetime-local" />
</div>
</Modal>
{/* Transaction History Modal */}
<Modal
className='transaction-history-modal'
isOpen={showHistoryModal}
onClose={() => setShowHistoryModal(false)}
title={<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<div className="avatar" style={{
background: `linear-gradient(135deg, ${selectedCustomer?.color === 'from-green-400 to-green-500' ? '#34D399, #10B981' :
selectedCustomer?.color === 'from-red-400 to-red-500' ? '#F87171, #EF4444' :
selectedCustomer?.color === 'from-orange-400 to-orange-500' ? '#FB923C, #F97316' :
selectedCustomer?.color === 'from-blue-400 to-blue-500' ? '#60A5FA, #3B82F6' :
selectedCustomer?.color === 'from-purple-400 to-purple-500' ? '#A78BFA, #8B5CF6' : '#F472B6, #EC4899'})`,
width: '3rem',
height: '3rem',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '1.25rem',
fontWeight: 700
}}>
{selectedCustomer?.avatar}
</div>
<div style={{ flex: 1 }}>
<h3 style={{ margin: 0 }}>{selectedCustomer?.name}</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginTop: '0.25rem' }}>
<p style={{ margin: 0, fontSize: '0.875rem', color: '#6B7280', display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<Phone style={{ width: '0.875rem', height: '0.875rem' }} />
{selectedCustomer?.phone}
</p>
<div style={{ display: 'flex', gap: '0.25rem', marginLeft: '0.5rem' }}>
<button
onClick={() => window.open(`tel:${selectedCustomer?.phone?.replace(/\s/g, '')}`)}
style={{
padding: '0.375rem',
backgroundColor: '#10B981',
border: 'none',
borderRadius: '0.5rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s'
}}
title="Call"
>
<PhoneCall style={{ width: '0.875rem', height: '0.875rem', color: 'white' }} />
</button>
<button
onClick={() => window.open(`https://wa.me/${selectedCustomer?.phone.replace(/\s/g, '')}`, '_blank')}
style={{
padding: '0.375rem',
backgroundColor: '#25D366',
border: 'none',
borderRadius: '0.5rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s'
}}
title="WhatsApp"
>
<MessageCircle style={{ width: '0.875rem', height: '0.875rem', color: 'white' }} />
</button>
<button
onClick={() => window.open(`sms:${selectedCustomer?.phone.replace(/\s/g, '')}`)}
style={{
padding: '0.375rem',
backgroundColor: '#3B82F6',
border: 'none',
borderRadius: '0.5rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s'
}}
title="SMS"
>
<MessageSquare style={{ width: '0.875rem', height: '0.875rem', color: 'white' }} />
</button>
</div>
</div>
</div>
</div>}
submitText={
<>
<Plus style={{ width: '1rem', height: '1rem' }} />
<div>Add New Entry</div>
</>
}
submitClassName="submit-btn add-btn"
onSubmit={() => {
setShowAddEntryModal(true);
setShowHistoryModal(false);
}}
>
{selectedCustomer && (
<>
<div style={{
padding: '1.5rem',
background: selectedCustomer?.type === 'credit' ? 'linear-gradient(to right, #F0FDF4, #DCFCE7)' : 'linear-gradient(to right, #FEF2F2, #FEE2E2)',
borderRadius: '0.75rem',
marginBottom: '1rem'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<p style={{ fontSize: '0.875rem', color: '#6B7280', margin: 0 }}>Current Balance</p>
<p style={{ fontSize: '0.75rem', color: '#9CA3AF', margin: '0.25rem 0 0 0' }}>
{selectedCustomer?.type === 'credit' ? "You'll Get" : "You'll Give"}
</p>
</div>
<div style={{
fontSize: '2rem',
fontWeight: 700,
color: selectedCustomer?.type === 'credit' ? '#059669' : '#DC2626'
}}>
{selectedCustomer?.balance.toLocaleString()}
</div>
</div>
</div>
<h4 style={{ fontSize: '0.875rem', fontWeight: 700, color: '#111827', marginBottom: '1rem' }}>
Transaction History ({selectedCustomer.history?.length || 0} entries)
</h4>
{selectedCustomer.history && selectedCustomer.history.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{selectedCustomer.history.map((transaction) => (
<div key={transaction.id} style={{
padding: '1rem',
border: '1px solid #E5E7EB',
borderRadius: '0.75rem',
backgroundColor: transaction.type === 'credit' ? '#F0FDF4' : '#FEF2F2',
borderLeft: `4px solid ${transaction.type === 'credit' ? '#10B981' : '#EF4444'}`
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '0.5rem' }}>
<div>
<span style={{
display: 'inline-block',
padding: '0.25rem 0.5rem',
borderRadius: '0.375rem',
fontSize: '0.75rem',
fontWeight: 600,
backgroundColor: transaction.type === 'credit' ? '#D1FAE5' : '#FEE2E2',
color: transaction.type === 'credit' ? '#065F46' : '#991B1B',
marginBottom: '0.25rem'
}}>
{transaction.type === 'credit' ? '↓ Received' : '↑ Paid'}
</span>
<p style={{ fontSize: '0.875rem', fontWeight: 600, color: '#111827', margin: '0.25rem 0' }}>
{transaction.note}
</p>
<p style={{ fontSize: '0.75rem', color: '#6B7280', margin: 0 }}>
{transaction.date} {transaction.method}
</p>
</div>
<div style={{
fontSize: '1.25rem',
fontWeight: 700,
color: transaction.type === 'credit' ? '#059669' : '#DC2626'
}}>
{transaction.type === 'credit' ? '+' : '-'}{transaction.amount.toLocaleString()}
</div>
</div>
</div>
))}
</div>
) : (
<div style={{
padding: '2rem',
textAlign: 'center',
color: '#9CA3AF',
fontSize: '0.875rem'
}}>
No transaction history available
</div>
)}
</>
)}
</Modal>
</>
);
}

View File

@ -0,0 +1,904 @@
// Summary Cards
.khata-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
.khata-card {
border-radius: 1rem;
padding: 1rem;
color: white;
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 8rem;
height: 8rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
margin-right: -4rem;
margin-top: -4rem;
}
&.green {
background: linear-gradient(to bottom right, #10B981, #059669);
}
&.red {
background: linear-gradient(to bottom right, #EF4444, #DC2626);
}
&.orange {
background: linear-gradient(to bottom right, #FF6B00, #FF8534);
}
.card-content {
position: relative;
z-index: 1;
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
svg {
width: 1.25rem;
height: 1.25rem;
}
span {
font-size: 0.875rem;
font-weight: 500;
opacity: 0.9;
}
}
.amount {
font-size: 2.25rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.subtitle {
font-size: 0.875rem;
opacity: 0.8;
display: flex;
align-items: center;
gap: 0.2rem;
svg {
width: 0.875rem;
height: 0.875rem;
}
}
}
}
}
// Customer Ledger Section
.main-content {
display: grid;
grid-template-columns:
minmax(350px, 2fr) minmax(250px, 1fr);
gap: 1.5rem;
margin-bottom: 2rem;
.customer-list {
.customer-list-card {
background: white;
border-radius: 1rem;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
border: 1px solid #F3F4F6;
.customer-header {
padding: 1.25rem;
border-bottom: 1px solid #F3F4F6;
.header-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
flex-wrap: wrap;
.header-text {
h2 {
font-size: 1.125rem;
font-weight: 700;
color: #111827;
}
p {
font-size: 0.75rem;
color: #6B7280;
margin-top: 0.125rem;
margin-bottom: 4px;
}
}
.header-actions-right {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
justify-content: flex-end;
.tabs {
display: flex;
align-items: center;
gap: 0.5rem;
background-color: #F3F4F6;
padding: 0.25rem;
border-radius: 0.75rem;
.tab {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: transparent;
border: none;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: #6B7280;
cursor: pointer;
transition: all 0.2s;
svg {
width: 1rem;
height: 1rem;
}
&:hover {
color: #374151;
}
&.active {
background-color: white;
color: #FF6B00;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
}
}
}
}
.add-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background-color: #FF6B00;
color: white;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
border: none;
cursor: pointer;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
transition: all 0.2s;
&:hover {
background-color: #FF8534;
}
svg {
width: 1rem;
height: 1rem;
}
>div {
white-space: nowrap;
}
}
}
.search-filter {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
.search-wrapper {
flex: 1;
position: relative;
width: 90%;
svg {
position: absolute;
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
width: 1rem;
height: 1rem;
color: #9CA3AF;
}
input {
width: 100%;
padding: 0.5rem 1rem 0.5rem 2.5rem;
background-color: #F9FAFB;
border: 1px solid #E5E7EB;
border-radius: 0.5rem;
font-size: 0.875rem;
color: #111827;
&:focus {
outline: none;
ring: 2px;
ring-color: #FF6B00;
border-color: transparent;
}
&::placeholder {
color: #9CA3AF;
}
}
}
.filter-btn {
padding: 0.5rem;
background-color: #F9FAFB;
border: 1px solid #E5E7EB;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
width: 10%;
&:hover {
background-color: #F3F4F6;
}
svg {
width: 1rem;
height: 1rem;
color: #4B5563;
}
}
}
}
.customer-cards {
padding: 1rem;
height: max-content;
max-height: 500px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.75rem;
.customer-card {
border-radius: 0.75rem;
padding: 1rem;
border: 2px solid;
cursor: pointer;
transition: all 0.2s;
&:hover {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
&.credit {
background-color: #F0FDF4;
border-color: #BBF7D0;
}
&.debit {
background-color: #FEF2F2;
border-color: #FECACA;
}
.card-main {
display: flex;
align-items: start;
justify-content: space-between;
.customer-info {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
.avatar {
width: 3.5rem;
height: 3.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.25rem;
font-weight: 700;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
flex-shrink: 0;
}
.customer-details {
flex: 1;
.name-row {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
h3 {
font-weight: 700;
color: #111827;
font-size: 1rem;
}
.badge {
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
&.get {
background-color: #D1FAE5;
color: #065F46;
}
&.give {
background-color: #FEE2E2;
color: #991B1B;
}
}
}
.phone-row {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
color: #6B7280;
margin-bottom: 0.25rem;
svg {
width: 0.75rem;
height: 0.75rem;
}
}
.transaction-row {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
color: #9CA3AF;
svg {
width: 0.75rem;
height: 0.75rem;
}
}
}
}
.balance-info {
display: block;
text-align: right;
.amount {
font-size: 1.875rem;
font-weight: 700;
margin-bottom: 0.25rem;
&.get {
color: #059669;
}
&.give {
color: #DC2626;
}
}
.label {
font-size: 0.75rem;
font-weight: 600;
&.get {
color: #059669;
}
&.give {
color: #DC2626;
}
}
}
}
.card-actions {
display: flex;
align-items: center;
gap: 4px;
margin-top: 1rem;
padding-top: 0.75rem;
border-top: 1px solid #E5E7EB;
button {
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
padding: 0.5rem;
background: white;
border: 1px solid #E5E7EB;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
cursor: pointer;
transition: all 0.2s;
&:hover {
background-color: #F9FAFB;
}
&:first-child,
&:nth-child(2) {
flex: 1;
}
svg {
width: 1rem;
height: 1rem;
}
}
}
}
}
}
}
.sidebar {
display: flex;
flex-direction: column;
gap: 1rem;
.quick-actions-card {
background: white;
border-radius: 1rem;
padding: 1.25rem;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
border: 1px solid #F3F4F6;
h3 {
font-weight: 700;
color: #111827;
font-size: 0.875rem;
margin-bottom: 1rem;
}
.actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
button {
width: 100%;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border-radius: 0.75rem;
font-weight: 600;
font-size: 0.875rem;
border: none;
cursor: pointer;
transition: all 0.2s;
&.gave-money {
background: linear-gradient(to right, #F0FDF4, #DCFCE7);
color: #15803D;
.icon {
width: 2rem;
height: 2rem;
background-color: #22C55E;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
svg {
width: 1rem;
height: 1rem;
color: white;
}
}
&:hover {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
}
&.got-money {
background: linear-gradient(to right, #FEF2F2, #FEE2E2);
color: #991B1B;
.icon {
width: 2rem;
height: 2rem;
background-color: #EF4444;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
svg {
width: 1rem;
height: 1rem;
color: white;
}
}
&:hover {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
}
&.add-customer {
background: linear-gradient(to right, #EFF6FF, #DBEAFE);
color: #1E40AF;
.icon {
width: 2rem;
height: 2rem;
background-color: #3B82F6;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
svg {
width: 1rem;
height: 1rem;
color: white;
}
}
&:hover {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
}
>span {
text-align: left !important;
}
}
}
}
.total-customers-card {
background: linear-gradient(to bottom right, #A855F7, #9333EA);
border-radius: 1rem;
padding: 0.5rem 1rem;
color: white;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
display: flex;
align-items: center;
justify-content: space-between;
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
svg {
width: 1.25rem;
height: 1.25rem;
}
span {
font-size: 0.875rem;
font-weight: 500;
opacity: 0.9;
}
}
.count {
font-size: 1.875rem;
font-weight: 700;
}
}
.recent-activity-card {
background: white;
border-radius: 1rem;
padding: 1.25rem;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
border: 1px solid #F3F4F6;
h3 {
font-weight: 700;
color: #111827;
font-size: 0.875rem;
margin-bottom: 1rem;
}
.activities {
display: flex;
flex-direction: column;
gap: 0.75rem;
.activity-item {
display: flex;
align-items: start;
gap: 0.5rem;
.dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
margin-top: 0.375rem;
&.green {
background-color: #22C55E;
}
&.red {
background-color: #EF4444;
}
&.blue {
background-color: #3B82F6;
}
}
.activity-content {
flex: 1;
.title {
font-size: 0.75rem;
font-weight: 500;
color: #111827;
}
.description {
font-size: 0.75rem;
color: #6B7280;
}
.time {
font-size: 0.75rem;
color: #9CA3AF;
margin-top: 0.125rem;
}
}
}
}
}
.reminders-card {
background-color: #FEFCE8;
border-radius: 1rem;
padding: 1.25rem;
border: 2px solid #FDE047;
.reminders-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
svg {
width: 1.25rem;
height: 1.25rem;
color: #CA8A04;
}
h3 {
font-weight: 700;
color: #78350F;
font-size: 0.875rem;
}
}
p {
font-size: 0.75rem;
color: #92400E;
margin-bottom: 0.5rem;
}
button {
width: 100%;
padding: 0.5rem;
background-color: #EAB308;
color: white;
border-radius: 0.5rem;
font-size: 0.75rem;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.2s;
&:hover {
background-color: #CA8A04;
}
}
}
}
}
.submit-btn {
color: white;
&.received-btn {
background: linear-gradient(to right, #10B981, #059669);
box-shadow: 0 4px 6px -1px rgba(16, 185, 129, 0.3);
&:hover {
box-shadow: 0 10px 15px -3px rgba(16, 185, 129, 0.4);
transform: translateY(-1px);
}
}
&.made-btn {
background: linear-gradient(to right, #EF4444, #DC2626);
box-shadow: 0 4px 6px -1px rgba(239, 68, 68, 0.3);
&:hover {
box-shadow: 0 10px 15px -3px rgba(239, 68, 68, 0.4);
transform: translateY(-1px);
}
}
&.add-btn {
background: linear-gradient(to right, #3B82F6, #2563EB);
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3);
&:hover {
box-shadow: 0 10px 15px -3px rgba(59, 130, 246, 0.4);
transform: translateY(-1px);
}
}
}
.transaction-history-modal {
.submit-btn {
&.add-btn {
display: flex;
align-items: center;
gap: 10px;
}
}
}
@media (max-width: 992px) {
.main-content {
grid-template-columns: 1fr !important;
}
}
@media (max-width: 768px) {
.customer-card {
flex-direction: column;
gap: 1rem;
padding: 1rem;
.customer-left {
width: 100%;
}
.customer-right {
width: 100%;
justify-content: space-between;
}
.customer-actions {
width: 100%;
flex-wrap: wrap;
}
}
.search-filter {
gap: 0.75rem;
}
}
@media (max-width: 576px) {
.customer-cards {
padding: 12px !important;
.customer-card {
padding: 0.5rem !important;
.card-main {
flex-wrap: wrap !important;
.amount {
font-size: 1.5rem !important;
}
}
.customer-info {
.avatar {
width: 2.5rem !important;
height: 2.5rem !important;
}
}
.customer-details {
.badge {
font-size: 10px !important;
}
}
}
.card-actions {
>button {
>div {
white-space: nowrap !important;
font-size: 13px !important;
}
>svg {
width: 0.8rem !important;
height: 0.8rem !important;
}
}
}
}
.customer-header {
padding: 12px !important;
}
.tabs .tab {
padding: 0.5rem !important;
font-size: 12px !important;
font-weight: 500 !important;
}
}
@media (max-width: 480px) {
.customer-card {
.customer-avatar {
width: 44px;
height: 44px;
font-size: 1rem;
}
.customer-name {
font-size: 0.9375rem;
}
.customer-actions button {
font-size: 0.75rem;
padding: 0.5rem 0.75rem;
}
}
}
@media (max-width: 400px) {
.customer-cards {
.customer-card {
.card-main {
flex-direction: column !important;
.balance-info {
margin-top: 10px !important;
align-items: center !important;
justify-content: center !important;
gap: 10px !important;
display: flex !important;
width: 100% !important;
}
}
}
}
}

38
src/components/Modal.jsx Normal file
View File

@ -0,0 +1,38 @@
import { X } from 'lucide-react';
import './Modal.scss'
export default function Modal({
isOpen,
onClose,
title,
children,
onSubmit,
submitText = 'Submit',
submitClassName = 'submit-btn',
showFooter = true,
className = ''
}) {
if (!isOpen) return null;
return (
<div className={`modal-overlay ${className}`} onClick={onClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
{title}
<button className="close-btn" onClick={onClose}>
<X />
</button>
</div>
<div className="modal-body">
{children}
</div>
{showFooter && (
<div className="modal-footer">
<button className="cancel-btn" onClick={onClose}>Cancel</button>
<button className={submitClassName} onClick={onSubmit}>{submitText}</button>
</div>
)}
</div>
</div>
);
}

199
src/components/Modal.scss Normal file
View File

@ -0,0 +1,199 @@
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.2s ease-in-out;
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal {
background: white;
border-radius: 1.5rem;
width: 90%;
max-width: 500px;
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
animation: slideUp 0.3s ease-out;
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem;
border-bottom: 1px solid #E5E7EB;
h3 {
font-size: 1.25rem;
font-weight: 700;
color: #111827;
}
.close-btn {
padding: 0.5rem;
background: transparent;
border: none;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
&:hover {
background-color: #F3F4F6;
}
svg {
width: 1.25rem;
height: 1.25rem;
color: #6B7280;
}
}
}
.modal-body {
padding: 1.5rem;
max-height: 60vh;
overflow-y: auto;
.form-group {
margin-bottom: 1.25rem;
&:last-child {
margin-bottom: 0;
}
label {
display: block;
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
input,
select,
textarea {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid #D1D5DB;
border-radius: 0.75rem;
font-size: 0.875rem;
color: #111827;
transition: all 0.2s;
&:focus {
outline: none;
border-color: #FF6B00;
box-shadow: 0 0 0 3px rgba(255, 107, 0, 0.1);
}
&::placeholder {
color: #9CA3AF;
}
}
select {
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236B7280'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size: 1.25rem;
padding-right: 2.5rem;
}
textarea {
resize: vertical;
min-height: 4rem;
font-family: inherit;
}
}
}
.modal-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.75rem;
padding: 1.5rem;
border-top: 1px solid #E5E7EB;
button {
padding: 0.75rem 1.5rem;
border-radius: 0.75rem;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
border: none;
&.cancel-btn {
background-color: #F3F4F6;
color: #374151;
&:hover {
background-color: #E5E7EB;
}
}
&.submit-btn {
color: white;
&.gave {
background: linear-gradient(to right, #10B981, #059669);
box-shadow: 0 4px 6px -1px rgba(16, 185, 129, 0.3);
&:hover {
box-shadow: 0 10px 15px -3px rgba(16, 185, 129, 0.4);
transform: translateY(-1px);
}
}
&.got {
background: linear-gradient(to right, #EF4444, #DC2626);
box-shadow: 0 4px 6px -1px rgba(239, 68, 68, 0.3);
&:hover {
box-shadow: 0 10px 15px -3px rgba(239, 68, 68, 0.4);
transform: translateY(-1px);
}
}
&.add {
background: linear-gradient(to right, #3B82F6, #2563EB);
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3);
&:hover {
box-shadow: 0 10px 15px -3px rgba(59, 130, 246, 0.4);
transform: translateY(-1px);
}
}
}
}
}
}
}

View File

@ -0,0 +1,176 @@
export function PaymentModalContent({ customers, suppliers, selectedCustomer, onCustomerChange }) {
return (
<>
<div className="form-group">
<label>Select Customer/Supplier</label>
<select onChange={onCustomerChange}>
<option value="">Choose...</option>
{[...customers, ...suppliers].map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div>
{selectedCustomer && (
<div className="form-group">
<label>Current Balance</label>
<div style={{
padding: '1rem',
backgroundColor: selectedCustomer.type === 'credit' ? '#F0FDF4' : '#FEF2F2',
borderRadius: '0.75rem',
border: `2px solid ${selectedCustomer.type === 'credit' ? '#BBF7D0' : '#FECACA'}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<span style={{ fontSize: '0.875rem', fontWeight: 600, color: '#6B7280' }}>
{selectedCustomer.type === 'credit' ? "You'll Get" : "You'll Give"}
</span>
<span style={{ fontSize: '1.5rem', fontWeight: 700, color: selectedCustomer.type === 'credit' ? '#059669' : '#DC2626' }}>
{selectedCustomer.balance.toLocaleString()}
</span>
</div>
</div>
)}
<div className="form-group">
<label>Amount ()</label>
<input type="number" placeholder="Enter amount" />
</div>
<div className="form-group">
<label>Note</label>
<textarea placeholder="Add a note (optional)" rows="3"></textarea>
</div>
<div className="form-group">
<label>Date</label>
<input type="date" defaultValue={new Date().toISOString().split('T')[0]} />
</div>
</>
);
}
export function AddCustomerModalContent() {
return (
<>
<div className="form-group">
<label>Name</label>
<input type="text" placeholder="Enter name" />
</div>
<div className="form-group">
<label>Phone Number</label>
<input type="tel" placeholder="98755 43210" />
</div>
<div className="form-group">
<label>Opening Balance ()</label>
<input type="number" placeholder="0" />
</div>
<div className="form-group">
<label>Balance Type</label>
<select>
<option value="credit">You'll Get (Credit)</option>
<option value="debit">You'll Give (Debit)</option>
</select>
</div>
<div className="form-group">
<label>Address (Optional)</label>
<textarea placeholder="Enter address" rows="2"></textarea>
</div>
</>
);
}
export function AddEntryModalContent({ selectedCustomer }) {
return (
<>
{selectedCustomer && (
<div className="form-group">
<label>Current Balance</label>
<div style={{
padding: '1rem',
backgroundColor: selectedCustomer.type === 'credit' ? '#F0FDF4' : '#FEF2F2',
borderRadius: '0.75rem',
border: `2px solid ${selectedCustomer.type === 'credit' ? '#BBF7D0' : '#FECACA'}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<span style={{ fontSize: '0.875rem', fontWeight: 600, color: '#6B7280' }}>
{selectedCustomer.type === 'credit' ? "You'll Get" : "You'll Give"}
</span>
<span style={{ fontSize: '1.5rem', fontWeight: 700, color: selectedCustomer.type === 'credit' ? '#059669' : '#DC2626' }}>
{selectedCustomer.balance.toLocaleString()}
</span>
</div>
</div>
)}
<div className="form-group">
<label>Transaction Type</label>
<select>
<option value="credit">Payment Received (You'll Get)</option>
<option value="debit">Payment Made (You'll Give)</option>
</select>
</div>
<div className="form-group">
<label>Amount ()</label>
<input type="number" placeholder="Enter amount" />
</div>
<div className="form-group">
<label>Description</label>
<textarea placeholder="Add description (optional)" rows="3"></textarea>
</div>
<div className="form-group">
<label>Payment Method</label>
<select>
<option value="cash">Cash</option>
<option value="upi">UPI</option>
<option value="bank">Bank Transfer</option>
<option value="card">Card</option>
</select>
</div>
<div className="form-group">
<label>Date</label>
<input type="date" defaultValue={new Date().toISOString().split('T')[0]} />
</div>
</>
);
}
export function ReminderModalContent({ selectedCustomer }) {
return (
<>
<div className="form-group">
<label>Reminder Type</label>
<select>
<option value="payment">Payment Reminder</option>
<option value="followup">Follow-up</option>
<option value="custom">Custom Message</option>
</select>
</div>
<div className="form-group">
<label>Message</label>
<textarea
placeholder={`Hi ${selectedCustomer?.name}, this is a friendly reminder about your pending payment of ₹${selectedCustomer?.balance}. Please settle at your earliest convenience. Thank you!`}
rows="5"
defaultValue={`Hi ${selectedCustomer?.name}, this is a friendly reminder about your pending payment of ₹${selectedCustomer?.balance}. Please settle at your earliest convenience. Thank you!`}
></textarea>
</div>
<div className="form-group">
<label>Send Via</label>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<input type="checkbox" defaultChecked />
<span>WhatsApp</span>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<input type="checkbox" />
<span>SMS</span>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<input type="checkbox" />
<span>Email</span>
</label>
</div>
</div>
<div className="form-group">
<label>Schedule (Optional)</label>
<input type="datetime-local" />
</div>
</>
);
}

View File

@ -0,0 +1,185 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useSelector } from "react-redux";
import { selectUser } from "../redux/slices/userSlice";
import { Bell, ChevronDown, HelpCircle } from "lucide-react";
import Avatar from "./Avatar";
/**
* Props:
* - user: { name, avatar (string or JSX), email?, phone? }
* - onLogout: () => void
* - className: extra classes if needed
*/
export default function ProfileDropdown({ onLogout = () => { }, className = "", top = 'top-16' }) {
const user = useSelector(selectUser) ?? { name: "User", avatar: "U" };
const [open, setOpen] = useState(false);
const [isDark, setIsDark] = useState(() => {
if (typeof window === "undefined") return false;
const saved = localStorage.getItem("theme");
if (saved) return saved === "dark";
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
});
const rootRef = useRef(null);
const toggleOpen = useCallback(() => setOpen(v => !v), []);
const close = useCallback(() => setOpen(false), []);
// apply dark mode class on <html>
useEffect(() => {
if (typeof document !== "undefined") {
document.documentElement.classList.toggle("dark", isDark);
localStorage.setItem("theme", isDark ? "dark" : "light");
}
}, [isDark]);
// click outside & escape key handling
useEffect(() => {
function handlePointerDown(e) {
if (!rootRef.current) return;
if (!rootRef.current.contains(e.target)) close();
}
function handleKeyDown(e) {
if (e.key === "Escape") close();
// optionally: handle Enter/Space on focused menu items if you add focus management
}
document.addEventListener("mousedown", handlePointerDown);
document.addEventListener("touchstart", handlePointerDown);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("mousedown", handlePointerDown);
document.removeEventListener("touchstart", handlePointerDown);
document.removeEventListener("keydown", handleKeyDown);
};
}, [close]);
const handleLogout = useCallback(() => {
close();
onLogout();
}, [onLogout, close]);
return (
<>
{/* Header (left content + action buttons) */}
<div className="header-actions flex items-center">
<div className="help-and-bell flex items-center flex-row">
<button className="p-2 hover:bg-gray-100 rounded-xl transition-all md:flex items-center justify-center" aria-label="Help">
<HelpCircle className="w-5 h-5 text-gray-600" />
</button>
<button
className="notification-btn p-2 hover:bg-gray-100 rounded-xl transition-all relative md:flex items-center justify-center"
aria-label="Notifications"
>
<Bell className="w-5 h-5 text-gray-600" />
<span className="notification-dot absolute top-1 right-2 w-2 h-2 bg-[#FF6B00] rounded-full" />
</button>
</div>
{/* PROFILE BUTTON: single source of truth for opening dropdown */}
<div ref={rootRef} className={`${className}`}>
<button
aria-haspopup="true"
aria-expanded={open}
onClick={toggleOpen}
className="profile-btn flex items-center gap-2 hover:bg-gray-100 rounded-xl p-2 transition-all md:flex"
aria-label="Open profile menu"
>
<Avatar className="avatar w-7 h-7 md:w-9 md:h-9 bg-gradient-to-br from-[#FF6B00] to-[#FF8534] rounded-full flex items-center justify-center text-white text-sm font-semibold shadow-lg">
{user.avatar ?? (user.name ? user.name[0].toUpperCase() : "U")}
</Avatar>
<span className="text-sm font-medium avatar-name">{user.name}</span>
<ChevronDown className="w-4 h-4 text-gray-600" />
</button>
{/* DROPDOWN */}
{open && (
<div
role="menu"
aria-label="Profile menu"
className={`absolute ${top} right-0 mt-3 w-72 bg-card rounded-2xl shadow-xl p-4 z-50 bg-white`}
>
{/* Top: avatar left, details right */}
<div className="flex items-center gap-3 pb-3 border-b" style={{ borderColor: '#a1a1a1' }}>
<Avatar className="avatar w-12 h-12 bg-gradient-to-br from-[#FF6B00] to-[#FF8534] rounded-full flex items-center justify-center text-white text-sm font-semibold shadow-lg">
<div className="text-lg">{user.avatar ?? (user.name ? user.name[0].toUpperCase() : "U")}</div>
</Avatar>
<div className="flex-1">
<div className="text-sm font-semibold text-card-foreground text-[#2f2f2f]">{user.name}</div>
{user.phone && <div className="text-xs text-muted-foreground mt-1">{user.phone}</div>}
{user.email && <div className="text-xs text-muted-foreground">{user.email}</div>}
</div>
</div>
{/* Middle: display toggle */}
<div className="mt-2 mb-2">
<button
onClick={() => setIsDark(v => !v)}
type="button"
className="w-full flex items-center justify-between px-3 py-2 rounded-lg hover:bg-accent transition"
role="menuitem"
>
<div className="flex items-center gap-3">
{isDark ? (
<svg className="w-5 h-5 text-yellow-400" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
</svg>
) : (
<svg className="w-5 h-5 text-muted-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="12" cy="12" r="4" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)}
<span className="text-sm font-medium text-card-foreground text-[#2f2f2f]">Display: {isDark ? "Dark" : "Light"}</span>
</div>
<div className="" aria-hidden>
<div className={`w-10 h-5 flex items-center rounded-full transition-colors ${isDark ? "bg-blue-600" : "bg-gray-300"}`}>
<div className={`bg-white w-4 h-4 rounded-full shadow-sm transform transition-transform ${isDark ? "translate-x-5.5" : "translate-x-0.5"}`} />
</div>
</div>
</button>
</div>
{/* Bottom: logout */}
<div className="mt-2">
<button
onClick={handleLogout}
className="w-full text-left px-3 py-2 rounded-lg hover:bg-accent transition flex items-center gap-3"
role="menuitem"
>
<svg className="w-5 h-5 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M16 17l5-5-5-5" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M21 12H9" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<span className="text-sm font-medium text-red-600 dark:text-red-400">Logout</span>
</button>
</div>
</div>
)}
</div>
</div>
<style jsx>{`
.avatar-name {
display: block;
}
.help-and-bell {
display: flex;
}
@media (max-width: 576px) {
.avatar-name {
display: none !important;
}
.help-and-bell {
display: none !important;
}
}
`}
</style>
</>
);
}

186
src/components/Sidebar.jsx Normal file
View File

@ -0,0 +1,186 @@
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
import { LayoutDashboard, Receipt, Users, FileBarChart, Settings, Sparkles, Home, BookOpen, Menu, X, Wallet } from 'lucide-react';
export default function Sidebar({ isOpen, onClose, isCollapsed, onToggle, isMobile }) {
const location = useLocation();
const navigate = useNavigate();
const menuItems = [
{ path: 'dashboard', icon: LayoutDashboard, label: 'Dashboard' },
{ path: 'ledger', icon: Home, label: 'My Ledger' },
{ path: 'expenses', icon: Wallet, label: 'Expenses' },
{ path: 'cashbook', icon: BookOpen, label: 'Cashbook' },
{ path: 'transactions', icon: Receipt, label: 'Transactions' },
{ path: 'customers', icon: Users, label: 'Customers' },
{ path: 'reports', icon: FileBarChart, label: 'Reports' },
{ path: 'settings', icon: Settings, label: 'Settings' },
];
const handleMenuClick = () => {
// Close sidebar on mobile after navigation
if (isMobile) {
onClose();
}
};
const handleNavigate = () => {
if (location.pathname !== '/dashboard') {
navigate('/dashboard');
handleMenuClick();
}
}
return (
<>
{/* Mobile Overlay */}
<div
className={`fixed inset-0 lg:hidden transition-opacity duration-500
${isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
style={{
zIndex: 7,
backdropFilter: "blur(1px)",
backgroundColor: "rgba(0,0,0,0.4)",
height: "100vh",
}}
onClick={onClose}
/>
<aside
className={`bg-white border-r border-[#E5E7EB] left-0 top-0 h-screen flex flex-col transition-all duration-150 ease-in-out
${isMobile ? 'fixed' : ''}
` }
style={{
width: isCollapsed && !isMobile ? '80px' : '280px',
transform: (isMobile && !isOpen) ? 'translateX(-100%)' : 'translateX(0)',
zIndex: 8,
boxShadow: '0 0 15px rgba(0, 0, 0, 0.08)'
}}
>
{/* Logo & Toggle Button */}
<div className={`border-b border-[#E5E7EB] ${isCollapsed ? 'p-2' : 'px-3 py-2.5'} block`}>
{isCollapsed ? (
// Collapsed: Logo + Hamburger (Desktop only)
<div className="flex flex-col items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-[#FF6B00] to-[#FF8534] rounded-xl flex items-center justify-center shadow-lg">
<span className="text-white text-xl font-bold">P</span>
</div>
{!isMobile && (
<button
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
onClick={onToggle}
title="Expand sidebar"
>
<Menu className="w-5 h-5 text-gray-600" />
</button>
)}
</div>
) : (
// Expanded: Logo + Text + Toggle/Close button
<div className="flex items-center justify-between gap-3 ">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-[#FF6B00] to-[#FF8534] rounded-xl flex items-center justify-center shadow-lg cursor-pointer" onClick={handleNavigate}>
<span className="text-white text-xl font-bold text-white">P</span>
</div>
<span className="text-xl font-bold text-gray-900 whitespace-nowrap flex items-center cursor-pointer" onClick={handleNavigate}>
<div className='text-[#c1282b]'>Pozo</div>
<div className='text-[#29282e]'>Accounts</div>
</span>
</div>
{!isMobile ? (
// Desktop: Hamburger for collapse
<button
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
onClick={onToggle}
title="Collapse sidebar"
>
<Menu className="w-5 h-5 text-gray-600" />
</button>
) : (
// Mobile: X button to close
<button
className="hover:bg-gray-100 rounded-lg transition-colors
flex items-center justify-center max-md:hidden"
onClick={onClose}
title="Close sidebar"
>
<X className="w-5 h-5 text-gray-600 " />
</button>
)}
</div>
)}
</div>
{/* Main Navigation */}
<nav className="flex-1 p-4 overflow-y-auto">
<div className="space-y-1">
{menuItems.map((item) => {
const Icon = item.icon;
return (
<NavLink
key={item.path}
to={item.path}
end
onClick={handleMenuClick}
title={isCollapsed ? item.label : ''}
className={({ isActive }) =>
`w-full flex items-center gap-3 rounded-xl transition-all duration-200 ${isCollapsed ? 'justify-center px-4 py-3' : 'px-4 py-3'
} ${isActive
? 'bg-[#FF6B00] shadow-lg text-white '
: 'text-gray-600 hover:bg-[#FFF4F0] hover:text-[#FF6B00]'
}`
}
>
{({ isActive }) => (
<>
<Icon className="w-5 h-5 flex-shrink-0" strokeWidth={isActive ? 2.5 : 2} />
<span className={`text-sm transition-all duration-300 overflow-hidden whitespace-nowrap ${isCollapsed ? 'w-0 opacity-0 hidden' : 'w-auto opacity-100'
} ${isActive ? 'font-semibold' : 'font-medium'}
`}
style={{
trasnsition: 'opacity 0.5s ease-in-out',
}}>
{item.label}
</span>
</>
)}
</NavLink>
);
})}
</div>
</nav>
{/* Upgrade Section */}
<div className={`border-t border-[#E5E7EB] transition-all duration-300 overflow-hidden ${isCollapsed ? 'max-h-0 p-0' : 'max-h-96 p-4'
}`}>
<div className="bg-gradient-to-br from-[#FFF4F0] to-[#FFE8DC] rounded-xl p-3 border border-orange-100">
<div className="flex items-center justify-center gap-2 mb-2">
<Sparkles className="w-5 h-5 text-[#FF6B00]" />
<span className="text-sm font-bold text-gray-900">Upgrade to Pro</span>
</div>
<p className="text-xs text-gray-600 text-center mb-3 leading-relaxed">
Unlock premium features and advanced analytics
</p>
<button className="w-full bg-black bg-gradient-to-r from-[#FF6B00] to-[#FF8534] text-white py-2 px-3 cursor-pointer rounded-lg text-sm font-semibold hover:shadow-lg hover:shadow-orange-200 transition-all duration-200">
UPGRADE NOW
</button>
</div>
</div>
{/* Collapsed Upgrade Icon */}
{isCollapsed && (
<div className="p-4 border-t border-[#E5E7EB] flex justify-center">
<button
className="p-3 bg-gradient-to-br from-[#FFF4F0] to-[#FFE8DC] rounded-lg hover:shadow-lg transition-all"
title="Upgrade to Pro"
>
<Sparkles className="w-5 h-5 text-[#FF6B00]" />
</button>
</div>
)}
</aside>
</>
);
}

View File

@ -0,0 +1,28 @@
import React, { useState } from 'react'
const ERROR_IMG_SRC =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=='
export function ImageWithFallback(props) {
const [didError, setDidError] = useState(false)
const handleError = () => {
setDidError(true)
}
const { src, alt, style, className, ...rest } = props
return didError ? (
<div
className={`inline-block bg-gray-100 text-center align-middle ${className ?? ''}`}
style={style}
>
<div className="flex items-center justify-center w-full h-full">
<img src={ERROR_IMG_SRC} alt="Error loading image" {...rest} data-original-url={src} />
</div>
</div>
) : (
<img src={src} alt={alt} className={className} style={style} {...rest} onError={handleError} />
)
}

View File

@ -0,0 +1,67 @@
"use client";
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion@1.2.3";
import { ChevronDownIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function Accordion({
...props
}) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
}
function AccordionItem({
className,
...props
}) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
);
}
function AccordionTrigger({
className,
children,
...props
}) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
function AccordionContent({
className,
children,
...props
}) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@ -0,0 +1,158 @@
"use client";
import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog@1.1.6";
import { cn } from "./utils";
import { buttonVariants } from "./button";
function AlertDialog({
...props
}) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
}
function AlertDialogTrigger({
...props
}) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
);
}
function AlertDialogPortal({
...props
}) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
);
}
function AlertDialogOverlay({
className,
...props
}) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function AlertDialogContent({
className,
...props
}) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className,
)}
{...props}
/>
</AlertDialogPortal>
);
}
function AlertDialogHeader({
className,
...props
}) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function AlertDialogFooter({
className,
...props
}) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
function AlertDialogTitle({
className,
...props
}) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
);
}
function AlertDialogDescription({
className,
...props
}) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function AlertDialogAction({
className,
...props
}) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
);
}
function AlertDialogCancel({
className,
...props
}) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
);
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};

View File

@ -0,0 +1,67 @@
import * as React from "react";
import { cva } from "class-variance-authority@0.7.1";
import { cn } from "./utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Alert({
className,
variant,
...props
}) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
);
}
function AlertTitle({ className, ...props }) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className,
)}
{...props}
/>
);
}
function AlertDescription({
className,
...props
}) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className,
)}
{...props}
/>
);
}
export { Alert, AlertTitle, AlertDescription };

View File

@ -0,0 +1,12 @@
"use client";
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio@1.1.2";
function AspectRatio({
...props
}) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
}
export { AspectRatio };

View File

@ -0,0 +1,54 @@
"use client";
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar@1.1.3";
import { cn } from "./utils";
function Avatar({
className,
...props
}) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-10 shrink-0 overflow-hidden rounded-full",
className,
)}
{...props}
/>
);
}
function AvatarImage({
className,
...props
}) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
);
}
function AvatarFallback({
className,
...props
}) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className,
)}
{...props}
/>
);
}
export { Avatar, AvatarImage, AvatarFallback };

View File

@ -0,0 +1,47 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot@1.1.2";
import { cva } from "class-variance-authority@0.7.1";
import { cn } from "./utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Badge({
className,
variant,
asChild = false,
...props
} &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span";
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
);
}
export { Badge, badgeVariants };

View File

@ -0,0 +1,96 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "./utils";
function Breadcrumb({ ...props }) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
}
function BreadcrumbList({ className, ...props }) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
)}
{...props}
/>
);
}
function BreadcrumbItem({ className, ...props }) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
);
}
function BreadcrumbLink({ asChild, className, ...props }) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
);
}
function BreadcrumbPage({ className, ...props }) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
);
}
function BreadcrumbSeparator({ children, className, ...props }) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
}
function BreadcrumbEllipsis({ className, ...props }) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
);
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};

View File

@ -0,0 +1,59 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot@1.1.2";
import { cva } from "class-variance-authority@0.7.1";
import { cn } from "./utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background text-foreground hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9 rounded-md",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
} &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };

View File

@ -0,0 +1,76 @@
"use client";
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react@0.487.0";
import { DayPicker } from "react-day-picker@8.10.1";
import { cn } from "./utils";
import { buttonVariants } from "./button";
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row gap-2",
month: "flex flex-col gap-4",
caption: "flex justify-center pt-1 relative items-center w-full",
caption_label: "text-sm font-medium",
nav: "flex items-center gap-1",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-x-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md",
),
day: cn(
buttonVariants({ variant: "ghost" }),
"size-8 p-0 font-normal aria-selected:opacity-100",
),
day_range_start:
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
day_range_end:
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ className, ...props }) => (
<ChevronLeft className={cn("size-4", className)} {...props} />
),
IconRight: ({ className, ...props }) => (
<ChevronRight className={cn("size-4", className)} {...props} />
),
}}
{...props}
/>
);
}
export { Calendar };

View File

@ -0,0 +1,93 @@
import * as React from "react";
import { cn } from "./utils";
function Card({ className, ...props }) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border",
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 pt-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }) {
return (
<h4
data-slot="card-title"
className={cn("leading-none", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }) {
return (
<p
data-slot="card-description"
className={cn("text-muted-foreground", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }) {
return (
<div
data-slot="card-content"
className={cn("px-6 [&:last-child]:pb-6", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 pb-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};

View File

@ -0,0 +1,212 @@
"use client";
import * as React from "react";
import useEmblaCarousel, from "embla-carousel-react@8.6.0";
import { ArrowLeft, ArrowRight } from "lucide-react@0.487.0";
import { cn } from "./utils";
import { Button } from "./button";
const CarouselContext = React.createContext(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api) => {
if (!api) return;
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) return;
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) return;
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
}
function CarouselContent({ className, ...props }) {
const { carouselRef, orientation } = useCarousel();
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className,
)}
{...props}
/>
</div>
);
}
function CarouselItem({ className, ...props }) {
const { orientation } = useCarousel();
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className,
)}
{...props}
/>
);
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
);
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}) {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
);
}
;

346
src/components/ui/chart.jsx Normal file
View File

@ -0,0 +1,346 @@
"use client";
import * as React from "react";
import * as RechartsPrimitive from "recharts@2.15.2";
import { cn } from "./utils";
// Format: { THEME_NAME }
const THEMES = { light: "", dark: ".dark" };
icon?: React.ComponentType;
} & (
| { color?: string; theme?: never }
| { color?: never; theme }
);
};
};
const ChartContext = React.createContext(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
function ChartContainer({
id,
className,
children,
config,
...props
} & {
config;
children["children"];
}) {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
}
const ChartStyle = ({ id, config }: { id; config }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color,
);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`,
)
.join("\n"),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
} &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}) {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
);
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className,
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center",
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
},
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
}
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center",
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
const ChartLegend = RechartsPrimitive.Legend;
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
} &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}) {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className,
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config,
payload,
key,
) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined;
let configLabelKey = key;
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
};

View File

@ -0,0 +1,33 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox@1.1.4";
import { CheckIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function Checkbox({
className,
...props
}) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border bg-input-background dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };

View File

@ -0,0 +1,33 @@
"use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible@1.1.3";
function Collapsible({
...props
}) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
}
function CollapsibleTrigger({
...props
}) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
);
}
function CollapsibleContent({
...props
}) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
);
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@ -0,0 +1,177 @@
"use client";
import * as React from "react";
import { Command as CommandPrimitive } from "cmdk@1.1.1";
import { SearchIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "./dialog";
function Command({
className,
...props
}) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className,
)}
{...props}
/>
);
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
...props
} & {
title?: string;
description?: string;
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
}
function CommandInput({
className,
...props
}) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
);
}
function CommandList({
className,
...props
}) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className,
)}
{...props}
/>
);
}
function CommandEmpty({
...props
}) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
);
}
function CommandGroup({
className,
...props
}) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className,
)}
{...props}
/>
);
}
function CommandSeparator({
className,
...props
}) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
);
}
function CommandItem({
className,
...props
}) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function CommandShortcut({
className,
...props
}) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View File

@ -0,0 +1,252 @@
"use client";
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu@2.2.6";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function ContextMenu({
...props
}) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
}
function ContextMenuTrigger({
...props
}) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
);
}
function ContextMenuGroup({
...props
}) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
);
}
function ContextMenuPortal({
...props
}) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
);
}
function ContextMenuSub({
...props
}) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
}
function ContextMenuRadioGroup({
...props
}) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
);
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
} & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
);
}
function ContextMenuSubContent({
className,
...props
}) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
function ContextMenuContent({
className,
...props
}) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
);
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
} & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
);
}
function ContextMenuRadioItem({
className,
children,
...props
}) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
);
}
function ContextMenuLabel({
className,
inset,
...props
} & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function ContextMenuSeparator({
className,
...props
}) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function ContextMenuShortcut({
className,
...props
}) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};

View File

@ -0,0 +1,136 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog@1.1.6";
import { XIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function Dialog({
...props
}) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogTrigger({
...props
}) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({
...props
}) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({
...props
}) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
className,
...props
}) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
...props
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function DialogFooter({ className, ...props }) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
function DialogTitle({
className,
...props
}) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};

View File

@ -0,0 +1,132 @@
"use client";
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul@1.1.2";
import { cn } from "./utils";
function Drawer({
...props
}) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
}
function DrawerTrigger({
...props
}) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
}
function DrawerPortal({
...props
}) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
}
function DrawerClose({
...props
}) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
}
function DrawerOverlay({
className,
...props
}) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function DrawerContent({
className,
children,
...props
}) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className,
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
);
}
function DrawerHeader({ className, ...props }) {
return (
<div
data-slot="drawer-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function DrawerFooter({ className, ...props }) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function DrawerTitle({
className,
...props
}) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function DrawerDescription({
className,
...props
}) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};

View File

@ -0,0 +1,257 @@
"use client";
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu@2.1.6";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function DropdownMenu({
...props
}) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
}) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuTrigger({
...props
}) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({
...props
}) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
} & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
...props
}) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
} & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
}) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
} & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};

163
src/components/ui/form.jsx Normal file
View File

@ -0,0 +1,163 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label@2.1.2";
import { Slot } from "@radix-ui/react-slot@1.1.2";
import {
Controller,
FormProvider,
useFormContext,
useFormState,
} from "react-hook-form@7.55.0";
import { cn } from "./utils";
import { Label } from "./label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name;
};
const FormFieldContext = React.createContext(
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
};
const FormItemContext = React.createContext(
{} as FormItemContextValue,
);
function FormItem({ className, ...props }) {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
);
}
function FormLabel({
className,
...props
}) {
const { error, formItemId } = useFormField();
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
}
function FormControl({ ...props }) {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
}
function FormDescription({ className, ...props }) {
const { formDescriptionId } = useFormField();
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function FormMessage({ className, ...props }) {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? "") : props.children;
if (!body) {
return null;
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
);
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};

View File

@ -0,0 +1,44 @@
"use client";
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card@1.1.6";
import { cn } from "./utils";
function HoverCard({
...props
}) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
}
function HoverCardTrigger({
...props
}) {
return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
);
}
function HoverCardContent({
className,
align = "center",
sideOffset = 4,
...props
}) {
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
<HoverCardPrimitive.Content
data-slot="hover-card-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className,
)}
{...props}
/>
</HoverCardPrimitive.Portal>
);
}
export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@ -0,0 +1,77 @@
"use client";
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp@1.4.2";
import { MinusIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function InputOTP({
className,
containerClassName,
...props
} & {
containerClassName?: string;
}) {
return (
<OTPInput
data-slot="input-otp"
containerClassName={cn(
"flex items-center gap-2 has-disabled:opacity-50",
containerClassName,
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
);
}
function InputOTPGroup({ className, ...props }) {
return (
<div
data-slot="input-otp-group"
className={cn("flex items-center gap-1", className)}
{...props}
/>
);
}
function InputOTPSlot({
index,
className,
...props
} & {
index;
}) {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
return (
<div
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm bg-input-background transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
</div>
)}
</div>
);
}
function InputOTPSeparator({ ...props }) {
return (
<div data-slot="input-otp-separator" role="separator" {...props}>
<MinusIcon />
</div>
);
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View File

@ -0,0 +1,22 @@
import * as React from "react";
import { cn } from "./utils";
function Input({ className, type, ...props }) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base bg-input-background transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
/>
);
}
export { Input };

View File

@ -0,0 +1,25 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label@2.1.2";
import { cn } from "./utils";
function Label({
className,
...props
}) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className,
)}
{...props}
/>
);
}
export { Label };

View File

@ -0,0 +1,276 @@
"use client";
import * as React from "react";
import * as MenubarPrimitive from "@radix-ui/react-menubar@1.1.6";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function Menubar({
className,
...props
}) {
return (
<MenubarPrimitive.Root
data-slot="menubar"
className={cn(
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
className,
)}
{...props}
/>
);
}
function MenubarMenu({
...props
}) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
}
function MenubarGroup({
...props
}) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
}
function MenubarPortal({
...props
}) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
}
function MenubarRadioGroup({
...props
}) {
return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
);
}
function MenubarTrigger({
className,
...props
}) {
return (
<MenubarPrimitive.Trigger
data-slot="menubar-trigger"
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
className,
)}
{...props}
/>
);
}
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</MenubarPortal>
);
}
function MenubarItem({
className,
inset,
variant = "default",
...props
} & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<MenubarPrimitive.Item
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function MenubarCheckboxItem({
className,
children,
checked,
...props
}) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
);
}
function MenubarRadioItem({
className,
children,
...props
}) {
return (
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
);
}
function MenubarLabel({
className,
inset,
...props
} & {
inset?: boolean;
}) {
return (
<MenubarPrimitive.Label
data-slot="menubar-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function MenubarSeparator({
className,
...props
}) {
return (
<MenubarPrimitive.Separator
data-slot="menubar-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function MenubarShortcut({
className,
...props
}) {
return (
<span
data-slot="menubar-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
function MenubarSub({
...props
}) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
}
function MenubarSubTrigger({
className,
inset,
children,
...props
} & {
inset?: boolean;
}) {
return (
<MenubarPrimitive.SubTrigger
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
);
}
function MenubarSubContent({
className,
...props
}) {
return (
<MenubarPrimitive.SubContent
data-slot="menubar-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
export {
Menubar,
MenubarPortal,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarSeparator,
MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
};

View File

@ -0,0 +1,168 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu@1.2.5";
import { cva } from "class-variance-authority@0.7.1";
import { ChevronDownIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function NavigationMenu({
className,
children,
viewport = true,
...props
} & {
viewport?: boolean;
}) {
return (
<NavigationMenuPrimitive.Root
data-slot="navigation-menu"
data-viewport={viewport}
className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className,
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
);
}
function NavigationMenuList({
className,
...props
}) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
"group flex flex-1 list-none items-center justify-center gap-1",
className,
)}
{...props}
/>
);
}
function NavigationMenuItem({
className,
...props
}) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn("relative", className)}
{...props}
/>
);
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
);
function NavigationMenuTrigger({
className,
children,
...props
}) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
);
}
function NavigationMenuContent({
className,
...props
}) {
return (
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className,
)}
{...props}
/>
);
}
function NavigationMenuViewport({
className,
...props
}) {
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center",
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
{...props}
/>
</div>
);
}
function NavigationMenuLink({
className,
...props
}) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function NavigationMenuIndicator({
className,
...props
}) {
return (
<NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator"
className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className,
)}
{...props}
>
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator>
);
}
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
};

View File

@ -0,0 +1,125 @@
import * as React from "react";
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react@0.487.0";
import { cn } from "./utils";
import { Button, buttonVariants } from "./button";
function Pagination({ className, ...props }) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
);
}
function PaginationContent({
className,
...props
}) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
);
}
function PaginationItem({ ...props }) {
return <li data-slot="pagination-item" {...props} />;
}
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">;
function PaginationLink({
className,
isActive,
size = "icon",
...props
}) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className,
)}
{...props}
/>
);
}
function PaginationPrevious({
className,
...props
}) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
);
}
function PaginationNext({
className,
...props
}) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
);
}
function PaginationEllipsis({
className,
...props
}) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
);
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
};

View File

@ -0,0 +1,48 @@
"use client";
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover@1.1.6";
import { cn } from "./utils";
function Popover({
...props
}) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}
function PopoverTrigger({
...props
}) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
);
}
function PopoverAnchor({
...props
}) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@ -0,0 +1,31 @@
"use client";
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress@1.1.2";
import { cn } from "./utils";
function Progress({
className,
value,
...props
}) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
);
}
export { Progress };

View File

@ -0,0 +1,45 @@
"use client";
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group@1.2.3";
import { CircleIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function RadioGroup({
className,
...props
}) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
);
}
function RadioGroupItem({
className,
...props
}) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
}
export { RadioGroup, RadioGroupItem };

View File

@ -0,0 +1,56 @@
"use client";
import * as React from "react";
import { GripVerticalIcon } from "lucide-react@0.487.0";
import * as ResizablePrimitive from "react-resizable-panels@2.1.7";
import { cn } from "./utils";
function ResizablePanelGroup({
className,
...props
}) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className,
)}
{...props}
/>
);
}
function ResizablePanel({
...props
}) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
}
function ResizableHandle({
withHandle,
className,
...props
} & {
withHandle?: boolean;
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View File

@ -0,0 +1,58 @@
"use client";
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area@1.2.3";
import { cn } from "./utils";
function ScrollArea({
className,
children,
...props
}) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = "vertical",
...props
}) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
export { ScrollArea, ScrollBar };

View File

@ -0,0 +1,190 @@
"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select@2.1.6";
import {
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "lucide-react@0.487.0";
import { cn } from "./utils";
function Select({
...props
}) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = "default",
children,
...props
} & {
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-full items-center justify-between gap-2 rounded-md border bg-input-background px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = "popper",
...props
}) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({
className,
...props
}) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
);
}
function SelectItem({
className,
children,
...props
}) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function SelectScrollUpButton({
className,
...props
}) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};

View File

@ -0,0 +1,28 @@
"use client";
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator@1.1.2";
import { cn } from "./utils";
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}) {
return (
<SeparatorPrimitive.Root
data-slot="separator-root"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className,
)}
{...props}
/>
);
}
export { Separator };

139
src/components/ui/sheet.jsx Normal file
View File

@ -0,0 +1,139 @@
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog@1.1.6";
import { XIcon } from "lucide-react@0.487.0";
import { cn } from "./utils";
function Sheet({ ...props }) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetTrigger({
...props
}) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose({
...props
}) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({
...props
}) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({
className,
...props
}) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = "right",
...props
} & {
side?: "top" | "right" | "bottom" | "left";
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className,
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function SheetFooter({ className, ...props }) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function SheetTitle({
className,
...props
}) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function SheetDescription({
className,
...props
}) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};

View File

@ -0,0 +1,724 @@
"use client";
import * as React from "react";
import { Slot } from "@radix-ui/react-slot@1.1.2";
import { VariantProps, cva } from "class-variance-authority@0.7.1";
import { PanelLeftIcon } from "lucide-react@0.487.0";
import { useIsMobile } from "./use-mobile";
import { cn } from "./utils";
import { Button } from "./button";
import { Input } from "./input";
import { Separator } from "./separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "./sheet";
import { Skeleton } from "./skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
open;
setOpen: (open) => void;
openMobile;
setOpenMobile: (open) => void;
isMobile;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
} & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open) => void;
}) {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value | ((value) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo(SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
}
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className,
)}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
} & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className,
)}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
}
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
className="group peer text-sidebar-foreground hidden md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
>
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot="sidebar-gap"
className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
)}
/>
<div
data-slot="sidebar-container"
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className,
)}
{...props}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
>
{children}
</div>
</div>
</div>
);
}
function SidebarTrigger({
className,
onClick,
...props
}) {
const { toggleSidebar } = useSidebar();
return (
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
className={cn("size-7", className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
}
function SidebarRail({ className, ...props }) {
const { toggleSidebar } = useSidebar();
return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className,
)}
{...props}
/>
);
}
function SidebarInset({ className, ...props }) {
return (
<main
data-slot="sidebar-inset"
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className,
)}
{...props}
/>
);
}
function SidebarInput({
className,
...props
}) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
/>
);
}
function SidebarHeader({ className, ...props }) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarFooter({ className, ...props }) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarSeparator({
className,
...props
}) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
);
}
function SidebarContent({ className, ...props }) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className,
)}
{...props}
/>
);
}
function SidebarGroup({ className, ...props }) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
);
}
function SidebarGroupLabel({
className,
asChild = false,
...props
} & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div";
return (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
)}
{...props}
/>
);
}
function SidebarGroupAction({
className,
asChild = false,
...props
} & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
function SidebarGroupContent({
className,
...props
}) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
);
}
function SidebarMenu({ className, ...props }) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
);
}
function SidebarMenuItem({ className, ...props }) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
);
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function SidebarMenuButton({
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
} & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
);
}
function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
} & {
asChild?: boolean;
showOnHover?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className,
)}
{...props}
/>
);
}
function SidebarMenuBadge({
className,
...props
}) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
} & {
showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
}
}
/>
</div>
);
}
function SidebarMenuSub({ className, ...props }) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
function SidebarMenuSubItem({
className,
...props
}) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
);
}
function SidebarMenuSubButton({
asChild = false,
size = "md",
isActive = false,
className,
...props
} & {
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
};

View File

@ -0,0 +1,13 @@
import { cn } from "./utils";
function Skeleton({ className, ...props }) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
);
}
export { Skeleton };

View File

@ -0,0 +1,62 @@
"use client";
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider@1.2.3";
import { cn } from "./utils";
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}) {
const _values = React.useMemo(
() =>
Array.isArray(value)
? value.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max],
);
return (
<SliderPrimitive.Root
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className,
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
className={cn(
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-4 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
)}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn(
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
)}
/>
</SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className="border-primary bg-background ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Root>
);
}
export { Slider };

View File

@ -0,0 +1,25 @@
"use client";
import { useTheme } from "next-themes@0.4.6";
import { Toaster as Sonner, ToasterProps } from "sonner@2.0.3";
const Toaster = ({ ...props }) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
}
}
{...props}
/>
);
};
export { Toaster };

View File

@ -0,0 +1,31 @@
"use client";
import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch@1.1.3";
import { cn } from "./utils";
function Switch({
className,
...props
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-switch-background focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-card dark:data-[state=unchecked]:bg-card-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitive.Root>
);
}
export { Switch };

116
src/components/ui/table.jsx Normal file
View File

@ -0,0 +1,116 @@
"use client";
import * as React from "react";
import { cn } from "./utils";
function Table({ className, ...props }) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
);
}
function TableHeader({ className, ...props }) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
);
}
function TableBody({ className, ...props }) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
);
}
function TableFooter({ className, ...props }) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className,
)}
{...props}
/>
);
}
function TableRow({ className, ...props }) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className,
)}
{...props}
/>
);
}
function TableHead({ className, ...props }) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
);
}
function TableCell({ className, ...props }) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
);
}
function TableCaption({
className,
...props
}) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
);
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

View File

@ -0,0 +1,66 @@
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs@1.1.3";
import { cn } from "./utils";
function Tabs({
className,
...props
}) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
);
}
function TabsList({
className,
...props
}) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-xl p-[3px] flex",
className,
)}
{...props}
/>
);
}
function TabsTrigger({
className,
...props
}) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-card dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-xl border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function TabsContent({
className,
...props
}) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@ -0,0 +1,18 @@
import * as React from "react";
import { cn } from "./utils";
function Textarea({ className, ...props }) {
return (
<textarea
data-slot="textarea"
className={cn(
"resize-none border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-input-background px-3 py-2 text-base transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
{...props}
/>
);
}
export { Textarea };

View File

@ -0,0 +1,72 @@
"use client";
import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group@1.1.2";
from "class-variance-authority@0.7.1";
import { cn } from "./utils";
import { toggleVariants } from "./toggle";
const ToggleGroupContext = React.createContext
>({
size: "default",
variant: "default",
});
function ToggleGroup({
className,
variant,
size,
children,
...props
} &
VariantProps<typeof toggleVariants>) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
className={cn(
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
className,
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
}
function ToggleGroupItem({
className,
children,
variant,
size,
...props
} &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className,
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
}
export { ToggleGroup, ToggleGroupItem };

View File

@ -0,0 +1,47 @@
"use client";
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle@1.1.2";
import { cva } from "class-variance-authority@0.7.1";
import { cn } from "./utils";
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Toggle({
className,
variant,
size,
...props
} &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Toggle, toggleVariants };

View File

@ -0,0 +1,61 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip@1.1.8";
import { cn } from "./utils";
function TooltipProvider({
delayDuration = 0,
...props
}) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
);
}
function Tooltip({
...props
}) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({
...props
}) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@ -0,0 +1,22 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState(
undefined,
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}

View File

@ -0,0 +1,7 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs) {
return twMerge(clsx(inputs));
}

107
src/hooks/useFetch.js Normal file
View File

@ -0,0 +1,107 @@
import { useState, useEffect, useCallback, useRef } from 'react';
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const abortControllerRef = useRef(null);
const {
method = 'GET',
headers = {},
body = null,
skip = false, // Don't auto-fetch on mount
...restOptions
} = options;
const fetchData = useCallback(async (dynamicBody = null, dynamicOptions = {}) => {
setLoading(true);
setError(null);
// Cancel previous request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
try {
const finalBody = dynamicBody || body;
const finalMethod = dynamicOptions.method || method;
const config = {
method: finalMethod,
headers: {
'Content-Type': 'application/json',
...headers,
...dynamicOptions.headers
},
signal: abortControllerRef.current.signal,
...restOptions,
...dynamicOptions
};
// Add body for POST, PUT, PATCH, DELETE (if needed)
if (finalBody && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(finalMethod)) {
config.body = typeof finalBody === 'string'
? finalBody
: JSON.stringify(finalBody);
}
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Handle empty responses (like 204 No Content)
const contentType = response.headers.get('content-type');
let result = null;
if (contentType && contentType.includes('application/json')) {
result = await response.json();
} else if (response.status !== 204) {
result = await response.text();
}
setData(result);
setError(null);
return result;
} catch (err) {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
setError(err.message);
throw err;
}
} finally {
setLoading(false);
}
}, [url, method, body, JSON.stringify(headers), JSON.stringify(restOptions)]);
useEffect(() => {
if (!skip && method === 'GET') {
fetchData();
}
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [fetchData, skip, method]);
const refetch = useCallback(() => {
return fetchData();
}, [fetchData]);
return {
data,
loading,
error,
refetch,
execute: fetchData // Alias for manual calls
};
}
export default useFetch;

141
src/index.css Normal file
View File

@ -0,0 +1,141 @@
@import "tailwindcss";
:root {
--font-size: 16px;
--background: #fff;
--foreground: oklch(.145 0 0);
--card: #fff;
--card-foreground: oklch(.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(.145 0 0);
--primary: #FF6B00;
--primary-foreground: #FFFFFF;
--secondary: #FFFFFF;
--secondary-foreground: #1F2937;
--accent: #F5F5F5;
--muted: #ececf0;
--muted-foreground: #717182;
--accent-foreground: #030213;
--destructive: #d4183d;
--destructive-foreground: #fff;
--border: #E5E7EB;
--input: transparent;
--input-background: #f3f3f5;
--switch-background: #cbced4;
--font-weight-medium: 500;
--font-weight-normal: 400;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--ring: oklch(.708 0 0);
--chart-1: oklch(.646 .222 41.116);
--chart-2: oklch(.6 .118 184.704);
--chart-3: oklch(.398 .07 227.392);
--chart-4: oklch(.828 .189 84.429);
--chart-5: oklch(.769 .188 70.08);
--radius: .75rem;
--sidebar: oklch(.985 0 0);
--sidebar-foreground: oklch(.145 0 0);
--sidebar-primary: #030213;
--sidebar-primary-foreground: oklch(.985 0 0);
--sidebar-accent: oklch(.97 0 0);
--sidebar-accent-foreground: oklch(.205 0 0);
--sidebar-border: oklch(.922 0 0);
--sidebar-ring: oklch(.708 0 0);
}
* {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
/* Mobile Responsive Global Styles */
body {
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Prevent zoom on input focus (iOS) */
@media (max-width: 768px) {
input,
select,
textarea {
font-size: 16px !important;
}
}
@media (max-width: 500px) {
input,
select,
textarea {
font-size: 12px !important;
white-space: nowrap;
}
}
/* Touch-friendly buttons */
@media (max-width: 1024px) {
button,
a {
min-height: 30px;
min-width: 44px;
}
}
/* Smooth scroll for mobile */
* {
-webkit-overflow-scrolling: touch;
}
/* Hide scrollbar but keep functionality */
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.dark {
--background: oklch(.145 0 0);
--foreground: oklch(.985 0 0);
--card: oklch(.145 0 0);
--card-foreground: oklch(.985 0 0);
--popover: oklch(.145 0 0);
--popover-foreground: oklch(.985 0 0);
--primary: oklch(.985 0 0);
--primary-foreground: oklch(.205 0 0);
--secondary: oklch(.269 0 0);
--secondary-foreground: oklch(.985 0 0);
--muted: oklch(.269 0 0);
--muted-foreground: oklch(.708 0 0);
--accent: oklch(.269 0 0);
--accent-foreground: oklch(.985 0 0);
--destructive: oklch(.396 .141 25.723);
--destructive-foreground: oklch(.637 .237 25.331);
--border: oklch(.269 0 0);
--input: oklch(.269 0 0);
--ring: oklch(.439 0 0);
--font-weight-medium: 500;
--font-weight-normal: 400;
--chart-1: oklch(.488 .243 264.376);
--chart-2: oklch(.696 .17 162.48);
--chart-3: oklch(.769 .188 70.08);
--chart-4: oklch(.627 .265 303.9);
--chart-5: oklch(.645 .246 16.439);
--sidebar: oklch(.205 0 0);
--sidebar-foreground: oklch(.985 0 0);
--sidebar-primary: oklch(.488 .243 264.376);
--sidebar-primary-foreground: oklch(.985 0 0);
--sidebar-accent: oklch(.269 0 0);
--sidebar-accent-foreground: oklch(.985 0 0);
--sidebar-border: oklch(.269 0 0);
--sidebar-ring: oklch(.439 0 0);
}
html {
font-size: var(--font-size);
}

101
src/layouts/Layout.jsx Normal file
View File

@ -0,0 +1,101 @@
import { useEffect, useState } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import { Menu } from 'lucide-react';
import Sidebar from '../components/Sidebar';
import { isMobile as mobile } from 'react-device-detect';
export default function Layout({ user }) {
const location = useLocation();
const navigate = useNavigate();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [])
const handleToggleSidebar = () => {
if (!isMobile) {
// Desktop: Toggle collapse/expand
setIsSidebarCollapsed(prev => !prev);
} else {
// Mobile: Toggle open/close
setIsSidebarCollapsed(false)
setIsMobileMenuOpen(!isMobileMenuOpen);
}
};
const handleNavigate = () => {
if (location.pathname !== '/dashboard') {
navigate('/dashboard');
setIsMobileMenuOpen(false);
}
}
console.log(mobile, "isMobile")
return (
<div className="flex min-h-screen bg-[#F5F5F5]"
style={{
display: "flex", alignItems: "flex-start", width: "100vw",
height: "100vh", overflow: "hidden"
}}
>
<Sidebar
isOpen={isMobileMenuOpen}
onClose={() => setIsMobileMenuOpen(false)}
isCollapsed={isSidebarCollapsed}
onToggle={handleToggleSidebar}
isMobile={isMobile}
/>
{/* Mobile Header with Hamburger - Only show when sidebar is closed */}
{isMobile && (
<div className="fixed top-0 left-0 right-0 bg-white border-b border-[#E5E7EB] px-4 py-2 flex items-center shadow-sm justify-between" style={{ zIndex: 6 }}>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-[#FF6B00] to-[#FF8534] rounded-xl flex items-center justify-center shadow-lg cursor-pointer" onClick={handleNavigate}>
<span className="text-white text-xl font-bold">P</span>
</div>
<span className="text-xl font-bold text-gray-900 whitespace-nowrap flex items-center cursor-pointer" onClick={handleNavigate}>
<div className='text-[#c1282b]'>Pozo</div>
<div className='text-[#29282e]'>Accounts</div>
</span>
</div>
<button
onClick={handleToggleSidebar}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<Menu className="w-6 h-6 text-gray-700" />
</button>
</div>
)}
<main
className={`w-full pt-16 ${!mobile ? 'md:pt-0' : ''} transition-all duration-500 ease-in-out`}
// style={{
// marginLeft: isDesktop ? (isSidebarCollapsed ? '80px' : '256px') : '0'
// }}
>
<div className={`p-4 md:p-6 lg:p-8 w-full max-md:pb-20 ${mobile ? 'sm:pb-20' : ''}`} style={{
height: "100vh",
overflow: "auto",
scrollbarWidth: "thin",
scrollBehavior: "smooth",
WebkitOverflowScrolling: "touch"
}}>
<Outlet />
</div>
</main>
</div>
);
}

View File

@ -0,0 +1,68 @@
import { useState, useEffect } from 'react';
import { Outlet } from 'react-router-dom';
import { Menu } from 'lucide-react';
import Sidebar from '../components/Sidebar';
export default function MainLayout() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isDesktop, setIsDesktop] = useState(false);
useEffect(() => {
const handleResize = () => {
setIsDesktop(window.innerWidth >= 1024);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const handleToggleCollapse = () => {
setIsSidebarCollapsed(!isSidebarCollapsed);
};
return (
<div className="flex min-h-screen bg-[#F5F5F5]">
<Sidebar
isOpen={isMobileMenuOpen}
onClose={() => setIsMobileMenuOpen(false)}
isCollapsed={isSidebarCollapsed}
onToggleCollapse={handleToggleCollapse}
/>
{/* Mobile Header with Hamburger */}
<div className="lg:hidden fixed top-0 left-0 right-0 bg-white border-b border-[#E5E7EB] px-4 py-3 flex items-center justify-between z-30 shadow-sm">
<button
onClick={() => setIsMobileMenuOpen(true)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<Menu className="w-6 h-6 text-gray-700" />
</button>
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-gradient-to-br from-[#FF6B00] to-[#FF8534] rounded-lg flex items-center justify-center shadow-lg">
<span className="text-white text-sm font-bold">P</span>
</div>
<span className="text-lg font-bold text-gray-900 flex items-center">
<div className='text-[#c1282b]'>Pozo</div>
<div className='text-[#29282e]'>Accounts</div>
</span>
</div>
<div className="w-10"></div>
</div>
<main
className="w-full pt-16 lg:pt-0 transition-all duration-300"
style={{
// marginLeft: isDesktop ? (isSidebarCollapsed ? '80px' : '256px') : '0'
}}
>
<div className="p-4 md:p-6 lg:p-8 w-full " >
<Outlet />
</div>
</main>
</div>
);
}

15
src/main.jsx Normal file
View File

@ -0,0 +1,15 @@
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import "./smooth-scroll.css";
import { BrowserRouter } from "react-router-dom";
import { Provider } from 'react-redux';
import { store } from "./redux/store.js";
createRoot(document.getElementById("root")).render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
);

View File

@ -0,0 +1,603 @@
import { useState } from 'react';
import { Calendar, ChevronDown, ChevronRight, FileText, X, Upload, Edit, Trash2, Plus, Home as HomeIcon, BookOpen, ArrowUpRight, ArrowDownLeft, TrendingUp, IndianRupee } from 'lucide-react';
import BreadCrumb from '../../components/BreadCrumb';
import './Cashbook.scss';
import { isMobile } from 'react-device-detect';
export default function Cashbook({ user }) {
const [activeTab, setActiveTab] = useState('all');
const [showInModal, setShowInModal] = useState(false);
const [showOutModal, setShowOutModal] = useState(false);
const [selectedEntry, setSelectedEntry] = useState(null);
const [showDetailModal, setShowDetailModal] = useState(false);
const [selectedInFile, setSelectedInFile] = useState(null);
const [selectedOutFile, setSelectedOutFile] = useState(null);
// Sample data
const entries = [
{
id: 1,
time: '10:22 AM',
date: '03 Dec 2025',
type: 'out',
paymentType: 'ONLINE',
description: 'Office Office OfficeS Office upplies Purchase',
category: 'Expenses',
amount: 1200,
photo: null
},
{
id: 2,
time: '02:30 PM',
date: '03 Dec 2025',
type: 'in',
paymentType: 'CASH',
description: 'Sales Revenue',
category: 'Income',
amount: 5000,
photo: null
},
{
id: 3,
time: '04:15 PM',
date: '03 Dec 2025',
type: 'out',
paymentType: 'CASH',
description: 'Petty Cash Expense',
category: 'Miscellaneous',
amount: 350,
photo: null
},
{
id: 4,
time: '05:45 PM',
date: '03 Dec 2025',
type: 'in',
paymentType: 'ONLINE',
description: 'Client Payment Received',
category: 'Income',
amount: 8500,
photo: null
}
];
const totalIn = entries.filter(e => e.type === 'in').reduce((sum, e) => sum + e.amount, 0);
const totalOut = entries.filter(e => e.type === 'out').reduce((sum, e) => sum + e.amount, 0);
const netBalance = totalIn - totalOut;
const todayIn = totalIn;
const todayOut = totalOut;
const filteredEntries = activeTab === 'all'
? entries
: entries.filter(e => e.type === activeTab);
const handleViewDetails = (entry) => {
setSelectedEntry(entry);
setShowDetailModal(true);
};
const handleInFileChange = (e) => {
const file = e.target.files[0];
if (file) {
const fileType = file.type;
if (fileType === 'image/jpeg' || fileType === 'image/png' || fileType === 'image/jpg') {
setSelectedInFile(file);
} else {
alert('Only JPG and PNG files are allowed!');
e.target.value = '';
}
}
};
const handleOutFileChange = (e) => {
const file = e.target.files[0];
if (file) {
const fileType = file.type;
if (fileType === 'image/jpeg' || fileType === 'image/png' || fileType === 'image/jpg') {
setSelectedOutFile(file);
} else {
alert('Only JPG and PNG files are allowed!');
e.target.value = '';
}
}
};
const handleCloseInModal = () => {
setShowInModal(false);
setSelectedInFile(null);
};
const handleCloseOutModal = () => {
setShowOutModal(false);
setSelectedOutFile(null);
};
const breadcrumbItems = [
{ icon: <HomeIcon />, label: 'Home', path: '/dashboard', active: false },
{ icon: <BookOpen />, label: 'Cashbook', path: '/cashbook', active: true }
];
return (
<div className="cashbook-home">
<div className='relative'>
<BreadCrumb items={breadcrumbItems} showUser={true} />
</div>
{/* Main Content */}
<div className="cashbook-content">
{/* Left Main Section */}
<div className="main-section">
{/* Summary Cards */}
<div className="summary-cards" >
<div className="summary-card green">
<div className="card-icon">
<ArrowDownLeft size={24} />
</div>
<div className="card-content">
<span className="card-label">Money In</span>
<span className="card-amount">{totalIn.toLocaleString()}</span>
<span className="card-subtitle">{entries.filter(e => e.type === 'in').length} transactions</span>
</div>
</div>
<div className="summary-card red">
<div className="card-icon">
<ArrowUpRight size={24} />
</div>
<div className="card-content">
<span className="card-label">Money Out</span>
<span className="card-amount">{totalOut.toLocaleString()}</span>
<span className="card-subtitle">{entries.filter(e => e.type === 'out').length} transactions</span>
</div>
</div>
<div className="summary-card blue">
<div className="card-icon">
<TrendingUp size={24} />
</div>
<div className="card-content">
<span className="card-label">Net Balance</span>
<span className="card-amount">{netBalance.toLocaleString()}</span>
<span className="card-subtitle">Current balance</span>
</div>
</div>
</div>
{/* Transaction Ledger Section */}
<div className="ledger-section">
<div className="ledger-header">
<div className="ledger-title">
<h2>Transaction Ledger</h2>
<p>Track all your cash transactions</p>
</div>
<div className="ledger-actions">
<button className="entry-btn in-entry-btn" onClick={() => setShowInModal(true)}>
<ArrowDownLeft size={18} />
Money In Entry
</button>
<button className="entry-btn out-entry-btn" onClick={() => setShowOutModal(true)}>
<ArrowUpRight size={18} />
Money Out Entry
</button>
</div>
</div>
{/* Tabs */}
<div className="tabs-container">
<div className="tabs">
<button
className={`tab ${activeTab === 'all' ? 'active' : ''}`}
onClick={() => setActiveTab('all')}
>
<FileText size={18} />
All {!isMobile ? "Transactions" : ""}
</button>
<button
className={`tab ${activeTab === 'in' ? 'active' : ''}`}
onClick={() => setActiveTab('in')}
>
<ArrowDownLeft size={18} />
Money In
</button>
<button
className={`tab ${activeTab === 'out' ? 'active' : ''}`}
onClick={() => setActiveTab('out')}
>
<ArrowUpRight size={18} />
Money Out
</button>
</div>
</div>
{/* Search and Filter */}
<div className="search-filter">
<div className="search-box">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M9 17A8 8 0 1 0 9 1a8 8 0 0 0 0 16zM18 18l-4.35-4.35" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
<input type="text" placeholder="Search transactions..." />
</div>
<button className="filter-btn">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M4 6h12M6 10h8M8 14h4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
</button>
</div>
{/* Entries List */}
<div className="entries-list">
{filteredEntries.map((entry) => (
<div
key={entry.id}
className={`entry-card ${entry.type}`}
onClick={() => handleViewDetails(entry)}
>
<div className="entry-left">
<div className={`entry-avatar ${entry.type}`}>
{entry.type === 'in' ? <ArrowDownLeft size={20} /> : <ArrowUpRight size={20} />}
</div>
<div className="entry-info">
<div className="entry-header">
<span className="entry-desc">{entry.description}</span>
<span className={`entry-badge ${entry.type}`}>
{entry.type === 'in' ? 'IN' : 'OUT'}
</span>
</div>
<div className="entry-meta">
<span className="entry-time">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<circle cx="7" cy="7" r="6" stroke="currentColor" strokeWidth="1.5" />
<path d="M7 4v3l2 2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
{entry.time} {entry.date}
</span>
<span className={`payment-type ${entry.paymentType.toLowerCase()}`}>
{entry.paymentType}
</span>
</div>
</div>
</div>
<div className="entry-right">
<span className={`entry-amount ${entry.type}`}>
{entry.type === 'in' ? '+ ' : '- '}{entry.amount.toLocaleString()}
</span>
<button className="chevron-btn">
<ChevronRight size={18} />
</button>
</div>
</div>
))}
</div>
</div>
</div>
{/* Right Sidebar */}
<div className="sidebar-section">
{/* Quick Actions */}
<div className="quick-actions-card">
<h3>Quick Actions</h3>
<div className="quick-actions">
<button className="quick-action-btn in" onClick={() => setShowInModal(true)}>
<ArrowDownLeft size={20} />
Money In
</button>
<button className="quick-action-btn out" onClick={() => setShowOutModal(true)}>
<ArrowUpRight size={20} />
Money Out
</button>
</div>
</div>
{/* Today's Summary */}
<div className="today-summary-card">
<h3>Today's Summary</h3>
<div className="summary-items">
<div className="summary-item in">
<div className="summary-icon">
<ArrowDownLeft size={18} />
</div>
<div className="summary-details">
<span className="summary-label">Money In</span>
<span className="summary-value">{todayIn.toLocaleString()}</span>
</div>
</div>
<div className="summary-item out">
<div className="summary-icon">
<ArrowUpRight size={18} />
</div>
<div className="summary-details">
<span className="summary-label">Money Out</span>
<span className="summary-value">{todayOut.toLocaleString()}</span>
</div>
</div>
</div>
<div className="summary-divider"></div>
<div className="summary-total">
<span>Net Today</span>
<span className={todayIn - todayOut >= 0 ? 'positive' : 'negative'}>
{(todayIn - todayOut).toLocaleString()}
</span>
</div>
</div>
{/* Categories */}
<div className="categories-card">
<h3>Top Categories</h3>
<div className="category-list">
<div className="category-item">
<div className="category-color" style={{ background: '#10B981' }}></div>
<span className="category-name">Income</span>
<span className="category-amount">13,500</span>
</div>
<div className="category-item">
<div className="category-color" style={{ background: '#EF4444' }}></div>
<span className="category-name">Expenses</span>
<span className="category-amount">1,200</span>
</div>
<div className="category-item">
<div className="category-color" style={{ background: '#F59E0B' }}></div>
<span className="category-name">Miscellaneous</span>
<span className="category-amount">350</span>
</div>
</div>
</div>
</div>
</div>
{/* Detail Modal */}
{showDetailModal && selectedEntry && (
<div className="modal-overlay" onClick={() => setShowDetailModal(false)}>
<div className="modal-content detail-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2 className={selectedEntry.type === 'in' ? 'in-title' : 'out-title'}>
Transaction Details
</h2>
<button className="close-btn" onClick={() => setShowDetailModal(false)}>
<X size={20} />
</button>
</div>
<div className="modal-body">
<div className={`detail-amount-card ${selectedEntry.type}`}>
<div className="detail-icon">
{selectedEntry.type === 'in' ? <ArrowDownLeft size={28} /> : <ArrowUpRight size={28} />}
</div>
<div className="detail-amount-info">
<span className="detail-label">{selectedEntry.type === 'in' ? 'Money In' : 'Money Out'}</span>
<span className="detail-amount">{selectedEntry.amount.toLocaleString()}</span>
</div>
</div>
<div className="detail-info-section">
<div className="detail-row">
<span className="detail-row-label">Description</span>
<span className="detail-row-value">{selectedEntry.description}</span>
</div>
<div className="detail-row">
<span className="detail-row-label">Category</span>
<span className="detail-row-value">{selectedEntry.category}</span>
</div>
<div className="detail-row">
<span className="detail-row-label">Payment Type</span>
<span className={`payment-badge ${selectedEntry.paymentType.toLowerCase()}`}>
{selectedEntry.paymentType}
</span>
</div>
<div className="detail-row">
<span className="detail-row-label">Date & Time</span>
<span className="detail-row-value">{selectedEntry.time} {selectedEntry.date}</span>
</div>
</div>
<div className="detail-actions">
<button className="detail-edit-btn">
<Edit size={16} />
<div>Edit</div>
</button>
<button className="detail-delete-btn">
<Trash2 size={16} />
<div>Delete</div>
</button>
</div>
</div>
</div>
</div>
)}
{/* In Entry Modal */}
{showInModal && (
<div className="modal-overlay" onClick={handleCloseInModal}>
<div className="modal-content entry-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2 className="in-title">Money In Entry</h2>
<button className="close-btn" onClick={handleCloseInModal}>
<X size={20} />
</button>
</div>
<div className="modal-body">
<div className="form-group">
<label>Amount</label>
<div className="input-with-icon">
<span className="rupee-icon"></span>
<input type="number" placeholder="Enter Amount" />
</div>
</div>
<div className="form-group">
<label>Description</label>
<textarea
placeholder="Enter transaction details"
rows={3}
/>
</div>
<div className="form-group">
<label>Category</label>
<div className="select-wrapper">
<select>
<option>Select Category</option>
<option>Income</option>
<option>Sales</option>
<option>Other</option>
</select>
<ChevronDown size={16} />
</div>
</div>
<div className="form-group">
<label>Payment Mode</label>
<div className="radio-group">
<label className="radio-label">
<input type="radio" name="payment-in" value="cash" defaultChecked />
<span>Cash</span>
</label>
<label className="radio-label">
<input type="radio" name="payment-in" value="online" />
<span>Online</span>
</label>
</div>
</div>
<div className="form-group">
<label>Date</label>
<div className="input-with-icon date-input">
<input type="text" value="03 Dec 2025" readOnly />
<Calendar size={16} />
</div>
</div>
<div className="form-group">
<label>Attach Bill (JPG or PNG only)</label>
<div className="upload-area">
<input
type="file"
id="in-file-upload"
accept=".jpg,.jpeg,.png"
onChange={handleInFileChange}
style={{ display: 'none' }}
/>
<label htmlFor="in-file-upload" className="upload-label">
{selectedInFile ? (
<div className="file-preview">
<FileText size={32} className="file-icon" />
<p className="file-name">{selectedInFile.name}</p>
<p className="file-size">{(selectedInFile.size / 1024).toFixed(2)} KB</p>
</div>
) : (
<>
<Upload size={32} className="upload-icon" />
<p className="upload-text">Click to upload</p>
<p className="upload-hint">Only JPG or PNG file format supported</p>
</>
)}
</label>
</div>
</div>
<button className="save-btn">
Save Transaction
</button>
</div>
</div>
</div>
)}
{/* Out Entry Modal */}
{showOutModal && (
<div className="modal-overlay" onClick={handleCloseOutModal}>
<div className="modal-content entry-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2 className="out-title">Money Out Entry</h2>
<button className="close-btn" onClick={handleCloseOutModal}>
<X size={20} />
</button>
</div>
<div className="modal-body">
<div className="form-group">
<label>Amount</label>
<div className="input-with-icon">
<span className="rupee-icon"></span>
<input type="number" placeholder="Enter Amount" />
</div>
</div>
<div className="form-group">
<label>Description</label>
<textarea
placeholder="Enter transaction details"
rows={3}
/>
</div>
<div className="form-group">
<label>Category</label>
<div className="select-wrapper">
<select>
<option>Select Category</option>
<option>Expenses</option>
<option>Purchase</option>
<option>Miscellaneous</option>
</select>
<ChevronDown size={16} />
</div>
</div>
<div className="form-group">
<label>Payment Mode</label>
<div className="radio-group">
<label className="radio-label">
<input type="radio" name="payment-out" value="cash" defaultChecked />
<span>Cash</span>
</label>
<label className="radio-label">
<input type="radio" name="payment-out" value="online" />
<span>Online</span>
</label>
</div>
</div>
<div className="form-group">
<label>Date</label>
<div className="input-with-icon date-input">
<input type="text" value="03 Dec 2025" readOnly />
<Calendar size={16} />
</div>
</div>
<div className="form-group">
<label>Attach Bill (JPG or PNG only)</label>
<div className="upload-area">
<input
type="file"
id="out-file-upload"
accept=".jpg,.jpeg,.png"
onChange={handleOutFileChange}
style={{ display: 'none' }}
/>
<label htmlFor="out-file-upload" className="upload-label">
{selectedOutFile ? (
<div className="file-preview">
<FileText size={32} className="file-icon" />
<p className="file-name">{selectedOutFile.name}</p>
<p className="file-size">{(selectedOutFile.size / 1024).toFixed(2)} KB</p>
</div>
) : (
<>
<Upload size={32} className="upload-icon" />
<p className="upload-text">Click to upload</p>
<p className="upload-hint">Only JPG or PNG file format supported</p>
</>
)}
</label>
</div>
</div>
<button className="save-btn">
Save Transaction
</button>
</div>
</div>
</div>
)}
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,232 @@
import { useState } from 'react';
import { ChevronDown, Search, Filter, MoreVertical, Plus, Users, TrendingUp, Home as HomeIcon } from 'lucide-react';
import BreadCrumb from '../../components/BreadCrumb';
export default function Customers({ user }) {
const [searchQuery, setSearchQuery] = useState('');
const customers = [
{
id: 1,
name: 'Rajesh Kumar',
phone: '+91 98765 43210',
avatar: 'RK',
balance: 45000,
type: 'credit',
status: 'Active',
statusColor: 'bg-green-100 text-green-700',
transactions: 24
},
{
id: 2,
name: 'Priya Sharma',
phone: '+91 98765 43211',
avatar: 'PS',
balance: 8500,
type: 'debit',
status: 'Active',
statusColor: 'bg-green-100 text-green-700',
transactions: 18
},
{
id: 3,
name: 'Amit Patel',
phone: '+91 98765 43212',
avatar: 'AP',
balance: 22000,
type: 'credit',
status: 'Active',
statusColor: 'bg-green-100 text-green-700',
transactions: 32
},
{
id: 4,
name: 'Sneha Reddy',
phone: '+91 98765 43213',
avatar: 'SR',
balance: 5000,
type: 'debit',
status: 'Pending',
statusColor: 'bg-yellow-100 text-yellow-700',
transactions: 12
},
{
id: 5,
name: 'Vikram Singh',
phone: '+91 98765 43214',
avatar: 'VS',
balance: 15000,
type: 'credit',
status: 'Active',
statusColor: 'bg-green-100 text-green-700',
transactions: 28
},
{
id: 6,
name: 'Anjali Gupta',
phone: '+91 98765 43215',
avatar: 'AG',
balance: 3200,
type: 'debit',
status: 'Active',
statusColor: 'bg-green-100 text-green-700',
transactions: 9
},
];
const totalCredit = customers.filter(c => c.type === 'credit').reduce((sum, c) => sum + c.balance, 0);
const totalDebit = customers.filter(c => c.type === 'debit').reduce((sum, c) => sum + c.balance, 0);
const breadcrumbItems = [
{ icon: <HomeIcon />, label: 'Home', path: '/dashboard', active: false },
{ icon: <Users />, label: 'Customers', path: '/customers', active: true }
];
return (
<div>
{/* Header */}
<div className='relative'>
<BreadCrumb items={breadcrumbItems} showUser={true} />
</div>
{/* Summary Cards */}
<div className="grid grid-cols-4 gap-5 mb-8">
<div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-sm hover:shadow-md transition-all">
<div className="flex items-center justify-between mb-4">
<div className="text-sm text-gray-500 font-medium">Total Customers</div>
<div className="w-10 h-10 bg-purple-50 rounded-xl flex items-center justify-center">
<Users className="w-5 h-5 text-purple-600" />
</div>
</div>
<div className="text-3xl font-bold text-gray-900 mb-2">{customers.length}</div>
<div className="flex items-center gap-1 text-xs text-green-600 font-semibold">
<TrendingUp className="w-3.5 h-3.5" />
+12% from last month
</div>
</div>
<div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-sm hover:shadow-md transition-all">
<div className="text-sm text-gray-500 font-medium mb-4">Total Receivable</div>
<div className="text-3xl font-bold text-green-600 mb-2">{totalCredit.toLocaleString()}</div>
<div className="text-xs text-gray-600 font-medium">From {customers.filter(c => c.type === 'credit').length} customers</div>
</div>
<div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-sm hover:shadow-md transition-all">
<div className="text-sm text-gray-500 font-medium mb-4">Total Payable</div>
<div className="text-3xl font-bold text-red-600 mb-2">{totalDebit.toLocaleString()}</div>
<div className="text-xs text-gray-600 font-medium">To {customers.filter(c => c.type === 'debit').length} customers</div>
</div>
<div className="bg-white rounded-2xl p-6 border border-gray-200 shadow-sm hover:shadow-md transition-all">
<div className="text-sm text-gray-500 font-medium mb-4">Active Customers</div>
<div className="text-3xl font-bold text-gray-900 mb-2">{customers.filter(c => c.status === 'Active').length}</div>
<div className="text-xs text-blue-600 font-semibold">{customers.filter(c => c.status === 'Pending').length} pending</div>
</div>
</div>
{/* Customers Table */}
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-gray-900 mb-1">Customer Directory</h2>
<p className="text-sm text-gray-500">Manage and view all your customer details</p>
</div>
<button className="flex items-center gap-2 px-6 py-2.5 bg-gradient-to-r from-[#FF6B00] to-[#FF8534] text-white rounded-xl text-sm font-semibold hover:shadow-lg transition-all">
<Plus className="w-4 h-4" />
Add Customer
</button>
</div>
</div>
<div className="p-6 border-b border-gray-200 bg-gray-50">
<div className="flex items-center gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder="Search for customers by name or phone..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-[#FF6B00] focus:border-transparent"
/>
</div>
<button className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 rounded-xl text-sm font-medium hover:bg-gray-50 transition-colors">
<Filter className="w-4 h-4 text-gray-600" />
<span>Filter</span>
<ChevronDown className="w-4 h-4 text-gray-600" />
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
Customer
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
Phone
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
Balance
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
Type
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
Transactions
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-4"></th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{customers.map((customer) => (
<tr key={customer.id} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-[#FF6B00] to-[#FF8534] rounded-xl flex items-center justify-center text-white text-sm font-semibold shadow-md">
{customer.avatar}
</div>
<span className="text-sm font-medium text-gray-900">{customer.name}</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
{customer.phone}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`text-sm font-bold ${customer.type === 'credit' ? 'text-green-600' : 'text-red-600'}`}>
{customer.balance.toLocaleString()}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${customer.type === 'credit'
? 'bg-green-100 text-green-700'
: 'bg-red-100 text-red-700'
}`}>
{customer.type === 'credit' ? 'You will get' : 'You will give'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{customer.transactions}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${customer.statusColor}`}>
{customer.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right">
<button className="text-gray-400 hover:text-gray-600 transition-colors">
<MoreVertical className="w-5 h-5" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,411 @@
import { Bell, HelpCircle, ChevronDown, TrendingUp, TrendingDown, Download, Clock, CheckCircle, XCircle, Send, ArrowDownLeft, Receipt, Users } from 'lucide-react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import './Dashboard.scss';
import LedgerSection from '../../components/LedgerSection';
import Avatar from '../../components/Avatar';
import ProfileDropdown from '../../components/ProfileDropdown';
export default function Dashboard({ user }) {
// Transaction Status Cards
const statusCards = [
{
label: 'Pending Payment',
count: 125,
change: '+8%',
positive: true,
colorClass: 'orange',
icon: Clock
},
{
label: 'Received',
count: 456,
change: '+12%',
positive: true,
colorClass: 'green',
icon: ArrowDownLeft
},
{
label: 'Paid Out',
count: 234,
change: '+5%',
positive: true,
colorClass: 'blue',
icon: Send
},
{
label: 'Completed',
count: 892,
change: '+15%',
positive: true,
colorClass: 'teal',
icon: CheckCircle
},
{
label: 'Cancelled',
count: 23,
change: '-3%',
positive: false,
colorClass: 'red',
icon: XCircle
},
];
// Customer Data
const customers = [
{
id: 1,
name: 'Deepa',
phone: '98755 43210',
avatar: 'D',
balance: 800,
type: 'credit',
lastTransaction: 'Payment received',
lastDate: 'Today, 2:30 PM',
color: 'from-green-400 to-green-500',
bgColor: 'bg-green-50',
textColor: 'text-green-600',
history: [
{ id: 1, date: '02 Dec 2025', type: 'credit', amount: 500, note: 'Invoice payment', method: 'UPI' },
{ id: 2, date: '28 Nov 2025', type: 'debit', amount: 300, note: 'Product purchase', method: 'Cash' },
{ id: 3, date: '25 Nov 2025', type: 'credit', amount: 600, note: 'Service payment', method: 'Bank Transfer' },
{ id: 4, date: '20 Nov 2025', type: 'debit', amount: 200, note: 'Advance given', method: 'UPI' },
{ id: 5, date: '15 Nov 2025', type: 'credit', amount: 1000, note: 'Full settlement', method: 'Cash' }
]
},
{
id: 2,
name: 'Rakesh',
phone: '98765 12345',
avatar: 'R',
balance: 500,
type: 'debit',
lastTransaction: 'Outstanding payment',
lastDate: 'Yesterday, 5:00 PM',
color: 'from-red-400 to-red-500',
bgColor: 'bg-red-50',
textColor: 'text-red-600',
history: [
{ id: 1, date: '01 Dec 2025', type: 'debit', amount: 500, note: 'Product delivered', method: 'Pending' },
{ id: 2, date: '25 Nov 2025', type: 'credit', amount: 1000, note: 'Payment received', method: 'UPI' },
{ id: 3, date: '20 Nov 2025', type: 'debit', amount: 800, note: 'Supplies ordered', method: 'Cash' }
]
},
{
id: 3,
name: 'Sunil',
phone: '98755 67890',
avatar: 'S',
balance: 1200,
type: 'debit',
lastTransaction: 'Bought supplies',
lastDate: '2 days ago',
color: 'from-orange-400 to-orange-500',
bgColor: 'bg-orange-50',
textColor: 'text-orange-600',
history: [
{ id: 1, date: '30 Nov 2025', type: 'debit', amount: 1200, note: 'Bought supplies', method: 'Pending' },
{ id: 2, date: '22 Nov 2025', type: 'credit', amount: 500, note: 'Payment received', method: 'Cash' }
]
},
{
id: 4,
name: 'Priya',
phone: '98755 11111',
avatar: 'P',
balance: 2500,
type: 'credit',
lastTransaction: 'Invoice paid',
lastDate: '3 days ago',
color: 'from-blue-400 to-blue-500',
bgColor: 'bg-blue-50',
textColor: 'text-blue-600',
history: [
{ id: 1, date: '29 Nov 2025', type: 'credit', amount: 2500, note: 'Invoice paid', method: 'Bank Transfer' },
{ id: 2, date: '15 Nov 2025', type: 'debit', amount: 1000, note: 'Purchase order', method: 'UPI' }
]
},
{
id: 5,
name: 'Karthik',
phone: '98755 22222',
avatar: 'K',
balance: 1500,
type: 'credit',
lastTransaction: 'Payment sent',
lastDate: '4 days ago',
color: 'from-purple-400 to-purple-500',
bgColor: 'bg-purple-50',
textColor: 'text-purple-600',
history: [
{ id: 1, date: '28 Nov 2025', type: 'credit', amount: 1500, note: 'Payment sent', method: 'Cash' },
{ id: 2, date: '10 Nov 2025', type: 'debit', amount: 500, note: 'Advance payment', method: 'UPI' }
]
},
{
id: 6,
name: 'Anjali',
phone: '98755 33333',
avatar: 'A',
balance: 350,
type: 'debit',
lastTransaction: 'Pending payment',
lastDate: '5 days ago',
color: 'from-pink-400 to-pink-500',
bgColor: 'bg-pink-50',
textColor: 'text-pink-600',
history: [
{ id: 1, date: '27 Nov 2025', type: 'debit', amount: 350, note: 'Pending payment', method: 'Pending' }
]
},
];
// Supplier Data
const suppliers = [
{
id: 1,
name: 'ABC Materials',
phone: '98700 11111',
avatar: 'AM',
balance: 15000,
type: 'debit',
lastTransaction: 'Material purchased',
lastDate: 'Today, 10:00 AM',
color: 'from-red-400 to-red-500',
bgColor: 'bg-red-50',
textColor: 'text-red-600',
history: [
{ id: 1, date: '02 Dec 2025', type: 'debit', amount: 15000, note: 'Material purchased', method: 'Pending' },
{ id: 2, date: '20 Nov 2025', type: 'credit', amount: 10000, note: 'Payment made', method: 'Bank Transfer' }
]
},
{
id: 2,
name: 'Tech Supplies Co',
phone: '98700 22222',
avatar: 'TS',
balance: 8500,
type: 'debit',
lastTransaction: 'Equipment bought',
lastDate: 'Yesterday, 3:00 PM',
color: 'from-orange-400 to-orange-500',
bgColor: 'bg-orange-50',
textColor: 'text-orange-600'
},
{
id: 3,
name: 'Global Traders',
phone: '98700 33333',
avatar: 'GT',
balance: 22000,
type: 'debit',
lastTransaction: 'Bulk order',
lastDate: '2 days ago',
color: 'from-rose-400 to-rose-500',
bgColor: 'bg-rose-50',
textColor: 'text-rose-600'
},
{
id: 4,
name: 'Prime Wholesale',
phone: '98700 44444',
avatar: 'PW',
balance: 5000,
type: 'credit',
lastTransaction: 'Return credit',
lastDate: '3 days ago',
color: 'from-blue-400 to-blue-500',
bgColor: 'bg-blue-50',
textColor: 'text-blue-600'
},
{
id: 5,
name: 'Metro Suppliers',
phone: '98700 55555',
avatar: 'MS',
balance: 12000,
type: 'debit',
lastTransaction: 'Monthly payment',
lastDate: '4 days ago',
color: 'from-purple-400 to-purple-500',
bgColor: 'bg-purple-50',
textColor: 'text-purple-600'
},
];
const chartData = [
{ month: 'Jan', income: 45000, expense: 32000 },
{ month: 'Feb', income: 52000, expense: 38000 },
{ month: 'Mar', income: 48000, expense: 35000 },
{ month: 'Apr', income: 61000, expense: 42000 },
{ month: 'May', income: 55000, expense: 39000 },
{ month: 'Jun', income: 67000, expense: 45000 },
];
return (
<div className="dashboard">
{/* Header */}
<div className="header flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
<div className="header-content">
<h1 className="text-2xl md:text-3xl font-bold text-gray-900 mb-1">Morning, {user.name}! 👋</h1>
<p className="text-sm text-gray-500">Here's your business overview</p>
</div>
<div className='relative'>
<ProfileDropdown top={`top-12`}/>
</div>
</div>
{/* Transaction Summary Cards */}
<div className="transaction-summary">
<h2>Transaction Summary</h2>
<div className="status-cards-grid">
{statusCards.map((card, index) => {
const IconComponent = card.icon;
return (
<div key={index} className={`status-card ${card.colorClass}`} style={{
backgroundColor: card.colorClass === 'orange' ? '#FFF7ED' :
card.colorClass === 'green' ? '#F0FDF4' :
card.colorClass === 'blue' ? '#EFF6FF' :
card.colorClass === 'teal' ? '#F0FDFA' : '#FEF2F2',
borderColor: card.colorClass === 'orange' ? '#FED7AA' :
card.colorClass === 'green' ? '#BBF7D0' :
card.colorClass === 'blue' ? '#BFDBFE' :
card.colorClass === 'teal' ? '#99F6E4' : '#FECACA'
}}>
<div className="card-header">
<div className="icon-wrapper" style={{
backgroundColor: card.colorClass === 'orange' ? '#FFEDD5' :
card.colorClass === 'green' ? '#DCFCE7' :
card.colorClass === 'blue' ? '#DBEAFE' :
card.colorClass === 'teal' ? '#CCFBF1' : '#FEE2E2'
}}>
<IconComponent style={{
color: card.colorClass === 'orange' ? '#FF6B00' :
card.colorClass === 'green' ? '#059669' :
card.colorClass === 'blue' ? '#2563EB' :
card.colorClass === 'teal' ? '#0D9488' : '#DC2626'
}} />
</div>
</div>
<div className="card-label">{card.label}</div>
<div className="card-content">
<span className="count">{card.count}</span>
<div className={`trend ${card.positive ? 'positive' : 'negative'}`}>
{card.positive ? <TrendingUp /> : <TrendingDown />}
<span>{card.change}</span>
</div>
</div>
<button className="download-link">
<Download />
Download report
</button>
</div>
);
})}
</div>
</div>
<LedgerSection customers={customers} suppliers={suppliers} />
{/* Financial Overview Chart */}
<div className="financial-overview">
<div className="chart-header">
<div className="chart-title">
<h2>Financial Overview</h2>
<p>Monthly income vs expense comparison</p>
</div>
<div className="chart-legend">
<div className="legend-item">
<div className="legend-dot income"></div>
<div className="legend-text">
<div className="label">Total Income</div>
<div className="value income"> 3,28,000</div>
</div>
</div>
<div className="legend-item">
<div className="legend-dot expense"></div>
<div className="legend-text">
<div className="label">Total Expense</div>
<div className="value expense"> 2,31,000</div>
</div>
</div>
</div>
</div>
<ResponsiveContainer width="100%" height={320}>
<BarChart data={chartData} barGap={8}>
<CartesianGrid strokeDasharray="3 3" stroke="#F0F0F0" vertical={false} />
<XAxis
dataKey="month"
axisLine={false}
tickLine={false}
tick={{ fill: '#6B7280', fontSize: 13, fontWeight: 500 }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: '#6B7280', fontSize: 13, fontWeight: 500 }}
tickFormatter={(value) => `${value / 1000}k`}
/>
<Tooltip
cursor={{ fill: '#F9FAFB' }}
contentStyle={{
borderRadius: '12px',
border: '1px solid #E5E7EB',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
fontWeight: 600
}}
/>
<Legend
wrapperStyle={{ paddingTop: '20px' }}
iconType="circle"
/>
<Bar dataKey="income" fill="#27AE60" radius={[8, 8, 0, 0]} name="Income" />
<Bar dataKey="expense" fill="#EB5757" radius={[8, 8, 0, 0]} name="Expense" />
</BarChart>
</ResponsiveContainer>
</div>
{/* Quick Stats */}
<div className="quick-stats">
<div className="stat-card">
<div className="stat-header">
<div className="label">Total Customers</div>
<div className="icon-wrapper purple">
<Users />
</div>
</div>
<div className="stat-value">248</div>
<div className="stat-change positive">
<TrendingUp />
+12% from last month
</div>
</div>
<div className="stat-card">
<div className="stat-header">
<div className="label">Active Transactions</div>
<div className="icon-wrapper blue">
<Receipt />
</div>
</div>
<div className="stat-value">89</div>
<div className="stat-change info">23 pending approval</div>
</div>
<div className="stat-card">
<div className="stat-header">
<div className="label">Net Balance</div>
<div className="icon-wrapper green">
<TrendingUp />
</div>
</div>
<div className="stat-value"> 97,000</div>
<div className="stat-change positive">
<CheckCircle />
Positive cashflow
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,554 @@
// Dashboard SCSS Styles
.dashboard {
min-height: 100vh;
// Header Section
.header {
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: row;
margin-bottom: 2rem;
.header-content {
h1 {
font-size: 1.875rem;
font-weight: 700;
color: #111827;
margin-bottom: 0.25rem;
}
p {
color: #6B7280;
font-size: 0.875rem;
}
}
.header-actions {
display: flex;
align-items: center;
button {
padding: 0.625rem;
border-radius: 0.75rem;
transition: all 0.2s;
background: transparent;
border: none;
cursor: pointer;
&:hover {
background-color: #F3F4F6;
}
&.notification-btn {
position: relative;
.notification-dot {
position: absolute;
top: 0.5rem;
right: 0.5rem;
width: 0.5rem;
height: 0.5rem;
background-color: #FF6B00;
border-radius: 50%;
}
}
&.profile-btn {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.5rem 0.75rem;
.avatar {
width: 2.25rem;
height: 2.25rem;
background: linear-gradient(to bottom right, #FF6B00, #FF8534);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.875rem;
font-weight: 600;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
span {
font-size: 0.875rem;
font-weight: 500;
color: #111827;
}
}
svg {
width: 1.25rem;
height: 1.25rem;
color: #4B5563;
}
}
}
}
// Transaction Summary Section
.transaction-summary {
margin-bottom: 2rem;
h2 {
font-size: 1.25rem;
font-weight: 700;
color: #111827;
margin-bottom: 1.25rem;
}
.status-cards-grid {
// display: grid;
// grid-template-columns: repeat(5, 1fr);
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 1.25rem;
margin-bottom: 2rem;
.status-card {
border-radius: 1rem;
padding: 1.5rem;
border: 1px solid;
cursor: pointer;
transition: all 0.2s;
flex-grow: 1;
&:hover {
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
.card-header {
display: flex;
align-items: start;
justify-content: space-between;
margin-bottom: 0.5rem;
.icon-wrapper {
width: 3rem;
height: 3rem;
border-radius: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
svg {
width: 1.5rem;
height: 1.5rem;
stroke-width: 2.5;
}
}
}
.card-label {
font-size: 0.875rem;
font-weight: 500;
color: #4B5563;
margin-bottom: 0.5rem;
}
.card-content {
display: flex;
align-items: center;
justify-content: space-between;
.count {
font-size: 1.75rem;
font-weight: 700;
color: #111827;
}
.trend {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
&.positive {
color: #059669;
}
&.negative {
color: #DC2626;
}
svg {
width: 1rem;
height: 1rem;
stroke-width: 2.5;
}
}
}
.download-link {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.75rem;
font-weight: 500;
color: #6B7280;
background: none;
border: none;
cursor: pointer;
transition: color 0.2s;
&:hover {
color: #FF6B00;
}
svg {
width: 0.875rem;
height: 0.875rem;
}
}
}
}
}
// Financial Overview Section
.financial-overview {
background: white;
border-radius: 1rem;
padding: 1.75rem;
border: 1px solid #E5E7EB;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
margin-bottom: 2rem;
transition: box-shadow 0.2s;
&:hover {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
.chart-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 10px;
.chart-title {
h2 {
font-size: 1.25rem;
font-weight: 700;
color: #111827;
margin-bottom: 0.25rem;
}
p {
font-size: 0.875rem;
color: #6B7280;
}
}
.chart-legend {
display: flex;
align-items: center;
gap: 1.5rem;
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
.legend-dot {
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
&.income {
background-color: #27AE60;
}
&.expense {
background-color: #EB5757;
}
}
.legend-text {
.label {
font-size: 0.75rem;
font-weight: 500;
color: #6B7280;
}
.value {
font-size: 1.125rem;
font-weight: 700;
&.income {
color: #27AE60;
}
&.expense {
color: #EB5757;
}
}
}
}
}
}
}
// Quick Stats Section
.quick-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.25rem;
.stat-card {
background: white;
border-radius: 1rem;
padding: 1.5rem;
border: 1px solid #E5E7EB;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
transition: all 0.2s;
&:hover {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
.stat-header {
display: flex;
align-items: center;
justify-content: space-between;
// margin-bottom: 1rem;
.label {
font-size: 0.875rem;
font-weight: 500;
color: #6B7280;
}
.icon-wrapper {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
&.purple {
background-color: #FAF5FF;
svg {
width: 1.25rem;
height: 1.25rem;
color: #9333EA;
}
}
&.blue {
background-color: #EFF6FF;
svg {
width: 1.25rem;
height: 1.25rem;
color: #2563EB;
}
}
&.green {
background-color: #F0FDF4;
svg {
width: 1.25rem;
height: 1.25rem;
color: #16A34A;
}
}
}
}
.stat-value {
font-size: 1.875rem;
font-weight: 700;
color: #111827;
margin-bottom: 0.5rem;
}
.stat-change {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
&.positive {
color: #059669;
}
&.info {
color: #2563EB;
}
svg {
width: 0.875rem;
height: 0.875rem;
}
}
}
}
// Mobile Responsive Styles
@media (max-width: 1024px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
.summary-cards-grid,
.status-cards-grid {
// grid-template-columns: repeat(2, 1fr) !important;
gap: 1rem;
}
.balance-cards {
grid-template-columns: 1fr !important;
}
}
@media (max-width: 768px) {
.header {
padding: 0;
margin-bottom: 1.5rem;
.header-content {
h1 {
font-size: 1.5rem;
}
p {
font-size: 0.875rem;
}
}
}
.transaction-summary {
h2 {
font-size: 1.25rem;
margin-bottom: 1rem;
}
}
.summary-cards-grid,
.status-cards-grid {
// grid-template-columns: 1fr !important;
gap: 0.875rem;
}
.status-card,
.balance-card {
padding: 1rem !important;
.count,
.balance-amount {
font-size: 1.75rem;
}
}
.khata-section,
.transaction-section {
padding: 1rem;
margin-bottom: 1rem;
}
.khata-header,
.transaction-header {
flex-direction: column;
align-items: flex-start !important;
gap: 1rem;
h2 {
font-size: 1.125rem;
}
.header-actions {
width: 100%;
flex-direction: column;
.tabs {
width: 100%;
}
.add-btn {
width: 100%;
justify-content: center;
}
}
}
.chart-section {
padding: 1rem;
}
.status-cards-grid .status-card .card-header .icon-wrapper {
width: 2rem !important;
height: 2rem !important;
svg {
width: 1rem !important;
height: 1rem !important;
}
}
}
@media (max-width: 576px) {
.status-cards-grid {
.status-card {
width: 100% !important;
}
}
.quick-stats {
.stat-card {
padding: 1rem !important;
}
}
}
@media (max-width: 480px) {
.header {
.header-content h1 {
font-size: 1.25rem;
}
}
.transaction-summary h2 {
font-size: 1.125rem;
}
.status-card,
.balance-card {
padding: 1rem;
.count,
.balance-amount {
font-size: 1.5rem;
}
.card-label {
font-size: 0.8125rem;
}
}
}
@media (max-width: 400px) {
.header {
flex-direction: column !important;
// align-items: center !important;
}
.chart-legend {
flex-direction: column !important;
}
}
}

View File

@ -0,0 +1,761 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { Wallet, ArrowUpRight, Calendar, Filter, Download, IndianRupee, CreditCard, X, Plus, Home as HomeIcon, Receipt } from 'lucide-react';
import BreadCrumb from '@/components/BreadCrumb';
import "./Expenses.scss"
const defaultExpenses = [
{
id: 1,
title: 'Inventory Restock',
vendor: 'Fresh Farm Wholesale',
category: 'Inventory',
date: '2025-12-03',
paymentType: 'UPI',
amount: 18500,
status: 'Settled',
isFixedCommitment: false,
},
{
id: 2,
title: 'Shop Rent',
vendor: 'Sai Properties',
category: 'Fixed Cost',
date: '2025-12-01',
paymentType: 'Bank Transfer',
amount: 32000,
status: 'Scheduled',
isFixedCommitment: true,
},
{
id: 3,
title: 'Delivery Partner Payout',
vendor: 'Anil Kumar',
category: 'Logistics',
date: '2025-11-30',
paymentType: 'Cash',
amount: 6500,
status: 'Settled',
isFixedCommitment: false,
},
{
id: 4,
title: 'Electricity Bill',
vendor: 'TNEB',
category: 'Utilities',
date: '2025-11-28',
paymentType: 'NEFT',
amount: 5400,
status: 'Overdue',
isFixedCommitment: true,
},
{
id: 5,
title: 'Packing Material',
vendor: 'BoxIt Packaging',
category: 'Supplies',
date: '2025-11-27',
paymentType: 'Card',
amount: 2600,
status: 'Settled',
isFixedCommitment: false,
},
];
const defaultCategoryOptions = ['Inventory', 'Fixed Cost', 'Logistics', 'Utilities', 'Supplies', 'Miscellaneous'];
const statusOptions = ['Scheduled', 'Settled', 'Overdue'];
const formatDisplayDate = (isoDate) => {
return new Date(isoDate).toLocaleDateString('en-GB', {
day: '2-digit',
month: 'short',
year: 'numeric',
});
};
const formatRangeLabel = (start, end) => {
if (!start && !end) return 'All Time';
const startDate = start ? formatDisplayDate(start) : '...';
const endDate = end ? formatDisplayDate(end) : '...';
return `${startDate} - ${endDate}`;
};
export default function Expenses({ user }) {
const [searchQuery, setSearchQuery] = useState('');
const [expenses, setExpenses] = useState(defaultExpenses);
const [categoryOptions, setCategoryOptions] = useState(defaultCategoryOptions);
const [filters, setFilters] = useState({
startDate: '2025-11-15',
endDate: '2025-12-03',
categories: [],
statuses: [],
});
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false);
const [isFiltersOpen, setIsFiltersOpen] = useState(false);
const [isRecordModalOpen, setIsRecordModalOpen] = useState(false);
const [isAddingCategory, setIsAddingCategory] = useState(false);
const [newCategory, setNewCategory] = useState('');
const [formData, setFormData] = useState({
title: '',
vendor: '',
category: 'Inventory',
date: new Date().toISOString().slice(0, 10),
paymentType: 'Cash',
amount: '',
status: 'Scheduled',
isFixedCommitment: false,
});
const datePickerRef = useRef(null);
const filtersRef = useRef(null);
useEffect(() => {
const handleDocumentClick = (event) => {
const isInsideDatePicker = datePickerRef.current?.contains(event.target);
const isInsideFilters = filtersRef.current?.contains(event.target);
if (!isInsideDatePicker) {
setIsDatePickerOpen(false);
}
if (!isInsideFilters) {
setIsFiltersOpen(false);
}
};
document.addEventListener('click', handleDocumentClick);
return () => document.removeEventListener('click', handleDocumentClick);
}, []);
const totalSpent = expenses.reduce((sum, expense) => sum + expense.amount, 0);
const recurringExpenses = expenses.filter((expense) => expense.isFixedCommitment);
const pendingAmount = expenses
.filter((expense) => expense.status !== 'Settled')
.reduce((sum, expense) => sum + expense.amount, 0);
const filteredExpenses = useMemo(() => {
const query = searchQuery.toLowerCase();
return expenses.filter((expense) => {
const matchesSearch =
!query ||
expense.title.toLowerCase().includes(query) ||
expense.vendor.toLowerCase().includes(query) ||
expense.category.toLowerCase().includes(query);
const matchesCategory =
!filters.categories.length || filters.categories.includes(expense.category);
const matchesStatus =
!filters.statuses.length || filters.statuses.includes(expense.status);
const matchesDate =
(!filters.startDate || new Date(expense.date) >= new Date(filters.startDate)) &&
(!filters.endDate || new Date(expense.date) <= new Date(filters.endDate));
return matchesSearch && matchesCategory && matchesStatus && matchesDate;
});
}, [expenses, filters.categories, filters.endDate, filters.startDate, filters.statuses, searchQuery]);
const handleChange = (field) => (event) => {
const value =
field === 'amount'
? event.target.value.replace(/[^\d]/g, '')
: field === 'isFixedCommitment'
? event.target.checked
: event.target.value;
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleSubmit = (event) => {
event.preventDefault();
if (!formData.title.trim() || !formData.vendor.trim() || !formData.amount) {
return;
}
const newExpense = {
id: Date.now(),
...formData,
amount: Number(formData.amount),
};
setExpenses((prev) => [newExpense, ...prev]);
setIsRecordModalOpen(false);
setFormData({
title: '',
vendor: '',
category: 'Inventory',
date: new Date().toISOString().slice(0, 10),
paymentType: 'Cash',
amount: '',
status: 'Scheduled',
isFixedCommitment: false,
});
};
const breadcrumbItems = [
{ icon: <HomeIcon />, label: 'Home', active: false },
{ icon: <Receipt />, label: 'Expenses', active: true }
];
return (
<div className="space-y-4 sm:space-y-6 md:space-y-7 lg:space-y-8">
<div className='relative'>
<BreadCrumb items={breadcrumbItems} showUser={true} />
</div> <header className="flex flex-col gap-3 sm:gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 mt-1">Expenses</h1>
</div>
<div className="flex items-center gap-2 sm:gap-3">
<button
className="flex items-center gap-2 px-3 py-2 sm:px-4 sm:py-2.5
bg-gradient-to-r from-[#FF6B00] to-[#FF8534] text-white rounded-xl
text-xs sm:text-sm font-semibold hover:shadow-lg transition-all sm:w-auto justify-center"
onClick={() => setIsRecordModalOpen(true)}
>
<span className="hidden sm:inline">Record Expense</span>
<span className="sm:hidden">Record</span>
<Plus className="w-4 h-4" />
</button>
</div>
</header>
<section className="grid gap-4 sm:gap-5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
{/* Total Spent */}
<div className="rounded-xl sm:rounded-2xl p-4 sm:p-5 shadow-sm border border-orange-100 bg-orange-50/80">
<div className="flex items-center justify-between text-gray-500 text-xs font-semibold uppercase tracking-wide">
Total Spent
<Wallet className="w-4 h-4" />
</div>
<p className="text-2xl sm:text-3xl font-bold text-gray-900 mt-2 sm:mt-3"> {totalSpent.toLocaleString()}</p>
<p className="text-xs sm:text-sm text-gray-500 mt-1 sm:mt-2">Across {expenses.length} expense entries</p>
</div>
{/* Recurring Bills / Fixed commitments */}
<div className="rounded-xl sm:rounded-2xl p-4 sm:p-5 shadow-sm border border-emerald-100 bg-emerald-50/80">
<div className="flex items-center justify-between text-gray-500 text-xs font-semibold uppercase tracking-wide">
Recurring Bills
<CreditCard className="w-4 h-4" />
</div>
<p className="text-2xl sm:text-3xl font-bold text-gray-900 mt-2 sm:mt-3"> {recurringExpenses.reduce((sum, expense) => sum + expense.amount, 0).toLocaleString()}</p>
<p className="text-xs sm:text-sm text-gray-500 mt-1 sm:mt-2">{recurringExpenses.length} upcoming fixed commitments</p>
</div>
{/* Pending exposure */}
<div className="rounded-xl sm:rounded-2xl p-4 sm:p-5 shadow-sm border border-rose-100 bg-rose-50/80">
<div className="flex items-center justify-between text-gray-500 text-xs font-semibold uppercase tracking-wide">
Pending
<IndianRupee className="w-4 h-4" />
</div>
<p className="text-2xl sm:text-3xl font-bold text-red-600 mt-2 sm:mt-3"> {pendingAmount.toLocaleString()}</p>
<p className="text-xs sm:text-sm text-gray-500 mt-1 sm:mt-2">Needs attention before month end</p>
</div>
</section>
<section className="bg-white border border-gray-200 rounded-xl sm:rounded-2xl shadow-sm overflow-hidden">
<div className="flex flex-col flex-wrap gap-3 sm:gap-4 border-b border-gray-100 p-4 sm:p-6 md:flex-row md:items-center md:justify-between">
<div>
<h2 className="text-lg sm:text-xl font-bold text-gray-900">Expense Ledger</h2>
<p className="text-xs sm:text-sm text-gray-500">Search, filter and export detailed expenses</p>
</div>
<div className="flex items-center gap-2 flex-wrap max-sm:flex-col">
<div className="relative max-sm:w-full lg:flex-none" ref={datePickerRef}>
<button
type="button"
className="flex items-center gap-1.5 px-2 sm:px-4 py-2
border border-gray-200 rounded-xl text-xs sm:text-sm font-medium text-gray-700
hover:bg-gray-50 transition-colors w-full sm:w-auto justify-center "
onClick={(event) => {
event.stopPropagation();
setIsDatePickerOpen((prev) => !prev);
setIsFiltersOpen(false);
}}
>
<Calendar className="w-4 h-4 flex-shrink-0" />
<span className="truncate">{formatRangeLabel(filters.startDate, filters.endDate)}</span>
</button>
{isDatePickerOpen && (
<div className="fixed sm:absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 sm:left-auto sm:right-40 sm:top-auto sm:translate-x-0 sm:translate-y-0 mt-0 sm:mt-2 w-[calc(100vw-4rem)] sm:w-64 max-w-sm rounded-xl sm:rounded-2xl border border-gray-200 bg-white shadow-xl p-3 sm:p-4 space-y-3 z-50">
<div>
<label className="text-xs font-semibold text-gray-500 uppercase">From</label>
<input
type="date"
value={filters.startDate}
onChange={(event) =>
setFilters((prev) => ({ ...prev, startDate: event.target.value }))
}
className="mt-1 w-full rounded-xl border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#FF6B00]"
/>
</div>
<div>
<label className="text-xs font-semibold text-gray-500 uppercase">To</label>
<input
type="date"
value={filters.endDate}
onChange={(event) =>
setFilters((prev) => ({ ...prev, endDate: event.target.value }))
}
className="mt-1 w-full rounded-xl border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#FF6B00]"
/>
</div>
<button
type="button"
className="w-full rounded-xl border border-gray-200 px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-50"
onClick={(event) => {
event.stopPropagation();
setFilters((prev) => ({ ...prev, startDate: '', endDate: '' }));
}}
>
Clear Range
</button>
</div>
)}
</div>
<div className="relative max-sm:w-full lg:flex-none" ref={filtersRef}>
<button
type="button"
className="flex items-center gap-1.5 px-2 sm:px-4 py-2 border border-gray-200 rounded-xl text-xs sm:text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors w-full sm:w-auto justify-center"
onClick={(event) => {
event.stopPropagation();
setIsFiltersOpen((prev) => !prev);
setIsDatePickerOpen(false);
}}
>
<Filter className=" h-4 " />
<span>Filters</span>
{(filters.categories.length > 0 || filters.statuses.length > 0) && (
<span className="ml-1 text-xs font-semibold text-[#FF6B00]">
{filters.categories.length + filters.statuses.length}
</span>
)}
</button>
{isFiltersOpen && (
<div className="fixed sm:absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 sm:left-auto sm:right-0 sm:top-auto sm:translate-x-0 sm:translate-y-0 mt-0 sm:mt-2 w-[calc(100vw-4rem)] sm:w-72 max-w-sm rounded-xl sm:rounded-2xl border border-gray-200 bg-white shadow-xl p-3 sm:p-4 space-y-4 z-10">
<div>
<p className="text-xs font-semibold text-gray-500 uppercase mb-2">Categories</p>
<div className="flex flex-wrap gap-2">
{categoryOptions.map((category) => {
const isActive = filters.categories.includes(category);
return (
<button
key={category}
type="button"
onClick={() =>
setFilters((prev) => ({
...prev,
categories: isActive
? prev.categories.filter((item) => item !== category)
: [...prev.categories, category],
}))
}
className={`px-3 py-1.5 rounded-full text-xs font-semibold border ${isActive
? 'bg-[#FFF4F0] text-[#FF6B00] border-[#FFBB8B]'
: 'text-gray-600 border-gray-200 hover:bg-gray-50'
}`}
>
{category}
</button>
);
})}
</div>
</div>
<div>
<p className="text-xs font-semibold text-gray-500 uppercase mb-2">Status</p>
<div className="flex flex-wrap gap-2">
{statusOptions.map((status) => {
const isActive = filters.statuses.includes(status);
return (
<button
key={status}
type="button"
onClick={() =>
setFilters((prev) => ({
...prev,
statuses: isActive
? prev.statuses.filter((item) => item !== status)
: [...prev.statuses, status],
}))
}
className={`px-3 py-1.5 rounded-full text-xs font-semibold border ${isActive
? 'bg-[#ECFDF3] text-[#15803D] border-[#86EFAC]'
: 'text-gray-600 border-gray-200 hover:bg-gray-50'
}`}
>
{status}
</button>
);
})}
</div>
</div>
<div className="flex justify-between">
<button
type="button"
className="text-xs font-semibold text-gray-500 hover:text-gray-700"
onClick={() => setFilters((prev) => ({ ...prev, categories: [], statuses: [] }))}
>
Clear filters
</button>
<button
type="button"
className="text-xs font-semibold text-[#FF6B00] hover:text-[#FF8534]"
onClick={() => setIsFiltersOpen(false)}
>
Done
</button>
</div>
</div>
)}
</div>
<button
type="button"
className="flex items-center gap-1.5 px-2 sm:px-5 py-2 bg-gray-900 text-white
rounded-xl text-xs sm:text-sm font-semibold hover:bg-gray-800 transition-colors
justify-center max-sm:w-full"
onClick={() => {
if (!filteredExpenses.length) return;
const header = ['Title', 'Vendor', 'Category', 'Date', 'Payment Mode', 'Status', 'Amount'];
const rows = filteredExpenses.map((expense) => [
expense.title,
expense.vendor,
expense.category,
formatDisplayDate(expense.date),
expense.paymentType,
expense.status,
expense.amount,
]);
const csv = [header, ...rows]
.map((row) => row.map((value) => `"${String(value).replace(/"/g, '""')}"`).join(','))
.join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `expenses-${Date.now()}.csv`;
link.click();
URL.revokeObjectURL(url);
}}
>
<Download className="w-4 h-4" />
Export CSV
</button>
</div>
</div>
<div className="p-4 sm:p-6 border-b border-gray-100 bg-gray-50">
<div className="relative">
<input
type="text"
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder="Search vendor, category, amount..."
className=" pl-9 sm:pl-11 pr-4 py-2.5 sm:py-3 bg-white
border border-gray-200 rounded-xl text-xs sm:text-sm focus:outline-none focus:ring-2
focus:ring-[#FF6B00] focus:border-transparent max-sm:w-full w-80"
/>
</div>
</div>
</section>
{/* Table Section */}
<div className="overflow-x-auto 2xl:w-[90%] md:w-[100%] ">
<div className="inline-block min-w-full align-middle tableSectionMedia">
<table className="min-w-full divide-y divide-gray-100">
<thead className="bg-gray-50">
<tr>
<th className="px-3 sm:px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wide">Expense</th>
<th className="sm:table-cell px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wide">Category</th>
<th className="md:table-cell px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wide">Date</th>
<th className="lg:table-cell px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wide w-max whitespace-nowrap">Payment Mode</th>
<th className="px-3 sm:px-6 py-3 text-left text-xs font-semibold text-gray-500 uppercase tracking-wide">Status</th>
<th className="px-3 sm:px-6 py-3 text-right text-xs font-semibold text-gray-500 uppercase tracking-wide">Amount</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-100">
{filteredExpenses.map((expense) => (
<tr key={expense.id} className="hover:bg-gray-50 transitioStatus Amount
Inventory Restock
Fresh Farm Wholesale
UPI
Inventory 03 Dec 2025
Settled
18,500
Shop Rent
Sai Properties
Bank Transfer
Fixed Cost 01 Dec 2025
Scheduled
32,000
Delivery Partner Payout
Anil Kumar
Cash
Logistics 30 Nov 2025
Settled
6,500
Electricity Bill
TNEB
NEFT
Utilities 28 Nov 2025
Overdue
5,400
Packing Material
BoxIt Packaging
Card
Supplies 27 Nov 2025
Settled
2,600n-colors">
<td className="px-3 sm:px-6 py-3 sm:py-4">
<p className="text-xs sm:text-sm font-semibold text-gray-900">{expense.title}</p>
<p className="text-xs text-gray-500 mt-0.5">{expense.vendor}</p>
<div className="sm:hidden mt-1 flex flex-wrap gap-1.5">
<span className="text-xs text-gray-500">{expense.category}</span>
<span className="text-xs text-gray-400"></span>
<span className="text-xs text-gray-500">{formatDisplayDate(expense.date)}</span>
</div>
<div className="md:hidden sm:flex hidden mt-1 flex-wrap gap-1.5">
<span className="text-xs text-gray-500">{formatDisplayDate(expense.date)}</span>
</div>
<div className="lg:hidden md:flex hidden mt-1">
<span className="text-xs text-gray-500">{expense.paymentType}</span>
</div>
</td>
<td className="sm:table-cell px-6 py-4 text-sm font-medium text-gray-700">{expense.category}</td>
<td className="md:table-cell px-6 py-4 text-sm text-gray-500">{formatDisplayDate(expense.date)}</td>
<td className="lg:table-cell px-6 py-4 text-sm text-gray-500">{expense.paymentType}</td>
<td className="px-3 sm:px-6 py-3 sm:py-4">
<span
className={`inline-flex items-center gap-1 px-2 sm:px-3 py-1 rounded-full text-xs font-semibold whitespace-nowrap ${expense.status === 'Settled'
? 'bg-green-100 text-green-700'
: expense.status === 'Overdue'
? 'bg-red-100 text-red-700'
: 'bg-amber-100 text-amber-700'
}`}
>
<ArrowUpRight className="w-3 h-3 rotate-45 flex-shrink-0" />
<span>{expense.status}</span>
</span>
</td>
<td className="px-3 sm:px-6 py-3 sm:py-4 text-right text-xs sm:text-sm font-bold text-gray-900 ">
<div className="flex whitespace-nowrap gap-4 justify-end">
{expense.amount.toLocaleString()}
</div> </td>
</tr>
))}
{!filteredExpenses.length && (
<tr>
<td colSpan={6} className="px-4 sm:px-6 py-8 sm:py-10 text-center text-xs sm:text-sm text-gray-500">
No expenses match "{searchQuery}".
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{isRecordModalOpen && (
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center px-4 py-4 sm:py-8 z-50 overflow-y-auto" onClick={() => setIsRecordModalOpen(false)}>
<div
className="w-full max-w-lg bg-white rounded-xl sm:rounded-2xl shadow-2xl border border-gray-100 p-4 sm:p-6 my-auto max-h-[90vh] overflow-y-auto"
onClick={(event) => event.stopPropagation()}
>
<div className="flex items-center justify-between mb-4 sm:mb-6">
<div>
<h3 className="text-lg sm:text-xl font-bold text-gray-900">Record Expense</h3>
<p className="text-xs sm:text-sm text-gray-500">Capture business spending instantly</p>
</div>
<button className="p-2 rounded-full hover:bg-gray-100 flex-shrink-0" onClick={() => setIsRecordModalOpen(false)}>
<X className="w-5 h-5 text-gray-500" />
</button>
</div>
<form className="space-y-3 sm:space-y-4" onSubmit={handleSubmit}>
<div className="grid gap-3 sm:gap-4 grid-cols-1 sm:grid-cols-2">
<label className="text-xs sm:text-sm font-medium text-gray-700">
Expense Title
<input
type="text"
value={formData.title}
onChange={handleChange('title')}
placeholder="e.g., Rent payment"
className="mt-1 w-full rounded-xl border border-gray-200 px-3 py-2 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-[#FF6B00]"
required
/>
</label>
<label className="text-xs sm:text-sm font-medium text-gray-700">
Vendor / Payee
<input
type="text"
value={formData.vendor}
onChange={handleChange('vendor')}
placeholder="e.g., Sai Properties"
className="mt-1 w-full rounded-xl border border-gray-200 px-3 py-2 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-[#FF6B00]"
required
/>
</label>
</div>
<div className="grid gap-3 sm:gap-4 grid-cols-1 sm:grid-cols-2">
<label className="text-xs sm:text-sm font-medium text-gray-700">
Category
<select
value={formData.category}
onChange={handleChange('category')}
className="mt-1 w-full rounded-xl border border-gray-200 px-3 py-2 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-[#FF6B00]"
>
{categoryOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<button
type="button"
className="mt-1 text-xs font-semibold text-[#FF6B00] hover:text-[#FF8534]"
onClick={() => {
setIsAddingCategory(true);
setNewCategory('');
}}
>
+ Add new category
</button>
{isAddingCategory && (
<div className="mt-2 flex flex-col sm:flex-row gap-2 items-stretch sm:items-center">
<input
type="text"
value={newCategory}
onChange={(event) => setNewCategory(event.target.value)}
placeholder="e.g., Salary, Personal, Marketing"
className="flex-1 rounded-xl border border-gray-200 px-3 py-2 text-xs focus:outline-none focus:ring-2 focus:ring-[#FF6B00]"
/>
<div className="flex gap-2">
<button
type="button"
className="flex-1 sm:flex-none px-3 py-1.5 rounded-xl bg-gray-900 text-white text-xs font-semibold hover:bg-gray-800"
onClick={() => {
const label = newCategory.trim();
if (!label) return;
if (!categoryOptions.includes(label)) {
setCategoryOptions((prev) => [...prev, label]);
}
setFormData((prev) => ({ ...prev, category: label }));
setIsAddingCategory(false);
setNewCategory('');
}}
>
Save
</button>
<button
type="button"
className="flex-1 sm:flex-none px-2 py-1.5 rounded-xl border border-gray-200 text-xs font-medium text-gray-600 hover:bg-gray-50"
onClick={() => {
setIsAddingCategory(false);
setNewCategory('');
}}
>
Cancel
</button>
</div>
</div>
)}
</label>
<label className="text-xs sm:text-sm font-medium text-gray-700">
Payment Mode
<select
value={formData.paymentType}
onChange={handleChange('paymentType')}
className="mt-1 w-full rounded-xl border border-gray-200 px-3 py-2 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-[#FF6B00]"
>
<option>Cash</option>
<option>Card</option>
<option>UPI</option>
<option>Bank Transfer</option>
<option>NEFT</option>
</select>
</label>
</div>
<div className="grid gap-3 sm:gap-4 grid-cols-1 sm:grid-cols-2">
<label className="text-xs sm:text-sm font-medium text-gray-700">
Expense Date
<input
type="date"
value={formData.date}
onChange={handleChange('date')}
className="mt-1 w-full rounded-xl border border-gray-200 px-3 py-2 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-[#FF6B00]"
/>
</label>
<label className="text-xs sm:text-sm font-medium text-gray-700">
Status
<select
value={formData.status}
onChange={handleChange('status')}
className="mt-1 w-full rounded-xl border border-gray-200 px-3 py-2 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-[#FF6B00]"
>
<option>Scheduled</option>
<option>Settled</option>
<option>Overdue</option>
</select>
</label>
</div>
<div className="grid gap-3 sm:gap-4 grid-cols-1 sm:grid-cols-2 items-end">
<label className="text-xs sm:text-sm font-medium text-gray-700">
Amount
<div className="mt-1 flex items-center rounded-xl border border-gray-200 px-3 py-2 focus-within:ring-2 focus-within:ring-[#FF6B00]">
<span className="text-gray-500 mr-2 text-xs sm:text-sm"></span>
<input
type="number"
value={formData.amount}
onChange={handleChange('amount')}
placeholder="0"
min="0"
className="w-full bg-transparent text-xs sm:text-sm focus:outline-none"
required
/>
</div>
</label>
<label className="flex items-center gap-2 text-xs sm:text-sm font-medium text-gray-700">
<input
type="checkbox"
checked={formData.isFixedCommitment}
onChange={handleChange('isFixedCommitment')}
className="h-4 w-4 rounded border-gray-300 text-[#FF6B00] focus:ring-[#FF6B00] flex-shrink-0"
/>
<span>
Mark as <span className="font-semibold">fixed commitment</span>
</span>
</label>
</div>
<div className="flex flex-col-reverse sm:flex-row items-stretch sm:items-center justify-end gap-2 sm:gap-3 pt-2">
<button
type="button"
className="px-4 py-2.5 sm:py-2 rounded-xl border border-gray-200 text-xs sm:text-sm font-medium text-gray-600 hover:bg-gray-50"
onClick={() => setIsRecordModalOpen(false)}
>
Cancel
</button>
<button
type="submit"
className="px-5 py-2.5 sm:py-2 rounded-xl bg-gradient-to-r from-[#FF6B00] to-[#FF8534] text-white text-xs sm:text-sm font-semibold hover:shadow-lg transition-shadow"
>
Save Expense
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,7 @@
@media (min-width:768px) and (max-width:992px) {
.tableSectionMedia {
width: 58vw !important;
overflow: auto !important;
scrollbar-width: thin !important;
}
}

205
src/pages/Ledger/Ledger.jsx Normal file
View File

@ -0,0 +1,205 @@
import { Home as HomeIcon, BookOpen } from 'lucide-react';
import BreadCrumb from '../../components/BreadCrumb';
import './Ledger.scss';
import LedgerSection from '../../components/LedgerSection';
export default function LedgerHome({ user }) {
// Customer Data (From Screenshot)
const customers = [
{
id: 1,
name: 'Deepa',
phone: '98755 43210',
avatar: 'D',
balance: 800,
type: 'credit',
lastTransaction: 'Payment received',
lastDate: 'Today, 2:30 PM',
color: 'from-green-400 to-green-500',
bgColor: 'bg-green-50',
textColor: 'text-green-600',
history: [
{ id: 1, date: '02 Dec 2025', type: 'credit', amount: 500, note: 'Invoice payment', method: 'UPI' },
{ id: 2, date: '28 Nov 2025', type: 'debit', amount: 300, note: 'Product purchase', method: 'Cash' },
{ id: 3, date: '25 Nov 2025', type: 'credit', amount: 600, note: 'Service payment', method: 'Bank Transfer' },
{ id: 4, date: '20 Nov 2025', type: 'debit', amount: 200, note: 'Advance given', method: 'UPI' },
{ id: 5, date: '15 Nov 2025', type: 'credit', amount: 1000, note: 'Full settlement', method: 'Cash' }
]
},
{
id: 2,
name: 'Rakesh',
phone: '98765 12345',
avatar: 'R',
balance: 500,
type: 'debit',
lastTransaction: 'Outstanding payment',
lastDate: 'Yesterday, 5:00 PM',
color: 'from-red-400 to-red-500',
bgColor: 'bg-red-50',
textColor: 'text-red-600',
history: [
{ id: 1, date: '01 Dec 2025', type: 'debit', amount: 500, note: 'Product delivered', method: 'Pending' },
{ id: 2, date: '25 Nov 2025', type: 'credit', amount: 1000, note: 'Payment received', method: 'UPI' },
{ id: 3, date: '20 Nov 2025', type: 'debit', amount: 800, note: 'Supplies ordered', method: 'Cash' }
]
},
{
id: 3,
name: 'Sunil',
phone: '98755 67890',
avatar: 'S',
balance: 1200,
type: 'debit',
lastTransaction: 'Bought supplies',
lastDate: '2 days ago',
color: 'from-orange-400 to-orange-500',
bgColor: 'bg-orange-50',
textColor: 'text-orange-600',
history: [
{ id: 1, date: '30 Nov 2025', type: 'debit', amount: 1200, note: 'Bought supplies', method: 'Pending' },
{ id: 2, date: '22 Nov 2025', type: 'credit', amount: 500, note: 'Payment received', method: 'Cash' }
]
},
{
id: 4,
name: 'Priya',
phone: '98755 11111',
avatar: 'P',
balance: 2500,
type: 'credit',
lastTransaction: 'Invoice paid',
lastDate: '3 days ago',
color: 'from-blue-400 to-blue-500',
bgColor: 'bg-blue-50',
textColor: 'text-blue-600',
history: [
{ id: 1, date: '29 Nov 2025', type: 'credit', amount: 2500, note: 'Invoice paid', method: 'Bank Transfer' },
{ id: 2, date: '15 Nov 2025', type: 'debit', amount: 1000, note: 'Purchase order', method: 'UPI' }
]
},
{
id: 5,
name: 'Karthik',
phone: '98755 22222',
avatar: 'K',
balance: 1500,
type: 'credit',
lastTransaction: 'Payment sent',
lastDate: '4 days ago',
color: 'from-purple-400 to-purple-500',
bgColor: 'bg-purple-50',
textColor: 'text-purple-600',
history: [
{ id: 1, date: '28 Nov 2025', type: 'credit', amount: 1500, note: 'Payment sent', method: 'Cash' },
{ id: 2, date: '10 Nov 2025', type: 'debit', amount: 500, note: 'Advance payment', method: 'UPI' }
]
},
{
id: 6,
name: 'Anjali',
phone: '98755 33333',
avatar: 'A',
balance: 350,
type: 'debit',
lastTransaction: 'Pending payment',
lastDate: '5 days ago',
color: 'from-pink-400 to-pink-500',
bgColor: 'bg-pink-50',
textColor: 'text-pink-600',
history: [
{ id: 1, date: '27 Nov 2025', type: 'debit', amount: 350, note: 'Pending payment', method: 'Pending' }
]
},
];
// Supplier Data
const suppliers = [
{
id: 1,
name: 'ABC Materials',
phone: '98700 11111',
avatar: 'AM',
balance: 15000,
type: 'debit',
lastTransaction: 'Material purchased',
lastDate: 'Today, 10:00 AM',
color: 'from-red-400 to-red-500',
bgColor: 'bg-red-50',
textColor: 'text-red-600',
history: [
{ id: 1, date: '02 Dec 2025', type: 'debit', amount: 15000, note: 'Material purchased', method: 'Pending' },
{ id: 2, date: '20 Nov 2025', type: 'credit', amount: 10000, note: 'Payment made', method: 'Bank Transfer' }
]
},
{
id: 2,
name: 'Tech Supplies Co',
phone: '98700 22222',
avatar: 'TS',
balance: 8500,
type: 'debit',
lastTransaction: 'Equipment bought',
lastDate: 'Yesterday, 3:00 PM',
color: 'from-orange-400 to-orange-500',
bgColor: 'bg-orange-50',
textColor: 'text-orange-600'
},
{
id: 3,
name: 'Global Traders',
phone: '98700 33333',
avatar: 'GT',
balance: 22000,
type: 'debit',
lastTransaction: 'Bulk order',
lastDate: '2 days ago',
color: 'from-rose-400 to-rose-500',
bgColor: 'bg-rose-50',
textColor: 'text-rose-600'
},
{
id: 4,
name: 'Prime Wholesale',
phone: '98700 44444',
avatar: 'PW',
balance: 5000,
type: 'credit',
lastTransaction: 'Return credit',
lastDate: '3 days ago',
color: 'from-blue-400 to-blue-500',
bgColor: 'bg-blue-50',
textColor: 'text-blue-600'
},
{
id: 5,
name: 'Metro Suppliers',
phone: '98700 55555',
avatar: 'MS',
balance: 12000,
type: 'debit',
lastTransaction: 'Monthly payment',
lastDate: '4 days ago',
color: 'from-purple-400 to-purple-500',
bgColor: 'bg-purple-50',
textColor: 'text-purple-600'
},
];
const breadcrumbItems = [
{ icon: <HomeIcon />, label: 'Home', path: '/dashboard', active: false },
{ icon: <BookOpen />, label: 'My Ledger', path: '/ledger', active: true },
// { icon: activeTab === 'customers' ? <UsersIcon /> : <Package />, label: activeTab === 'customers' ? 'Customers' : 'Suppliers', active: true }
];
return (
<div className="khata-home-web">
<div className='relative'>
<BreadCrumb items={breadcrumbItems} showUser={true} />
</div>
<LedgerSection customers={customers} suppliers={suppliers} />
</div>
);
}

View File

@ -0,0 +1,96 @@
.khata-home-web {
// Profile/Greeting Section - Removed
.profile-greeting-removed {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.5rem;
padding: 1.5rem 1.75rem;
background: white;
border-radius: 1.25rem;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
border: 1px solid #F3F4F6;
.greeting-left {
display: flex;
align-items: center;
gap: 1.25rem;
.user-avatar-large {
width: 4rem;
height: 4rem;
border-radius: 50%;
background: linear-gradient(135deg, #FB923C, #F97316);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.75rem;
font-weight: 700;
box-shadow: 0 4px 12px -2px rgba(249, 115, 22, 0.4);
flex-shrink: 0;
}
.greeting-text {
h2 {
font-size: 0.875rem;
font-weight: 500;
color: #9CA3AF;
margin: 0 0 0.375rem 0;
line-height: 1;
}
.user-name {
font-size: 1.5rem;
font-weight: 700;
color: #111827;
margin: 0;
line-height: 1;
}
}
}
.greeting-right {
.notification-btn {
position: relative;
padding: 0.875rem;
background-color: #F9FAFB;
border: 1px solid #E5E7EB;
border-radius: 0.875rem;
cursor: pointer;
transition: all 0.2s;
&:hover {
background-color: #F3F4F6;
border-color: #D1D5DB;
}
svg {
width: 1.375rem;
height: 1.375rem;
color: #4B5563;
}
.notification-badge {
position: absolute;
top: 0.5rem;
right: 0.5rem;
min-width: 1.25rem;
height: 1.25rem;
padding: 0 0.25rem;
background-color: #EF4444;
color: white;
border-radius: 9999px;
font-size: 0.625rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px 0 rgba(239, 68, 68, 0.4);
}
}
}
}
}

View File

@ -0,0 +1,573 @@
import { useState } from 'react';
import { Home as HomeIcon, ChevronRight, BookOpen, FileText, Users, Package, Download, FileSpreadsheet, Calendar, Filter, Search, PieChart as PieChartIcon, BarChart3 } from 'lucide-react';
import { PieChart, Pie, Cell, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, Legend, CartesianGrid } from 'recharts';
import BreadCrumb from '../../components/BreadCrumb';
import './Reports.scss';
export default function Reports({ user }) {
const [activeTab, setActiveTab] = useState('cashbook');
const [cashbookDateRange, setCashbookDateRange] = useState('This Month');
const [transactionDateRange, setTransactionDateRange] = useState('This Month');
const [transactionType, setTransactionType] = useState('All');
const [showCashbookCharts, setShowCashbookCharts] = useState(false);
const [showTransactionCharts, setShowTransactionCharts] = useState(false);
// Sample Cashbook Data
const cashbookData = {
totalIn: 13500,
totalOut: 1550,
netBalance: 11950,
transactions: 4,
period: 'December 2025'
};
// Cashbook Chart Data
const cashbookChartData = [
{ name: 'Week 1', moneyIn: 3500, moneyOut: 400 },
{ name: 'Week 2', moneyIn: 2800, moneyOut: 350 },
{ name: 'Week 3', moneyIn: 4200, moneyOut: 500 },
{ name: 'Week 4', moneyIn: 3000, moneyOut: 300 },
];
const cashbookPieData = [
{ name: 'Money In', value: cashbookData.totalIn, color: '#10B981' },
{ name: 'Money Out', value: cashbookData.totalOut, color: '#EF4444' },
];
// Sample Transaction Data
const transactionData = {
customers: {
totalReceivable: 59550,
totalReceived: 13500,
pending: 46050,
count: 6
},
suppliers: {
totalPayable: 9800,
totalPaid: 1550,
pending: 8250,
count: 5
}
};
// Transaction Chart Data
const transactionChartData = [
{ name: 'Customers', receivable: 59550, received: 13500, pending: 46050 },
{ name: 'Suppliers', receivable: 9800, received: 1550, pending: 8250 },
];
const transactionPieData = [
{ name: 'Customer Receivable', value: transactionData.customers.totalReceivable, color: '#3B82F6' },
{ name: 'Supplier Payable', value: transactionData.suppliers.totalPayable, color: '#EF4444' },
];
const handleDownloadPDF = (reportType) => {
// Create simple text-based PDF content
let content = '';
if (reportType === 'Cashbook Report') {
content = `
CASHBOOK REPORT
${cashbookData.period}
======================
SUMMARY:
- Total Money In: ${cashbookData.totalIn.toLocaleString()}
- Total Money Out: ${cashbookData.totalOut.toLocaleString()}
- Net Balance: ${cashbookData.netBalance.toLocaleString()}
- Total Transactions: ${cashbookData.transactions}
Generated on: ${new Date().toLocaleString()}
`;
} else {
content = `
ALL TRANSACTION REPORT
${cashbookData.period}
======================
CUSTOMER TRANSACTIONS:
- Total Receivable: ${transactionData.customers.totalReceivable.toLocaleString()}
- Total Received: ${transactionData.customers.totalReceived.toLocaleString()}
- Pending Amount: ${transactionData.customers.pending.toLocaleString()}
- Total Customers: ${transactionData.customers.count}
SUPPLIER TRANSACTIONS:
- Total Payable: ${transactionData.suppliers.totalPayable.toLocaleString()}
- Total Paid: ${transactionData.suppliers.totalPaid.toLocaleString()}
- Pending Amount: ${transactionData.suppliers.pending.toLocaleString()}
- Total Suppliers: ${transactionData.suppliers.count}
Generated on: ${new Date().toLocaleString()}
`;
}
// Create blob and download
const blob = new Blob([content], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${reportType.replace(/ /g, '_')}_${new Date().toISOString().split('T')[0]}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
};
const handleDownloadExcel = (reportType) => {
let csvContent = '';
if (reportType === 'Cashbook Report') {
csvContent = `Cashbook Report - ${cashbookData.period}\n\n`;
csvContent += `Category,Amount\n`;
csvContent += `Total Money In,${cashbookData.totalIn}\n`;
csvContent += `Total Money Out,${cashbookData.totalOut}\n`;
csvContent += `Net Balance,${cashbookData.netBalance}\n`;
csvContent += `Total Transactions,${cashbookData.transactions}\n`;
} else {
csvContent = `All Transaction Report - ${cashbookData.period}\n\n`;
csvContent += `Customer Transactions\n`;
csvContent += `Category,Amount\n`;
csvContent += `Total Receivable,${transactionData.customers.totalReceivable}\n`;
csvContent += `Total Received,${transactionData.customers.totalReceived}\n`;
csvContent += `Pending Amount,${transactionData.customers.pending}\n`;
csvContent += `Total Customers,${transactionData.customers.count}\n\n`;
csvContent += `Supplier Transactions\n`;
csvContent += `Category,Amount\n`;
csvContent += `Total Payable,${transactionData.suppliers.totalPayable}\n`;
csvContent += `Total Paid,${transactionData.suppliers.totalPaid}\n`;
csvContent += `Pending Amount,${transactionData.suppliers.pending}\n`;
csvContent += `Total Suppliers,${transactionData.suppliers.count}\n`;
}
// Create blob and download as CSV (Excel compatible)
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${reportType.replace(/ /g, '_')}_${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
};
const breadcrumbItems = [
{ icon: <HomeIcon />, label: 'Home', path: '/dashboard', active: false },
{ icon: <FileText />, label: 'Reports', path: '/reports', active: true }
];
return (
<div className="reports-container">
<div className='relative'>
<BreadCrumb items={breadcrumbItems} showUser={true} />
</div>
{/* Page Header */}
<div className="reports-header">
<div className="header-title">
<h1>Reports</h1>
<p>Download and view your business reports with detailed analytics</p>
</div>
</div>
{/* Report Tabs */}
<div className="report-tabs">
<button
className={`report-tab ${activeTab === 'cashbook' ? 'active' : ''}`}
onClick={() => setActiveTab('cashbook')}
>
<BookOpen size={18} />
Cashbook Report
</button>
<button
className={`report-tab ${activeTab === 'transaction' ? 'active' : ''}`}
onClick={() => setActiveTab('transaction')}
>
<FileText size={18} />
All Transaction Report
</button>
</div>
{/* Cashbook Report Section */}
{activeTab === 'cashbook' && (
<div className="report-section">
<div className="section-card">
<div className="card-header">
<div className="header-left">
<div className="icon-wrapper blue">
<BookOpen size={24} />
</div>
<div className="header-info">
<h2>Cashbook Report</h2>
<p>Complete record of all cash transactions</p>
</div>
</div>
<div className="header-actions">
<button
className={`chart-toggle-btn ${showCashbookCharts ? 'active' : ''}`}
onClick={() => setShowCashbookCharts(!showCashbookCharts)}
>
<BarChart3 size={16} />
{showCashbookCharts ? 'Hide Charts' : 'Show Charts'}
</button>
<select
className="date-select"
value={cashbookDateRange}
onChange={(e) => setCashbookDateRange(e.target.value)}
>
<option>Today</option>
<option>Yesterday</option>
<option>This Week</option>
<option>This Month</option>
<option>Last Month</option>
<option>Custom Range</option>
</select>
</div>
</div>
<div className="card-body">
<div className="summary-grid">
<div className="summary-item green">
<div className="summary-label">Total Money In</div>
<div className="summary-value">{cashbookData.totalIn.toLocaleString()}</div>
</div>
<div className="summary-item red">
<div className="summary-label">Total Money Out</div>
<div className="summary-value">{cashbookData.totalOut.toLocaleString()}</div>
</div>
<div className="summary-item blue">
<div className="summary-label">Net Balance</div>
<div className="summary-value">{cashbookData.netBalance.toLocaleString()}</div>
</div>
<div className="summary-item grey">
<div className="summary-label">Total Transactions</div>
<div className="summary-value">{cashbookData.transactions}</div>
</div>
</div>
{/* Charts Section */}
{showCashbookCharts && (
<div className="charts-section">
<div className="chart-container">
<div className="chart-header">
<BarChart3 size={20} />
<h3>Weekly Money Flow</h3>
</div>
<ResponsiveContainer width="100%" height={280}>
<BarChart data={cashbookChartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#F0F0F0" vertical={false} />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fill: '#6B7280', fontSize: 12 }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: '#6B7280', fontSize: 12 }}
tickFormatter={(value) => `${value}`}
/>
<Tooltip
contentStyle={{
borderRadius: '8px',
border: '1px solid #E5E7EB',
fontSize: '13px'
}}
/>
<Legend iconType="circle" />
<Bar dataKey="moneyIn" fill="#10B981" radius={[8, 8, 0, 0]} name="Money In" />
<Bar dataKey="moneyOut" fill="#EF4444" radius={[8, 8, 0, 0]} name="Money Out" />
</BarChart>
</ResponsiveContainer>
</div>
<div className="chart-container">
<div className="chart-header">
<PieChartIcon size={20} />
<h3>Money Distribution</h3>
</div>
<ResponsiveContainer width="100%" height={280}>
<PieChart>
<Pie
data={cashbookPieData}
cx="50%"
cy="50%"
labelLine={false}
label={({ cx, cy, midAngle, outerRadius, percent }) => {
const RADIAN = Math.PI / 180;
const radius = outerRadius * 0.6;
const x = cx + radius * Math.cos(-midAngle * RADIAN);
const y = cy + radius * Math.sin(-midAngle * RADIAN);
return (
<text x={x} y={y} fill="white" textAnchor="middle" dominantBaseline="central" fontSize="14" fontWeight="bold">
{`${(percent * 100).toFixed(0)}%`}
</text>
);
}}
outerRadius={90}
fill="#8884d8"
dataKey="value"
>
{cashbookPieData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{
borderRadius: '8px',
border: '1px solid #E5E7EB',
fontSize: '13px'
}}
/>
</PieChart>
</ResponsiveContainer>
<div className='PoiChartChartMain'>
{cashbookPieData.map((entry, index) => (
<div key={index} className='PoiChartChart'>
<div className='indicationOfColor' style={{ backgroundColor: entry.color }}></div>
<div className='indicationOfValue'>
<div>{entry.name}</div>
<p>{entry.value.toLocaleString()}</p>
</div>
</div>
))}
</div>
</div>
</div>
)}
<div className="download-actions">
<button
className="download-btn pdf"
onClick={() => handleDownloadPDF('Cashbook Report')}
>
<Download size={18} />
<div>Download PDF</div>
</button>
<button
className="download-btn excel"
onClick={() => handleDownloadExcel('Cashbook Report')}
>
<FileSpreadsheet size={18} />
<div>Download Excel</div>
</button>
</div>
</div>
</div>
</div>
)}
{/* All Transaction Report Section */}
{activeTab === 'transaction' && (
<div className="report-section">
<div className="section-card">
<div className="card-header">
<div className="header-left">
<div className="icon-wrapper orange">
<FileText size={24} />
</div>
<div className="header-info">
<h2>All Transaction Report</h2>
<p>Comprehensive report of customer and supplier transactions</p>
</div>
</div>
<div className="header-actions">
<button
className={`chart-toggle-btn ${showTransactionCharts ? 'active' : ''}`}
onClick={() => setShowTransactionCharts(!showTransactionCharts)}
>
<BarChart3 size={16} />
{showTransactionCharts ? 'Hide Charts' : 'Show Charts'}
</button>
<select
className="type-select"
value={transactionType}
onChange={(e) => setTransactionType(e.target.value)}
>
<option>All</option>
<option>Customers Only</option>
<option>Suppliers Only</option>
</select>
<select
className="date-select"
value={transactionDateRange}
onChange={(e) => setTransactionDateRange(e.target.value)}
>
<option>Today</option>
<option>Yesterday</option>
<option>This Week</option>
<option>This Month</option>
<option>Last Month</option>
<option>Custom Range</option>
</select>
</div>
</div>
<div className="card-body">
{/* Customer Transactions */}
{(transactionType === 'All' || transactionType === 'Customers Only') && (
<div className="transaction-group">
<div className="group-header">
<Users size={20} />
<h3>Customer Transactions</h3>
<span className="count-badge">{transactionData.customers.count} Customers</span>
</div>
<div className="summary-grid">
<div className="summary-item green">
<div className="summary-label">Total Receivable</div>
<div className="summary-value">{transactionData.customers.totalReceivable.toLocaleString()}</div>
</div>
<div className="summary-item blue">
<div className="summary-label">Total Received</div>
<div className="summary-value">{transactionData.customers.totalReceived.toLocaleString()}</div>
</div>
<div className="summary-item orange">
<div className="summary-label">Pending Amount</div>
<div className="summary-value">{transactionData.customers.pending.toLocaleString()}</div>
</div>
</div>
</div>
)}
{/* Supplier Transactions */}
{(transactionType === 'All' || transactionType === 'Suppliers Only') && (
<div className="transaction-group">
<div className="group-header">
<Package size={20} />
<h3>Supplier Transactions</h3>
<span className="count-badge">{transactionData.suppliers.count} Suppliers</span>
</div>
<div className="summary-grid">
<div className="summary-item red">
<div className="summary-label">Total Payable</div>
<div className="summary-value">{transactionData.suppliers.totalPayable.toLocaleString()}</div>
</div>
<div className="summary-item blue">
<div className="summary-label">Total Paid</div>
<div className="summary-value">{transactionData.suppliers.totalPaid.toLocaleString()}</div>
</div>
<div className="summary-item orange">
<div className="summary-label">Pending Amount</div>
<div className="summary-value">{transactionData.suppliers.pending.toLocaleString()}</div>
</div>
</div>
</div>
)}
{/* Charts Section */}
{showTransactionCharts && (
<div className="charts-section">
<div className="chart-container">
<div className="chart-header">
<BarChart3 size={20} />
<h3>Transaction Comparison</h3>
</div>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={transactionChartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#F0F0F0" vertical={false} />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fill: '#6B7280', fontSize: 12 }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: '#6B7280', fontSize: 12 }}
tickFormatter={(value) => `${value}`}
/>
<Tooltip
contentStyle={{
borderRadius: '8px',
border: '1px solid #E5E7EB',
fontSize: '13px'
}}
/>
<Legend iconType="circle" />
<Bar dataKey="receivable" fill="#3B82F6" radius={[8, 8, 0, 0]} name="Total Amount" />
<Bar dataKey="received" fill="#10B981" radius={[8, 8, 0, 0]} name="Paid/Received" />
<Bar dataKey="pending" fill="#F59E0B" radius={[8, 8, 0, 0]} name="Pending" />
</BarChart>
</ResponsiveContainer>
</div>
<div className="chart-container">
<div className="chart-header">
<PieChartIcon size={20} />
<h3>Amount Distribution</h3>
</div>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={transactionPieData}
cx="50%"
cy="50%"
labelLine={false}
label={({ cx, cy, midAngle, outerRadius, percent }) => {
const RADIAN = Math.PI / 180;
const radius = outerRadius * 0.6;
const x = cx + radius * Math.cos(-midAngle * RADIAN);
const y = cy + radius * Math.sin(-midAngle * RADIAN);
return (
<text x={x} y={y} fill="white" textAnchor="middle" dominantBaseline="central" fontSize="14" fontWeight="bold">
{`${(percent * 100).toFixed(0)}%`}
</text>
);
}}
outerRadius={95}
fill="#8884d8"
dataKey="value"
>
{transactionPieData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{
borderRadius: '8px',
border: '1px solid #E5E7EB',
fontSize: '13px'
}}
/>
</PieChart>
</ResponsiveContainer>
<div className='PoiChartChartMain'>
{transactionPieData.map((entry, index) => (
<div key={index} className='PoiChartChart'>
<div className='indicationOfColor' style={{ backgroundColor: entry.color }}></div>
<div className='indicationOfValue'>
<div>{entry.name}</div>
<p>{entry.value.toLocaleString()}</p>
</div>
</div>
))}
</div>
</div>
</div>
)}
<div className="download-actions">
<button
className="download-btn pdf"
onClick={() => handleDownloadPDF('All Transaction Report')}
>
<Download size={18} />
Download PDF
</button>
<button
className="download-btn excel"
onClick={() => handleDownloadExcel('All Transaction Report')}
>
<FileSpreadsheet size={18} />
Download Excel
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,608 @@
.reports-container {
// Page Header
.reports-header {
margin-bottom: 1.5rem;
.header-title {
h1 {
font-size: 1.5rem;
font-weight: 700;
color: #111827;
}
p {
font-size: 0.8rem;
color: #6B7280;
margin: 0;
}
}
}
// Report Tabs
.report-tabs {
display: flex;
gap: 0.625rem;
margin-bottom: 1.5rem;
padding: 0.3rem;
background: white;
border-radius: 0.875rem;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
width: max-content;
.report-tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.625rem;
padding: 0.75rem 1.5rem;
background: transparent;
border: none;
border-radius: 0.625rem;
font-size: 0.9375rem;
font-weight: 500;
color: #6B7280;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
svg {
width: 18px;
height: 18px;
}
&:hover {
color: #374151;
background: #F9FAFB;
}
&.active {
background: linear-gradient(135deg, #FF6B00 0%, #FF8534 100%);
color: white;
box-shadow: 0 2px 8px rgba(255, 107, 0, 0.3);
font-weight: 600;
}
}
}
// Report Section
.report-section {
margin-bottom: 2rem;
.section-card {
background: white;
border-radius: 1rem;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
overflow: hidden;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid #E5E7EB;
flex-wrap: wrap;
gap: 1rem;
.header-left {
display: flex;
align-items: center;
gap: 1rem;
.icon-wrapper {
width: 56px;
height: 56px;
border-radius: 0.875rem;
display: flex;
align-items: center;
justify-content: center;
color: white;
&.blue {
background: linear-gradient(135deg, #3B82F6 0%, #2563EB 100%);
}
&.orange {
background: linear-gradient(135deg, #FF6B00 0%, #FF8534 100%);
}
&.green {
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
}
}
.header-info {
h2 {
font-size: 1.25rem;
font-weight: 700;
color: #111827;
margin: 0 0 0.25rem;
}
p {
font-size: 0.875rem;
color: #6B7280;
margin: 0;
}
}
}
.header-actions {
display: flex;
gap: 0.75rem;
.chart-toggle-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1rem;
border: 1px solid #D1D5DB;
border-radius: 0.625rem;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
background: white;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #FF6B00;
background: #FFF4F0;
color: #FF6B00;
}
&.active {
background: linear-gradient(135deg, #FF6B00 0%, #FF8534 100%);
color: white;
border-color: #FF6B00;
}
}
select {
padding: 0.625rem 1rem;
border: 1px solid #D1D5DB;
border-radius: 0.625rem;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
background: white;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #9CA3AF;
}
&:focus {
outline: none;
border-color: #FF6B00;
box-shadow: 0 0 0 3px rgba(255, 107, 0, 0.1);
}
}
.date-select {
min-width: 150px;
}
.type-select {
min-width: 160px;
}
}
}
.card-body {
padding: 1.5rem;
.transaction-group {
margin-bottom: 2rem;
&:last-child {
margin-bottom: 1rem;
}
.group-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.25rem;
padding-bottom: 0.75rem;
border-bottom: 2px solid #F3F4F6;
svg {
color: #FF6B00;
}
h3 {
font-size: 1.125rem;
font-weight: 700;
color: #111827;
margin: 0;
}
.count-badge {
white-space: nowrap;
margin-left: auto;
padding: 0.375rem 0.875rem;
background: #FFF4F0;
color: #FF6B00;
font-size: 0.8125rem;
font-weight: 600;
border-radius: 0.5rem;
}
}
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.25rem;
margin-bottom: 1.5rem;
.summary-item {
padding: 1rem;
border-radius: 0.875rem;
transition: transform 0.2s;
&:hover {
transform: translateY(-2px);
}
&.green {
background: #D1FAE5;
.summary-value {
color: #059669;
}
}
&.red {
background: #FEE2E2;
.summary-value {
color: #DC2626;
}
}
&.blue {
background: #DBEAFE;
.summary-value {
color: #2563EB;
}
}
&.orange {
background: #FED7AA;
.summary-value {
color: #C2410C;
}
}
&.grey {
background: #F3F4F6;
.summary-value {
color: #374151;
}
}
.summary-label {
font-size: 0.875rem;
color: #6B7280;
font-weight: 500;
margin-bottom: 0.5rem;
}
.summary-value {
font-size: 1.75rem;
font-weight: 700;
}
}
}
.charts-section {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
margin: 1.5rem 0;
padding: 1.5rem;
background: #F9FAFB;
border-radius: 0.875rem;
.chart-container {
background: white;
padding: 1.25rem;
border-radius: 0.75rem;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
.recharts-sector,
.recharts-layer {
outline: none !important;
border: none !important;
}
.chart-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
svg {
color: #FF6B00;
}
h3 {
font-size: 1rem;
font-weight: 700;
color: #111827;
margin: 0;
}
}
}
}
.download-actions {
display: flex;
gap: 1rem;
padding-top: 0.5rem;
// flex-wrap: wrap;
width: max-content;
.download-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.625rem;
padding: 0.875rem 1.5rem;
border: none;
border-radius: 0.75rem;
font-size: 0.9375rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
&.pdf {
background: linear-gradient(135deg, #EF4444 0%, #DC2626 100%);
color: white;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
}
&.excel {
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
color: white;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
}
}
}
}
}
}
.PoiChartChartMain {
width: 100%;
height: max-content;
max-height: 50vh;
overflow: auto;
padding: 12px;
background-color: #ffecec;
border-radius: 8px;
}
.PoiChartChart {
display: flex;
align-items: center;
gap: 10px;
}
.indicationOfColor {
width: 15px;
height: 15px;
border-radius: 5pc;
}
.indicationOfValue {
font-family: "Poppins";
div {
font-size: 14px;
color: #222;
font-weight: 500;
}
p {
font-size: 13px;
color: #444;
}
}
.recharts-sector,
.recharts-layer {
outline: none !important;
border: none !important;
}
// Mobile Responsive Styles
@media (max-width: 1024px) {
.report-tabs {
// flex-direction: column;
.report-tab {
justify-content: center;
}
}
}
@media (max-width: 768px) {
.reports-header {
.header-title h1 {
font-size: 1.5rem;
}
p {
font-size: 0.875rem;
}
}
.report-tabs {
padding: 0.1.5rem !important;
width: 100% !important;
.report-tab {
padding: 0.75rem 1rem;
font-size: 0.875rem;
}
}
.report-section {
.section-card {
.card-header {
flex-direction: column;
align-items: flex-start !important;
gap: 1rem;
padding: 1rem;
.icon-wrapper {
width: 40px !important;
height: 40px !important;
border-radius: 6px !important;
padding: 10px !important;
}
.header-info h2 {
font-size: 1.125rem !important;
}
.header-info p {
font-size: 0.7rem !important;
}
.header-actions {
width: 100%;
flex-wrap: wrap;
gap: 0.5rem;
white-space: nowrap;
.chart-toggle-btn,
select {
flex: 1;
min-width: 120px;
font-size: 0.75rem;
padding: 0.625rem 0.875rem;
}
}
}
.card-body {
padding: 0.7rem;
.transaction-group {
margin-bottom: 1.5rem;
.group-header {
font-size: 1rem;
padding-bottom: 0.625rem;
h3 {
font-size: 14px;
}
.count-badge {
font-size: 0.65rem;
padding: 0.25rem 0.625rem;
}
}
}
.summary-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.charts-section {
grid-template-columns: 1fr;
padding: 0;
gap: 1rem;
.chart-container {
padding: 1rem;
.chart-header h3 {
font-size: 0.9375rem;
}
}
// Make charts responsive
.recharts-wrapper {
width: 100% !important;
}
.recharts-surface {
width: 100% !important;
}
}
.download-actions {
// flex-direction: column;
gap: 0.75rem;
.download-btn {
width: 100%;
padding: 0.875rem 1.25rem;
}
}
}
}
}
}
@media (max-width: 480px) {
.summary-label {
margin-bottom: 0 !important;
}
.report-tabs .report-tab {
font-size: 0.8rem !important;
padding: 0.625rem 0.3rem !important;
gap: 4px !important;
svg {
width: 14px !important;
height: 14px !important;
}
}
.summary-item {
.summary-value {
font-size: 1.25rem;
}
}
.download-btn {
font-size: 0.8rem !important;
padding: 0.5rem 0.5rem !important;
flex: unset !important;
}
}
@media (max-width: 992px) {
.download-actions {
width: 100% !important;
}
}
}

View File

@ -0,0 +1,101 @@
import { Bell, HelpCircle, ChevronDown, ChevronRight, User, Lock, CreditCard, Palette, Globe, Mail, XCircle, Home as HomeIcon, Settings as SettingsIcon } from 'lucide-react';
import BreadCrumb from '../../components/BreadCrumb';
export default function Settings({ user }) {
const settingsGroups = [
{
title: 'Account Settings',
items: [
{ icon: User, label: 'Profile Information', description: 'Update your personal details', color: 'bg-blue-50', iconColor: 'text-blue-600' },
{ icon: Lock, label: 'Security', description: 'Password and authentication', color: 'bg-purple-50', iconColor: 'text-purple-600' },
{ icon: Mail, label: 'Email Preferences', description: 'Manage email notifications', color: 'bg-green-50', iconColor: 'text-green-600' },
]
},
{
title: 'Business Settings',
items: [
{ icon: CreditCard, label: 'Billing & Subscription', description: 'Manage your plan and payments', color: 'bg-orange-50', iconColor: 'text-[#FF6B00]' },
{ icon: Palette, label: 'Appearance', description: 'Customize your dashboard theme', color: 'bg-pink-50', iconColor: 'text-pink-600' },
{ icon: Globe, label: 'Language & Region', description: 'Set your preferred language', color: 'bg-teal-50', iconColor: 'text-teal-600' },
]
}
];
const breadcrumbItems = [
{ icon: <HomeIcon />, label: 'Home', path: '/dashboard', active: false },
{ icon: <SettingsIcon />, label: 'Settings', path: '/settings', active: true }
];
return (
<div>
{/* Header */}
<div className='relative'>
<BreadCrumb items={breadcrumbItems} showUser={true} />
</div>
{/* Profile Card */}
<div className="bg-white rounded-2xl p-7 border border-gray-200 mb-8 shadow-sm hover:shadow-md transition-all">
<div className="flex items-center gap-5">
<div className="w-24 h-24 bg-gradient-to-br from-[#FF6B00] to-[#FF8534] rounded-2xl flex items-center justify-center text-white text-3xl font-bold shadow-lg">
{user.avatar}
</div>
<div className="flex-1">
<h2 className="text-2xl font-bold text-gray-900 mb-1">{user.name}</h2>
<p className="text-sm text-gray-600 font-medium mb-2">Business Owner</p>
<p className="text-sm text-gray-500">ram@business.com</p>
</div>
<button className="px-6 py-2.5 border-2 border-gray-300 rounded-xl text-sm font-semibold hover:bg-gray-50 transition-all">
Edit Profile
</button>
</div>
</div>
{/* Settings Groups */}
{settingsGroups.map((group, groupIndex) => (
<div key={groupIndex} className="mb-8">
<h3 className="text-lg font-bold text-gray-900 mb-4">{group.title}</h3>
<div className="bg-white rounded-2xl border border-gray-200 overflow-hidden shadow-sm">
{group.items.map((item, itemIndex) => {
const Icon = item.icon;
return (
<button
key={itemIndex}
className={`w-full flex items-center gap-4 px-6 py-5 hover:bg-gray-50 transition-all ${itemIndex !== group.items.length - 1 ? 'border-b border-gray-200' : ''
}`}
>
<div className={`w-12 h-12 ${item.color} rounded-xl flex items-center justify-center`}>
<Icon className={`w-6 h-6 ${item.iconColor}`} strokeWidth={2.5} />
</div>
<div className="flex-1 text-left">
<div className="text-sm font-semibold text-gray-900 mb-1">{item.label}</div>
<p className="text-xs text-gray-500 font-medium">{item.description}</p>
</div>
<ChevronRight className="w-5 h-5 text-gray-400" />
</button>
);
})}
</div>
</div>
))}
{/* Danger Zone */}
<div className="mb-6">
<h3 className="text-lg font-bold text-gray-900 mb-4">Danger Zone</h3>
<div className="bg-white rounded-2xl border-2 border-red-200 overflow-hidden shadow-sm">
<button className="w-full flex items-center justify-between px-6 py-5 hover:bg-red-50 transition-all">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-red-50 rounded-xl flex items-center justify-center">
<XCircle className="w-6 h-6 text-red-600" strokeWidth={2.5} />
</div>
<div className="text-left">
<div className="text-sm font-semibold text-red-600 mb-1">Delete Account</div>
<p className="text-xs text-gray-500 font-medium">Permanently delete your account and all data</p>
</div>
</div>
<ChevronRight className="w-5 h-5 text-red-400" />
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,250 @@
import { useState } from 'react';
import { Globe, Phone, Download, User as UserIcon, Smartphone } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
export default function TransactionHistory({ user }) {
const navigate = useNavigate();
const [dateRange] = useState({ start: '04 Dec 2025', end: '04 Dec 2025' });
const [ownerPhone] = useState('+919344489998'); // Owner phone number
// Sample transaction data
const transactions = [
{ id: 1, date: '04 Dec', debit: 500, credit: null, isLatest: true },
{ id: 2, date: '04 Dec', debit: 200, credit: null, isLatest: false },
{ id: 3, date: '04 Dec', debit: null, credit: 200, isLatest: false },
{ id: 4, date: '04 Dec', debit: null, credit: 1000, isLatest: false },
{ id: 5, date: '04 Dec', debit: 1000, credit: null, isLatest: false },
];
// Calculate running balance
const calculateTransactions = () => {
let balance = 0;
const transactionsWithBalance = [];
for (let i = transactions.length - 1; i >= 0; i--) {
const txn = transactions[i];
if (txn.debit) {
balance += txn.debit;
}
if (txn.credit) {
balance -= txn.credit;
}
transactionsWithBalance.unshift({
...txn,
balance,
});
}
return transactionsWithBalance;
};
const transactionsWithBalance = calculateTransactions();
const totalDebit = transactions.reduce((sum, txn) => sum + (txn.debit || 0), 0);
const totalCredit = transactions.reduce((sum, txn) => sum + (txn.credit || 0), 0);
const netBalance = totalDebit - totalCredit;
const formatBalance = (balance) => {
if (balance === 0) return '0';
if (balance > 0) return `${balance.toLocaleString('en-IN')} Dr`;
return `${Math.abs(balance).toLocaleString('en-IN')} Cr`;
};
const getBalanceColor = (balance) => {
if (balance === 0) return 'text-gray-900';
if (balance > 0) return 'text-red-600';
return 'text-green-600';
};
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center">
{/* Header */}
<div className="bg-white border-b border-gray-200 shadow-sm" style={{ maxWidth: '800px', width: '100%' }}>
<div className="px-4 md:px-6 py-3 md:py-4">
<div className="flex items-center justify-between gap-2">
{/* Logo */}
<div
className="text-white px-4 py-2 md:px-6 md:py-3 rounded-lg font-bold text-base md:text-xl shadow-md cursor-pointer"
style={{ background: 'linear-gradient(to right, #FF6B00, #FF8534)' }}
onClick={() => navigate('/dashboard')}
>
LedgerPro
</div>
{/* Actions */}
<div className="flex items-center gap-2 md:gap-4">
<button className="flex items-center gap-1 md:gap-2 px-2 md:px-4 py-1.5 md:py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
<Globe className="w-3 h-3 md:w-4 md:h-4" />
<span className="text-xs md:text-sm font-medium hidden sm:inline">English</span>
</button>
<button className="flex items-center gap-1 md:gap-2 px-2 md:px-4 py-1.5 md:py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
<Phone className="w-3 h-3 md:w-4 md:h-4" />
<span className="text-xs md:text-sm font-medium hidden sm:inline">Call Support</span>
</button>
</div>
</div>
</div>
</div>
{/* Main Content */}
<div className="px-4 md:px-6 py-6 md:py-8" style={{ maxWidth: '800px', width: '100%' }}>
{/* Owner Contact Card */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 px-4 py-4 md:px-6 mb-6">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3 md:gap-4 flex-1 min-w-0">
<div className="w-12 h-12 md:w-14 md:h-14 bg-gray-200 rounded-full flex items-center justify-center flex-shrink-0">
<UserIcon className="w-6 h-6 md:w-7 md:h-7 text-gray-500" />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-gray-500 font-medium mb-1">Owner Contact</p>
<p className="text-lg md:text-xl font-bold text-gray-900 truncate">{ownerPhone}</p>
</div>
</div>
<a
href={`tel:${ownerPhone}`}
className="w-14 h-14 md:w-16 md:h-16 bg-green-500 rounded-full flex items-center justify-center hover:bg-green-600 transition-colors shadow-lg flex-shrink-0"
title="Call Owner"
style={{ backgroundColor: '#10b981' }}
>
<Phone className="w-6 h-6 md:w-8 md:h-8 text-white" />
</a>
</div>
</div>
{/* Title on Left, Summary Card on Right - Same Row (Desktop) / Stacked (Mobile/Tablet) */}
<div className="flex flex-col lg:flex-row items-start justify-between gap-4 mb-4">
{/* Left: Title */}
<div className="w-full lg:w-auto">
<h1 className="text-2xl md:text-3xl font-bold text-gray-900 mb-1">Transaction History</h1>
<p className="text-sm md:text-base text-gray-500">({dateRange.start} - {dateRange.end})</p>
</div>
{/* Right: Summary Card */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 px-4 py-4 md:px-6 w-full lg:w-auto lg:min-w-[550px]">
<div className="grid grid-cols-3 gap-4 md:gap-6 max-sm:grid-cols-[repeat(auto-fit,minmax(100px,1fr))]">
<div className="text-center">
<div className="text-xs md:text-sm text-gray-500 font-medium mb-1">Total Debit(-)</div>
<div className="text-2xl md:text-3xl font-bold text-gray-900">{totalDebit.toLocaleString('en-IN')}</div>
</div>
<div className="text-center">
<div className="text-xs md:text-sm text-gray-500 font-medium mb-1">Total Credit(+)</div>
<div className="text-2xl md:text-3xl font-bold text-gray-900">{totalCredit.toLocaleString('en-IN')}</div>
</div>
<div className="text-center">
<div className="text-xs md:text-sm text-gray-500 font-medium mb-1">Net Balance</div>
<div className={`text-2xl md:text-3xl font-bold ${getBalanceColor(netBalance)}`}>
{formatBalance(Math.abs(netBalance))}
</div>
</div>
</div>
</div>
</div>
{/* Transaction Table - Full Width */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full min-w-[600px]">
<thead>
<tr className="bg-gray-50 border-b border-gray-300">
<th className="px-3 md:px-6 py-3 text-left text-xs md:text-sm font-semibold text-gray-700 border-r border-gray-300">Date</th>
<th className="px-3 md:px-6 py-3 text-right text-xs md:text-sm font-semibold text-gray-700 border-r border-gray-300">Debit(-)</th>
<th className="px-3 md:px-6 py-3 text-right text-xs md:text-sm font-semibold text-gray-700 border-r border-gray-300">Credit(+)</th>
<th className="px-3 md:px-6 py-3 text-right text-xs md:text-sm font-semibold text-gray-700">Balance</th>
</tr>
</thead>
<tbody>
{transactionsWithBalance.map((transaction) => {
const rowBgColor = transaction.debit ? 'bg-red-50' : transaction.credit ? 'bg-green-50' : '';
return (
<tr key={transaction.id} className={`border-b border-gray-300 ${rowBgColor}`}>
<td className="px-3 md:px-6 py-3 border-r border-gray-300">
<div className="flex flex-col">
{transaction.isLatest && (
<span className="text-xs font-semibold text-gray-500 mb-0.5">Latest</span>
)}
<span className="text-xs md:text-sm font-medium text-gray-900">{transaction.date}</span>
</div>
</td>
<td className="px-3 md:px-6 py-3 text-right border-r border-gray-300">
{transaction.debit ? (
<span className="text-xs md:text-sm font-semibold text-gray-900">
{transaction.debit.toLocaleString('en-IN')}
</span>
) : (
<span className="text-gray-400"></span>
)}
</td>
<td className="px-3 md:px-6 py-3 text-right border-r border-gray-300">
{transaction.credit ? (
<span className="text-xs md:text-sm font-semibold text-gray-900">
{transaction.credit.toLocaleString('en-IN')}
</span>
) : (
<span className="text-gray-400"></span>
)}
</td>
<td className="px-3 md:px-6 py-3 text-right">
<span className={`font-bold text-xs md:text-sm ${getBalanceColor(transaction.balance)}`}>
{formatBalance(transaction.balance)}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
{/* App Download Banner */}
<div className="mt-8 bg-gradient-to-r from-blue-600 via-blue-500 to-orange-500 rounded-xl shadow-lg overflow-hidden">
<div className="px-4 md:px-8 py-6 md:py-8">
<div className="flex flex-col md:flex-row items-start justify-between gap-6">
{/* Left: Text Content */}
<div className="flex-1 text-left text-white">
<h2 className="text-2xl md:text-3xl font-bold text-black mb-2 text-white">
Digitize Your Ledger,<br />
Simplify Your Business!
</h2>
<p className="text-black text-sm md:text-base mb-4 text-white">
Manage all your transactions on the go with LedgerPro App
</p>
<button
onClick={(e) => {
e.preventDefault();
// TODO: Add actual Play Store link when app is ready
alert('App download link will be available soon!');
}}
className="inline-flex flex-row items-center gap-3 font-bold px-6 py-3 rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 cursor-pointer active:scale-95"
style={{ backgroundColor: '#2563eb', color: '#ffffff', display: 'inline-flex', flexDirection: 'row', alignItems: 'center' }}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#1d4ed8'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#2563eb'}
>
<Smartphone className="w-5 h-5 flex-shrink-0" style={{ color: '#ffffff' }} />
<span style={{ whiteSpace: 'nowrap' }}>Download LedgerPro</span>
</button>
</div>
{/* Right: Phone Icon/Placeholder */}
<div className="flex-shrink-0">
<div className="w-32 h-32 md:w-40 md:h-40 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm">
<Smartphone className="w-16 h-16 md:w-20 md:h-20 text-white" />
</div>
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="mt-6 text-center text-sm text-gray-500">
<p>Showing {transactions.length} transactions</p>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,312 @@
import { useState } from 'react';
import { Bell, HelpCircle, ChevronDown, Search, Calendar, Filter, Download, TrendingUp, TrendingDown, MoreVertical, Clock, CheckCircle, XCircle, Loader, Home as HomeIcon, ArrowLeftRight } from 'lucide-react';
import BreadCrumb from '../../components/BreadCrumb';
export default function Transactions({ user }) {
const [searchQuery, setSearchQuery] = useState('');
const [dateRange] = useState('1 Apr - 31 May 2024');
const statusCards = [
{
label: 'Waiting Payment',
count: 435,
change: '+56%',
positive: true,
color: 'bg-yellow-50',
iconBg: 'bg-yellow-100',
iconColor: 'text-yellow-600',
icon: Clock,
borderColor: 'border-yellow-200'
},
{
label: 'On Process',
count: 234,
change: '+26%',
positive: true,
color: 'bg-blue-50',
iconBg: 'bg-blue-100',
iconColor: 'text-blue-600',
icon: Loader,
borderColor: 'border-blue-200'
},
{
label: 'On Delivery',
count: 456,
change: '+68%',
positive: true,
color: 'bg-cyan-50',
iconBg: 'bg-cyan-100',
iconColor: 'text-cyan-600',
icon: CheckCircle,
borderColor: 'border-cyan-200'
},
{
label: 'Completed',
count: 675,
change: '+86%',
positive: true,
color: 'bg-green-50',
iconBg: 'bg-green-100',
iconColor: 'text-green-600',
icon: CheckCircle,
borderColor: 'border-green-200'
},
{
label: 'Cancelled',
count: 675,
change: '+23%',
positive: false,
color: 'bg-red-50',
iconBg: 'bg-red-100',
iconColor: 'text-red-600',
icon: XCircle,
borderColor: 'border-red-200'
},
];
const transactions = [
{
id: 1,
customer: 'Rajesh Kumar',
avatar: 'RK',
category: 'Payment',
type: 'Credit',
date: '30 May 2024',
amount: '₹ 45,000',
status: 'Completed',
statusColor: 'bg-green-100 text-green-700'
},
{
id: 2,
customer: 'Priya Sharma',
avatar: 'PS',
category: 'Payment',
type: 'Credit',
date: '30 May 2024',
amount: '₹ 44,000',
status: 'Completed',
statusColor: 'bg-green-100 text-green-700'
},
{
id: 3,
customer: 'Amit Patel',
avatar: 'AP',
category: 'Expense',
type: 'Debit',
date: '31 May 2024',
amount: '₹ 15,000',
status: 'On Process',
statusColor: 'bg-blue-100 text-blue-700'
},
{
id: 4,
customer: 'Sneha Reddy',
avatar: 'SR',
category: 'Payment',
type: 'Credit',
date: '28 May 2024',
amount: '₹ 11,000',
status: 'Waiting Payment',
statusColor: 'bg-yellow-100 text-yellow-700'
},
{
id: 5,
customer: 'Vikram Singh',
avatar: 'VS',
category: 'Expense',
type: 'Debit',
date: '25 May 2024',
amount: '₹ 8,000',
status: 'Cancelled',
statusColor: 'bg-red-100 text-red-700'
},
{
id: 6,
customer: 'Anjali Gupta',
avatar: 'AG',
category: 'Expense',
type: 'Debit',
date: '24 May 2024',
amount: '₹ 5,000',
status: 'On Process',
statusColor: 'bg-blue-100 text-blue-700'
},
{
id: 7,
customer: 'Karthik Iyer',
avatar: 'KI',
category: 'Payment',
type: 'Credit',
date: '24 May 2024',
amount: '₹ 6,000',
status: 'Completed',
statusColor: 'bg-green-100 text-green-700'
},
];
const breadcrumbItems = [
{ icon: <HomeIcon />, label: 'Home', path: '/dashboard', active: false },
{ icon: <ArrowLeftRight />, label: 'Transactions', path: '/transactions', active: true }
];
return (
<div>
{/* Header */}
<div className='relative'>
<BreadCrumb items={breadcrumbItems} showUser={true} />
</div>
{/* Status Cards */}
<div>
<h2 className="text-xl font-bold text-gray-900 mb-5">Transaction Status</h2>
<div className="grid grid-cols-5 gap-5 mb-8">
{statusCards.map((card, index) => {
const IconComponent = card.icon;
return (
<div
key={index}
className={`${card.color} rounded-2xl p-6 border ${card.borderColor} hover:shadow-lg transition-all duration-200 cursor-pointer`}
>
<div className="flex items-start justify-between mb-4">
<div className={`w-12 h-12 ${card.iconBg} rounded-xl flex items-center justify-center`}>
<IconComponent className={`w-6 h-6 ${card.iconColor}`} strokeWidth={2.5} />
</div>
</div>
<div className="mb-2 text-sm font-medium text-gray-600">{card.label}</div>
<div className="flex items-end justify-between mb-3">
<span className="text-3xl font-bold text-gray-900">{card.count}</span>
<div className={`flex items-center gap-1 text-sm font-semibold ${card.positive ? 'text-green-600' : 'text-red-600'}`}>
{card.positive ? (
<TrendingUp className="w-4 h-4" strokeWidth={2.5} />
) : (
<TrendingDown className="w-4 h-4" strokeWidth={2.5} />
)}
<span>{card.change}</span>
</div>
</div>
<button className="text-xs text-gray-500 hover:text-[#FF6B00] font-medium flex items-center gap-1.5 transition-colors">
<Download className="w-3.5 h-3.5" />
Download report
</button>
</div>
);
})}
</div>
</div>
{/* Transactions Table */}
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between mb-2">
<div>
<h2 className="text-xl font-bold text-gray-900 mb-1">All Transactions</h2>
<p className="text-sm text-gray-500">View detailed records of user transactions</p>
</div>
<div className="flex items-center gap-6">
<div className="text-right">
<div className="text-xs text-gray-500 font-medium">Pending payments</div>
<div className="text-lg font-bold text-red-600"> 34,000</div>
</div>
<div className="text-right">
<div className="text-xs text-gray-500 font-medium">Received payments</div>
<div className="text-lg font-bold text-green-600"> 5,50,870</div>
</div>
</div>
</div>
</div>
<div className="p-6 border-b border-gray-200 bg-gray-50">
<div className="flex items-center gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder="Search for customers, brand, category etc"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-[#FF6B00] focus:border-transparent"
/>
</div>
<button className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 rounded-xl text-sm font-medium hover:bg-gray-50 transition-colors">
<Calendar className="w-4 h-4 text-gray-600" />
<span>{dateRange}</span>
</button>
<button className="flex items-center gap-2 px-4 py-2.5 bg-white border border-gray-300 rounded-xl text-sm font-medium hover:bg-gray-50 transition-colors">
<Filter className="w-4 h-4 text-gray-600" />
<span>Filter</span>
<ChevronDown className="w-4 h-4 text-gray-600" />
</button>
<button className="flex items-center gap-2 px-6 py-2.5 bg-gradient-to-r from-[#FF6B00] to-[#FF8534] text-white rounded-xl text-sm font-semibold hover:shadow-lg transition-all">
<Download className="w-4 h-4" />
Export CSV
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
Customer
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
Category
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
Type
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
Time Order
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
Amount
</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-4"></th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{transactions.map((transaction) => (
<tr key={transaction.id} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-[#FF6B00] to-[#FF8534] rounded-xl flex items-center justify-center text-white text-sm font-semibold shadow-md">
{transaction.avatar}
</div>
<span className="text-sm font-medium text-gray-900">{transaction.customer}</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600 font-medium">
{transaction.category}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-semibold text-gray-900">
{transaction.type}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
{transaction.date}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-bold text-gray-900">
{transaction.amount}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${transaction.statusColor}`}>
{transaction.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right">
<button className="text-gray-400 hover:text-gray-600 transition-colors">
<MoreVertical className="w-5 h-5" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,7 @@
export const userSliceReducers = {
changeUserData: (state, action) => {
const { user } = action?.payload;
state.user = user;
},
};

View File

@ -0,0 +1,44 @@
// src/store/slices/userSlice.js
import { createSlice } from "@reduxjs/toolkit";
import { userSliceReducers } from "../reducers/userSliceReducers";
import { getUserData } from "../thunks/userThunk";
/**
* Initial shape of User State
*/
const initialUserState = {
user: {
name: "Ram",
avatar: "R",
},
};
/**
* User Slice: manages profile data, preferences, etc.
*/
const userSlice = createSlice({
name: "user",
initialState: initialUserState,
reducers: userSliceReducers,
extraReducers: (builder) => {
builder.addCase(getUserData.fulfilled, (state, action) => {
state.user = action.payload;
});
},
});
/**
* Selector: Get user object from store
*/
export const selectUser = (state) => state.user.user;
/**
* Export reducers (actions) from userSliceReducers
*/
export const { changeUserData } = userSlice.actions;
/**
* Export slice reducer
*/
export default userSlice.reducer;

8
src/redux/store.js Normal file
View File

@ -0,0 +1,8 @@
import { configureStore } from '@reduxjs/toolkit';
import userSliceReducer from './slices/userSlice.js';
export const store = configureStore({
reducer: {
user: userSliceReducer
},
});

View File

@ -0,0 +1,18 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { axiosCommonInstance, axiosInstance } from '../../utils/axiosInstances';
export const getUserData = createAsyncThunk('userSlice/getEmpAccess', async (data) => {
const { UserId } = data;
if (UserId) {
return await axiosInstance.get(`/User?UserId=${data?.UserId}`);
}
});
export const getSessionDataFromCommon = createAsyncThunk(
'userSlice/getSessionDataFromCommon',
async (postData) => {
return await axiosCommonInstance.get(
`/URLshortener?referenceNo=${postData}`
);
}
);

60
src/routes/AppRoutes.jsx Normal file
View File

@ -0,0 +1,60 @@
import { Routes, Route } from "react-router-dom";
import { RouteDefinitions } from "./RouteDefinitions";
import { useDispatch } from "react-redux";
import { getSessionDataFromCommon } from "../redux/thunks/userThunk";
import { decryptToObject } from "../utils/others";
import { sessionStore } from "../utils/sessionUtils";
import { useEffect } from "react";
//This is where We Get SessionUrlCode By Tamilselvan
const SessionCode = import.meta.env.ENV_SESSION_CODE
const generateRoutes = (routes) =>
routes.map(({ path, element, children, index }, routeIndex) => (
<Route key={path || `route-${routeIndex}`} path={path} element={element} index={index}>
{children && generateRoutes(children)}
</Route>
));
export default function AppRoutes() {
const dispatch = useDispatch()
useEffect(() => {
const init = async () => {
try {
const qrResponse = await dispatch(
getSessionDataFromCommon(SessionCode)
).unwrap();
if (qrResponse?.data?.statusCode == 1) {
const urldata = decryptToObject(qrResponse.data.data?.[0]?.OldUrl);
console.log("Decrypted URL Data:", urldata);
sessionStore("AppId", urldata?.AppId);
sessionStore("CompId", urldata?.CompId);
sessionStore("UserId", urldata?.UserId);
sessionStore("UserType", urldata?.UserType);
sessionStore("MobileNo", urldata?.MobileNo);
sessionStore("SessionId", urldata?.SessionId);
if (urldata?.BranchId) sessionStore("BranchId", urldata?.BranchId);
sessionStorage.setItem("auth", urldata?.auth);
}
}
catch (err) {
}
};
init();
}, []);
const routes = generateRoutes(RouteDefinitions);
return <Routes>{routes}</Routes>;
}

View File

@ -0,0 +1,6 @@
import { Navigate } from "react-router-dom";
export default function ProtectedRoute({ isAuth, children }) {
if (!isAuth) return <Navigate to="/login" replace />;
return children;
}

View File

@ -0,0 +1,65 @@
import { Navigate } from "react-router-dom";
import Customers from "../pages/Customers/Customers.jsx";
import Dashboard from "../pages/Dashboard/Dashboard.jsx";
import Layout from "../layouts/Layout.jsx";
import Ledger from "../pages/Ledger/Ledger.jsx";
import Reports from "../pages/Reports/Reports.jsx";
import Settings from "../pages/Settings/Settings.jsx";
import Transactions from "../pages/Transactions/Transactions.jsx";
import Cashbook from "../pages/Cashbook/Cashbook.jsx";
import Expenses from "../pages/Expenses/Expenses.jsx";
import ProtectedRoute from "./ProtectedRoute.jsx";
const user = { name: "Ram", avatar: "R" };
const isAuthenticated = true;
export const RouteDefinitions = [
{
path: "/",
element: <Layout user={user} />,
children: [
{
index: true,
element: <Navigate to="dashboard" replace />
},
{
path: "dashboard",
element: <Dashboard user={user} />
},
{
path: "ledger",
element: (
<ProtectedRoute isAuth={isAuthenticated}>
<Ledger user={user} />
</ProtectedRoute>
)
},
{
path: "expenses",
element: <Expenses user={user} />
},
{
path: "transactions",
element: <Transactions user={user} />
},
{
path: "customers",
element: <Customers user={user} />
},
{
path: "reports",
element: <Reports user={user} />
},
{
path: "settings",
element: <Settings user={user} />
},
{
path: "cashbook",
element: <Cashbook />
}
]
}
];

24
src/smooth-scroll.css Normal file
View File

@ -0,0 +1,24 @@
* {
scroll-behavior: smooth;
}
html {
scroll-behavior: smooth;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}

Some files were not shown because too many files have changed in this diff Show More