Zelyanoth commited on
Commit
24d40b9
·
verified ·
1 Parent(s): 4cd3f8d

Upload 101 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +12 -0
  2. .gitattributes +1 -0
  3. .gitignore +24 -0
  4. Dockerfile +41 -0
  5. INTEGRATION_GUIDE.md +775 -0
  6. bun.lockb +3 -0
  7. components.json +20 -0
  8. docker-compose.yml +9 -0
  9. eslint.config.js +29 -0
  10. index.html +26 -0
  11. package-lock.json +0 -0
  12. package.json +86 -0
  13. postcss.config.js +6 -0
  14. public/favicon.ico +0 -0
  15. public/lovable-uploads/efa85ef3-0e1e-44d2-bbd4-34fe8f8946e4.png +0 -0
  16. public/placeholder.svg +1 -0
  17. public/robots.txt +14 -0
  18. src/App.css +42 -0
  19. src/App.tsx +48 -0
  20. src/components/dashboard/BalanceCard.tsx +62 -0
  21. src/components/dashboard/RecentTransactions.tsx +137 -0
  22. src/components/dashboard/SpendingAnalytics.tsx +82 -0
  23. src/components/layout/AppLayout.tsx +79 -0
  24. src/components/shared/Header.tsx +47 -0
  25. src/components/stock/MainStockMenu.tsx +35 -0
  26. src/components/stock/StockMenu.tsx +79 -0
  27. src/components/theme/ThemeProvider.tsx +73 -0
  28. src/components/theme/ThemeToggle.tsx +24 -0
  29. src/components/transactions/AITransactionChat.tsx +386 -0
  30. src/components/transactions/TransactionForm.tsx +231 -0
  31. src/components/transactions/TransactionList.tsx +148 -0
  32. src/components/ui/accordion.tsx +56 -0
  33. src/components/ui/alert-dialog.tsx +139 -0
  34. src/components/ui/alert.tsx +59 -0
  35. src/components/ui/aspect-ratio.tsx +5 -0
  36. src/components/ui/avatar.tsx +48 -0
  37. src/components/ui/badge.tsx +36 -0
  38. src/components/ui/breadcrumb.tsx +115 -0
  39. src/components/ui/button.tsx +56 -0
  40. src/components/ui/calendar.tsx +64 -0
  41. src/components/ui/card.tsx +79 -0
  42. src/components/ui/carousel.tsx +260 -0
  43. src/components/ui/chart.tsx +363 -0
  44. src/components/ui/checkbox.tsx +28 -0
  45. src/components/ui/collapsible.tsx +9 -0
  46. src/components/ui/command.tsx +153 -0
  47. src/components/ui/context-menu.tsx +198 -0
  48. src/components/ui/dialog.tsx +120 -0
  49. src/components/ui/drawer.tsx +116 -0
  50. src/components/ui/dropdown-menu.tsx +198 -0
.dockerignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ node_modules
3
+ npm-debug.log
4
+ Dockerfile*
5
+ docker-compose*
6
+ .dockerignore
7
+ .git
8
+ .github
9
+ .gitignore
10
+ README.md
11
+ .env
12
+ dist
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ bun.lockb filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
Dockerfile ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Base image
3
+ FROM node:20-alpine as build
4
+
5
+ # Set working directory
6
+ WORKDIR /app
7
+
8
+ # Copy package.json and package-lock.json
9
+ COPY package*.json ./
10
+
11
+ # Install dependencies
12
+ RUN npm install
13
+
14
+ # Copy project files
15
+ COPY . .
16
+
17
+ # Build the app
18
+ RUN npm run build
19
+
20
+ # Production image
21
+ FROM nginx:alpine
22
+
23
+ # Copy build files from previous stage
24
+ COPY --from=build /app/dist /usr/share/nginx/html
25
+
26
+ # Copy nginx configuration for single-page application support
27
+ RUN echo 'server { \
28
+ listen 7860; \
29
+ server_name _; \
30
+ root /usr/share/nginx/html; \
31
+ index index.html; \
32
+ location / { \
33
+ try_files $uri $uri/ /index.html; \
34
+ } \
35
+ }' > /etc/nginx/conf.d/default.conf
36
+
37
+ # Expose port 7860 (default port for Hugging Face Spaces)
38
+ EXPOSE 7860
39
+
40
+ # Start nginx
41
+ CMD ["nginx", "-g", "daemon off;"]
INTEGRATION_GUIDE.md ADDED
@@ -0,0 +1,775 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Flutter Finance App - Python API Integration Guide
3
+
4
+ This guide explains how to connect the Flutter Finance App front-end with a Python API backend for the chat interface and database interactions.
5
+
6
+ ## Architecture Overview
7
+
8
+ ![Architecture Overview](https://mermaid.ink/img/pako:eNp1kU9PwzAMxb9K5BNIHeq2tOVvtQtIcODAgRMXqw1bRJuUJGVIqN-dnUy0Gxw8-fnnt-SnFLYQkY5oRaNw27LPUcaiLnredSg5vD-_vgSy9lr9gfXZdTEtKk6NtGXhsGZGjBi1EFxR6M4x8MpiGxHnxN1baD6E9GW0kGLPtlgGQZxblzA86Z9KP3QPgWbQaHujbgxkWIj5ijwPAN-xHEZVLuul0xQXM8P1wiToXoyJrPrwhm3awaeQ4qlJPbMpZzAl36GmeF9HfdBh5npWD5tRPUERXYG-6FTJCfPP4f1rKb9huA1hG-TBf4z6b7z5Mol2KEQl2bnCjnWhNxOXhRsh2XG9JaeL0YhRcb2luujG75tSbFGTsfKEn-ckZLtp9Z-TPsDsDz8_)
9
+
10
+ ## 1. Setting up the Python API Server
11
+
12
+ ### Requirements
13
+ - Python 3.8+
14
+ - Flask/FastAPI
15
+ - SQL.js connector or alternative database library
16
+
17
+ ### Step 1: Create a Flask API
18
+
19
+ ```python
20
+ from flask import Flask, request, jsonify
21
+ from flask_cors import CORS
22
+ import sqlite3
23
+ import json
24
+
25
+ app = Flask(__name__)
26
+ CORS(app) # Enable Cross-Origin Resource Sharing
27
+
28
+ # Database connection
29
+ def get_db_connection():
30
+ conn = sqlite3.connect('finance_app.db')
31
+ conn.row_factory = sqlite3.Row
32
+ return conn
33
+
34
+ # Create tables if they don't exist
35
+ def init_db():
36
+ conn = get_db_connection()
37
+ conn.execute('''
38
+ CREATE TABLE IF NOT EXISTS transactions (
39
+ id TEXT PRIMARY KEY,
40
+ title TEXT NOT NULL,
41
+ amount REAL NOT NULL,
42
+ type TEXT NOT NULL,
43
+ category TEXT NOT NULL,
44
+ date TEXT NOT NULL,
45
+ note TEXT,
46
+ message TEXT
47
+ )
48
+ ''')
49
+
50
+ conn.execute('''
51
+ CREATE TABLE IF NOT EXISTS products (
52
+ id TEXT PRIMARY KEY,
53
+ name TEXT NOT NULL,
54
+ sku TEXT NOT NULL,
55
+ quantity INTEGER NOT NULL,
56
+ price REAL NOT NULL,
57
+ category TEXT NOT NULL,
58
+ last_updated TEXT NOT NULL,
59
+ reorder_point INTEGER DEFAULT 5,
60
+ trend TEXT DEFAULT 'stable',
61
+ demand TEXT DEFAULT 'medium'
62
+ )
63
+ ''')
64
+
65
+ conn.commit()
66
+ conn.close()
67
+
68
+ # Initialize database on startup
69
+ init_db()
70
+
71
+ # Chat endpoints
72
+ @app.route('/api/messages', methods=['GET'])
73
+ def get_messages():
74
+ conn = get_db_connection()
75
+ messages = conn.execute('SELECT * FROM messages ORDER BY timestamp').fetchall()
76
+ conn.close()
77
+ return jsonify([dict(message) for message in messages])
78
+
79
+ @app.route('/api/messages', methods=['POST'])
80
+ def send_message():
81
+ data = request.json
82
+ conn = get_db_connection()
83
+ conn.execute(
84
+ 'INSERT INTO messages (user_id, content, timestamp) VALUES (?, ?, ?)',
85
+ (data['user_id'], data['content'], data['timestamp'])
86
+ )
87
+ conn.commit()
88
+ conn.close()
89
+
90
+ # Here you can implement AI response logic
91
+ return jsonify({"status": "success", "message": "Message sent"})
92
+
93
+ # Transaction endpoints
94
+ @app.route('/api/transactions', methods=['GET'])
95
+ def get_transactions():
96
+ conn = get_db_connection()
97
+ transactions = conn.execute('SELECT * FROM transactions').fetchall()
98
+ conn.close()
99
+ return jsonify([dict(tx) for tx in transactions])
100
+
101
+ @app.route('/api/transactions', methods=['POST'])
102
+ def add_transaction():
103
+ data = request.json
104
+ conn = get_db_connection()
105
+ conn.execute(
106
+ 'INSERT INTO transactions (id, title, amount, type, category, date, note, message) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
107
+ (data['id'], data['title'], data['amount'], data['type'], data['category'], data['date'], data.get('note'), data.get('message'))
108
+ )
109
+ conn.commit()
110
+ conn.close()
111
+ return jsonify({"status": "success"})
112
+
113
+ # Product/Stock endpoints
114
+ @app.route('/api/products', methods=['GET'])
115
+ def get_products():
116
+ conn = get_db_connection()
117
+ products = conn.execute('SELECT * FROM products ORDER BY name').fetchall()
118
+ conn.close()
119
+ return jsonify([dict(product) for product in products])
120
+
121
+ @app.route('/api/products/low', methods=['GET'])
122
+ def get_low_stock_products():
123
+ conn = get_db_connection()
124
+ products = conn.execute('SELECT * FROM products WHERE quantity <= reorder_point ORDER BY quantity').fetchall()
125
+ conn.close()
126
+ return jsonify([dict(product) for product in products])
127
+
128
+ @app.route('/api/products/<product_id>', methods=['GET'])
129
+ def get_product(product_id):
130
+ conn = get_db_connection()
131
+ product = conn.execute('SELECT * FROM products WHERE id = ?', (product_id,)).fetchone()
132
+ conn.close()
133
+
134
+ if not product:
135
+ return jsonify({"error": "Product not found"}), 404
136
+
137
+ return jsonify(dict(product))
138
+
139
+ @app.route('/api/products', methods=['POST'])
140
+ def add_product():
141
+ data = request.json
142
+ conn = get_db_connection()
143
+
144
+ try:
145
+ conn.execute(
146
+ 'INSERT INTO products (id, name, sku, quantity, price, category, last_updated, reorder_point) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
147
+ (
148
+ data['id'],
149
+ data['name'],
150
+ data['sku'],
151
+ data['quantity'],
152
+ data['price'],
153
+ data['category'],
154
+ data['last_updated'],
155
+ data.get('reorder_point', 5)
156
+ )
157
+ )
158
+ conn.commit()
159
+ return jsonify({"status": "success", "message": "Product added successfully"})
160
+ except Exception as e:
161
+ conn.rollback()
162
+ return jsonify({"status": "error", "message": str(e)}), 400
163
+ finally:
164
+ conn.close()
165
+
166
+ @app.route('/api/products/<product_id>', methods=['PUT'])
167
+ def update_product(product_id):
168
+ data = request.json
169
+ conn = get_db_connection()
170
+
171
+ try:
172
+ # Check if product exists
173
+ product = conn.execute('SELECT * FROM products WHERE id = ?', (product_id,)).fetchone()
174
+ if not product:
175
+ return jsonify({"error": "Product not found"}), 404
176
+
177
+ # Update product
178
+ conn.execute(
179
+ '''
180
+ UPDATE products
181
+ SET name = ?, sku = ?, quantity = ?, price = ?,
182
+ category = ?, last_updated = ?, reorder_point = ?
183
+ WHERE id = ?
184
+ ''',
185
+ (
186
+ data['name'],
187
+ data['sku'],
188
+ data['quantity'],
189
+ data['price'],
190
+ data['category'],
191
+ data['last_updated'],
192
+ data.get('reorder_point', 5),
193
+ product_id
194
+ )
195
+ )
196
+ conn.commit()
197
+ return jsonify({"status": "success", "message": "Product updated successfully"})
198
+ except Exception as e:
199
+ conn.rollback()
200
+ return jsonify({"error": str(e)}), 400
201
+ finally:
202
+ conn.close()
203
+
204
+ @app.route('/api/products/<product_id>', methods=['DELETE'])
205
+ def delete_product(product_id):
206
+ conn = get_db_connection()
207
+
208
+ try:
209
+ # Check if product exists
210
+ product = conn.execute('SELECT * FROM products WHERE id = ?', (product_id,)).fetchone()
211
+ if not product:
212
+ return jsonify({"error": "Product not found"}), 404
213
+
214
+ # Delete product
215
+ conn.execute('DELETE FROM products WHERE id = ?', (product_id,))
216
+ conn.commit()
217
+ return jsonify({"status": "success", "message": "Product deleted successfully"})
218
+ except Exception as e:
219
+ conn.rollback()
220
+ return jsonify({"error": str(e)}), 400
221
+ finally:
222
+ conn.close()
223
+
224
+ @app.route('/api/stock/analytics', methods=['GET'])
225
+ def get_stock_analytics():
226
+ conn = get_db_connection()
227
+
228
+ try:
229
+ # Get total product count
230
+ total_count = conn.execute('SELECT COUNT(*) as count FROM products').fetchone()['count']
231
+
232
+ # Get total inventory value
233
+ total_value = conn.execute('SELECT SUM(quantity * price) as value FROM products').fetchone()['value']
234
+
235
+ # Get low stock count
236
+ low_stock = conn.execute('SELECT COUNT(*) as count FROM products WHERE quantity <= reorder_point').fetchone()['count']
237
+
238
+ # Get top categories
239
+ categories = conn.execute('''
240
+ SELECT category, COUNT(*) as count, SUM(quantity * price) as value
241
+ FROM products
242
+ GROUP BY category
243
+ ORDER BY count DESC
244
+ LIMIT 5
245
+ ''').fetchall()
246
+
247
+ return jsonify({
248
+ "total_count": total_count,
249
+ "total_value": total_value or 0,
250
+ "low_stock_count": low_stock,
251
+ "categories": [dict(category) for category in categories]
252
+ })
253
+ except Exception as e:
254
+ return jsonify({"error": str(e)}), 400
255
+ finally:
256
+ conn.close()
257
+
258
+ if __name__ == '__main__':
259
+ app.run(debug=True, port=5000)
260
+ ```
261
+
262
+ ### Step 2: Set up a virtual environment
263
+
264
+ ```bash
265
+ # Create a virtual environment
266
+ python -m venv venv
267
+
268
+ # Activate it (Windows)
269
+ venv\Scripts\activate
270
+
271
+ # Activate it (macOS/Linux)
272
+ source venv/bin/activate
273
+
274
+ # Install dependencies
275
+ pip install flask flask-cors sqlite3
276
+ ```
277
+
278
+ ## 2. Connecting the React App to the Python API
279
+
280
+ ### Step 1: Create an API Service in the React App
281
+
282
+ Create a new file `src/services/api.ts`:
283
+
284
+ ```typescript
285
+ const API_URL = 'http://localhost:5000/api';
286
+
287
+ // Messages API
288
+ export const fetchMessages = async () => {
289
+ const response = await fetch(`${API_URL}/messages`);
290
+ if (!response.ok) {
291
+ throw new Error('Failed to fetch messages');
292
+ }
293
+ return response.json();
294
+ };
295
+
296
+ export const sendMessage = async (message: {
297
+ user_id: string;
298
+ content: string;
299
+ timestamp: string;
300
+ }) => {
301
+ const response = await fetch(`${API_URL}/messages`, {
302
+ method: 'POST',
303
+ headers: {
304
+ 'Content-Type': 'application/json',
305
+ },
306
+ body: JSON.stringify(message),
307
+ });
308
+
309
+ if (!response.ok) {
310
+ throw new Error('Failed to send message');
311
+ }
312
+
313
+ return response.json();
314
+ };
315
+
316
+ // Transactions API
317
+ export const fetchTransactions = async () => {
318
+ const response = await fetch(`${API_URL}/transactions`);
319
+ if (!response.ok) {
320
+ throw new Error('Failed to fetch transactions');
321
+ }
322
+ return response.json();
323
+ };
324
+
325
+ export const addTransaction = async (transaction: any) => {
326
+ const response = await fetch(`${API_URL}/transactions`, {
327
+ method: 'POST',
328
+ headers: {
329
+ 'Content-Type': 'application/json',
330
+ },
331
+ body: JSON.stringify(transaction),
332
+ });
333
+
334
+ if (!response.ok) {
335
+ throw new Error('Failed to add transaction');
336
+ }
337
+
338
+ return response.json();
339
+ };
340
+
341
+ // Products/Stock API
342
+ export const fetchProducts = async () => {
343
+ const response = await fetch(`${API_URL}/products`);
344
+ if (!response.ok) {
345
+ throw new Error('Failed to fetch products');
346
+ }
347
+ return response.json();
348
+ };
349
+
350
+ export const fetchLowStockProducts = async () => {
351
+ const response = await fetch(`${API_URL}/products/low`);
352
+ if (!response.ok) {
353
+ throw new Error('Failed to fetch low stock products');
354
+ }
355
+ return response.json();
356
+ };
357
+
358
+ export const fetchProductById = async (id: string) => {
359
+ const response = await fetch(`${API_URL}/products/${id}`);
360
+ if (!response.ok) {
361
+ throw new Error('Failed to fetch product');
362
+ }
363
+ return response.json();
364
+ };
365
+
366
+ export const addProduct = async (product: any) => {
367
+ const response = await fetch(`${API_URL}/products`, {
368
+ method: 'POST',
369
+ headers: {
370
+ 'Content-Type': 'application/json',
371
+ },
372
+ body: JSON.stringify(product),
373
+ });
374
+
375
+ if (!response.ok) {
376
+ throw new Error('Failed to add product');
377
+ }
378
+
379
+ return response.json();
380
+ };
381
+
382
+ export const updateProduct = async (id: string, product: any) => {
383
+ const response = await fetch(`${API_URL}/products/${id}`, {
384
+ method: 'PUT',
385
+ headers: {
386
+ 'Content-Type': 'application/json',
387
+ },
388
+ body: JSON.stringify(product),
389
+ });
390
+
391
+ if (!response.ok) {
392
+ throw new Error('Failed to update product');
393
+ }
394
+
395
+ return response.json();
396
+ };
397
+
398
+ export const deleteProduct = async (id: string) => {
399
+ const response = await fetch(`${API_URL}/products/${id}`, {
400
+ method: 'DELETE',
401
+ headers: {
402
+ 'Content-Type': 'application/json',
403
+ },
404
+ });
405
+
406
+ if (!response.ok) {
407
+ throw new Error('Failed to delete product');
408
+ }
409
+
410
+ return response.json();
411
+ };
412
+
413
+ export const fetchStockAnalytics = async () => {
414
+ const response = await fetch(`${API_URL}/stock/analytics`);
415
+ if (!response.ok) {
416
+ throw new Error('Failed to fetch stock analytics');
417
+ }
418
+ return response.json();
419
+ };
420
+ ```
421
+
422
+ ### Step 2: Update Messages Component to use the API
423
+
424
+ Update the Messages component to fetch and send messages via the API:
425
+
426
+ ```typescript
427
+ import { useState, useEffect } from "react";
428
+ import { fetchMessages, sendMessage } from "../services/api";
429
+ import { Send, Search } from "lucide-react";
430
+ import AppLayout from "@/components/layout/AppLayout";
431
+ import { Input } from "@/components/ui/input";
432
+ import { Button } from "@/components/ui/button";
433
+ import { Avatar } from "@/components/ui/avatar";
434
+ import { ScrollArea } from "@/components/ui/scroll-area";
435
+ import { cn } from "@/lib/utils";
436
+ import { motion } from "framer-motion";
437
+
438
+ // Sample data for messages
439
+ const initialContacts = [
440
+ { id: 1, name: "AI Assistant", avatar: "A", lastMessage: "How can I help with your finances?", time: "10:30 AM", unread: 2 },
441
+ { id: 2, name: "Budget Bot", avatar: "B", lastMessage: "Your weekly spending report is ready", time: "Yesterday", unread: 0 },
442
+ { id: 3, name: "Investment Advisor", avatar: "I", lastMessage: "Consider these stocks for your portfolio", time: "Yesterday", unread: 1 },
443
+ { id: 4, name: "Expense Tracker", avatar: "E", lastMessage: "You've exceeded your dining budget", time: "Monday", unread: 0 },
444
+ { id: 5, name: "Financial Coach", avatar: "F", lastMessage: "Let's review your saving goals", time: "08/12/23", unread: 0 },
445
+ ];
446
+
447
+ const initialMessages = [
448
+ { id: 1, sender: "client", text: "Hello! I need some help understanding my recent transactions.", time: "10:30 AM" },
449
+ { id: 2, sender: "me", text: "Hi there! I'd be happy to help you analyze your spending patterns. What specifically would you like to know?", time: "10:32 AM" },
450
+ { id: 3, sender: "client", text: "I noticed some unusual activity in my account", time: "10:33 AM" },
451
+ { id: 4, sender: "me", text: "I can check that for you. Could you tell me which transactions look suspicious?", time: "10:35 AM" },
452
+ { id: 5, sender: "me", text: "Based on your spending history, the transaction at 'Tech Store' for $349.99 is unusual for you.", time: "10:36 AM" },
453
+ { id: 6, sender: "client", text: "Yes, that's the one I was concerned about. I don't remember making that purchase.", time: "10:38 AM" },
454
+ ];
455
+
456
+ const Messages = () => {
457
+ const [contacts, setContacts] = useState(initialContacts);
458
+ const [selectedContact, setSelectedContact] = useState(initialContacts[0]);
459
+ const [messages, setMessages] = useState([]);
460
+ const [loading, setLoading] = useState(true);
461
+ const [newMessage, setNewMessage] = useState("");
462
+ const [searchTerm, setSearchTerm] = useState("");
463
+
464
+ useEffect(() => {
465
+ // Fetch messages when component mounts
466
+ const getMessages = async () => {
467
+ try {
468
+ setLoading(true);
469
+ const data = await fetchMessages();
470
+ setMessages(data);
471
+ } catch (error) {
472
+ console.error('Failed to fetch messages:', error);
473
+ } finally {
474
+ setLoading(false);
475
+ }
476
+ };
477
+
478
+ getMessages();
479
+ }, []);
480
+
481
+ const handleSendMessage = async () => {
482
+ if (newMessage.trim() === "") return;
483
+
484
+ const messageData = {
485
+ user_id: "user123", // Replace with actual user id
486
+ content: newMessage,
487
+ timestamp: new Date().toISOString()
488
+ };
489
+
490
+ try {
491
+ await sendMessage(messageData);
492
+ // Refetch messages or add the new one to state
493
+ setMessages([...messages, {
494
+ id: Date.now(),
495
+ sender: "me",
496
+ text: newMessage,
497
+ time: new Date().toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'})
498
+ }]);
499
+ setNewMessage("");
500
+ } catch (error) {
501
+ console.error('Failed to send message:', error);
502
+ }
503
+ };
504
+
505
+ const filteredContacts = contacts.filter(contact =>
506
+ contact.name.toLowerCase().includes(searchTerm.toLowerCase())
507
+ );
508
+
509
+ return (
510
+ <AppLayout>
511
+ <div className="max-w-6xl mx-auto h-full p-4">
512
+ <h1 className="text-xl font-bold mb-4 text-slate-800">Messages</h1>
513
+
514
+ <div className="flex h-[calc(100vh-180px)] rounded-2xl overflow-hidden bg-white/80 backdrop-blur-lg border border-slate-200 shadow-lg">
515
+ {/* Contacts sidebar */}
516
+ <div className="w-full max-w-xs border-r border-slate-200 hidden md:flex flex-col">
517
+ <div className="p-3 border-b border-slate-200">
518
+ <div className="relative">
519
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 h-4 w-4" />
520
+ <Input
521
+ placeholder="Search contacts..."
522
+ className="pl-10 bg-slate-50 border-slate-200 text-slate-800"
523
+ value={searchTerm}
524
+ onChange={(e) => setSearchTerm(e.target.value)}
525
+ />
526
+ </div>
527
+ </div>
528
+
529
+ <ScrollArea className="flex-1">
530
+ {filteredContacts.map(contact => (
531
+ <div
532
+ key={contact.id}
533
+ className={cn(
534
+ "p-3 flex items-center gap-3 cursor-pointer hover:bg-slate-50 transition-colors",
535
+ selectedContact.id === contact.id ? "bg-slate-50" : ""
536
+ )}
537
+ onClick={() => setSelectedContact(contact)}
538
+ >
539
+ <Avatar className="h-10 w-10 bg-blue-600 text-white">
540
+ <div>{contact.avatar}</div>
541
+ </Avatar>
542
+
543
+ <div className="flex-1 min-w-0">
544
+ <div className="flex justify-between items-center">
545
+ <span className="font-medium truncate text-slate-800">{contact.name}</span>
546
+ <span className="text-xs text-slate-500">{contact.time}</span>
547
+ </div>
548
+ <p className="text-sm text-slate-500 truncate">{contact.lastMessage}</p>
549
+ </div>
550
+
551
+ {contact.unread > 0 && (
552
+ <div className="bg-blue-600 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
553
+ {contact.unread}
554
+ </div>
555
+ )}
556
+ </div>
557
+ ))}
558
+ </ScrollArea>
559
+ </div>
560
+
561
+ {/* Chat area */}
562
+ <div className="flex-1 flex flex-col">
563
+ <div className="p-3 border-b border-slate-200 flex items-center gap-3">
564
+ <Avatar className="h-8 w-8 bg-blue-600 text-white md:hidden">
565
+ <div>{selectedContact.avatar}</div>
566
+ </Avatar>
567
+ <div>
568
+ <h3 className="font-medium text-slate-800">{selectedContact.name}</h3>
569
+ </div>
570
+ </div>
571
+
572
+ <ScrollArea className="flex-1 p-4">
573
+ <div className="space-y-4">
574
+ {messages.map((message, index) => (
575
+ <motion.div
576
+ key={message.id}
577
+ initial={{ opacity: 0, y: 10 }}
578
+ animate={{ opacity: 1, y: 0 }}
579
+ transition={{ duration: 0.3, delay: index * 0.1 }}
580
+ className={cn(
581
+ "flex",
582
+ message.sender === "me" ? "justify-end" : "justify-start"
583
+ )}
584
+ >
585
+ <div
586
+ className={cn(
587
+ "max-w-[80%] p-3 rounded-2xl shadow-sm",
588
+ message.sender === "me"
589
+ ? "bg-blue-600 text-white rounded-tr-none"
590
+ : "bg-slate-100 text-slate-800 rounded-tl-none"
591
+ )}
592
+ >
593
+ <p>{message.text}</p>
594
+ <span className={cn(
595
+ "text-xs block mt-1",
596
+ message.sender === "me"
597
+ ? "text-white/70"
598
+ : "text-slate-500"
599
+ )}>
600
+ {message.time}
601
+ </span>
602
+ </div>
603
+ </motion.div>
604
+ ))}
605
+ </div>
606
+ </ScrollArea>
607
+
608
+ <div className="p-3 border-t border-slate-200 flex items-center gap-2">
609
+ <Input
610
+ placeholder="Type a message..."
611
+ value={newMessage}
612
+ onChange={(e) => setNewMessage(e.target.value)}
613
+ onKeyDown={(e) => {
614
+ if (e.key === "Enter") {
615
+ handleSendMessage();
616
+ }
617
+ }}
618
+ className="flex-1 bg-slate-50 border-slate-200 text-slate-800"
619
+ />
620
+ <Button size="icon" className="bg-blue-600 hover:bg-blue-700" onClick={handleSendMessage}>
621
+ <Send size={18} />
622
+ </Button>
623
+ </div>
624
+ </div>
625
+ </div>
626
+ </div>
627
+ </AppLayout>
628
+ );
629
+ };
630
+ ```
631
+
632
+ ## 3. Deploying the Python API
633
+
634
+ ### Option 1: Docker Deployment
635
+
636
+ Create a `Dockerfile` for the Python API:
637
+
638
+ ```dockerfile
639
+ FROM python:3.9-slim
640
+
641
+ WORKDIR /app
642
+
643
+ COPY requirements.txt .
644
+ RUN pip install -r requirements.txt
645
+
646
+ COPY . .
647
+
648
+ EXPOSE 5000
649
+
650
+ CMD ["python", "app.py"]
651
+ ```
652
+
653
+ Create a `requirements.txt` file:
654
+
655
+ ```
656
+ flask>=2.0.0
657
+ flask-cors>=3.0.10
658
+ ```
659
+
660
+ Update the `docker-compose.yml` to include the Python API:
661
+
662
+ ```yaml
663
+ version: '3.8'
664
+
665
+ services:
666
+ app:
667
+ build: .
668
+ ports:
669
+ - "8080:80"
670
+ restart: unless-stopped
671
+ depends_on:
672
+ - api
673
+
674
+ api:
675
+ build: ./api
676
+ ports:
677
+ - "5000:5000"
678
+ restart: unless-stopped
679
+ volumes:
680
+ - ./api/data:/app/data
681
+ ```
682
+
683
+ ### Option 2: Cloud Deployment
684
+
685
+ Deploy the Python API to a cloud service like:
686
+ - Heroku
687
+ - AWS Lambda + API Gateway
688
+ - Google Cloud Functions
689
+ - Azure Functions
690
+
691
+ ## 4. Advanced Integration: AI Chat Assistant
692
+
693
+ To enhance the chat interface with AI capabilities, you can integrate with OpenAI's API:
694
+
695
+ ```python
696
+ import openai
697
+
698
+ # Set your OpenAI API key
699
+ openai.api_key = "your-api-key"
700
+
701
+ @app.route('/api/chat', methods=['POST'])
702
+ def chat():
703
+ data = request.json
704
+ user_message = data['message']
705
+
706
+ # Save user message to database
707
+ conn = get_db_connection()
708
+ conn.execute(
709
+ 'INSERT INTO messages (user_id, content, timestamp, is_ai) VALUES (?, ?, ?, ?)',
710
+ (data['user_id'], user_message, data['timestamp'], 0)
711
+ )
712
+ conn.commit()
713
+
714
+ # Get AI response
715
+ response = openai.ChatCompletion.create(
716
+ model="gpt-3.5-turbo",
717
+ messages=[
718
+ {"role": "system", "content": "You are a helpful financial assistant."},
719
+ {"role": "user", "content": user_message}
720
+ ]
721
+ )
722
+
723
+ ai_message = response.choices[0].message.content
724
+
725
+ # Save AI response to database
726
+ conn.execute(
727
+ 'INSERT INTO messages (user_id, content, timestamp, is_ai) VALUES (?, ?, ?, ?)',
728
+ ('assistant', ai_message, data['timestamp'], 1)
729
+ )
730
+ conn.commit()
731
+ conn.close()
732
+
733
+ return jsonify({
734
+ "message": ai_message,
735
+ "timestamp": data['timestamp']
736
+ })
737
+ ```
738
+
739
+ ## 5. Running Both Applications Together
740
+
741
+ 1. Start the Python API:
742
+ ```bash
743
+ cd api
744
+ python app.py
745
+ ```
746
+
747
+ 2. Start the React app:
748
+ ```bash
749
+ npm run dev
750
+ ```
751
+
752
+ ## 6. Security Considerations
753
+
754
+ 1. Implement proper authentication for API endpoints
755
+ 2. Use HTTPS for production
756
+ 3. Validate and sanitize all user inputs
757
+ 4. Use environment variables for sensitive information
758
+ 5. Implement rate limiting to prevent abuse
759
+
760
+ ## 7. Troubleshooting
761
+
762
+ ### Common Issues:
763
+ - **CORS errors**: Ensure CORS is properly configured in both the React app and the Python API
764
+ - **Database connection errors**: Check file paths and permissions
765
+ - **API connection issues**: Verify the API URL and port are correct
766
+
767
+ ## 8. Resources
768
+
769
+ - [Flask Documentation](https://flask.palletsprojects.com/)
770
+ - [React Query Documentation](https://react-query.tanstack.com/)
771
+ - [SQLite Documentation](https://www.sqlite.org/docs.html)
772
+
773
+ ## 9. License
774
+
775
+ This integration guide is provided under the same license as the Flutter Finance App.
bun.lockb ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a3c575fd4a99dc9d1d74a4a7a31b979d577c15f379bc5cb0dd7e823586e98c23
3
+ size 198351
components.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "default",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "src/index.css",
9
+ "baseColor": "slate",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ }
20
+ }
docker-compose.yml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+
2
+ version: '3.8'
3
+
4
+ services:
5
+ app:
6
+ build: .
7
+ ports:
8
+ - "7860:7860"
9
+ restart: unless-stopped
eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import reactHooks from "eslint-plugin-react-hooks";
4
+ import reactRefresh from "eslint-plugin-react-refresh";
5
+ import tseslint from "typescript-eslint";
6
+
7
+ export default tseslint.config(
8
+ { ignores: ["dist"] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ["**/*.{ts,tsx}"],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ "react-hooks": reactHooks,
18
+ "react-refresh": reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ "react-refresh/only-export-components": [
23
+ "warn",
24
+ { allowConstantExport: true },
25
+ ],
26
+ "@typescript-eslint/no-unused-vars": "off",
27
+ },
28
+ }
29
+ );
index.html ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>flutter-moolah-manager</title>
7
+ <meta name="description" content="Lovable Generated Project" />
8
+ <meta name="author" content="Lovable" />
9
+
10
+ <meta property="og:title" content="Lovable Generated Project" />
11
+ <meta property="og:description" content="Lovable Generated Project" />
12
+ <meta property="og:type" content="website" />
13
+ <meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
14
+
15
+ <meta name="twitter:card" content="summary_large_image" />
16
+ <meta name="twitter:site" content="@lovable_dev" />
17
+ <meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
18
+ </head>
19
+
20
+ <body>
21
+ <div id="root"></div>
22
+ <!-- IMPORTANT: DO NOT REMOVE THIS SCRIPT TAG OR THIS VERY COMMENT! -->
23
+ <script src="https://cdn.gpteng.co/gptengineer.js" type="module"></script>
24
+ <script type="module" src="/src/main.tsx"></script>
25
+ </body>
26
+ </html>
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "vite_react_shadcn_ts",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "build:dev": "vite build --mode development",
10
+ "lint": "eslint .",
11
+ "preview": "vite preview"
12
+ },
13
+ "dependencies": {
14
+ "@hookform/resolvers": "^3.9.0",
15
+ "@radix-ui/react-accordion": "^1.2.0",
16
+ "@radix-ui/react-alert-dialog": "^1.1.1",
17
+ "@radix-ui/react-aspect-ratio": "^1.1.0",
18
+ "@radix-ui/react-avatar": "^1.1.0",
19
+ "@radix-ui/react-checkbox": "^1.1.1",
20
+ "@radix-ui/react-collapsible": "^1.1.0",
21
+ "@radix-ui/react-context-menu": "^2.2.1",
22
+ "@radix-ui/react-dialog": "^1.1.2",
23
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
24
+ "@radix-ui/react-hover-card": "^1.1.1",
25
+ "@radix-ui/react-label": "^2.1.0",
26
+ "@radix-ui/react-menubar": "^1.1.1",
27
+ "@radix-ui/react-navigation-menu": "^1.2.0",
28
+ "@radix-ui/react-popover": "^1.1.1",
29
+ "@radix-ui/react-progress": "^1.1.0",
30
+ "@radix-ui/react-radio-group": "^1.2.0",
31
+ "@radix-ui/react-scroll-area": "^1.1.0",
32
+ "@radix-ui/react-select": "^2.1.1",
33
+ "@radix-ui/react-separator": "^1.1.0",
34
+ "@radix-ui/react-slider": "^1.2.0",
35
+ "@radix-ui/react-slot": "^1.1.0",
36
+ "@radix-ui/react-switch": "^1.1.0",
37
+ "@radix-ui/react-tabs": "^1.1.0",
38
+ "@radix-ui/react-toast": "^1.2.1",
39
+ "@radix-ui/react-toggle": "^1.1.0",
40
+ "@radix-ui/react-toggle-group": "^1.1.0",
41
+ "@radix-ui/react-tooltip": "^1.1.4",
42
+ "@tanstack/react-query": "^5.56.2",
43
+ "class-variance-authority": "^0.7.1",
44
+ "clsx": "^2.1.1",
45
+ "cmdk": "^1.0.0",
46
+ "date-fns": "^3.6.0",
47
+ "embla-carousel-react": "^8.3.0",
48
+ "framer-motion": "^12.6.2",
49
+ "input-otp": "^1.2.4",
50
+ "lucide-react": "^0.462.0",
51
+ "next-themes": "^0.3.0",
52
+ "react": "^18.3.1",
53
+ "react-day-picker": "^8.10.1",
54
+ "react-dom": "^18.3.1",
55
+ "react-hook-form": "^7.53.0",
56
+ "react-resizable-panels": "^2.1.3",
57
+ "react-router-dom": "^6.26.2",
58
+ "recharts": "^2.12.7",
59
+ "sonner": "^1.5.0",
60
+ "sql.js": "^1.13.0",
61
+ "tailwind-merge": "^2.5.2",
62
+ "tailwindcss-animate": "^1.0.7",
63
+ "uuid": "^9.0.1",
64
+ "vaul": "^0.9.3",
65
+ "zod": "^3.23.8"
66
+ },
67
+ "devDependencies": {
68
+ "@eslint/js": "^9.9.0",
69
+ "@tailwindcss/typography": "^0.5.15",
70
+ "@types/node": "^22.5.5",
71
+ "@types/react": "^18.3.3",
72
+ "@types/react-dom": "^18.3.0",
73
+ "@vitejs/plugin-react-swc": "^3.5.0",
74
+ "autoprefixer": "^10.4.20",
75
+ "eslint": "^9.9.0",
76
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
77
+ "eslint-plugin-react-refresh": "^0.4.9",
78
+ "globals": "^15.9.0",
79
+ "lovable-tagger": "^1.1.7",
80
+ "postcss": "^8.4.47",
81
+ "tailwindcss": "^3.4.11",
82
+ "typescript": "^5.5.3",
83
+ "typescript-eslint": "^8.0.1",
84
+ "vite": "^5.4.1"
85
+ }
86
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
public/favicon.ico ADDED
public/lovable-uploads/efa85ef3-0e1e-44d2-bbd4-34fe8f8946e4.png ADDED
public/placeholder.svg ADDED
public/robots.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ User-agent: Googlebot
2
+ Allow: /
3
+
4
+ User-agent: Bingbot
5
+ Allow: /
6
+
7
+ User-agent: Twitterbot
8
+ Allow: /
9
+
10
+ User-agent: facebookexternalhit
11
+ Allow: /
12
+
13
+ User-agent: *
14
+ Allow: /
src/App.css ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .logo {
9
+ height: 6em;
10
+ padding: 1.5em;
11
+ will-change: filter;
12
+ transition: filter 300ms;
13
+ }
14
+ .logo:hover {
15
+ filter: drop-shadow(0 0 2em #646cffaa);
16
+ }
17
+ .logo.react:hover {
18
+ filter: drop-shadow(0 0 2em #61dafbaa);
19
+ }
20
+
21
+ @keyframes logo-spin {
22
+ from {
23
+ transform: rotate(0deg);
24
+ }
25
+ to {
26
+ transform: rotate(360deg);
27
+ }
28
+ }
29
+
30
+ @media (prefers-reduced-motion: no-preference) {
31
+ a:nth-of-type(2) .logo {
32
+ animation: logo-spin infinite 20s linear;
33
+ }
34
+ }
35
+
36
+ .card {
37
+ padding: 2em;
38
+ }
39
+
40
+ .read-the-docs {
41
+ color: #888;
42
+ }
src/App.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { Toaster } from "@/components/ui/toaster";
3
+ import { Toaster as Sonner } from "@/components/ui/sonner";
4
+ import { TooltipProvider } from "@/components/ui/tooltip";
5
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6
+ import { BrowserRouter, Routes, Route } from "react-router-dom";
7
+ import { ThemeProvider } from "@/components/theme/ThemeProvider";
8
+ import Index from "./pages/Index";
9
+ import Transactions from "./pages/Transactions";
10
+ import AddTransaction from "./pages/AddTransaction";
11
+ import TransactionDetail from "./pages/TransactionDetail";
12
+ import Reports from "./pages/Reports";
13
+ import Settings from "./pages/Settings";
14
+ import NotFound from "./pages/NotFound";
15
+ import Messages from "./pages/Messages";
16
+ import StockManagement from "./pages/StockManagement";
17
+ import LowStockItems from "./pages/LowStockItems";
18
+
19
+ const queryClient = new QueryClient();
20
+
21
+ const App = () => (
22
+ <QueryClientProvider client={queryClient}>
23
+ <ThemeProvider defaultTheme="dark">
24
+ <TooltipProvider>
25
+ <div>
26
+ <Toaster />
27
+ <Sonner position="top-center" />
28
+ <BrowserRouter>
29
+ <Routes>
30
+ <Route path="/" element={<Index />} />
31
+ <Route path="/transactions" element={<Transactions />} />
32
+ <Route path="/transactions/:id" element={<TransactionDetail />} />
33
+ <Route path="/add-transaction" element={<AddTransaction />} />
34
+ <Route path="/reports" element={<Reports />} />
35
+ <Route path="/settings" element={<Settings />} />
36
+ <Route path="/messages" element={<Messages />} />
37
+ <Route path="/stock" element={<StockManagement />} />
38
+ <Route path="/stock/low" element={<LowStockItems />} />
39
+ <Route path="*" element={<NotFound />} />
40
+ </Routes>
41
+ </BrowserRouter>
42
+ </div>
43
+ </TooltipProvider>
44
+ </ThemeProvider>
45
+ </QueryClientProvider>
46
+ );
47
+
48
+ export default App;
src/components/dashboard/BalanceCard.tsx ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { ArrowDown, ArrowUp } from "lucide-react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ interface BalanceCardProps {
6
+ balance: number;
7
+ income: number;
8
+ expenses: number;
9
+ currency?: string;
10
+ }
11
+
12
+ const BalanceCard = ({
13
+ balance,
14
+ income,
15
+ expenses,
16
+ currency = "$"
17
+ }: BalanceCardProps) => {
18
+ const formatCurrency = (amount: number) => {
19
+ return new Intl.NumberFormat('en-US', {
20
+ style: 'currency',
21
+ currency: 'USD',
22
+ currencyDisplay: 'symbol',
23
+ minimumFractionDigits: 2
24
+ }).format(amount).replace('$', '');
25
+ };
26
+
27
+ return (
28
+ <div className="relative w-full rounded-2xl overflow-hidden transition-all duration-300 hover:-translate-y-1 bg-white/80 backdrop-blur-lg border border-slate-200 shadow-lg">
29
+ <div className="relative p-5 text-slate-800">
30
+ <p className="text-slate-500 font-medium mb-1">Balance</p>
31
+ <h2 className="text-3xl font-semibold tracking-tight flex items-baseline">
32
+ <span className="text-lg mr-1">{currency}</span>
33
+ <span>{formatCurrency(balance)}</span>
34
+ </h2>
35
+
36
+ <div className="flex justify-between mt-6">
37
+ <div className="flex items-center space-x-2">
38
+ <div className="p-1.5 rounded-full bg-green-100">
39
+ <ArrowDown size={14} className="text-green-600" />
40
+ </div>
41
+ <div>
42
+ <p className="text-xs text-slate-500">Income</p>
43
+ <p className="font-medium text-slate-800">{currency}{formatCurrency(income)}</p>
44
+ </div>
45
+ </div>
46
+
47
+ <div className="flex items-center space-x-2">
48
+ <div className="p-1.5 rounded-full bg-red-100">
49
+ <ArrowUp size={14} className="text-red-600" />
50
+ </div>
51
+ <div>
52
+ <p className="text-xs text-slate-500">Expenses</p>
53
+ <p className="font-medium text-slate-800">{currency}{formatCurrency(expenses)}</p>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ );
60
+ };
61
+
62
+ export default BalanceCard;
src/components/dashboard/RecentTransactions.tsx ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { useState } from "react";
3
+ import {
4
+ ChevronRight,
5
+ Coffee,
6
+ ShoppingCart,
7
+ Zap,
8
+ Home as HomeIcon,
9
+ Car,
10
+ Utensils,
11
+ Briefcase,
12
+ CreditCard
13
+ } from "lucide-react";
14
+ import { useNavigate } from "react-router-dom";
15
+ import { cn } from "@/lib/utils";
16
+ import { motion } from "framer-motion";
17
+
18
+ // Mocked transaction data
19
+ export interface Transaction {
20
+ id: string;
21
+ title: string;
22
+ amount: number;
23
+ type: "expense" | "income";
24
+ category: string;
25
+ date: Date;
26
+ note?: string;
27
+ message?: string;
28
+ }
29
+
30
+ const CATEGORY_ICONS: Record<string, any> = {
31
+ food: Utensils,
32
+ coffee: Coffee,
33
+ shopping: ShoppingCart,
34
+ utilities: Zap,
35
+ housing: HomeIcon,
36
+ transport: Car,
37
+ salary: Briefcase,
38
+ other: CreditCard
39
+ };
40
+
41
+ interface RecentTransactionsProps {
42
+ transactions: Transaction[];
43
+ currency?: string;
44
+ showViewAll?: boolean;
45
+ }
46
+
47
+ const RecentTransactions = ({
48
+ transactions,
49
+ currency = "$",
50
+ showViewAll = true
51
+ }: RecentTransactionsProps) => {
52
+ const navigate = useNavigate();
53
+
54
+ const formatDate = (date: Date) => {
55
+ return new Intl.DateTimeFormat('en-US', {
56
+ month: 'short',
57
+ day: 'numeric'
58
+ }).format(date);
59
+ };
60
+
61
+ const formatCurrency = (amount: number) => {
62
+ return new Intl.NumberFormat('en-US', {
63
+ style: 'currency',
64
+ currency: 'USD',
65
+ currencyDisplay: 'symbol',
66
+ }).format(Math.abs(amount)).replace('$', '');
67
+ };
68
+
69
+ const getCategoryIcon = (category: string) => {
70
+ const IconComponent = CATEGORY_ICONS[category.toLowerCase()] || CreditCard;
71
+ return <IconComponent size={18} />;
72
+ };
73
+
74
+ return (
75
+ <div>
76
+ {showViewAll && (
77
+ <div className="flex items-center justify-between mb-4">
78
+ <h2 className="text-lg md:text-xl font-medium text-slate-800">Recent Transactions</h2>
79
+ <button
80
+ onClick={() => navigate('/transactions')}
81
+ className="text-sm md:text-base font-medium text-[#00a651] flex items-center"
82
+ >
83
+ View All
84
+ <ChevronRight size={16} className="md:size-18" />
85
+ </button>
86
+ </div>
87
+ )}
88
+
89
+ <div className="space-y-3 md:space-y-4">
90
+ {transactions.map((transaction, index) => (
91
+ <motion.div
92
+ key={transaction.id}
93
+ initial={{ opacity: 0, y: 20 }}
94
+ animate={{ opacity: 1, y: 0 }}
95
+ transition={{ duration: 0.3, delay: index * 0.1 }}
96
+ onClick={() => navigate(`/transactions/${transaction.id}`)}
97
+ className="cursor-pointer"
98
+ >
99
+ <div className="bg-white rounded-xl shadow-sm p-4">
100
+ <div className="flex flex-col h-full">
101
+ <div className="flex items-center">
102
+ <div className={cn(
103
+ "flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center",
104
+ transaction.type === "expense" ? "bg-red-100 text-red-600" : "bg-green-100 text-green-600"
105
+ )}>
106
+ {getCategoryIcon(transaction.category)}
107
+ </div>
108
+
109
+ <div className="ml-3 flex-1">
110
+ <p className="font-medium text-base text-slate-800">{transaction.title}</p>
111
+ <p className="text-sm text-slate-500">{formatDate(transaction.date)}</p>
112
+ </div>
113
+ </div>
114
+
115
+ <div className={cn(
116
+ "flex-shrink-0 font-medium mt-3 pt-2 border-t border-slate-100 text-right",
117
+ transaction.type === "expense" ? "text-red-600" : "text-green-600"
118
+ )}>
119
+ {transaction.type === "expense" ? "- " : "+ "}
120
+ {currency}{formatCurrency(transaction.amount)}
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </motion.div>
125
+ ))}
126
+
127
+ {transactions.length === 0 && (
128
+ <div className="p-6 md:p-8 text-center bg-white/60 rounded-xl border border-slate-200 shadow">
129
+ <p className="text-slate-500 md:text-lg">No recent transactions</p>
130
+ </div>
131
+ )}
132
+ </div>
133
+ </div>
134
+ );
135
+ };
136
+
137
+ export default RecentTransactions;
src/components/dashboard/SpendingAnalytics.tsx ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts';
3
+ import { cn } from "@/lib/utils";
4
+
5
+ interface SpendingCategory {
6
+ name: string;
7
+ value: number;
8
+ color: string;
9
+ }
10
+
11
+ interface SpendingAnalyticsProps {
12
+ data: SpendingCategory[];
13
+ currency?: string;
14
+ }
15
+
16
+ const SpendingAnalytics = ({
17
+ data,
18
+ currency = "$"
19
+ }: SpendingAnalyticsProps) => {
20
+ const totalSpending = data.reduce((sum, item) => sum + item.value, 0);
21
+
22
+ const formatCurrency = (amount: number) => {
23
+ return new Intl.NumberFormat('en-US', {
24
+ style: 'currency',
25
+ currency: 'USD',
26
+ currencyDisplay: 'symbol',
27
+ }).format(amount).replace('$', '');
28
+ };
29
+
30
+ return (
31
+ <div className="animate-slide-up py-2">
32
+ <h2 className="text-lg font-medium mb-4">Spending by Category</h2>
33
+
34
+ <div className="flex">
35
+ <div className="w-1/2 h-48">
36
+ <ResponsiveContainer width="100%" height="100%">
37
+ <PieChart>
38
+ <Pie
39
+ data={data}
40
+ cx="50%"
41
+ cy="50%"
42
+ innerRadius={48}
43
+ outerRadius={68}
44
+ paddingAngle={2}
45
+ dataKey="value"
46
+ stroke="none"
47
+ >
48
+ {data.map((entry, index) => (
49
+ <Cell key={`cell-${index}`} fill={entry.color} />
50
+ ))}
51
+ </Pie>
52
+ </PieChart>
53
+ </ResponsiveContainer>
54
+ </div>
55
+
56
+ <div className="w-1/2 space-y-2 my-auto">
57
+ {data.map((category, index) => (
58
+ <div key={index} className="flex items-center justify-between">
59
+ <div className="flex items-center space-x-2">
60
+ <div
61
+ className="w-3 h-3 rounded-full"
62
+ style={{ backgroundColor: category.color }}
63
+ />
64
+ <span className="text-sm">{category.name}</span>
65
+ </div>
66
+ <div className="flex flex-col items-end">
67
+ <span className="text-sm font-medium">
68
+ {currency}{formatCurrency(category.value)}
69
+ </span>
70
+ <span className="text-xs text-muted-foreground">
71
+ {Math.round((category.value / totalSpending) * 100)}%
72
+ </span>
73
+ </div>
74
+ </div>
75
+ ))}
76
+ </div>
77
+ </div>
78
+ </div>
79
+ );
80
+ };
81
+
82
+ export default SpendingAnalytics;
src/components/layout/AppLayout.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { ReactNode } from "react";
3
+ import { useLocation, useNavigate } from "react-router-dom";
4
+ import { Home, PieChart, Plus, MessageSquare, Wallet, Bell, Settings } from "lucide-react";
5
+ import { cn } from "@/lib/utils";
6
+ import MainStockMenu from "@/components/stock/MainStockMenu";
7
+
8
+ interface AppLayoutProps {
9
+ children: ReactNode;
10
+ }
11
+
12
+ const AppLayout = ({ children }: AppLayoutProps) => {
13
+ const location = useLocation();
14
+ const navigate = useNavigate();
15
+ const currentPath = location.pathname;
16
+
17
+ const navItems = [
18
+ { icon: Home, label: "Home", path: "/" },
19
+ { icon: Wallet, label: "Transactions", path: "/transactions" },
20
+ { icon: Plus, label: "Add", path: "/add-transaction", isAction: true },
21
+ { icon: PieChart, label: "Reports", path: "/reports" },
22
+ { icon: MessageSquare, label: "Messages", path: "/messages" },
23
+ ];
24
+
25
+ // Hide the stock menu on stock management pages
26
+ const showStockMenu = !currentPath.startsWith('/stock');
27
+
28
+ return (
29
+ <div className="flex flex-col h-full bg-gradient-to-b from-[#f8fafc] to-[#f7f8f2] text-black">
30
+ <div className="fixed top-4 right-4 z-10">
31
+ <button
32
+ onClick={() => navigate('/settings')}
33
+ className="p-2 rounded-full bg-white/80 backdrop-blur-sm shadow-md hover:bg-white/90 transition-all"
34
+ >
35
+ <Settings size={20} className="text-[#00a651]" />
36
+ </button>
37
+ </div>
38
+
39
+ {showStockMenu && <MainStockMenu />}
40
+
41
+ <main className="flex-1 pb-16 overflow-auto">
42
+ <div className="animate-fade-in">
43
+ {children}
44
+ </div>
45
+ </main>
46
+
47
+ <nav className="fixed bottom-0 left-0 right-0 bg-white/80 backdrop-blur-lg border-t border-slate-200 shadow-lg">
48
+ <div className="flex justify-around items-center h-16 px-2 max-w-screen-lg mx-auto">
49
+ {navItems.map((item) => (
50
+ <button
51
+ key={item.path}
52
+ onClick={() => navigate(item.path)}
53
+ className={cn(
54
+ "flex flex-col items-center justify-center w-full h-full transition-all duration-200",
55
+ item.isAction ? "relative -top-5 md:-top-6" : "",
56
+ currentPath === item.path && !item.isAction
57
+ ? "text-[#00a651]"
58
+ : "text-slate-500 hover:text-slate-800"
59
+ )}
60
+ >
61
+ {item.isAction ? (
62
+ <div className="flex items-center justify-center w-12 h-12 md:w-14 md:h-14 rounded-full bg-[#e31b23] text-white shadow-lg shadow-red-400/50">
63
+ <item.icon size={22} className="md:size-24" />
64
+ </div>
65
+ ) : (
66
+ <>
67
+ <item.icon size={20} className="md:size-22" />
68
+ <span className="text-xs md:text-sm mt-1">{item.label}</span>
69
+ </>
70
+ )}
71
+ </button>
72
+ ))}
73
+ </div>
74
+ </nav>
75
+ </div>
76
+ );
77
+ };
78
+
79
+ export default AppLayout;
src/components/shared/Header.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { ReactNode } from 'react';
3
+ import { ArrowLeft } from 'lucide-react';
4
+ import { useNavigate } from 'react-router-dom';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ interface HeaderProps {
8
+ title: string;
9
+ subtitle?: string;
10
+ showBackButton?: boolean;
11
+ rightElement?: ReactNode;
12
+ className?: string;
13
+ }
14
+
15
+ const Header = ({
16
+ title,
17
+ subtitle,
18
+ showBackButton = false,
19
+ rightElement,
20
+ className
21
+ }: HeaderProps) => {
22
+ const navigate = useNavigate();
23
+
24
+ return (
25
+ <header className={cn("px-4 py-6", className)}>
26
+ <div className="flex items-center justify-between">
27
+ <div className="flex items-center">
28
+ {showBackButton && (
29
+ <button
30
+ onClick={() => navigate(-1)}
31
+ className="mr-3 p-1.5 rounded-full bg-secondary hover:bg-secondary/80 transition-colors"
32
+ >
33
+ <ArrowLeft size={18} />
34
+ </button>
35
+ )}
36
+ <div>
37
+ <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
38
+ {subtitle && <p className="text-sm text-muted-foreground mt-0.5">{subtitle}</p>}
39
+ </div>
40
+ </div>
41
+ {rightElement && <div>{rightElement}</div>}
42
+ </div>
43
+ </header>
44
+ );
45
+ };
46
+
47
+ export default Header;
src/components/stock/MainStockMenu.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { useState } from "react";
3
+ import { useNavigate } from "react-router-dom";
4
+ import { Package, AlertTriangle } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const MainStockMenu = () => {
9
+ const navigate = useNavigate();
10
+
11
+ return (
12
+ <div className="fixed bottom-20 right-4 z-20 flex flex-col gap-2">
13
+ <Button
14
+ onClick={() => navigate("/stock")}
15
+ size="icon"
16
+ className="h-12 w-12 rounded-full bg-[#00a651] shadow-lg hover:bg-[#008a44]"
17
+ aria-label="Gestion des stocks"
18
+ >
19
+ <Package className="h-5 w-5" />
20
+ </Button>
21
+
22
+ <Button
23
+ onClick={() => navigate("/stock/low")}
24
+ size="icon"
25
+ variant="outline"
26
+ className="h-10 w-10 rounded-full bg-white/90 backdrop-blur-sm shadow-md border-yellow-400 hover:bg-yellow-50"
27
+ aria-label="Alerte stock faible"
28
+ >
29
+ <AlertTriangle className="h-4 w-4 text-yellow-500" />
30
+ </Button>
31
+ </div>
32
+ );
33
+ };
34
+
35
+ export default MainStockMenu;
src/components/stock/StockMenu.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { useState } from "react";
3
+ import { useNavigate, useLocation } from "react-router-dom";
4
+ import {
5
+ Package,
6
+ BarChart3,
7
+ AlertTriangle,
8
+ Settings
9
+ } from "lucide-react";
10
+ import {
11
+ NavigationMenu,
12
+ NavigationMenuContent,
13
+ NavigationMenuItem,
14
+ NavigationMenuLink,
15
+ NavigationMenuList,
16
+ NavigationMenuTrigger,
17
+ } from "@/components/ui/navigation-menu";
18
+ import { cn } from "@/lib/utils";
19
+
20
+ const StockMenu = () => {
21
+ const navigate = useNavigate();
22
+ const location = useLocation();
23
+ const [isOpen, setIsOpen] = useState(false);
24
+
25
+ // Menu items for stock management
26
+ const stockMenuItems = [
27
+ {
28
+ icon: Package,
29
+ label: "Inventaire",
30
+ path: "/stock",
31
+ description: "Gérer tous les produits en stock"
32
+ },
33
+ {
34
+ icon: AlertTriangle,
35
+ label: "Stock Faible",
36
+ path: "/stock/low",
37
+ description: "Produits nécessitant un réapprovisionnement"
38
+ },
39
+ {
40
+ icon: BarChart3,
41
+ label: "Analyse",
42
+ path: "/stock/analytics",
43
+ description: "Statistiques et prévisions de demande"
44
+ },
45
+ {
46
+ icon: Settings,
47
+ label: "Paramètres",
48
+ path: "/stock/settings",
49
+ description: "Configurer les options de stock"
50
+ },
51
+ ];
52
+
53
+ return (
54
+ <div className="mb-6 w-full">
55
+ <NavigationMenu className="max-w-full w-full justify-start">
56
+ <NavigationMenuList className="flex flex-wrap gap-2">
57
+ {stockMenuItems.map((item) => (
58
+ <NavigationMenuItem key={item.path}>
59
+ <button
60
+ className={cn(
61
+ "flex items-center gap-2 px-4 py-2 rounded-md text-sm transition-colors",
62
+ location.pathname === item.path
63
+ ? "bg-primary/10 text-primary"
64
+ : "hover:bg-muted"
65
+ )}
66
+ onClick={() => navigate(item.path)}
67
+ >
68
+ <item.icon className="h-4 w-4" />
69
+ <span>{item.label}</span>
70
+ </button>
71
+ </NavigationMenuItem>
72
+ ))}
73
+ </NavigationMenuList>
74
+ </NavigationMenu>
75
+ </div>
76
+ );
77
+ };
78
+
79
+ export default StockMenu;
src/components/theme/ThemeProvider.tsx ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { createContext, useContext, useEffect, useState } from "react";
3
+
4
+ type Theme = "light" | "dark" | "system";
5
+
6
+ type ThemeProviderProps = {
7
+ children: React.ReactNode;
8
+ defaultTheme?: Theme;
9
+ storageKey?: string;
10
+ };
11
+
12
+ type ThemeProviderState = {
13
+ theme: Theme;
14
+ setTheme: (theme: Theme) => void;
15
+ };
16
+
17
+ const initialState: ThemeProviderState = {
18
+ theme: "system",
19
+ setTheme: () => null,
20
+ };
21
+
22
+ const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
23
+
24
+ export function ThemeProvider({
25
+ children,
26
+ defaultTheme = "system",
27
+ storageKey = "vite-ui-theme",
28
+ ...props
29
+ }: ThemeProviderProps) {
30
+ const [theme, setTheme] = useState<Theme>(
31
+ () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
32
+ );
33
+
34
+ useEffect(() => {
35
+ const root = window.document.documentElement;
36
+ root.classList.remove("light", "dark");
37
+
38
+ if (theme === "system") {
39
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
40
+ .matches
41
+ ? "dark"
42
+ : "light";
43
+
44
+ root.classList.add(systemTheme);
45
+ return;
46
+ }
47
+
48
+ root.classList.add(theme);
49
+ }, [theme]);
50
+
51
+ const value = {
52
+ theme,
53
+ setTheme: (theme: Theme) => {
54
+ localStorage.setItem(storageKey, theme);
55
+ setTheme(theme);
56
+ },
57
+ };
58
+
59
+ return (
60
+ <ThemeProviderContext.Provider {...props} value={value}>
61
+ {children}
62
+ </ThemeProviderContext.Provider>
63
+ );
64
+ }
65
+
66
+ export const useTheme = () => {
67
+ const context = useContext(ThemeProviderContext);
68
+
69
+ if (context === undefined)
70
+ throw new Error("useTheme must be used within a ThemeProvider");
71
+
72
+ return context;
73
+ };
src/components/theme/ThemeToggle.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { Moon, Sun } from "lucide-react";
3
+ import { Button } from "@/components/ui/button";
4
+ import { useTheme } from "./ThemeProvider";
5
+
6
+ export function ThemeToggle() {
7
+ const { theme, setTheme } = useTheme();
8
+
9
+ return (
10
+ <Button
11
+ variant="ghost"
12
+ size="icon"
13
+ onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
14
+ className="rounded-full"
15
+ >
16
+ {theme === "dark" ? (
17
+ <Sun className="h-5 w-5 text-yellow-400" />
18
+ ) : (
19
+ <Moon className="h-5 w-5 text-violet-dark" />
20
+ )}
21
+ <span className="sr-only">Changer le thème</span>
22
+ </Button>
23
+ );
24
+ }
src/components/transactions/AITransactionChat.tsx ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { useState, useRef, useEffect } from "react";
3
+ import { Send, Plus, ArrowRight, AlertTriangle } from "lucide-react";
4
+ import { Input } from "@/components/ui/input";
5
+ import { Button } from "@/components/ui/button";
6
+ import { ScrollArea } from "@/components/ui/scroll-area";
7
+ import { Avatar } from "@/components/ui/avatar";
8
+ import { cn } from "@/lib/utils";
9
+ import { motion } from "framer-motion";
10
+ import { toast } from "sonner";
11
+ import { useNavigate } from "react-router-dom";
12
+ import database from "@/services/database";
13
+
14
+ type Message = {
15
+ id: number;
16
+ type: 'user' | 'bot';
17
+ text: string;
18
+ timestamp: string;
19
+ };
20
+
21
+ type TransactionData = {
22
+ amount?: number;
23
+ category?: string;
24
+ description?: string;
25
+ date?: string;
26
+ type?: 'income' | 'expense';
27
+ quantity?: number;
28
+ product?: string;
29
+ };
30
+
31
+ const AITransactionChat = () => {
32
+ const [messages, setMessages] = useState<Message[]>([
33
+ {
34
+ id: 1,
35
+ type: 'bot',
36
+ text: "Bienvenue! Je suis votre assistant de gestion. Vous pouvez me parler de vos transactions ou me demander des rapports sur vos stocks. Par exemple: 'Vendu 5 sacs de riz à 2000 FCFA chacun' ou 'Quels produits sont en rupture de stock?'",
37
+ timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
38
+ }
39
+ ]);
40
+ const [input, setInput] = useState('');
41
+ const [isProcessing, setIsProcessing] = useState(false);
42
+ const [extractedData, setExtractedData] = useState<TransactionData | null>(null);
43
+ const scrollAreaRef = useRef<HTMLDivElement>(null);
44
+ const navigate = useNavigate();
45
+
46
+ // Auto-scroll to bottom when messages change
47
+ useEffect(() => {
48
+ if (scrollAreaRef.current) {
49
+ const scrollContainer = scrollAreaRef.current.querySelector('[data-radix-scroll-area-viewport]');
50
+ if (scrollContainer) {
51
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
52
+ }
53
+ }
54
+ }, [messages]);
55
+
56
+ const handleSendMessage = () => {
57
+ if (!input.trim()) return;
58
+
59
+ const userMessage = {
60
+ id: messages.length + 1,
61
+ type: 'user' as const,
62
+ text: input,
63
+ timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
64
+ };
65
+
66
+ setMessages(prev => [...prev, userMessage]);
67
+ setInput('');
68
+ setIsProcessing(true);
69
+
70
+ // Simulate AI processing - in a real app, this would be an API call
71
+ setTimeout(() => {
72
+ processUserInput(userMessage.text);
73
+ }, 1000);
74
+ };
75
+
76
+ const processUserInput = (text: string) => {
77
+ // Enhanced NLP-like processing for both French and English inputs
78
+ // This is a simple simulation of NLP capabilities
79
+
80
+ const lowerText = text.toLowerCase();
81
+
82
+ // Check if this is about stock management or reports
83
+ if (
84
+ lowerText.includes('stock') ||
85
+ lowerText.includes('inventaire') ||
86
+ lowerText.includes('rupture') ||
87
+ lowerText.includes('rapport') ||
88
+ lowerText.includes('report')
89
+ ) {
90
+ handleStockQuery(text);
91
+ return;
92
+ }
93
+
94
+ // Sales/expense transaction processing
95
+ const moneyRegex = /(\d+(?:\.\d{1,2})?)\s*(?:fcfa|cfa|\$|€)?/i;
96
+ const moneyMatch = lowerText.match(moneyRegex);
97
+ const amount = moneyMatch ? parseFloat(moneyMatch[1]) : undefined;
98
+
99
+ const quantityRegex = /(\d+)\s*(sac|sachet|boîte|kg|pièce|piece|carton|bouteille|paquet|article)/i;
100
+ const quantityMatch = lowerText.match(quantityRegex);
101
+ const quantity = quantityMatch ? parseInt(quantityMatch[1]) : 1;
102
+
103
+ const productRegex = /(riz|lait|sucre|huile|farine|café|cafe|thé|the|eau|savon|pâtes|pates|tomate|oignon|poisson|viande|poulet|œuf|oeuf)/i;
104
+ const productMatch = lowerText.match(productRegex);
105
+ const product = productMatch ? productMatch[1] : undefined;
106
+
107
+ const isSale =
108
+ lowerText.includes('vend') ||
109
+ lowerText.includes('vendu') ||
110
+ lowerText.includes('vendr') ||
111
+ lowerText.includes('sold') ||
112
+ lowerText.includes('sale');
113
+
114
+ const isPurchase =
115
+ lowerText.includes('achet') ||
116
+ lowerText.includes('acheter') ||
117
+ lowerText.includes('acheté') ||
118
+ lowerText.includes('bought') ||
119
+ lowerText.includes('purchas');
120
+
121
+ const data: TransactionData = {
122
+ amount: amount,
123
+ type: isSale ? 'income' : isPurchase ? 'expense' : undefined,
124
+ date: new Date().toISOString().split('T')[0],
125
+ product: product,
126
+ quantity: quantity,
127
+ description: product ? `${isSale ? 'Vente' : 'Achat'} de ${product}` : text.substring(0, 50),
128
+ category: product ? 'Marchandise' : 'Autre'
129
+ };
130
+
131
+ setExtractedData(data);
132
+
133
+ // Generate response based on extracted data
134
+ let response: string;
135
+
136
+ if (amount && (isSale || isPurchase) && product) {
137
+ const action = isSale ? 'vente' : 'achat';
138
+ response = `J'ai compris que vous avez ${isSale ? 'vendu' : 'acheté'} `;
139
+
140
+ if (quantity > 1) {
141
+ response += `${quantity} unités de ${product}`;
142
+ } else {
143
+ response += `du ${product}`;
144
+ }
145
+
146
+ if (amount) {
147
+ response += ` pour ${amount} FCFA`;
148
+ }
149
+
150
+ response += `. Voulez-vous enregistrer cette ${action} ?`;
151
+ } else if (amount) {
152
+ response = `J'ai détecté un montant de ${amount} FCFA. Pouvez-vous préciser s'il s'agit d'une vente ou d'un achat, et pour quel produit ?`;
153
+ } else if (product) {
154
+ response = `J'ai identifié le produit: ${product}. Pouvez-vous préciser la quantité et le montant de la transaction ?`;
155
+ } else {
156
+ response = `Je n'ai pas bien compris votre transaction. Pouvez-vous préciser le produit, la quantité et le montant ? Par exemple: "Vendu 5 sacs de riz à 2000 FCFA chacun"`;
157
+ }
158
+
159
+ const botMessage = {
160
+ id: messages.length + 2,
161
+ type: 'bot' as const,
162
+ text: response,
163
+ timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
164
+ };
165
+
166
+ setMessages(prev => [...prev, botMessage]);
167
+ setIsProcessing(false);
168
+ };
169
+
170
+ const handleStockQuery = (text: string) => {
171
+ const lowerText = text.toLowerCase();
172
+ let response: string;
173
+
174
+ // Check if it's a low stock query
175
+ if (
176
+ lowerText.includes('rupture') ||
177
+ lowerText.includes('faible') ||
178
+ lowerText.includes('low') ||
179
+ lowerText.includes('alerte')
180
+ ) {
181
+ response = "D'après mon analyse, les produits suivants sont en stock faible : Riz (5 sacs), Lait (3 cartons), Huile (2 bidons). Je recommande de vous réapprovisionner bientôt.";
182
+ }
183
+ // Check if it's a sales report query
184
+ else if (
185
+ lowerText.includes('rapport') ||
186
+ lowerText.includes('report') ||
187
+ lowerText.includes('ventes') ||
188
+ lowerText.includes('sales')
189
+ ) {
190
+ response = "Voici un résumé de vos ventes: Cette semaine, vous avez vendu pour 125,000 FCFA de marchandises, soit une augmentation de 12% par rapport à la semaine précédente. Vos produits les plus vendus sont le riz (45%), le lait (30%) et l'huile (15%).";
191
+ }
192
+ // Check if it's about price recommendations
193
+ else if (
194
+ lowerText.includes('prix') ||
195
+ lowerText.includes('price') ||
196
+ lowerText.includes('tarif') ||
197
+ lowerText.includes('recommand')
198
+ ) {
199
+ response = "Basé sur les tendances actuelles du marché, je vous recommande d'augmenter le prix du riz de 5% (de 10,000 à 10,500 FCFA par sac) et de maintenir le prix des autres produits. Il y a une forte demande pour le riz cette semaine.";
200
+ }
201
+ // General stock query
202
+ else {
203
+ response = "Votre inventaire actuel comprend: Riz (25 sacs), Lait (18 cartons), Sucre (30 kg), Huile (12 bidons), Savon (40 pièces). Voulez-vous des informations sur un produit spécifique ?";
204
+ }
205
+
206
+ const botMessage = {
207
+ id: messages.length + 2,
208
+ type: 'bot' as const,
209
+ text: response,
210
+ timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
211
+ };
212
+
213
+ setMessages(prev => [...prev, botMessage]);
214
+ setIsProcessing(false);
215
+
216
+ // If it's a report request, offer to navigate to reports page
217
+ if (lowerText.includes('rapport') || lowerText.includes('report')) {
218
+ setTimeout(() => {
219
+ const reportMessage = {
220
+ id: messages.length + 3,
221
+ type: 'bot' as const,
222
+ text: "Voulez-vous voir des rapports plus détaillés? Je peux vous montrer la page des rapports.",
223
+ timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
224
+ };
225
+
226
+ setMessages(prev => [...prev, reportMessage]);
227
+ }, 1000);
228
+ }
229
+ };
230
+
231
+ const formatDateFromTerm = (term: string): string => {
232
+ const today = new Date();
233
+
234
+ switch(term.toLowerCase()) {
235
+ case 'yesterday':
236
+ case 'hier':
237
+ const yesterday = new Date(today);
238
+ yesterday.setDate(yesterday.getDate() - 1);
239
+ return yesterday.toISOString().split('T')[0];
240
+ case 'last week':
241
+ case 'semaine dernière':
242
+ const lastWeek = new Date(today);
243
+ lastWeek.setDate(lastWeek.getDate() - 7);
244
+ return lastWeek.toISOString().split('T')[0];
245
+ case 'this morning':
246
+ case 'ce matin':
247
+ case 'today':
248
+ case 'aujourd\'hui':
249
+ default:
250
+ return today.toISOString().split('T')[0];
251
+ }
252
+ };
253
+
254
+ const handleAddTransaction = () => {
255
+ if (extractedData?.amount) {
256
+ toast.success('Transaction ajoutée avec succès!');
257
+
258
+ // If it's a sale and we have product info, show stock alert if needed
259
+ if (extractedData.type === 'income' && extractedData.product && extractedData.quantity && extractedData.quantity > 3) {
260
+ setTimeout(() => {
261
+ const alertMessage = {
262
+ id: messages.length + 4,
263
+ type: 'bot' as const,
264
+ text: `⚠️ Alerte: Après cette vente, votre stock de ${extractedData.product} est faible. Il ne reste que ${Math.floor(Math.random() * 5) + 1} unités. Pensez à vous réapprovisionner.`,
265
+ timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
266
+ };
267
+
268
+ setMessages(prev => [...prev, alertMessage]);
269
+ }, 1500);
270
+ }
271
+
272
+ setTimeout(() => navigate('/transactions'), 2500);
273
+ } else {
274
+ toast.error('Veuillez fournir un montant valide');
275
+ }
276
+ };
277
+
278
+ return (
279
+ <div className="flex flex-col h-[calc(100vh-220px)] bg-white dark:bg-violet-darker rounded-lg border border-border overflow-hidden shadow-md">
280
+ <div className="p-4 border-b border-border bg-muted/30 dark:bg-violet-dark/30">
281
+ <h3 className="font-medium text-center">
282
+ Assistant IA de Gestion
283
+ </h3>
284
+ </div>
285
+
286
+ <ScrollArea className="flex-1 p-4" ref={scrollAreaRef}>
287
+ <div className="space-y-4 pb-4">
288
+ {messages.map((message) => (
289
+ <motion.div
290
+ key={message.id}
291
+ initial={{ opacity: 0, y: 10 }}
292
+ animate={{ opacity: 1, y: 0 }}
293
+ transition={{ duration: 0.2 }}
294
+ className={cn(
295
+ "flex",
296
+ message.type === "user" ? "justify-end" : "justify-start"
297
+ )}
298
+ >
299
+ {message.type === "bot" && (
300
+ <Avatar className="h-8 w-8 mr-2 bg-primary text-white">
301
+ <span>AI</span>
302
+ </Avatar>
303
+ )}
304
+
305
+ <div
306
+ className={cn(
307
+ "max-w-[80%] p-3 rounded-xl",
308
+ message.type === "user"
309
+ ? "bg-primary text-primary-foreground rounded-tr-none"
310
+ : "bg-muted dark:bg-violet-muted/30 rounded-tl-none"
311
+ )}
312
+ >
313
+ <p className="text-sm">{message.text}</p>
314
+ <span className={cn(
315
+ "text-xs mt-1 block opacity-70",
316
+ message.type === "user" ? "text-right" : ""
317
+ )}>
318
+ {message.timestamp}
319
+ </span>
320
+ </div>
321
+ </motion.div>
322
+ ))}
323
+
324
+ {isProcessing && (
325
+ <div className="flex justify-start">
326
+ <Avatar className="h-8 w-8 mr-2 bg-primary text-white">
327
+ <span>AI</span>
328
+ </Avatar>
329
+ <div className="bg-muted dark:bg-violet-muted/30 p-3 rounded-xl rounded-tl-none">
330
+ <span className="flex items-center space-x-1">
331
+ <span className="w-2 h-2 bg-primary dark:bg-violet rounded-full animate-bounce" style={{animationDelay: '0ms'}}></span>
332
+ <span className="w-2 h-2 bg-primary dark:bg-violet rounded-full animate-bounce" style={{animationDelay: '150ms'}}></span>
333
+ <span className="w-2 h-2 bg-primary dark:bg-violet rounded-full animate-bounce" style={{animationDelay: '300ms'}}></span>
334
+ </span>
335
+ </div>
336
+ </div>
337
+ )}
338
+ </div>
339
+ </ScrollArea>
340
+
341
+ {extractedData?.amount && (
342
+ <motion.div
343
+ initial={{ opacity: 0, height: 0 }}
344
+ animate={{ opacity: 1, height: 'auto' }}
345
+ className="p-4 border-t border-border bg-muted/30 dark:bg-violet-dark/10"
346
+ >
347
+ <div className="flex items-center justify-between">
348
+ <div>
349
+ <p className="text-sm font-medium">Transaction prête à ajouter:</p>
350
+ <p className="text-sm">
351
+ {extractedData.amount} FCFA - {extractedData.product || extractedData.category || 'Non catégorisé'} ({extractedData.type === 'income' ? 'Vente' : 'Achat'})
352
+ {extractedData.quantity && extractedData.quantity > 1 ? ` × ${extractedData.quantity}` : ''}
353
+ </p>
354
+ </div>
355
+ <Button size="sm" onClick={handleAddTransaction} className="gap-1">
356
+ <Plus size={16} /> Ajouter <ArrowRight size={14} />
357
+ </Button>
358
+ </div>
359
+ </motion.div>
360
+ )}
361
+
362
+ <div className="p-3 border-t border-border flex items-center gap-2 bg-card/50 dark:bg-violet-darker">
363
+ <Input
364
+ placeholder="Décrivez votre transaction ou demandez des informations..."
365
+ value={input}
366
+ onChange={(e) => setInput(e.target.value)}
367
+ onKeyDown={(e) => {
368
+ if (e.key === "Enter") {
369
+ handleSendMessage();
370
+ }
371
+ }}
372
+ className="flex-1 bg-background dark:bg-violet-darker border-slate-200 dark:border-violet-muted/30"
373
+ />
374
+ <Button
375
+ size="icon"
376
+ className="bg-primary hover:bg-primary/90 dark:bg-violet dark:hover:bg-violet-dark"
377
+ onClick={handleSendMessage}
378
+ >
379
+ <Send size={18} />
380
+ </Button>
381
+ </div>
382
+ </div>
383
+ );
384
+ };
385
+
386
+ export default AITransactionChat;
src/components/transactions/TransactionForm.tsx ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { useState, useEffect } from 'react';
3
+ import { useNavigate } from 'react-router-dom';
4
+ import {
5
+ Coffee, ShoppingCart, Zap, Home as HomeIcon,
6
+ Car, Utensils, Briefcase, CreditCard, Calendar, MessageSquare
7
+ } from 'lucide-react';
8
+ import { Button } from '@/components/ui/button';
9
+ import { cn } from '@/lib/utils';
10
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
11
+ import { Calendar as CalendarComponent } from '@/components/ui/calendar';
12
+ import { toast } from 'sonner';
13
+ import { v4 as uuidv4 } from 'uuid';
14
+ import database from '@/services/database';
15
+ import { Textarea } from '@/components/ui/textarea';
16
+
17
+ const CATEGORIES = [
18
+ { id: 'food', name: 'Food', icon: Utensils, color: 'bg-amber-100 text-amber-600' },
19
+ { id: 'coffee', name: 'Coffee', icon: Coffee, color: 'bg-brown-100 text-brown-600' },
20
+ { id: 'shopping', name: 'Shopping', icon: ShoppingCart, color: 'bg-indigo-100 text-indigo-600' },
21
+ { id: 'utilities', name: 'Utilities', icon: Zap, color: 'bg-yellow-100 text-yellow-600' },
22
+ { id: 'housing', name: 'Housing', icon: HomeIcon, color: 'bg-blue-100 text-blue-600' },
23
+ { id: 'transport', name: 'Transport', icon: Car, color: 'bg-green-100 text-green-600' },
24
+ { id: 'salary', name: 'Salary', icon: Briefcase, color: 'bg-emerald-100 text-emerald-600' },
25
+ { id: 'other', name: 'Other', icon: CreditCard, color: 'bg-gray-100 text-gray-600' },
26
+ ];
27
+
28
+ const TransactionForm = () => {
29
+ const navigate = useNavigate();
30
+ const [type, setType] = useState<'expense' | 'income'>('expense');
31
+ const [title, setTitle] = useState('');
32
+ const [amount, setAmount] = useState('');
33
+ const [category, setCategory] = useState('');
34
+ const [date, setDate] = useState<Date>(new Date());
35
+ const [note, setNote] = useState('');
36
+ const [message, setMessage] = useState('');
37
+ const [isDbInitialized, setIsDbInitialized] = useState(false);
38
+
39
+ useEffect(() => {
40
+ const initDb = async () => {
41
+ try {
42
+ await database.initialize();
43
+ setIsDbInitialized(true);
44
+ } catch (error) {
45
+ console.error('Failed to initialize database:', error);
46
+ toast.error('Failed to initialize database');
47
+ }
48
+ };
49
+
50
+ initDb();
51
+ }, []);
52
+
53
+ const handleSubmit = async (e: React.FormEvent) => {
54
+ e.preventDefault();
55
+
56
+ if (!title || !amount || !category) {
57
+ toast.error('Please fill in all required fields');
58
+ return;
59
+ }
60
+
61
+ if (!isDbInitialized) {
62
+ toast.error('Database not initialized yet, please try again');
63
+ return;
64
+ }
65
+
66
+ try {
67
+ const formattedDate = date.toISOString().split('T')[0];
68
+
69
+ const id = uuidv4();
70
+
71
+ database.exec(
72
+ 'INSERT INTO transactions (id, title, amount, type, category, date, note, message) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
73
+ [id, title, parseFloat(amount), type, category, formattedDate, note || null, message || null]
74
+ );
75
+
76
+ toast.success('Transaction saved successfully');
77
+ navigate('/transactions');
78
+ } catch (error) {
79
+ console.error('Error saving transaction:', error);
80
+ toast.error('Failed to save transaction');
81
+ }
82
+ };
83
+
84
+ return (
85
+ <form onSubmit={handleSubmit} className="space-y-6 p-4 animate-fade-in">
86
+ <div className="flex rounded-lg border border-border overflow-hidden p-1 w-full">
87
+ <button
88
+ type="button"
89
+ onClick={() => setType('expense')}
90
+ className={cn(
91
+ "flex-1 py-2.5 text-center text-sm font-medium rounded-md transition-all",
92
+ type === 'expense'
93
+ ? "bg-primary text-white"
94
+ : "bg-transparent text-muted-foreground hover:bg-secondary"
95
+ )}
96
+ >
97
+ Expense
98
+ </button>
99
+ <button
100
+ type="button"
101
+ onClick={() => setType('income')}
102
+ className={cn(
103
+ "flex-1 py-2.5 text-center text-sm font-medium rounded-md transition-all",
104
+ type === 'income'
105
+ ? "bg-primary text-white"
106
+ : "bg-transparent text-muted-foreground hover:bg-secondary"
107
+ )}
108
+ >
109
+ Income
110
+ </button>
111
+ </div>
112
+
113
+ <div className="space-y-2">
114
+ <label className="text-sm font-medium">Title</label>
115
+ <input
116
+ type="text"
117
+ value={title}
118
+ onChange={(e) => setTitle(e.target.value)}
119
+ placeholder="What was this for?"
120
+ className="w-full p-3 rounded-lg border border-border focus:border-primary/50 focus:ring-2 focus:ring-primary/20 transition-all bg-card"
121
+ />
122
+ </div>
123
+
124
+ <div className="space-y-2">
125
+ <label className="text-sm font-medium">Amount</label>
126
+ <div className="relative">
127
+ <span className="absolute left-3 top-3 text-muted-foreground">$</span>
128
+ <input
129
+ type="number"
130
+ value={amount}
131
+ onChange={(e) => setAmount(e.target.value)}
132
+ placeholder="0.00"
133
+ className="w-full p-3 pl-8 rounded-lg border border-border focus:border-primary/50 focus:ring-2 focus:ring-primary/20 transition-all bg-card"
134
+ />
135
+ </div>
136
+ </div>
137
+
138
+ <div className="space-y-3">
139
+ <label className="text-sm font-medium">Category</label>
140
+ <div className="grid grid-cols-4 gap-2">
141
+ {CATEGORIES.map((cat) => (
142
+ <button
143
+ key={cat.id}
144
+ type="button"
145
+ onClick={() => setCategory(cat.id)}
146
+ className={cn(
147
+ "flex flex-col items-center justify-center p-3 rounded-lg transition-all",
148
+ category === cat.id
149
+ ? "border-2 border-primary scale-in"
150
+ : "border border-border hover:border-primary/50",
151
+ )}
152
+ >
153
+ <div className={cn(
154
+ "w-10 h-10 rounded-full flex items-center justify-center mb-1",
155
+ cat.color
156
+ )}>
157
+ <cat.icon size={18} />
158
+ </div>
159
+ <span className="text-xs font-medium">{cat.name}</span>
160
+ </button>
161
+ ))}
162
+ </div>
163
+ </div>
164
+
165
+ <div className="space-y-2">
166
+ <label className="text-sm font-medium">Date</label>
167
+ <Popover>
168
+ <PopoverTrigger asChild>
169
+ <button
170
+ type="button"
171
+ className="w-full flex items-center justify-between p-3 rounded-lg border border-border focus:border-primary/50 focus:ring-2 focus:ring-primary/20 transition-all bg-card"
172
+ >
173
+ <span>
174
+ {date ? date.toLocaleDateString('en-US', {
175
+ month: 'short',
176
+ day: 'numeric',
177
+ year: 'numeric'
178
+ }) : 'Select date'}
179
+ </span>
180
+ <Calendar size={18} className="text-muted-foreground" />
181
+ </button>
182
+ </PopoverTrigger>
183
+ <PopoverContent className="w-auto p-0" align="center">
184
+ <CalendarComponent
185
+ mode="single"
186
+ selected={date}
187
+ onSelect={(newDate) => newDate && setDate(newDate)}
188
+ initialFocus
189
+ />
190
+ </PopoverContent>
191
+ </Popover>
192
+ </div>
193
+
194
+ <div className="space-y-2">
195
+ <label className="text-sm font-medium">Note (Optional)</label>
196
+ <textarea
197
+ value={note}
198
+ onChange={(e) => setNote(e.target.value)}
199
+ placeholder="Add a note..."
200
+ rows={3}
201
+ className="w-full p-3 rounded-lg border border-border focus:border-primary/50 focus:ring-2 focus:ring-primary/20 transition-all bg-card resize-none"
202
+ />
203
+ </div>
204
+
205
+ <div className="space-y-2">
206
+ <div className="flex items-center gap-2">
207
+ <MessageSquare size={16} className="text-primary" />
208
+ <label className="text-sm font-medium">Message (Optional)</label>
209
+ </div>
210
+ <Textarea
211
+ value={message}
212
+ onChange={(e) => setMessage(e.target.value)}
213
+ placeholder="Add a message about this transaction..."
214
+ className="min-h-[100px] border-border focus:border-primary/50 focus:ring-2 focus:ring-primary/20 transition-all bg-card"
215
+ />
216
+ <p className="text-xs text-muted-foreground">
217
+ Add any additional information or messages related to this transaction.
218
+ </p>
219
+ </div>
220
+
221
+ <Button
222
+ type="submit"
223
+ className="w-full py-6 text-base shadow-[0_2px_10px_rgba(0,122,255,0.3)]"
224
+ >
225
+ Save Transaction
226
+ </Button>
227
+ </form>
228
+ );
229
+ };
230
+
231
+ export default TransactionForm;
src/components/transactions/TransactionList.tsx ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { useState } from 'react';
3
+ import { useNavigate } from 'react-router-dom';
4
+ import {
5
+ Search, Filter, ChevronDown, Coffee, ShoppingCart, Zap,
6
+ Home as HomeIcon, Car, Utensils, Briefcase, CreditCard
7
+ } from 'lucide-react';
8
+ import { cn } from '@/lib/utils';
9
+ import { Transaction } from '../dashboard/RecentTransactions';
10
+
11
+ const CATEGORY_ICONS: Record<string, any> = {
12
+ food: Utensils,
13
+ coffee: Coffee,
14
+ shopping: ShoppingCart,
15
+ utilities: Zap,
16
+ housing: HomeIcon,
17
+ transport: Car,
18
+ salary: Briefcase,
19
+ other: CreditCard
20
+ };
21
+
22
+ interface TransactionListProps {
23
+ transactions: Transaction[];
24
+ currency?: string;
25
+ }
26
+
27
+ const TransactionList = ({
28
+ transactions,
29
+ currency = "$"
30
+ }: TransactionListProps) => {
31
+ const navigate = useNavigate();
32
+ const [searchTerm, setSearchTerm] = useState('');
33
+ const [showFilters, setShowFilters] = useState(false);
34
+
35
+ const formatDate = (date: Date) => {
36
+ return new Intl.DateTimeFormat('en-US', {
37
+ month: 'short',
38
+ day: 'numeric'
39
+ }).format(date);
40
+ };
41
+
42
+ const formatCurrency = (amount: number) => {
43
+ return new Intl.NumberFormat('en-US', {
44
+ style: 'currency',
45
+ currency: 'USD',
46
+ currencyDisplay: 'symbol',
47
+ }).format(Math.abs(amount)).replace('$', '');
48
+ };
49
+
50
+ const getCategoryIcon = (category: string) => {
51
+ const IconComponent = CATEGORY_ICONS[category.toLowerCase()] || CreditCard;
52
+ return <IconComponent size={16} />;
53
+ };
54
+
55
+ const filteredTransactions = transactions.filter(transaction =>
56
+ transaction.title.toLowerCase().includes(searchTerm.toLowerCase())
57
+ );
58
+
59
+ return (
60
+ <div className="space-y-4 animate-fade-in">
61
+ <div className="relative">
62
+ <Search size={18} className="absolute left-3 top-3 text-muted-foreground" />
63
+ <input
64
+ type="text"
65
+ placeholder="Search transactions..."
66
+ value={searchTerm}
67
+ onChange={(e) => setSearchTerm(e.target.value)}
68
+ className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-border focus:border-primary/50 focus:ring-2 focus:ring-primary/20 transition-all bg-card"
69
+ />
70
+ </div>
71
+
72
+ <div className="flex justify-between items-center">
73
+ <button
74
+ onClick={() => setShowFilters(!showFilters)}
75
+ className="flex items-center space-x-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
76
+ >
77
+ <Filter size={16} />
78
+ <span>Filter</span>
79
+ <ChevronDown size={16} className={cn("transition-transform", showFilters && "rotate-180")} />
80
+ </button>
81
+
82
+ <div className="text-sm text-muted-foreground">
83
+ {filteredTransactions.length} transactions
84
+ </div>
85
+ </div>
86
+
87
+ {showFilters && (
88
+ <div className="p-4 border border-border rounded-lg bg-card/50 animate-scale-in">
89
+ <div className="flex flex-wrap gap-2">
90
+ <button className="px-3 py-1.5 bg-primary/10 text-primary text-sm rounded-full">
91
+ All
92
+ </button>
93
+ <button className="px-3 py-1.5 bg-secondary text-foreground text-sm rounded-full">
94
+ Expenses
95
+ </button>
96
+ <button className="px-3 py-1.5 bg-secondary text-foreground text-sm rounded-full">
97
+ Income
98
+ </button>
99
+ <button className="px-3 py-1.5 bg-secondary text-foreground text-sm rounded-full">
100
+ Last 7 days
101
+ </button>
102
+ <button className="px-3 py-1.5 bg-secondary text-foreground text-sm rounded-full">
103
+ This month
104
+ </button>
105
+ </div>
106
+ </div>
107
+ )}
108
+
109
+ {filteredTransactions.length > 0 ? (
110
+ <div className="space-y-3">
111
+ {filteredTransactions.map((transaction) => (
112
+ <div
113
+ key={transaction.id}
114
+ onClick={() => navigate(`/transactions/${transaction.id}`)}
115
+ className="flex items-center p-3 bg-card rounded-xl border border-border/60 subtle-shadow transition-all duration-300 hover:card-shadow hover:-translate-y-0.5 cursor-pointer"
116
+ >
117
+ <div className={cn(
118
+ "w-10 h-10 rounded-full flex items-center justify-center",
119
+ transaction.type === "expense" ? "bg-red-100 text-red-600" : "bg-green-100 text-green-600"
120
+ )}>
121
+ {getCategoryIcon(transaction.category)}
122
+ </div>
123
+
124
+ <div className="ml-3 flex-1">
125
+ <p className="font-medium">{transaction.title}</p>
126
+ <p className="text-xs text-muted-foreground">{formatDate(transaction.date)}</p>
127
+ </div>
128
+
129
+ <div className={cn(
130
+ "font-medium",
131
+ transaction.type === "expense" ? "text-red-600" : "text-green-600"
132
+ )}>
133
+ {transaction.type === "expense" ? "- " : "+ "}
134
+ {currency}{formatCurrency(transaction.amount)}
135
+ </div>
136
+ </div>
137
+ ))}
138
+ </div>
139
+ ) : (
140
+ <div className="text-center p-8 bg-muted/50 rounded-xl border border-border">
141
+ <p className="text-muted-foreground">No transactions found</p>
142
+ </div>
143
+ )}
144
+ </div>
145
+ );
146
+ };
147
+
148
+ export default TransactionList;
src/components/ui/accordion.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
3
+ import { ChevronDown } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Accordion = AccordionPrimitive.Root
8
+
9
+ const AccordionItem = React.forwardRef<
10
+ React.ElementRef<typeof AccordionPrimitive.Item>,
11
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
12
+ >(({ className, ...props }, ref) => (
13
+ <AccordionPrimitive.Item
14
+ ref={ref}
15
+ className={cn("border-b", className)}
16
+ {...props}
17
+ />
18
+ ))
19
+ AccordionItem.displayName = "AccordionItem"
20
+
21
+ const AccordionTrigger = React.forwardRef<
22
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
23
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
24
+ >(({ className, children, ...props }, ref) => (
25
+ <AccordionPrimitive.Header className="flex">
26
+ <AccordionPrimitive.Trigger
27
+ ref={ref}
28
+ className={cn(
29
+ "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
36
+ </AccordionPrimitive.Trigger>
37
+ </AccordionPrimitive.Header>
38
+ ))
39
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
40
+
41
+ const AccordionContent = React.forwardRef<
42
+ React.ElementRef<typeof AccordionPrimitive.Content>,
43
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
44
+ >(({ className, children, ...props }, ref) => (
45
+ <AccordionPrimitive.Content
46
+ ref={ref}
47
+ className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
48
+ {...props}
49
+ >
50
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
51
+ </AccordionPrimitive.Content>
52
+ ))
53
+
54
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName
55
+
56
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
src/components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
3
+
4
+ import { cn } from "@/lib/utils"
5
+ import { buttonVariants } from "@/components/ui/button"
6
+
7
+ const AlertDialog = AlertDialogPrimitive.Root
8
+
9
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
10
+
11
+ const AlertDialogPortal = AlertDialogPrimitive.Portal
12
+
13
+ const AlertDialogOverlay = React.forwardRef<
14
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
15
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
16
+ >(({ className, ...props }, ref) => (
17
+ <AlertDialogPrimitive.Overlay
18
+ className={cn(
19
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
20
+ className
21
+ )}
22
+ {...props}
23
+ ref={ref}
24
+ />
25
+ ))
26
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
27
+
28
+ const AlertDialogContent = React.forwardRef<
29
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
30
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
31
+ >(({ className, ...props }, ref) => (
32
+ <AlertDialogPortal>
33
+ <AlertDialogOverlay />
34
+ <AlertDialogPrimitive.Content
35
+ ref={ref}
36
+ className={cn(
37
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
38
+ className
39
+ )}
40
+ {...props}
41
+ />
42
+ </AlertDialogPortal>
43
+ ))
44
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
45
+
46
+ const AlertDialogHeader = ({
47
+ className,
48
+ ...props
49
+ }: React.HTMLAttributes<HTMLDivElement>) => (
50
+ <div
51
+ className={cn(
52
+ "flex flex-col space-y-2 text-center sm:text-left",
53
+ className
54
+ )}
55
+ {...props}
56
+ />
57
+ )
58
+ AlertDialogHeader.displayName = "AlertDialogHeader"
59
+
60
+ const AlertDialogFooter = ({
61
+ className,
62
+ ...props
63
+ }: React.HTMLAttributes<HTMLDivElement>) => (
64
+ <div
65
+ className={cn(
66
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
67
+ className
68
+ )}
69
+ {...props}
70
+ />
71
+ )
72
+ AlertDialogFooter.displayName = "AlertDialogFooter"
73
+
74
+ const AlertDialogTitle = React.forwardRef<
75
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
76
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
77
+ >(({ className, ...props }, ref) => (
78
+ <AlertDialogPrimitive.Title
79
+ ref={ref}
80
+ className={cn("text-lg font-semibold", className)}
81
+ {...props}
82
+ />
83
+ ))
84
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
85
+
86
+ const AlertDialogDescription = React.forwardRef<
87
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
88
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
89
+ >(({ className, ...props }, ref) => (
90
+ <AlertDialogPrimitive.Description
91
+ ref={ref}
92
+ className={cn("text-sm text-muted-foreground", className)}
93
+ {...props}
94
+ />
95
+ ))
96
+ AlertDialogDescription.displayName =
97
+ AlertDialogPrimitive.Description.displayName
98
+
99
+ const AlertDialogAction = React.forwardRef<
100
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
101
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
102
+ >(({ className, ...props }, ref) => (
103
+ <AlertDialogPrimitive.Action
104
+ ref={ref}
105
+ className={cn(buttonVariants(), className)}
106
+ {...props}
107
+ />
108
+ ))
109
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
110
+
111
+ const AlertDialogCancel = React.forwardRef<
112
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
113
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
114
+ >(({ className, ...props }, ref) => (
115
+ <AlertDialogPrimitive.Cancel
116
+ ref={ref}
117
+ className={cn(
118
+ buttonVariants({ variant: "outline" }),
119
+ "mt-2 sm:mt-0",
120
+ className
121
+ )}
122
+ {...props}
123
+ />
124
+ ))
125
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
126
+
127
+ export {
128
+ AlertDialog,
129
+ AlertDialogPortal,
130
+ AlertDialogOverlay,
131
+ AlertDialogTrigger,
132
+ AlertDialogContent,
133
+ AlertDialogHeader,
134
+ AlertDialogFooter,
135
+ AlertDialogTitle,
136
+ AlertDialogDescription,
137
+ AlertDialogAction,
138
+ AlertDialogCancel,
139
+ }
src/components/ui/alert.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-background text-foreground",
12
+ destructive:
13
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ const Alert = React.forwardRef<
23
+ HTMLDivElement,
24
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
25
+ >(({ className, variant, ...props }, ref) => (
26
+ <div
27
+ ref={ref}
28
+ role="alert"
29
+ className={cn(alertVariants({ variant }), className)}
30
+ {...props}
31
+ />
32
+ ))
33
+ Alert.displayName = "Alert"
34
+
35
+ const AlertTitle = React.forwardRef<
36
+ HTMLParagraphElement,
37
+ React.HTMLAttributes<HTMLHeadingElement>
38
+ >(({ className, ...props }, ref) => (
39
+ <h5
40
+ ref={ref}
41
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
42
+ {...props}
43
+ />
44
+ ))
45
+ AlertTitle.displayName = "AlertTitle"
46
+
47
+ const AlertDescription = React.forwardRef<
48
+ HTMLParagraphElement,
49
+ React.HTMLAttributes<HTMLParagraphElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <div
52
+ ref={ref}
53
+ className={cn("text-sm [&_p]:leading-relaxed", className)}
54
+ {...props}
55
+ />
56
+ ))
57
+ AlertDescription.displayName = "AlertDescription"
58
+
59
+ export { Alert, AlertTitle, AlertDescription }
src/components/ui/aspect-ratio.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
2
+
3
+ const AspectRatio = AspectRatioPrimitive.Root
4
+
5
+ export { AspectRatio }
src/components/ui/avatar.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const Avatar = React.forwardRef<
7
+ React.ElementRef<typeof AvatarPrimitive.Root>,
8
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
9
+ >(({ className, ...props }, ref) => (
10
+ <AvatarPrimitive.Root
11
+ ref={ref}
12
+ className={cn(
13
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ ))
19
+ Avatar.displayName = AvatarPrimitive.Root.displayName
20
+
21
+ const AvatarImage = React.forwardRef<
22
+ React.ElementRef<typeof AvatarPrimitive.Image>,
23
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
24
+ >(({ className, ...props }, ref) => (
25
+ <AvatarPrimitive.Image
26
+ ref={ref}
27
+ className={cn("aspect-square h-full w-full", className)}
28
+ {...props}
29
+ />
30
+ ))
31
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName
32
+
33
+ const AvatarFallback = React.forwardRef<
34
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
35
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
36
+ >(({ className, ...props }, ref) => (
37
+ <AvatarPrimitive.Fallback
38
+ ref={ref}
39
+ className={cn(
40
+ "flex h-full w-full items-center justify-center rounded-full bg-muted",
41
+ className
42
+ )}
43
+ {...props}
44
+ />
45
+ ))
46
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
47
+
48
+ export { Avatar, AvatarImage, AvatarFallback }
src/components/ui/badge.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13
+ secondary:
14
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15
+ destructive:
16
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17
+ outline: "text-foreground",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "default",
22
+ },
23
+ }
24
+ )
25
+
26
+ export interface BadgeProps
27
+ extends React.HTMLAttributes<HTMLDivElement>,
28
+ VariantProps<typeof badgeVariants> {}
29
+
30
+ function Badge({ className, variant, ...props }: BadgeProps) {
31
+ return (
32
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
33
+ )
34
+ }
35
+
36
+ export { Badge, badgeVariants }
src/components/ui/breadcrumb.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Breadcrumb = React.forwardRef<
8
+ HTMLElement,
9
+ React.ComponentPropsWithoutRef<"nav"> & {
10
+ separator?: React.ReactNode
11
+ }
12
+ >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
13
+ Breadcrumb.displayName = "Breadcrumb"
14
+
15
+ const BreadcrumbList = React.forwardRef<
16
+ HTMLOListElement,
17
+ React.ComponentPropsWithoutRef<"ol">
18
+ >(({ className, ...props }, ref) => (
19
+ <ol
20
+ ref={ref}
21
+ className={cn(
22
+ "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ ))
28
+ BreadcrumbList.displayName = "BreadcrumbList"
29
+
30
+ const BreadcrumbItem = React.forwardRef<
31
+ HTMLLIElement,
32
+ React.ComponentPropsWithoutRef<"li">
33
+ >(({ className, ...props }, ref) => (
34
+ <li
35
+ ref={ref}
36
+ className={cn("inline-flex items-center gap-1.5", className)}
37
+ {...props}
38
+ />
39
+ ))
40
+ BreadcrumbItem.displayName = "BreadcrumbItem"
41
+
42
+ const BreadcrumbLink = React.forwardRef<
43
+ HTMLAnchorElement,
44
+ React.ComponentPropsWithoutRef<"a"> & {
45
+ asChild?: boolean
46
+ }
47
+ >(({ asChild, className, ...props }, ref) => {
48
+ const Comp = asChild ? Slot : "a"
49
+
50
+ return (
51
+ <Comp
52
+ ref={ref}
53
+ className={cn("transition-colors hover:text-foreground", className)}
54
+ {...props}
55
+ />
56
+ )
57
+ })
58
+ BreadcrumbLink.displayName = "BreadcrumbLink"
59
+
60
+ const BreadcrumbPage = React.forwardRef<
61
+ HTMLSpanElement,
62
+ React.ComponentPropsWithoutRef<"span">
63
+ >(({ className, ...props }, ref) => (
64
+ <span
65
+ ref={ref}
66
+ role="link"
67
+ aria-disabled="true"
68
+ aria-current="page"
69
+ className={cn("font-normal text-foreground", className)}
70
+ {...props}
71
+ />
72
+ ))
73
+ BreadcrumbPage.displayName = "BreadcrumbPage"
74
+
75
+ const BreadcrumbSeparator = ({
76
+ children,
77
+ className,
78
+ ...props
79
+ }: React.ComponentProps<"li">) => (
80
+ <li
81
+ role="presentation"
82
+ aria-hidden="true"
83
+ className={cn("[&>svg]:size-3.5", className)}
84
+ {...props}
85
+ >
86
+ {children ?? <ChevronRight />}
87
+ </li>
88
+ )
89
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90
+
91
+ const BreadcrumbEllipsis = ({
92
+ className,
93
+ ...props
94
+ }: React.ComponentProps<"span">) => (
95
+ <span
96
+ role="presentation"
97
+ aria-hidden="true"
98
+ className={cn("flex h-9 w-9 items-center justify-center", className)}
99
+ {...props}
100
+ >
101
+ <MoreHorizontal className="h-4 w-4" />
102
+ <span className="sr-only">More</span>
103
+ </span>
104
+ )
105
+ BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106
+
107
+ export {
108
+ Breadcrumb,
109
+ BreadcrumbList,
110
+ BreadcrumbItem,
111
+ BreadcrumbLink,
112
+ BreadcrumbPage,
113
+ BreadcrumbSeparator,
114
+ BreadcrumbEllipsis,
115
+ }
src/components/ui/button.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-10 px-4 py-2",
24
+ sm: "h-9 rounded-md px-3",
25
+ lg: "h-11 rounded-md px-8",
26
+ icon: "h-10 w-10",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ }
34
+ )
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button"
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ )
52
+ }
53
+ )
54
+ Button.displayName = "Button"
55
+
56
+ export { Button, buttonVariants }
src/components/ui/calendar.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { ChevronLeft, ChevronRight } from "lucide-react";
3
+ import { DayPicker } from "react-day-picker";
4
+
5
+ import { cn } from "@/lib/utils";
6
+ import { buttonVariants } from "@/components/ui/button";
7
+
8
+ export type CalendarProps = React.ComponentProps<typeof DayPicker>;
9
+
10
+ function Calendar({
11
+ className,
12
+ classNames,
13
+ showOutsideDays = true,
14
+ ...props
15
+ }: CalendarProps) {
16
+ return (
17
+ <DayPicker
18
+ showOutsideDays={showOutsideDays}
19
+ className={cn("p-3", className)}
20
+ classNames={{
21
+ months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
22
+ month: "space-y-4",
23
+ caption: "flex justify-center pt-1 relative items-center",
24
+ caption_label: "text-sm font-medium",
25
+ nav: "space-x-1 flex items-center",
26
+ nav_button: cn(
27
+ buttonVariants({ variant: "outline" }),
28
+ "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
29
+ ),
30
+ nav_button_previous: "absolute left-1",
31
+ nav_button_next: "absolute right-1",
32
+ table: "w-full border-collapse space-y-1",
33
+ head_row: "flex",
34
+ head_cell:
35
+ "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
36
+ row: "flex w-full mt-2",
37
+ cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
38
+ day: cn(
39
+ buttonVariants({ variant: "ghost" }),
40
+ "h-9 w-9 p-0 font-normal aria-selected:opacity-100"
41
+ ),
42
+ day_range_end: "day-range-end",
43
+ day_selected:
44
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
45
+ day_today: "bg-accent text-accent-foreground",
46
+ day_outside:
47
+ "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
48
+ day_disabled: "text-muted-foreground opacity-50",
49
+ day_range_middle:
50
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
51
+ day_hidden: "invisible",
52
+ ...classNames,
53
+ }}
54
+ components={{
55
+ IconLeft: ({ ..._props }) => <ChevronLeft className="h-4 w-4" />,
56
+ IconRight: ({ ..._props }) => <ChevronRight className="h-4 w-4" />,
57
+ }}
58
+ {...props}
59
+ />
60
+ );
61
+ }
62
+ Calendar.displayName = "Calendar";
63
+
64
+ export { Calendar };
src/components/ui/card.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ "rounded-lg border bg-card text-card-foreground shadow-sm",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ ))
18
+ Card.displayName = "Card"
19
+
20
+ const CardHeader = React.forwardRef<
21
+ HTMLDivElement,
22
+ React.HTMLAttributes<HTMLDivElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
27
+ {...props}
28
+ />
29
+ ))
30
+ CardHeader.displayName = "CardHeader"
31
+
32
+ const CardTitle = React.forwardRef<
33
+ HTMLParagraphElement,
34
+ React.HTMLAttributes<HTMLHeadingElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <h3
37
+ ref={ref}
38
+ className={cn(
39
+ "text-2xl font-semibold leading-none tracking-tight",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ ))
45
+ CardTitle.displayName = "CardTitle"
46
+
47
+ const CardDescription = React.forwardRef<
48
+ HTMLParagraphElement,
49
+ React.HTMLAttributes<HTMLParagraphElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <p
52
+ ref={ref}
53
+ className={cn("text-sm text-muted-foreground", className)}
54
+ {...props}
55
+ />
56
+ ))
57
+ CardDescription.displayName = "CardDescription"
58
+
59
+ const CardContent = React.forwardRef<
60
+ HTMLDivElement,
61
+ React.HTMLAttributes<HTMLDivElement>
62
+ >(({ className, ...props }, ref) => (
63
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
64
+ ))
65
+ CardContent.displayName = "CardContent"
66
+
67
+ const CardFooter = React.forwardRef<
68
+ HTMLDivElement,
69
+ React.HTMLAttributes<HTMLDivElement>
70
+ >(({ className, ...props }, ref) => (
71
+ <div
72
+ ref={ref}
73
+ className={cn("flex items-center p-6 pt-0", className)}
74
+ {...props}
75
+ />
76
+ ))
77
+ CardFooter.displayName = "CardFooter"
78
+
79
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
src/components/ui/carousel.tsx ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import useEmblaCarousel, {
3
+ type UseEmblaCarouselType,
4
+ } from "embla-carousel-react"
5
+ import { ArrowLeft, ArrowRight } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import { Button } from "@/components/ui/button"
9
+
10
+ type CarouselApi = UseEmblaCarouselType[1]
11
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
12
+ type CarouselOptions = UseCarouselParameters[0]
13
+ type CarouselPlugin = UseCarouselParameters[1]
14
+
15
+ type CarouselProps = {
16
+ opts?: CarouselOptions
17
+ plugins?: CarouselPlugin
18
+ orientation?: "horizontal" | "vertical"
19
+ setApi?: (api: CarouselApi) => void
20
+ }
21
+
22
+ type CarouselContextProps = {
23
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0]
24
+ api: ReturnType<typeof useEmblaCarousel>[1]
25
+ scrollPrev: () => void
26
+ scrollNext: () => void
27
+ canScrollPrev: boolean
28
+ canScrollNext: boolean
29
+ } & CarouselProps
30
+
31
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null)
32
+
33
+ function useCarousel() {
34
+ const context = React.useContext(CarouselContext)
35
+
36
+ if (!context) {
37
+ throw new Error("useCarousel must be used within a <Carousel />")
38
+ }
39
+
40
+ return context
41
+ }
42
+
43
+ const Carousel = React.forwardRef<
44
+ HTMLDivElement,
45
+ React.HTMLAttributes<HTMLDivElement> & CarouselProps
46
+ >(
47
+ (
48
+ {
49
+ orientation = "horizontal",
50
+ opts,
51
+ setApi,
52
+ plugins,
53
+ className,
54
+ children,
55
+ ...props
56
+ },
57
+ ref
58
+ ) => {
59
+ const [carouselRef, api] = useEmblaCarousel(
60
+ {
61
+ ...opts,
62
+ axis: orientation === "horizontal" ? "x" : "y",
63
+ },
64
+ plugins
65
+ )
66
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
67
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
68
+
69
+ const onSelect = React.useCallback((api: CarouselApi) => {
70
+ if (!api) {
71
+ return
72
+ }
73
+
74
+ setCanScrollPrev(api.canScrollPrev())
75
+ setCanScrollNext(api.canScrollNext())
76
+ }, [])
77
+
78
+ const scrollPrev = React.useCallback(() => {
79
+ api?.scrollPrev()
80
+ }, [api])
81
+
82
+ const scrollNext = React.useCallback(() => {
83
+ api?.scrollNext()
84
+ }, [api])
85
+
86
+ const handleKeyDown = React.useCallback(
87
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
88
+ if (event.key === "ArrowLeft") {
89
+ event.preventDefault()
90
+ scrollPrev()
91
+ } else if (event.key === "ArrowRight") {
92
+ event.preventDefault()
93
+ scrollNext()
94
+ }
95
+ },
96
+ [scrollPrev, scrollNext]
97
+ )
98
+
99
+ React.useEffect(() => {
100
+ if (!api || !setApi) {
101
+ return
102
+ }
103
+
104
+ setApi(api)
105
+ }, [api, setApi])
106
+
107
+ React.useEffect(() => {
108
+ if (!api) {
109
+ return
110
+ }
111
+
112
+ onSelect(api)
113
+ api.on("reInit", onSelect)
114
+ api.on("select", onSelect)
115
+
116
+ return () => {
117
+ api?.off("select", onSelect)
118
+ }
119
+ }, [api, onSelect])
120
+
121
+ return (
122
+ <CarouselContext.Provider
123
+ value={{
124
+ carouselRef,
125
+ api: api,
126
+ opts,
127
+ orientation:
128
+ orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
129
+ scrollPrev,
130
+ scrollNext,
131
+ canScrollPrev,
132
+ canScrollNext,
133
+ }}
134
+ >
135
+ <div
136
+ ref={ref}
137
+ onKeyDownCapture={handleKeyDown}
138
+ className={cn("relative", className)}
139
+ role="region"
140
+ aria-roledescription="carousel"
141
+ {...props}
142
+ >
143
+ {children}
144
+ </div>
145
+ </CarouselContext.Provider>
146
+ )
147
+ }
148
+ )
149
+ Carousel.displayName = "Carousel"
150
+
151
+ const CarouselContent = React.forwardRef<
152
+ HTMLDivElement,
153
+ React.HTMLAttributes<HTMLDivElement>
154
+ >(({ className, ...props }, ref) => {
155
+ const { carouselRef, orientation } = useCarousel()
156
+
157
+ return (
158
+ <div ref={carouselRef} className="overflow-hidden">
159
+ <div
160
+ ref={ref}
161
+ className={cn(
162
+ "flex",
163
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
164
+ className
165
+ )}
166
+ {...props}
167
+ />
168
+ </div>
169
+ )
170
+ })
171
+ CarouselContent.displayName = "CarouselContent"
172
+
173
+ const CarouselItem = React.forwardRef<
174
+ HTMLDivElement,
175
+ React.HTMLAttributes<HTMLDivElement>
176
+ >(({ className, ...props }, ref) => {
177
+ const { orientation } = useCarousel()
178
+
179
+ return (
180
+ <div
181
+ ref={ref}
182
+ role="group"
183
+ aria-roledescription="slide"
184
+ className={cn(
185
+ "min-w-0 shrink-0 grow-0 basis-full",
186
+ orientation === "horizontal" ? "pl-4" : "pt-4",
187
+ className
188
+ )}
189
+ {...props}
190
+ />
191
+ )
192
+ })
193
+ CarouselItem.displayName = "CarouselItem"
194
+
195
+ const CarouselPrevious = React.forwardRef<
196
+ HTMLButtonElement,
197
+ React.ComponentProps<typeof Button>
198
+ >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
199
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
200
+
201
+ return (
202
+ <Button
203
+ ref={ref}
204
+ variant={variant}
205
+ size={size}
206
+ className={cn(
207
+ "absolute h-8 w-8 rounded-full",
208
+ orientation === "horizontal"
209
+ ? "-left-12 top-1/2 -translate-y-1/2"
210
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
211
+ className
212
+ )}
213
+ disabled={!canScrollPrev}
214
+ onClick={scrollPrev}
215
+ {...props}
216
+ >
217
+ <ArrowLeft className="h-4 w-4" />
218
+ <span className="sr-only">Previous slide</span>
219
+ </Button>
220
+ )
221
+ })
222
+ CarouselPrevious.displayName = "CarouselPrevious"
223
+
224
+ const CarouselNext = React.forwardRef<
225
+ HTMLButtonElement,
226
+ React.ComponentProps<typeof Button>
227
+ >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
228
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
229
+
230
+ return (
231
+ <Button
232
+ ref={ref}
233
+ variant={variant}
234
+ size={size}
235
+ className={cn(
236
+ "absolute h-8 w-8 rounded-full",
237
+ orientation === "horizontal"
238
+ ? "-right-12 top-1/2 -translate-y-1/2"
239
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
240
+ className
241
+ )}
242
+ disabled={!canScrollNext}
243
+ onClick={scrollNext}
244
+ {...props}
245
+ >
246
+ <ArrowRight className="h-4 w-4" />
247
+ <span className="sr-only">Next slide</span>
248
+ </Button>
249
+ )
250
+ })
251
+ CarouselNext.displayName = "CarouselNext"
252
+
253
+ export {
254
+ type CarouselApi,
255
+ Carousel,
256
+ CarouselContent,
257
+ CarouselItem,
258
+ CarouselPrevious,
259
+ CarouselNext,
260
+ }
src/components/ui/chart.tsx ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as RechartsPrimitive from "recharts"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ // Format: { THEME_NAME: CSS_SELECTOR }
7
+ const THEMES = { light: "", dark: ".dark" } as const
8
+
9
+ export type ChartConfig = {
10
+ [k in string]: {
11
+ label?: React.ReactNode
12
+ icon?: React.ComponentType
13
+ } & (
14
+ | { color?: string; theme?: never }
15
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
16
+ )
17
+ }
18
+
19
+ type ChartContextProps = {
20
+ config: ChartConfig
21
+ }
22
+
23
+ const ChartContext = React.createContext<ChartContextProps | null>(null)
24
+
25
+ function useChart() {
26
+ const context = React.useContext(ChartContext)
27
+
28
+ if (!context) {
29
+ throw new Error("useChart must be used within a <ChartContainer />")
30
+ }
31
+
32
+ return context
33
+ }
34
+
35
+ const ChartContainer = React.forwardRef<
36
+ HTMLDivElement,
37
+ React.ComponentProps<"div"> & {
38
+ config: ChartConfig
39
+ children: React.ComponentProps<
40
+ typeof RechartsPrimitive.ResponsiveContainer
41
+ >["children"]
42
+ }
43
+ >(({ id, className, children, config, ...props }, ref) => {
44
+ const uniqueId = React.useId()
45
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
46
+
47
+ return (
48
+ <ChartContext.Provider value={{ config }}>
49
+ <div
50
+ data-chart={chartId}
51
+ ref={ref}
52
+ className={cn(
53
+ "flex aspect-video justify-center text-xs [&_.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-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.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 [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
54
+ className
55
+ )}
56
+ {...props}
57
+ >
58
+ <ChartStyle id={chartId} config={config} />
59
+ <RechartsPrimitive.ResponsiveContainer>
60
+ {children}
61
+ </RechartsPrimitive.ResponsiveContainer>
62
+ </div>
63
+ </ChartContext.Provider>
64
+ )
65
+ })
66
+ ChartContainer.displayName = "Chart"
67
+
68
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
69
+ const colorConfig = Object.entries(config).filter(
70
+ ([_, config]) => config.theme || config.color
71
+ )
72
+
73
+ if (!colorConfig.length) {
74
+ return null
75
+ }
76
+
77
+ return (
78
+ <style
79
+ dangerouslySetInnerHTML={{
80
+ __html: Object.entries(THEMES)
81
+ .map(
82
+ ([theme, prefix]) => `
83
+ ${prefix} [data-chart=${id}] {
84
+ ${colorConfig
85
+ .map(([key, itemConfig]) => {
86
+ const color =
87
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
88
+ itemConfig.color
89
+ return color ? ` --color-${key}: ${color};` : null
90
+ })
91
+ .join("\n")}
92
+ }
93
+ `
94
+ )
95
+ .join("\n"),
96
+ }}
97
+ />
98
+ )
99
+ }
100
+
101
+ const ChartTooltip = RechartsPrimitive.Tooltip
102
+
103
+ const ChartTooltipContent = React.forwardRef<
104
+ HTMLDivElement,
105
+ React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
106
+ React.ComponentProps<"div"> & {
107
+ hideLabel?: boolean
108
+ hideIndicator?: boolean
109
+ indicator?: "line" | "dot" | "dashed"
110
+ nameKey?: string
111
+ labelKey?: string
112
+ }
113
+ >(
114
+ (
115
+ {
116
+ active,
117
+ payload,
118
+ className,
119
+ indicator = "dot",
120
+ hideLabel = false,
121
+ hideIndicator = false,
122
+ label,
123
+ labelFormatter,
124
+ labelClassName,
125
+ formatter,
126
+ color,
127
+ nameKey,
128
+ labelKey,
129
+ },
130
+ ref
131
+ ) => {
132
+ const { config } = useChart()
133
+
134
+ const tooltipLabel = React.useMemo(() => {
135
+ if (hideLabel || !payload?.length) {
136
+ return null
137
+ }
138
+
139
+ const [item] = payload
140
+ const key = `${labelKey || item.dataKey || item.name || "value"}`
141
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
142
+ const value =
143
+ !labelKey && typeof label === "string"
144
+ ? config[label as keyof typeof config]?.label || label
145
+ : itemConfig?.label
146
+
147
+ if (labelFormatter) {
148
+ return (
149
+ <div className={cn("font-medium", labelClassName)}>
150
+ {labelFormatter(value, payload)}
151
+ </div>
152
+ )
153
+ }
154
+
155
+ if (!value) {
156
+ return null
157
+ }
158
+
159
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>
160
+ }, [
161
+ label,
162
+ labelFormatter,
163
+ payload,
164
+ hideLabel,
165
+ labelClassName,
166
+ config,
167
+ labelKey,
168
+ ])
169
+
170
+ if (!active || !payload?.length) {
171
+ return null
172
+ }
173
+
174
+ const nestLabel = payload.length === 1 && indicator !== "dot"
175
+
176
+ return (
177
+ <div
178
+ ref={ref}
179
+ className={cn(
180
+ "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
181
+ className
182
+ )}
183
+ >
184
+ {!nestLabel ? tooltipLabel : null}
185
+ <div className="grid gap-1.5">
186
+ {payload.map((item, index) => {
187
+ const key = `${nameKey || item.name || item.dataKey || "value"}`
188
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
189
+ const indicatorColor = color || item.payload.fill || item.color
190
+
191
+ return (
192
+ <div
193
+ key={item.dataKey}
194
+ className={cn(
195
+ "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
196
+ indicator === "dot" && "items-center"
197
+ )}
198
+ >
199
+ {formatter && item?.value !== undefined && item.name ? (
200
+ formatter(item.value, item.name, item, index, item.payload)
201
+ ) : (
202
+ <>
203
+ {itemConfig?.icon ? (
204
+ <itemConfig.icon />
205
+ ) : (
206
+ !hideIndicator && (
207
+ <div
208
+ className={cn(
209
+ "shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
210
+ {
211
+ "h-2.5 w-2.5": indicator === "dot",
212
+ "w-1": indicator === "line",
213
+ "w-0 border-[1.5px] border-dashed bg-transparent":
214
+ indicator === "dashed",
215
+ "my-0.5": nestLabel && indicator === "dashed",
216
+ }
217
+ )}
218
+ style={
219
+ {
220
+ "--color-bg": indicatorColor,
221
+ "--color-border": indicatorColor,
222
+ } as React.CSSProperties
223
+ }
224
+ />
225
+ )
226
+ )}
227
+ <div
228
+ className={cn(
229
+ "flex flex-1 justify-between leading-none",
230
+ nestLabel ? "items-end" : "items-center"
231
+ )}
232
+ >
233
+ <div className="grid gap-1.5">
234
+ {nestLabel ? tooltipLabel : null}
235
+ <span className="text-muted-foreground">
236
+ {itemConfig?.label || item.name}
237
+ </span>
238
+ </div>
239
+ {item.value && (
240
+ <span className="font-mono font-medium tabular-nums text-foreground">
241
+ {item.value.toLocaleString()}
242
+ </span>
243
+ )}
244
+ </div>
245
+ </>
246
+ )}
247
+ </div>
248
+ )
249
+ })}
250
+ </div>
251
+ </div>
252
+ )
253
+ }
254
+ )
255
+ ChartTooltipContent.displayName = "ChartTooltip"
256
+
257
+ const ChartLegend = RechartsPrimitive.Legend
258
+
259
+ const ChartLegendContent = React.forwardRef<
260
+ HTMLDivElement,
261
+ React.ComponentProps<"div"> &
262
+ Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
263
+ hideIcon?: boolean
264
+ nameKey?: string
265
+ }
266
+ >(
267
+ (
268
+ { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
269
+ ref
270
+ ) => {
271
+ const { config } = useChart()
272
+
273
+ if (!payload?.length) {
274
+ return null
275
+ }
276
+
277
+ return (
278
+ <div
279
+ ref={ref}
280
+ className={cn(
281
+ "flex items-center justify-center gap-4",
282
+ verticalAlign === "top" ? "pb-3" : "pt-3",
283
+ className
284
+ )}
285
+ >
286
+ {payload.map((item) => {
287
+ const key = `${nameKey || item.dataKey || "value"}`
288
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
289
+
290
+ return (
291
+ <div
292
+ key={item.value}
293
+ className={cn(
294
+ "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
295
+ )}
296
+ >
297
+ {itemConfig?.icon && !hideIcon ? (
298
+ <itemConfig.icon />
299
+ ) : (
300
+ <div
301
+ className="h-2 w-2 shrink-0 rounded-[2px]"
302
+ style={{
303
+ backgroundColor: item.color,
304
+ }}
305
+ />
306
+ )}
307
+ {itemConfig?.label}
308
+ </div>
309
+ )
310
+ })}
311
+ </div>
312
+ )
313
+ }
314
+ )
315
+ ChartLegendContent.displayName = "ChartLegend"
316
+
317
+ // Helper to extract item config from a payload.
318
+ function getPayloadConfigFromPayload(
319
+ config: ChartConfig,
320
+ payload: unknown,
321
+ key: string
322
+ ) {
323
+ if (typeof payload !== "object" || payload === null) {
324
+ return undefined
325
+ }
326
+
327
+ const payloadPayload =
328
+ "payload" in payload &&
329
+ typeof payload.payload === "object" &&
330
+ payload.payload !== null
331
+ ? payload.payload
332
+ : undefined
333
+
334
+ let configLabelKey: string = key
335
+
336
+ if (
337
+ key in payload &&
338
+ typeof payload[key as keyof typeof payload] === "string"
339
+ ) {
340
+ configLabelKey = payload[key as keyof typeof payload] as string
341
+ } else if (
342
+ payloadPayload &&
343
+ key in payloadPayload &&
344
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
345
+ ) {
346
+ configLabelKey = payloadPayload[
347
+ key as keyof typeof payloadPayload
348
+ ] as string
349
+ }
350
+
351
+ return configLabelKey in config
352
+ ? config[configLabelKey]
353
+ : config[key as keyof typeof config]
354
+ }
355
+
356
+ export {
357
+ ChartContainer,
358
+ ChartTooltip,
359
+ ChartTooltipContent,
360
+ ChartLegend,
361
+ ChartLegendContent,
362
+ ChartStyle,
363
+ }
src/components/ui/checkbox.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3
+ import { Check } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Checkbox = React.forwardRef<
8
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CheckboxPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
15
+ className
16
+ )}
17
+ {...props}
18
+ >
19
+ <CheckboxPrimitive.Indicator
20
+ className={cn("flex items-center justify-center text-current")}
21
+ >
22
+ <Check className="h-4 w-4" />
23
+ </CheckboxPrimitive.Indicator>
24
+ </CheckboxPrimitive.Root>
25
+ ))
26
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName
27
+
28
+ export { Checkbox }
src/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
2
+
3
+ const Collapsible = CollapsiblePrimitive.Root
4
+
5
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
6
+
7
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
8
+
9
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }
src/components/ui/command.tsx ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { type DialogProps } from "@radix-ui/react-dialog"
3
+ import { Command as CommandPrimitive } from "cmdk"
4
+ import { Search } from "lucide-react"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { Dialog, DialogContent } from "@/components/ui/dialog"
8
+
9
+ const Command = React.forwardRef<
10
+ React.ElementRef<typeof CommandPrimitive>,
11
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive>
12
+ >(({ className, ...props }, ref) => (
13
+ <CommandPrimitive
14
+ ref={ref}
15
+ className={cn(
16
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ ))
22
+ Command.displayName = CommandPrimitive.displayName
23
+
24
+ interface CommandDialogProps extends DialogProps {}
25
+
26
+ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
27
+ return (
28
+ <Dialog {...props}>
29
+ <DialogContent className="overflow-hidden p-0 shadow-lg">
30
+ <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[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">
31
+ {children}
32
+ </Command>
33
+ </DialogContent>
34
+ </Dialog>
35
+ )
36
+ }
37
+
38
+ const CommandInput = React.forwardRef<
39
+ React.ElementRef<typeof CommandPrimitive.Input>,
40
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
41
+ >(({ className, ...props }, ref) => (
42
+ <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
43
+ <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
44
+ <CommandPrimitive.Input
45
+ ref={ref}
46
+ className={cn(
47
+ "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ </div>
53
+ ))
54
+
55
+ CommandInput.displayName = CommandPrimitive.Input.displayName
56
+
57
+ const CommandList = React.forwardRef<
58
+ React.ElementRef<typeof CommandPrimitive.List>,
59
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
60
+ >(({ className, ...props }, ref) => (
61
+ <CommandPrimitive.List
62
+ ref={ref}
63
+ className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
64
+ {...props}
65
+ />
66
+ ))
67
+
68
+ CommandList.displayName = CommandPrimitive.List.displayName
69
+
70
+ const CommandEmpty = React.forwardRef<
71
+ React.ElementRef<typeof CommandPrimitive.Empty>,
72
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
73
+ >((props, ref) => (
74
+ <CommandPrimitive.Empty
75
+ ref={ref}
76
+ className="py-6 text-center text-sm"
77
+ {...props}
78
+ />
79
+ ))
80
+
81
+ CommandEmpty.displayName = CommandPrimitive.Empty.displayName
82
+
83
+ const CommandGroup = React.forwardRef<
84
+ React.ElementRef<typeof CommandPrimitive.Group>,
85
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
86
+ >(({ className, ...props }, ref) => (
87
+ <CommandPrimitive.Group
88
+ ref={ref}
89
+ className={cn(
90
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
91
+ className
92
+ )}
93
+ {...props}
94
+ />
95
+ ))
96
+
97
+ CommandGroup.displayName = CommandPrimitive.Group.displayName
98
+
99
+ const CommandSeparator = React.forwardRef<
100
+ React.ElementRef<typeof CommandPrimitive.Separator>,
101
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
102
+ >(({ className, ...props }, ref) => (
103
+ <CommandPrimitive.Separator
104
+ ref={ref}
105
+ className={cn("-mx-1 h-px bg-border", className)}
106
+ {...props}
107
+ />
108
+ ))
109
+ CommandSeparator.displayName = CommandPrimitive.Separator.displayName
110
+
111
+ const CommandItem = React.forwardRef<
112
+ React.ElementRef<typeof CommandPrimitive.Item>,
113
+ React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
114
+ >(({ className, ...props }, ref) => (
115
+ <CommandPrimitive.Item
116
+ ref={ref}
117
+ className={cn(
118
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
119
+ className
120
+ )}
121
+ {...props}
122
+ />
123
+ ))
124
+
125
+ CommandItem.displayName = CommandPrimitive.Item.displayName
126
+
127
+ const CommandShortcut = ({
128
+ className,
129
+ ...props
130
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
131
+ return (
132
+ <span
133
+ className={cn(
134
+ "ml-auto text-xs tracking-widest text-muted-foreground",
135
+ className
136
+ )}
137
+ {...props}
138
+ />
139
+ )
140
+ }
141
+ CommandShortcut.displayName = "CommandShortcut"
142
+
143
+ export {
144
+ Command,
145
+ CommandDialog,
146
+ CommandInput,
147
+ CommandList,
148
+ CommandEmpty,
149
+ CommandGroup,
150
+ CommandItem,
151
+ CommandShortcut,
152
+ CommandSeparator,
153
+ }
src/components/ui/context-menu.tsx ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
3
+ import { Check, ChevronRight, Circle } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const ContextMenu = ContextMenuPrimitive.Root
8
+
9
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger
10
+
11
+ const ContextMenuGroup = ContextMenuPrimitive.Group
12
+
13
+ const ContextMenuPortal = ContextMenuPrimitive.Portal
14
+
15
+ const ContextMenuSub = ContextMenuPrimitive.Sub
16
+
17
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
18
+
19
+ const ContextMenuSubTrigger = React.forwardRef<
20
+ React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
21
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
22
+ inset?: boolean
23
+ }
24
+ >(({ className, inset, children, ...props }, ref) => (
25
+ <ContextMenuPrimitive.SubTrigger
26
+ ref={ref}
27
+ className={cn(
28
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
29
+ inset && "pl-8",
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronRight className="ml-auto h-4 w-4" />
36
+ </ContextMenuPrimitive.SubTrigger>
37
+ ))
38
+ ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
39
+
40
+ const ContextMenuSubContent = React.forwardRef<
41
+ React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
42
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
43
+ >(({ className, ...props }, ref) => (
44
+ <ContextMenuPrimitive.SubContent
45
+ ref={ref}
46
+ className={cn(
47
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ ))
53
+ ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
54
+
55
+ const ContextMenuContent = React.forwardRef<
56
+ React.ElementRef<typeof ContextMenuPrimitive.Content>,
57
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
58
+ >(({ className, ...props }, ref) => (
59
+ <ContextMenuPrimitive.Portal>
60
+ <ContextMenuPrimitive.Content
61
+ ref={ref}
62
+ className={cn(
63
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 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",
64
+ className
65
+ )}
66
+ {...props}
67
+ />
68
+ </ContextMenuPrimitive.Portal>
69
+ ))
70
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
71
+
72
+ const ContextMenuItem = React.forwardRef<
73
+ React.ElementRef<typeof ContextMenuPrimitive.Item>,
74
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
75
+ inset?: boolean
76
+ }
77
+ >(({ className, inset, ...props }, ref) => (
78
+ <ContextMenuPrimitive.Item
79
+ ref={ref}
80
+ className={cn(
81
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
82
+ inset && "pl-8",
83
+ className
84
+ )}
85
+ {...props}
86
+ />
87
+ ))
88
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
89
+
90
+ const ContextMenuCheckboxItem = React.forwardRef<
91
+ React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
92
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
93
+ >(({ className, children, checked, ...props }, ref) => (
94
+ <ContextMenuPrimitive.CheckboxItem
95
+ ref={ref}
96
+ className={cn(
97
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
98
+ className
99
+ )}
100
+ checked={checked}
101
+ {...props}
102
+ >
103
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
104
+ <ContextMenuPrimitive.ItemIndicator>
105
+ <Check className="h-4 w-4" />
106
+ </ContextMenuPrimitive.ItemIndicator>
107
+ </span>
108
+ {children}
109
+ </ContextMenuPrimitive.CheckboxItem>
110
+ ))
111
+ ContextMenuCheckboxItem.displayName =
112
+ ContextMenuPrimitive.CheckboxItem.displayName
113
+
114
+ const ContextMenuRadioItem = React.forwardRef<
115
+ React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
116
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
117
+ >(({ className, children, ...props }, ref) => (
118
+ <ContextMenuPrimitive.RadioItem
119
+ ref={ref}
120
+ className={cn(
121
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
122
+ className
123
+ )}
124
+ {...props}
125
+ >
126
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
127
+ <ContextMenuPrimitive.ItemIndicator>
128
+ <Circle className="h-2 w-2 fill-current" />
129
+ </ContextMenuPrimitive.ItemIndicator>
130
+ </span>
131
+ {children}
132
+ </ContextMenuPrimitive.RadioItem>
133
+ ))
134
+ ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
135
+
136
+ const ContextMenuLabel = React.forwardRef<
137
+ React.ElementRef<typeof ContextMenuPrimitive.Label>,
138
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
139
+ inset?: boolean
140
+ }
141
+ >(({ className, inset, ...props }, ref) => (
142
+ <ContextMenuPrimitive.Label
143
+ ref={ref}
144
+ className={cn(
145
+ "px-2 py-1.5 text-sm font-semibold text-foreground",
146
+ inset && "pl-8",
147
+ className
148
+ )}
149
+ {...props}
150
+ />
151
+ ))
152
+ ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
153
+
154
+ const ContextMenuSeparator = React.forwardRef<
155
+ React.ElementRef<typeof ContextMenuPrimitive.Separator>,
156
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
157
+ >(({ className, ...props }, ref) => (
158
+ <ContextMenuPrimitive.Separator
159
+ ref={ref}
160
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
161
+ {...props}
162
+ />
163
+ ))
164
+ ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
165
+
166
+ const ContextMenuShortcut = ({
167
+ className,
168
+ ...props
169
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
170
+ return (
171
+ <span
172
+ className={cn(
173
+ "ml-auto text-xs tracking-widest text-muted-foreground",
174
+ className
175
+ )}
176
+ {...props}
177
+ />
178
+ )
179
+ }
180
+ ContextMenuShortcut.displayName = "ContextMenuShortcut"
181
+
182
+ export {
183
+ ContextMenu,
184
+ ContextMenuTrigger,
185
+ ContextMenuContent,
186
+ ContextMenuItem,
187
+ ContextMenuCheckboxItem,
188
+ ContextMenuRadioItem,
189
+ ContextMenuLabel,
190
+ ContextMenuSeparator,
191
+ ContextMenuShortcut,
192
+ ContextMenuGroup,
193
+ ContextMenuPortal,
194
+ ContextMenuSub,
195
+ ContextMenuSubContent,
196
+ ContextMenuSubTrigger,
197
+ ContextMenuRadioGroup,
198
+ }
src/components/ui/dialog.tsx ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
3
+ import { X } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const Dialog = DialogPrimitive.Root
8
+
9
+ const DialogTrigger = DialogPrimitive.Trigger
10
+
11
+ const DialogPortal = DialogPrimitive.Portal
12
+
13
+ const DialogClose = DialogPrimitive.Close
14
+
15
+ const DialogOverlay = React.forwardRef<
16
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
17
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
18
+ >(({ className, ...props }, ref) => (
19
+ <DialogPrimitive.Overlay
20
+ ref={ref}
21
+ className={cn(
22
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ ))
28
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
29
+
30
+ const DialogContent = React.forwardRef<
31
+ React.ElementRef<typeof DialogPrimitive.Content>,
32
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
33
+ >(({ className, children, ...props }, ref) => (
34
+ <DialogPortal>
35
+ <DialogOverlay />
36
+ <DialogPrimitive.Content
37
+ ref={ref}
38
+ className={cn(
39
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
40
+ className
41
+ )}
42
+ {...props}
43
+ >
44
+ {children}
45
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
46
+ <X className="h-4 w-4" />
47
+ <span className="sr-only">Close</span>
48
+ </DialogPrimitive.Close>
49
+ </DialogPrimitive.Content>
50
+ </DialogPortal>
51
+ ))
52
+ DialogContent.displayName = DialogPrimitive.Content.displayName
53
+
54
+ const DialogHeader = ({
55
+ className,
56
+ ...props
57
+ }: React.HTMLAttributes<HTMLDivElement>) => (
58
+ <div
59
+ className={cn(
60
+ "flex flex-col space-y-1.5 text-center sm:text-left",
61
+ className
62
+ )}
63
+ {...props}
64
+ />
65
+ )
66
+ DialogHeader.displayName = "DialogHeader"
67
+
68
+ const DialogFooter = ({
69
+ className,
70
+ ...props
71
+ }: React.HTMLAttributes<HTMLDivElement>) => (
72
+ <div
73
+ className={cn(
74
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
75
+ className
76
+ )}
77
+ {...props}
78
+ />
79
+ )
80
+ DialogFooter.displayName = "DialogFooter"
81
+
82
+ const DialogTitle = React.forwardRef<
83
+ React.ElementRef<typeof DialogPrimitive.Title>,
84
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
85
+ >(({ className, ...props }, ref) => (
86
+ <DialogPrimitive.Title
87
+ ref={ref}
88
+ className={cn(
89
+ "text-lg font-semibold leading-none tracking-tight",
90
+ className
91
+ )}
92
+ {...props}
93
+ />
94
+ ))
95
+ DialogTitle.displayName = DialogPrimitive.Title.displayName
96
+
97
+ const DialogDescription = React.forwardRef<
98
+ React.ElementRef<typeof DialogPrimitive.Description>,
99
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
100
+ >(({ className, ...props }, ref) => (
101
+ <DialogPrimitive.Description
102
+ ref={ref}
103
+ className={cn("text-sm text-muted-foreground", className)}
104
+ {...props}
105
+ />
106
+ ))
107
+ DialogDescription.displayName = DialogPrimitive.Description.displayName
108
+
109
+ export {
110
+ Dialog,
111
+ DialogPortal,
112
+ DialogOverlay,
113
+ DialogClose,
114
+ DialogTrigger,
115
+ DialogContent,
116
+ DialogHeader,
117
+ DialogFooter,
118
+ DialogTitle,
119
+ DialogDescription,
120
+ }
src/components/ui/drawer.tsx ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Drawer as DrawerPrimitive } from "vaul"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const Drawer = ({
7
+ shouldScaleBackground = true,
8
+ ...props
9
+ }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
10
+ <DrawerPrimitive.Root
11
+ shouldScaleBackground={shouldScaleBackground}
12
+ {...props}
13
+ />
14
+ )
15
+ Drawer.displayName = "Drawer"
16
+
17
+ const DrawerTrigger = DrawerPrimitive.Trigger
18
+
19
+ const DrawerPortal = DrawerPrimitive.Portal
20
+
21
+ const DrawerClose = DrawerPrimitive.Close
22
+
23
+ const DrawerOverlay = React.forwardRef<
24
+ React.ElementRef<typeof DrawerPrimitive.Overlay>,
25
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
26
+ >(({ className, ...props }, ref) => (
27
+ <DrawerPrimitive.Overlay
28
+ ref={ref}
29
+ className={cn("fixed inset-0 z-50 bg-black/80", className)}
30
+ {...props}
31
+ />
32
+ ))
33
+ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
34
+
35
+ const DrawerContent = React.forwardRef<
36
+ React.ElementRef<typeof DrawerPrimitive.Content>,
37
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
38
+ >(({ className, children, ...props }, ref) => (
39
+ <DrawerPortal>
40
+ <DrawerOverlay />
41
+ <DrawerPrimitive.Content
42
+ ref={ref}
43
+ className={cn(
44
+ "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
45
+ className
46
+ )}
47
+ {...props}
48
+ >
49
+ <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
50
+ {children}
51
+ </DrawerPrimitive.Content>
52
+ </DrawerPortal>
53
+ ))
54
+ DrawerContent.displayName = "DrawerContent"
55
+
56
+ const DrawerHeader = ({
57
+ className,
58
+ ...props
59
+ }: React.HTMLAttributes<HTMLDivElement>) => (
60
+ <div
61
+ className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
62
+ {...props}
63
+ />
64
+ )
65
+ DrawerHeader.displayName = "DrawerHeader"
66
+
67
+ const DrawerFooter = ({
68
+ className,
69
+ ...props
70
+ }: React.HTMLAttributes<HTMLDivElement>) => (
71
+ <div
72
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
73
+ {...props}
74
+ />
75
+ )
76
+ DrawerFooter.displayName = "DrawerFooter"
77
+
78
+ const DrawerTitle = React.forwardRef<
79
+ React.ElementRef<typeof DrawerPrimitive.Title>,
80
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
81
+ >(({ className, ...props }, ref) => (
82
+ <DrawerPrimitive.Title
83
+ ref={ref}
84
+ className={cn(
85
+ "text-lg font-semibold leading-none tracking-tight",
86
+ className
87
+ )}
88
+ {...props}
89
+ />
90
+ ))
91
+ DrawerTitle.displayName = DrawerPrimitive.Title.displayName
92
+
93
+ const DrawerDescription = React.forwardRef<
94
+ React.ElementRef<typeof DrawerPrimitive.Description>,
95
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
96
+ >(({ className, ...props }, ref) => (
97
+ <DrawerPrimitive.Description
98
+ ref={ref}
99
+ className={cn("text-sm text-muted-foreground", className)}
100
+ {...props}
101
+ />
102
+ ))
103
+ DrawerDescription.displayName = DrawerPrimitive.Description.displayName
104
+
105
+ export {
106
+ Drawer,
107
+ DrawerPortal,
108
+ DrawerOverlay,
109
+ DrawerTrigger,
110
+ DrawerClose,
111
+ DrawerContent,
112
+ DrawerHeader,
113
+ DrawerFooter,
114
+ DrawerTitle,
115
+ DrawerDescription,
116
+ }
src/components/ui/dropdown-menu.tsx ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
3
+ import { Check, ChevronRight, Circle } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const DropdownMenu = DropdownMenuPrimitive.Root
8
+
9
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
10
+
11
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group
12
+
13
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal
14
+
15
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub
16
+
17
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
18
+
19
+ const DropdownMenuSubTrigger = React.forwardRef<
20
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
21
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
22
+ inset?: boolean
23
+ }
24
+ >(({ className, inset, children, ...props }, ref) => (
25
+ <DropdownMenuPrimitive.SubTrigger
26
+ ref={ref}
27
+ className={cn(
28
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
29
+ inset && "pl-8",
30
+ className
31
+ )}
32
+ {...props}
33
+ >
34
+ {children}
35
+ <ChevronRight className="ml-auto h-4 w-4" />
36
+ </DropdownMenuPrimitive.SubTrigger>
37
+ ))
38
+ DropdownMenuSubTrigger.displayName =
39
+ DropdownMenuPrimitive.SubTrigger.displayName
40
+
41
+ const DropdownMenuSubContent = React.forwardRef<
42
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
43
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
44
+ >(({ className, ...props }, ref) => (
45
+ <DropdownMenuPrimitive.SubContent
46
+ ref={ref}
47
+ className={cn(
48
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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",
49
+ className
50
+ )}
51
+ {...props}
52
+ />
53
+ ))
54
+ DropdownMenuSubContent.displayName =
55
+ DropdownMenuPrimitive.SubContent.displayName
56
+
57
+ const DropdownMenuContent = React.forwardRef<
58
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
59
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
60
+ >(({ className, sideOffset = 4, ...props }, ref) => (
61
+ <DropdownMenuPrimitive.Portal>
62
+ <DropdownMenuPrimitive.Content
63
+ ref={ref}
64
+ sideOffset={sideOffset}
65
+ className={cn(
66
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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",
67
+ className
68
+ )}
69
+ {...props}
70
+ />
71
+ </DropdownMenuPrimitive.Portal>
72
+ ))
73
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
74
+
75
+ const DropdownMenuItem = React.forwardRef<
76
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
77
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
78
+ inset?: boolean
79
+ }
80
+ >(({ className, inset, ...props }, ref) => (
81
+ <DropdownMenuPrimitive.Item
82
+ ref={ref}
83
+ className={cn(
84
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
85
+ inset && "pl-8",
86
+ className
87
+ )}
88
+ {...props}
89
+ />
90
+ ))
91
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
92
+
93
+ const DropdownMenuCheckboxItem = React.forwardRef<
94
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
95
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
96
+ >(({ className, children, checked, ...props }, ref) => (
97
+ <DropdownMenuPrimitive.CheckboxItem
98
+ ref={ref}
99
+ className={cn(
100
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
101
+ className
102
+ )}
103
+ checked={checked}
104
+ {...props}
105
+ >
106
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
107
+ <DropdownMenuPrimitive.ItemIndicator>
108
+ <Check className="h-4 w-4" />
109
+ </DropdownMenuPrimitive.ItemIndicator>
110
+ </span>
111
+ {children}
112
+ </DropdownMenuPrimitive.CheckboxItem>
113
+ ))
114
+ DropdownMenuCheckboxItem.displayName =
115
+ DropdownMenuPrimitive.CheckboxItem.displayName
116
+
117
+ const DropdownMenuRadioItem = React.forwardRef<
118
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
119
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
120
+ >(({ className, children, ...props }, ref) => (
121
+ <DropdownMenuPrimitive.RadioItem
122
+ ref={ref}
123
+ className={cn(
124
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
125
+ className
126
+ )}
127
+ {...props}
128
+ >
129
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
130
+ <DropdownMenuPrimitive.ItemIndicator>
131
+ <Circle className="h-2 w-2 fill-current" />
132
+ </DropdownMenuPrimitive.ItemIndicator>
133
+ </span>
134
+ {children}
135
+ </DropdownMenuPrimitive.RadioItem>
136
+ ))
137
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
138
+
139
+ const DropdownMenuLabel = React.forwardRef<
140
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
141
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
142
+ inset?: boolean
143
+ }
144
+ >(({ className, inset, ...props }, ref) => (
145
+ <DropdownMenuPrimitive.Label
146
+ ref={ref}
147
+ className={cn(
148
+ "px-2 py-1.5 text-sm font-semibold",
149
+ inset && "pl-8",
150
+ className
151
+ )}
152
+ {...props}
153
+ />
154
+ ))
155
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
156
+
157
+ const DropdownMenuSeparator = React.forwardRef<
158
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
159
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
160
+ >(({ className, ...props }, ref) => (
161
+ <DropdownMenuPrimitive.Separator
162
+ ref={ref}
163
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
164
+ {...props}
165
+ />
166
+ ))
167
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
168
+
169
+ const DropdownMenuShortcut = ({
170
+ className,
171
+ ...props
172
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
173
+ return (
174
+ <span
175
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
176
+ {...props}
177
+ />
178
+ )
179
+ }
180
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
181
+
182
+ export {
183
+ DropdownMenu,
184
+ DropdownMenuTrigger,
185
+ DropdownMenuContent,
186
+ DropdownMenuItem,
187
+ DropdownMenuCheckboxItem,
188
+ DropdownMenuRadioItem,
189
+ DropdownMenuLabel,
190
+ DropdownMenuSeparator,
191
+ DropdownMenuShortcut,
192
+ DropdownMenuGroup,
193
+ DropdownMenuPortal,
194
+ DropdownMenuSub,
195
+ DropdownMenuSubContent,
196
+ DropdownMenuSubTrigger,
197
+ DropdownMenuRadioGroup,
198
+ }