create-bluecopa-react-app 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/latest/AGENT.md +13 -15
- package/templates/latest/README.md +2 -2
- package/templates/latest/clean.sh +1 -0
- package/templates/latest/setup.sh +5 -4
- package/templates/latest/src/App.tsx +11 -7
- package/templates/latest/src/components/charts/AreaChart.tsx +80 -0
- package/templates/latest/src/components/charts/DonutChart.tsx +73 -0
- package/templates/latest/src/components/charts/SparkAreaChart.tsx +52 -0
- package/templates/latest/src/components/layout/dashboard-header.tsx +1 -1
- package/templates/latest/src/components/layout/dashboard-layout.tsx +19 -11
- package/templates/latest/src/components/{page → layout}/navbar.tsx +2 -0
- package/templates/latest/src/components/layout/sidebar.tsx +2 -1
- package/templates/latest/src/components/page/dashboard/DashboardMetrics.tsx +97 -0
- package/templates/latest/src/components/page/dashboard/PaymentMethodsAnalysis.tsx +182 -0
- package/templates/latest/src/components/page/dashboard/RevenueAnalytics.tsx +505 -0
- package/templates/latest/src/components/page/dashboard/SalesAnalytics.tsx +313 -0
- package/templates/latest/src/components/page/dashboard/TransactionsTable.tsx +256 -0
- package/templates/latest/src/components/page/dashboard/dashboard-utils.ts +147 -0
- package/templates/latest/src/components/page/dashboard/dashboard.tsx +185 -0
- package/templates/latest/src/components/ui/bluecopa-logo.tsx +3 -0
- package/templates/latest/src/components/ui/label.tsx +0 -2
- package/templates/latest/src/components/ui/select.tsx +0 -2
- package/templates/latest/src/pages/Dashboard.tsx +1 -1
- package/templates/latest/src/single-spa.tsx +38 -28
- package/templates/latest/tailwind.config.js +1 -2
- package/templates/latest/tsconfig.app.json +6 -0
- package/templates/latest/tsconfig.json +6 -6
- package/templates/latest/tsconfig.node.json +5 -1
- package/templates/latest/vite.config.ts +1 -1
- package/templates/latest/src/components/page/dashboard.tsx +0 -1506
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ This template is designed to work seamlessly with AI coding assistants for rapid
|
|
|
13
13
|
- **Build Tool**: Vite (fast HMR and building)
|
|
14
14
|
- **Styling**: TailwindCSS with BlueCopa design system
|
|
15
15
|
- **UI Components**: Radix UI primitives (add via shadcn-ui for pre-styled components)
|
|
16
|
-
- **Data Fetching**: TanStack Query with @bluecopa/
|
|
16
|
+
- **Data Fetching**: TanStack Query with @bluecopa/react hooks
|
|
17
17
|
- **Charts**: Recharts for data visualization
|
|
18
18
|
- **Icons**: Lucide React
|
|
19
19
|
|
|
@@ -22,11 +22,9 @@ This template is designed to work seamlessly with AI coding assistants for rapid
|
|
|
22
22
|
src/
|
|
23
23
|
├── components/ui/ # Reusable UI components (e.g., Button, Card via shadcn-ui; bluecopa-logo)
|
|
24
24
|
├── components/ # Business logic components (Navbar, DashboardLayout, etc.)
|
|
25
|
-
├── hooks/
|
|
26
|
-
├──
|
|
27
|
-
|
|
28
|
-
├── lib/ # Utility functions and helpers (e.g., utils.ts with cn)
|
|
29
|
-
└── types/ # TypeScript type definitions (e.g., for API responses)
|
|
25
|
+
├── hooks/ # Custom React hooks (if needed; primarily use @bluecopa/react)
|
|
26
|
+
├── lib/ # Utility functions and helpers (e.g., utils.ts with cn)
|
|
27
|
+
└── types/ # TypeScript type definitions (e.g., for API responses)
|
|
30
28
|
```
|
|
31
29
|
|
|
32
30
|
## 🔧 Development Patterns
|
|
@@ -38,7 +36,7 @@ When asked to create a new page:
|
|
|
38
36
|
1. **Create the page component** in `src/pages/NewPage.tsx`:
|
|
39
37
|
```tsx
|
|
40
38
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
41
|
-
import { useGetWorkflowInstanceStatusById } from '@bluecopa/
|
|
39
|
+
import { useGetWorkflowInstanceStatusById } from '@bluecopa/react';
|
|
42
40
|
|
|
43
41
|
export default function NewPage() {
|
|
44
42
|
const { data: workflowStatus, isLoading } = useGetWorkflowInstanceStatusById('workflowId');
|
|
@@ -54,7 +52,7 @@ export default function NewPage() {
|
|
|
54
52
|
<CardContent>
|
|
55
53
|
{/* Page content */}
|
|
56
54
|
<p>Workflow status: {workflowStatus?.status}</p>
|
|
57
|
-
|
|
55
|
+
CardContent>
|
|
58
56
|
</Card>
|
|
59
57
|
</div>
|
|
60
58
|
);
|
|
@@ -69,7 +67,7 @@ import NewPage from '@/pages/NewPage';
|
|
|
69
67
|
<Route path="/new-page" element={<NewPage />} />
|
|
70
68
|
```
|
|
71
69
|
|
|
72
|
-
3. **Update navigation** in `src/components/
|
|
70
|
+
3. **Update navigation** in `src/components/layout/navbar.tsx` (import icon from lucide-react):
|
|
73
71
|
```tsx
|
|
74
72
|
import { Settings } from 'lucide-react';
|
|
75
73
|
|
|
@@ -81,9 +79,9 @@ const navigation = [
|
|
|
81
79
|
|
|
82
80
|
### 2. Using BlueCopa API Hooks
|
|
83
81
|
|
|
84
|
-
Install @bluecopa/
|
|
82
|
+
Install @bluecopa/react if not present: `npm install @bluecopa/react`
|
|
85
83
|
|
|
86
|
-
Use the pre-configured hooks from `@bluecopa/
|
|
84
|
+
Use the pre-configured hooks from `@bluecopa/react`:
|
|
87
85
|
```tsx
|
|
88
86
|
import { useGetFileUrlByFileId, useGetTableById, useGetWorkflowInstanceStatusById } from '@bluecopa/core';
|
|
89
87
|
|
|
@@ -201,7 +199,7 @@ function ChartComponent({ data }: { data: ChartData[] }) {
|
|
|
201
199
|
1. Create component in `src/components/` (e.g., NewCard.tsx)
|
|
202
200
|
2. Use Card component wrapper from ui/
|
|
203
201
|
3. Include loading and error states with TanStack Query
|
|
204
|
-
4. Use @bluecopa/
|
|
202
|
+
4. Use @bluecopa/react hooks for data (e.g., useGetTableById)
|
|
205
203
|
|
|
206
204
|
### Creating Forms
|
|
207
205
|
1. Use Radix UI form components (add via shadcn-ui: `npx shadcn-ui@latest add form`)
|
|
@@ -213,7 +211,7 @@ function ChartComponent({ data }: { data: ChartData[] }) {
|
|
|
213
211
|
1. For complex tables, install and use AG Grid (@bluecopa/aggrid)
|
|
214
212
|
2. For simple tables, use HTML table with Tailwind styling
|
|
215
213
|
3. Include sorting, filtering, and pagination as needed
|
|
216
|
-
4. Integrate with @bluecopa/
|
|
214
|
+
4. Integrate with @bluecopa/react for data
|
|
217
215
|
|
|
218
216
|
### Implementing Search
|
|
219
217
|
1. Add search state management with React state or form libraries
|
|
@@ -241,7 +239,7 @@ import { cn } from '@/lib/utils';
|
|
|
241
239
|
### TypeScript
|
|
242
240
|
Always include proper TypeScript types:
|
|
243
241
|
```tsx
|
|
244
|
-
import { FileUrlResponse, TableData, WorkflowStatus } from '
|
|
242
|
+
import { FileUrlResponse, TableData, WorkflowStatus } from '@bluecopa/core/types';
|
|
245
243
|
|
|
246
244
|
interface ComponentProps {
|
|
247
245
|
data: TableData[];
|
|
@@ -269,7 +267,7 @@ if (!data) return <div className="p-4 text-gray-500">No data available</div>;
|
|
|
269
267
|
- [Recharts Documentation](https://recharts.org)
|
|
270
268
|
- [TanStack Query Documentation](https://tanstack.com/query/latest)
|
|
271
269
|
- [Lucide React Icons](https://lucide.dev)
|
|
272
|
-
- [BlueCopa
|
|
270
|
+
- [BlueCopa React Package](https://www.npmjs.com/package/@bluecopa/react)
|
|
273
271
|
|
|
274
272
|
## 🔄 Development Workflow
|
|
275
273
|
|
|
@@ -103,7 +103,7 @@ npx create-bluecopa-react-app my-dashboard
|
|
|
103
103
|
src/
|
|
104
104
|
├── components/ # Reusable components
|
|
105
105
|
│ ├── layout/ # Layout components (dashboard-header.tsx, dashboard-layout.tsx, sidebar.tsx)
|
|
106
|
-
│ ├── page/ # Page-specific components (dashboard.tsx
|
|
106
|
+
│ ├── page/ # Page-specific components (dashboard.tsx)
|
|
107
107
|
│ ├── tables/ # Table components (data-grid.tsx)
|
|
108
108
|
│ └── ui/ # Base UI components (alert.tsx, avatar.tsx, badge.tsx, bluecopa-logo.tsx, button.tsx, card.tsx, dropdown-menu.tsx, input.tsx, label.tsx, select.tsx)
|
|
109
109
|
├── hooks/ # Custom React hooks
|
|
@@ -172,7 +172,7 @@ export default function App() {
|
|
|
172
172
|
```
|
|
173
173
|
|
|
174
174
|
### Adding Navigation Links
|
|
175
|
-
Update the navigation in `src/components/
|
|
175
|
+
Update the navigation in `src/components/layout/navbar.tsx`:
|
|
176
176
|
|
|
177
177
|
```tsx
|
|
178
178
|
const navigation = [
|
|
@@ -42,10 +42,11 @@ else
|
|
|
42
42
|
fi
|
|
43
43
|
|
|
44
44
|
echo "🎨 Setting up Tailwind CSS..."
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
if [ ! -f tailwind.config.js ] && [ ! -f tailwind.config.cjs ]; then
|
|
46
|
+
npx tailwindcss init -p
|
|
47
|
+
else
|
|
48
|
+
echo "✅ Tailwind already configured"
|
|
49
|
+
fi
|
|
49
50
|
|
|
50
51
|
echo "✅ Setup complete!"
|
|
51
52
|
echo ""
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { Routes, Route } from 'react-router-dom'
|
|
1
|
+
import { Routes, Route, Navigate } from 'react-router-dom'
|
|
2
|
+
import { Suspense, lazy } from 'react'
|
|
2
3
|
import QueryProvider from '@/providers/query-provider'
|
|
3
4
|
import Home from '@/pages/Home'
|
|
4
|
-
|
|
5
|
+
const Dashboard = lazy(() => import('@/pages/Dashboard'))
|
|
5
6
|
|
|
6
7
|
export default function App() {
|
|
7
8
|
return (
|
|
8
9
|
<QueryProvider>
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
<Suspense fallback={<div className="p-4 text-sm text-muted-foreground">Loading…</div>}>
|
|
11
|
+
<Routes>
|
|
12
|
+
<Route path="/" element={<Home />} />
|
|
13
|
+
<Route path="/dashboard" element={<Dashboard />} />
|
|
14
|
+
<Route path="*" element={<Navigate to="/" replace />} />
|
|
15
|
+
</Routes>
|
|
16
|
+
</Suspense>
|
|
13
17
|
</QueryProvider>
|
|
14
18
|
)
|
|
15
|
-
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ResponsiveContainer,
|
|
4
|
+
AreaChart as RechartsAreaChart,
|
|
5
|
+
Area,
|
|
6
|
+
XAxis,
|
|
7
|
+
YAxis,
|
|
8
|
+
CartesianGrid,
|
|
9
|
+
Tooltip,
|
|
10
|
+
Legend
|
|
11
|
+
} from 'recharts';
|
|
12
|
+
|
|
13
|
+
interface AreaChartProps {
|
|
14
|
+
data: any[];
|
|
15
|
+
index: string;
|
|
16
|
+
categories: string[];
|
|
17
|
+
colors?: string[];
|
|
18
|
+
className?: string;
|
|
19
|
+
valueFormatter?: (value: any) => any;
|
|
20
|
+
showLegend?: boolean;
|
|
21
|
+
showGridLines?: boolean;
|
|
22
|
+
showXAxis?: boolean;
|
|
23
|
+
showYAxis?: boolean;
|
|
24
|
+
yAxisWidth?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const AreaChart: React.FC<AreaChartProps> = ({
|
|
28
|
+
data,
|
|
29
|
+
index,
|
|
30
|
+
categories,
|
|
31
|
+
colors = ['#3B82F6'],
|
|
32
|
+
className = "h-80",
|
|
33
|
+
valueFormatter,
|
|
34
|
+
showLegend = true,
|
|
35
|
+
showGridLines = true,
|
|
36
|
+
showXAxis = true,
|
|
37
|
+
showYAxis = true,
|
|
38
|
+
yAxisWidth
|
|
39
|
+
}) => {
|
|
40
|
+
const colorMap: { [key: string]: string } = {
|
|
41
|
+
blue: '#3B82F6',
|
|
42
|
+
emerald: '#10B981',
|
|
43
|
+
violet: '#8B5CF6',
|
|
44
|
+
rose: '#F43F5E',
|
|
45
|
+
amber: '#F59E0B',
|
|
46
|
+
cyan: '#06B6D4',
|
|
47
|
+
slate: '#64748B',
|
|
48
|
+
indigo: '#6366F1'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getColor = (color: string, index: number) => {
|
|
52
|
+
return colorMap[color] || colors[index] || '#3B82F6';
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className={className}>
|
|
57
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
58
|
+
<RechartsAreaChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
59
|
+
{showGridLines && <CartesianGrid strokeDasharray="3 3" />}
|
|
60
|
+
{showXAxis && <XAxis dataKey={index} />}
|
|
61
|
+
{showYAxis && <YAxis width={yAxisWidth || 60} />}
|
|
62
|
+
<Tooltip
|
|
63
|
+
formatter={(value: any) => valueFormatter ? valueFormatter(value) : value}
|
|
64
|
+
/>
|
|
65
|
+
{showLegend && <Legend />}
|
|
66
|
+
{categories?.map((category: string, idx: number) => (
|
|
67
|
+
<Area
|
|
68
|
+
key={category}
|
|
69
|
+
type="monotone"
|
|
70
|
+
dataKey={category}
|
|
71
|
+
stroke={getColor(colors[idx], idx)}
|
|
72
|
+
fill={getColor(colors[idx], idx)}
|
|
73
|
+
fillOpacity={0.3}
|
|
74
|
+
/>
|
|
75
|
+
))}
|
|
76
|
+
</RechartsAreaChart>
|
|
77
|
+
</ResponsiveContainer>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ResponsiveContainer,
|
|
4
|
+
PieChart,
|
|
5
|
+
Pie,
|
|
6
|
+
Cell,
|
|
7
|
+
Tooltip,
|
|
8
|
+
Legend
|
|
9
|
+
} from 'recharts';
|
|
10
|
+
|
|
11
|
+
interface DonutChartProps {
|
|
12
|
+
data: any[];
|
|
13
|
+
category: string;
|
|
14
|
+
index: string;
|
|
15
|
+
colors?: string[];
|
|
16
|
+
className?: string;
|
|
17
|
+
valueFormatter?: (value: any) => any;
|
|
18
|
+
variant?: "donut" | "pie";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DonutChart: React.FC<DonutChartProps> = ({
|
|
22
|
+
data,
|
|
23
|
+
category,
|
|
24
|
+
index,
|
|
25
|
+
colors = ['#3B82F6'],
|
|
26
|
+
className = "h-64",
|
|
27
|
+
valueFormatter,
|
|
28
|
+
variant = "donut"
|
|
29
|
+
}) => {
|
|
30
|
+
const colorMap: { [key: string]: string } = {
|
|
31
|
+
blue: '#3B82F6',
|
|
32
|
+
emerald: '#10B981',
|
|
33
|
+
violet: '#8B5CF6',
|
|
34
|
+
rose: '#F43F5E',
|
|
35
|
+
amber: '#F59E0B',
|
|
36
|
+
cyan: '#06B6D4',
|
|
37
|
+
slate: '#64748B',
|
|
38
|
+
indigo: '#6366F1'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getColor = (color: string, index: number) => {
|
|
42
|
+
return colorMap[color] || colors[index] || '#3B82F6';
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const COLORS = colors.map((color: string, idx: number) => getColor(color, idx));
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className={className}>
|
|
49
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
50
|
+
<PieChart>
|
|
51
|
+
<Pie
|
|
52
|
+
data={data}
|
|
53
|
+
cx="50%"
|
|
54
|
+
cy="50%"
|
|
55
|
+
innerRadius={variant === "donut" ? 60 : 0}
|
|
56
|
+
outerRadius={80}
|
|
57
|
+
paddingAngle={5}
|
|
58
|
+
dataKey={category}
|
|
59
|
+
nameKey={index}
|
|
60
|
+
>
|
|
61
|
+
{data.map((_entry: any, index: number) => (
|
|
62
|
+
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
63
|
+
))}
|
|
64
|
+
</Pie>
|
|
65
|
+
<Tooltip
|
|
66
|
+
formatter={(value: any) => valueFormatter ? valueFormatter(value) : value}
|
|
67
|
+
/>
|
|
68
|
+
<Legend />
|
|
69
|
+
</PieChart>
|
|
70
|
+
</ResponsiveContainer>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ResponsiveContainer,
|
|
4
|
+
AreaChart as RechartsAreaChart,
|
|
5
|
+
Area
|
|
6
|
+
} from 'recharts';
|
|
7
|
+
|
|
8
|
+
interface SparkAreaChartProps {
|
|
9
|
+
data: any[];
|
|
10
|
+
categories: string[];
|
|
11
|
+
colors?: string[];
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const SparkAreaChart: React.FC<SparkAreaChartProps> = ({
|
|
16
|
+
data,
|
|
17
|
+
categories,
|
|
18
|
+
colors = ['#3B82F6'],
|
|
19
|
+
className = "h-10"
|
|
20
|
+
}) => {
|
|
21
|
+
const colorMap: { [key: string]: string } = {
|
|
22
|
+
blue: '#3B82F6',
|
|
23
|
+
emerald: '#10B981',
|
|
24
|
+
violet: '#8B5CF6',
|
|
25
|
+
rose: '#F43F5E',
|
|
26
|
+
amber: '#F59E0B',
|
|
27
|
+
cyan: '#06B6D4'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const getColor = (color: string, index: number) => {
|
|
31
|
+
return colorMap[color] || colors[index] || '#3B82F6';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className={className}>
|
|
36
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
37
|
+
<RechartsAreaChart data={data} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
|
38
|
+
{categories?.map((category: string, idx: number) => (
|
|
39
|
+
<Area
|
|
40
|
+
key={category}
|
|
41
|
+
type="monotone"
|
|
42
|
+
dataKey={category}
|
|
43
|
+
stroke={getColor(colors[idx], idx)}
|
|
44
|
+
fill={getColor(colors[idx], idx)}
|
|
45
|
+
fillOpacity={0.6}
|
|
46
|
+
/>
|
|
47
|
+
))}
|
|
48
|
+
</RechartsAreaChart>
|
|
49
|
+
</ResponsiveContainer>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -87,7 +87,7 @@ export default function DashboardHeader({ title, subtitle }: DashboardHeaderProp
|
|
|
87
87
|
</div>
|
|
88
88
|
|
|
89
89
|
{/* Notifications */}
|
|
90
|
-
<Button variant="ghost" size="sm" className="relative">
|
|
90
|
+
<Button variant="ghost" size="sm" className="relative" aria-label="Open notifications, 3 unread">
|
|
91
91
|
<Bell className="h-5 w-5" />
|
|
92
92
|
<span className="absolute -top-1 -right-1 h-4 w-4 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
|
|
93
93
|
3
|
|
@@ -1,29 +1,37 @@
|
|
|
1
|
-
import { ReactNode } from
|
|
2
|
-
import Sidebar from
|
|
3
|
-
import DashboardHeader from
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import Sidebar from "./sidebar";
|
|
3
|
+
import DashboardHeader from "./dashboard-header";
|
|
4
4
|
|
|
5
5
|
interface DashboardLayoutProps {
|
|
6
|
-
children: ReactNode
|
|
7
|
-
title?: string
|
|
8
|
-
subtitle?: string
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
title?: string;
|
|
8
|
+
subtitle?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export default function DashboardLayout({
|
|
11
|
+
export default function DashboardLayout({
|
|
12
|
+
children,
|
|
13
|
+
title,
|
|
14
|
+
subtitle,
|
|
15
|
+
}: DashboardLayoutProps) {
|
|
12
16
|
return (
|
|
13
17
|
<div className="flex h-screen bg-gray-50">
|
|
14
18
|
{/* Sidebar */}
|
|
15
19
|
<Sidebar />
|
|
16
|
-
|
|
20
|
+
|
|
17
21
|
{/* Main content area */}
|
|
18
22
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
19
23
|
{/* Header */}
|
|
20
24
|
<DashboardHeader title={title} subtitle={subtitle} />
|
|
21
|
-
|
|
25
|
+
|
|
22
26
|
{/* Main content */}
|
|
23
|
-
<main
|
|
27
|
+
<main
|
|
28
|
+
id="main-content"
|
|
29
|
+
role="main"
|
|
30
|
+
className="flex-1 overflow-y-auto p-6"
|
|
31
|
+
>
|
|
24
32
|
{children}
|
|
25
33
|
</main>
|
|
26
34
|
</div>
|
|
27
35
|
</div>
|
|
28
|
-
)
|
|
36
|
+
);
|
|
29
37
|
}
|
|
@@ -60,6 +60,8 @@ export default function Navbar() {
|
|
|
60
60
|
type="button"
|
|
61
61
|
className="inline-flex items-center justify-center p-2 rounded-md text-blue-200 hover:text-white hover:bg-blue-700"
|
|
62
62
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
63
|
+
aria-label={mobileMenuOpen ? "Close menu" : "Open menu"}
|
|
64
|
+
aria-expanded={mobileMenuOpen}
|
|
63
65
|
>
|
|
64
66
|
{mobileMenuOpen ? (
|
|
65
67
|
<X className="h-6 w-6" />
|
|
@@ -22,7 +22,7 @@ export default function Sidebar() {
|
|
|
22
22
|
</div>
|
|
23
23
|
|
|
24
24
|
{/* Navigation */}
|
|
25
|
-
<nav className="flex-1 space-y-1 px-3 py-4">
|
|
25
|
+
<nav className="flex-1 space-y-1 px-3 py-4" aria-label="Main navigation">
|
|
26
26
|
{navigation.map((item) => {
|
|
27
27
|
const isActive = location.pathname === item.href;
|
|
28
28
|
return (
|
|
@@ -35,6 +35,7 @@ export default function Sidebar() {
|
|
|
35
35
|
? "bg-blue-50 text-blue-700 border-r-2 border-blue-600"
|
|
36
36
|
: "text-gray-700 hover:bg-gray-50 hover:text-gray-900"
|
|
37
37
|
)}
|
|
38
|
+
aria-current={isActive ? "page" : undefined}
|
|
38
39
|
>
|
|
39
40
|
<item.icon
|
|
40
41
|
className={cn(
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Card, CardContent, CardTitle } from '@/components/ui/card';
|
|
3
|
+
import { ArrowUpRight, ArrowDownRight } from 'lucide-react';
|
|
4
|
+
import { SparkAreaChart } from '@/components/charts/SparkAreaChart';
|
|
5
|
+
|
|
6
|
+
interface Metric {
|
|
7
|
+
title: string;
|
|
8
|
+
value: string;
|
|
9
|
+
change: string;
|
|
10
|
+
trend: 'up' | 'down';
|
|
11
|
+
icon: string;
|
|
12
|
+
color: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DashboardMetricsProps {
|
|
16
|
+
ecommerceMetrics: Metric[];
|
|
17
|
+
monthlyRevenueData: any[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const iconMap: { [key: string]: React.ElementType } = {
|
|
21
|
+
DollarSign: ({ className }: { className?: string }) => (
|
|
22
|
+
<svg xmlns="http://www.w3.org/2000/svg" className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
23
|
+
<line x1="12" y1="1" x2="12" y2="23"></line>
|
|
24
|
+
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
|
|
25
|
+
</svg>
|
|
26
|
+
),
|
|
27
|
+
ShoppingCart: ({ className }: { className?: string }) => (
|
|
28
|
+
<svg xmlns="http://www.w3.org/2000/svg" className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
29
|
+
<circle cx="9" cy="21" r="1"></circle>
|
|
30
|
+
<circle cx="20" cy="21" r="1"></circle>
|
|
31
|
+
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
|
32
|
+
</svg>
|
|
33
|
+
),
|
|
34
|
+
TrendingUp: ({ className }: { className?: string }) => (
|
|
35
|
+
<svg xmlns="http://www.w3.org/2000/svg" className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
36
|
+
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline>
|
|
37
|
+
<polyline points="17 6 23 6 23 12"></polyline>
|
|
38
|
+
</svg>
|
|
39
|
+
),
|
|
40
|
+
Users: ({ className }: { className?: string }) => (
|
|
41
|
+
<svg xmlns="http://www.w3.org/2000/svg" className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
42
|
+
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
|
43
|
+
<circle cx="9" cy="7" r="4"></circle>
|
|
44
|
+
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
|
45
|
+
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
|
46
|
+
</svg>
|
|
47
|
+
)
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({
|
|
51
|
+
ecommerceMetrics,
|
|
52
|
+
monthlyRevenueData
|
|
53
|
+
}) => {
|
|
54
|
+
return (
|
|
55
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
56
|
+
{ecommerceMetrics.map((metric, index) => {
|
|
57
|
+
const IconComponent = iconMap[metric.icon];
|
|
58
|
+
return (
|
|
59
|
+
<Card key={index}>
|
|
60
|
+
<CardContent className="pt-6">
|
|
61
|
+
<div className="flex items-center justify-between space-y-0 pb-2">
|
|
62
|
+
<CardTitle className="text-sm font-medium text-gray-600">
|
|
63
|
+
{metric.title}
|
|
64
|
+
</CardTitle>
|
|
65
|
+
{IconComponent && <IconComponent className={`h-4 w-4 ${metric.color}`} />}
|
|
66
|
+
</div>
|
|
67
|
+
<div className="flex items-center space-x-2">
|
|
68
|
+
<div className="text-2xl font-bold">{metric.value}</div>
|
|
69
|
+
<div className={`flex items-center text-sm ${
|
|
70
|
+
metric.trend === 'up' ? 'text-green-600' : 'text-red-600'
|
|
71
|
+
}`}>
|
|
72
|
+
{metric.trend === 'up' ? (
|
|
73
|
+
<ArrowUpRight className="h-4 w-4" />
|
|
74
|
+
) : (
|
|
75
|
+
<ArrowDownRight className="h-4 w-4" />
|
|
76
|
+
)}
|
|
77
|
+
{metric.change}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
{/* Mini trend chart */}
|
|
81
|
+
{monthlyRevenueData.length > 0 && (
|
|
82
|
+
<div className="mt-4">
|
|
83
|
+
<SparkAreaChart
|
|
84
|
+
data={monthlyRevenueData.slice(-6)} // Last 6 months
|
|
85
|
+
categories={[index === 0 ? "revenue" : "orders"]}
|
|
86
|
+
colors={[index === 0 ? "emerald" : index === 1 ? "blue" : index === 2 ? "violet" : "amber"]}
|
|
87
|
+
className="h-10 w-full"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</CardContent>
|
|
92
|
+
</Card>
|
|
93
|
+
);
|
|
94
|
+
})}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
};
|