create-bluecopa-react-app 1.0.40 → 1.0.42
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/README.md +16 -14
- package/package.json +1 -1
- package/templates/latest/.claude/settings.local.json +56 -0
- package/templates/latest/.env.example +8 -0
- package/templates/latest/Agent.md +598 -775
- package/templates/latest/CLAUDE.md +759 -0
- package/templates/latest/README.md +17 -8
- package/templates/latest/app/app.css +292 -85
- package/templates/latest/app/app.tsx +48 -39
- package/templates/latest/app/components/bluecopa-logo.tsx +20 -0
- package/templates/latest/app/components/charts/bar-chart.tsx +132 -0
- package/templates/latest/app/components/charts/base-chart.tsx +149 -0
- package/templates/latest/app/components/charts/chart-provider.tsx +71 -0
- package/templates/latest/app/components/charts/chart-theme.ts +262 -0
- package/templates/latest/app/components/charts/chart-utils.ts +142 -0
- package/templates/latest/app/components/charts/donut-chart.tsx +110 -0
- package/templates/latest/app/components/charts/index.ts +5 -0
- package/templates/latest/app/components/layouts/app-layout.tsx +22 -0
- package/templates/latest/app/components/layouts/app-sidebar.tsx +88 -0
- package/templates/latest/app/components/layouts/nav-main.tsx +50 -0
- package/templates/latest/app/components/layouts/nav-user.tsx +38 -0
- package/templates/latest/app/components/layouts/site-header.tsx +93 -0
- package/templates/latest/app/components/loading-screen.tsx +41 -0
- package/templates/latest/app/components/ui/ag-grid-table.tsx +79 -0
- package/templates/latest/app/components/ui/button.tsx +23 -23
- package/templates/latest/app/components/ui/card.tsx +20 -20
- package/templates/latest/app/components/ui/dropdown-menu.tsx +54 -49
- package/templates/latest/app/components/ui/input.tsx +8 -8
- package/templates/latest/app/components/ui/label.tsx +8 -8
- package/templates/latest/app/components/ui/separator.tsx +7 -7
- package/templates/latest/app/components/ui/sheet.tsx +43 -32
- package/templates/latest/app/components/ui/sidebar.tsx +240 -235
- package/templates/latest/app/components/ui/skeleton.tsx +4 -4
- package/templates/latest/app/components/ui/sonner.tsx +6 -9
- package/templates/latest/app/components/ui/tabs.tsx +15 -15
- package/templates/latest/app/components/ui/tooltip.tsx +18 -12
- package/templates/latest/app/constants/index.ts +31 -0
- package/templates/latest/app/contexts/app-context.tsx +201 -0
- package/templates/latest/app/hooks/use-mobile.ts +13 -12
- package/templates/latest/app/main.tsx +1 -1
- package/templates/latest/app/pages/dashboard.tsx +246 -0
- package/templates/latest/app/pages/payments.tsx +182 -0
- package/templates/latest/app/pages/settings.tsx +128 -0
- package/templates/latest/app/routes/index.tsx +19 -0
- package/templates/latest/app/single-spa.tsx +68 -86
- package/templates/latest/app/types/index.ts +37 -0
- package/templates/latest/app/utils/ag-grid-datasource.ts +63 -0
- package/templates/latest/app/utils/ag-grid-license.ts +12 -0
- package/templates/latest/app/utils/ag-grid-theme.ts +9 -0
- package/templates/latest/app/utils/component-style.ts +7 -0
- package/templates/latest/app/utils/style-drivers.ts +24 -0
- package/templates/latest/app/utils/utils.ts +10 -0
- package/templates/latest/components.json +3 -3
- package/templates/latest/index.html +30 -2
- package/templates/latest/package-lock.json +30 -416
- package/templates/latest/package.json +8 -18
- package/templates/latest/preview/index.html +125 -285
- package/templates/latest/public/favicon.svg +1 -0
- package/templates/latest/vite.config.ts +2 -8
- package/templates/latest/app/components/app-sidebar.tsx +0 -182
- package/templates/latest/app/components/chart-area-interactive.tsx +0 -290
- package/templates/latest/app/components/data-table.tsx +0 -807
- package/templates/latest/app/components/nav-documents.tsx +0 -92
- package/templates/latest/app/components/nav-main.tsx +0 -40
- package/templates/latest/app/components/nav-secondary.tsx +0 -42
- package/templates/latest/app/components/nav-user.tsx +0 -111
- package/templates/latest/app/components/section-cards.tsx +0 -102
- package/templates/latest/app/components/site-header.tsx +0 -28
- package/templates/latest/app/components/ui/avatar.tsx +0 -53
- package/templates/latest/app/components/ui/badge.tsx +0 -46
- package/templates/latest/app/components/ui/breadcrumb.tsx +0 -109
- package/templates/latest/app/components/ui/chart.tsx +0 -352
- package/templates/latest/app/components/ui/checkbox.tsx +0 -30
- package/templates/latest/app/components/ui/drawer.tsx +0 -139
- package/templates/latest/app/components/ui/select.tsx +0 -183
- package/templates/latest/app/components/ui/table.tsx +0 -117
- package/templates/latest/app/components/ui/toggle-group.tsx +0 -73
- package/templates/latest/app/components/ui/toggle.tsx +0 -47
- package/templates/latest/app/data/data.json +0 -614
- package/templates/latest/app/data/mock-payments.json +0 -122
- package/templates/latest/app/data/mock-transactions.json +0 -128
- package/templates/latest/app/hooks/use-bluecopa-user.ts +0 -37
- package/templates/latest/app/lib/utils.ts +0 -6
- package/templates/latest/app/routes/apitest.tsx +0 -2118
- package/templates/latest/app/routes/comments.tsx +0 -588
- package/templates/latest/app/routes/dashboard.tsx +0 -36
- package/templates/latest/app/routes/payments.tsx +0 -342
- package/templates/latest/app/routes/statements.tsx +0 -493
- package/templates/latest/app/routes/websocket.tsx +0 -450
- package/templates/latest/app/routes.tsx +0 -22
- package/templates/latest/dist/assets/__federation_expose_App-OFfdinOR.js +0 -97
- package/templates/latest/dist/assets/__federation_fn_import-CzfA7kmP.js +0 -438
- package/templates/latest/dist/assets/__federation_shared_react-Bp6HVBS4.js +0 -16
- package/templates/latest/dist/assets/__federation_shared_react-dom-BCcRGiYp.js +0 -17
- package/templates/latest/dist/assets/client-CkHcT_xc.js +0 -76035
- package/templates/latest/dist/assets/index-B3cD3sP_.js +0 -60
- package/templates/latest/dist/assets/index-BzNimew1.js +0 -69
- package/templates/latest/dist/assets/index-DMFtQdNS.js +0 -412
- package/templates/latest/dist/assets/remoteEntry.css +0 -3996
- package/templates/latest/dist/assets/remoteEntry.js +0 -88
- package/templates/latest/dist/favicon.ico +0 -0
- package/templates/latest/dist/index.html +0 -19
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable Chart Option Builders
|
|
3
|
+
*
|
|
4
|
+
* Each function returns a partial ECharts option that can be spread
|
|
5
|
+
* into the final chart configuration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { EChartsOption } from "echarts";
|
|
9
|
+
import { resolveChartTokens } from "./chart-theme";
|
|
10
|
+
|
|
11
|
+
function tokens() {
|
|
12
|
+
return resolveChartTokens();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P] };
|
|
16
|
+
|
|
17
|
+
function merge<T extends Record<string, any>>(base: T, overrides?: DeepPartial<T>): T {
|
|
18
|
+
if (!overrides) return base;
|
|
19
|
+
const result = { ...base };
|
|
20
|
+
for (const key of Object.keys(overrides) as (keyof T)[]) {
|
|
21
|
+
const val = overrides[key];
|
|
22
|
+
if (val && typeof val === "object" && !Array.isArray(val) && typeof base[key] === "object") {
|
|
23
|
+
result[key] = merge(base[key] as any, val as any);
|
|
24
|
+
} else if (val !== undefined) {
|
|
25
|
+
result[key] = val as any;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Tooltip
|
|
32
|
+
export function chartTooltip(overrides?: Record<string, any>): Pick<EChartsOption, "tooltip"> {
|
|
33
|
+
const t = tokens();
|
|
34
|
+
const base = {
|
|
35
|
+
trigger: "axis" as const,
|
|
36
|
+
confine: true,
|
|
37
|
+
backgroundColor: t.backgroundColor,
|
|
38
|
+
borderColor: t.borderColor,
|
|
39
|
+
borderWidth: 1,
|
|
40
|
+
borderRadius: 8,
|
|
41
|
+
padding: [10, 14],
|
|
42
|
+
textStyle: { fontFamily: t.fontFamily, fontSize: 12, color: t.textColor },
|
|
43
|
+
extraCssText: "box-shadow: 0 4px 16px rgba(0,0,0,0.1); backdrop-filter: blur(8px);",
|
|
44
|
+
axisPointer: { type: "shadow" as const, shadowStyle: { color: "rgba(0,0,0,0.04)" } },
|
|
45
|
+
};
|
|
46
|
+
return { tooltip: merge(base, overrides as any) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Legend
|
|
50
|
+
export function chartLegend(overrides?: Record<string, any>): Pick<EChartsOption, "legend"> {
|
|
51
|
+
const t = tokens();
|
|
52
|
+
const pos = overrides?.position ?? "bottom";
|
|
53
|
+
const positionMap: Record<string, any> = {
|
|
54
|
+
top: { top: 0, left: "center" },
|
|
55
|
+
bottom: { bottom: 0, left: "center" },
|
|
56
|
+
left: { left: 0, top: "middle", orient: "vertical" },
|
|
57
|
+
right: { right: 0, top: "middle", orient: "vertical" },
|
|
58
|
+
};
|
|
59
|
+
const base = {
|
|
60
|
+
show: true, icon: "roundRect", itemWidth: 12, itemHeight: 12, itemGap: 16,
|
|
61
|
+
textStyle: { fontFamily: t.fontFamily, fontSize: 12, color: t.textColorSecondary },
|
|
62
|
+
...positionMap[pos],
|
|
63
|
+
};
|
|
64
|
+
const { position, ...rest } = overrides ?? {};
|
|
65
|
+
return { legend: merge(base, rest) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Category Axis
|
|
69
|
+
export function chartCategoryAxis(data?: string[], overrides?: Record<string, any>): any {
|
|
70
|
+
const t = tokens();
|
|
71
|
+
const base: any = {
|
|
72
|
+
type: "category", data,
|
|
73
|
+
axisLine: { show: true, lineStyle: { color: t.borderColor } },
|
|
74
|
+
axisTick: { show: false },
|
|
75
|
+
axisLabel: { fontFamily: t.fontFamily, fontSize: 11, color: t.textColorSecondary, rotate: overrides?.rotate ?? 0 },
|
|
76
|
+
splitLine: { show: false },
|
|
77
|
+
};
|
|
78
|
+
return merge(base, overrides);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Value Axis
|
|
82
|
+
export function chartValueAxis(overrides?: Record<string, any>): any {
|
|
83
|
+
const t = tokens();
|
|
84
|
+
const base: any = {
|
|
85
|
+
type: "value",
|
|
86
|
+
axisLine: { show: false }, axisTick: { show: false },
|
|
87
|
+
axisLabel: { fontFamily: t.fontFamily, fontSize: 11, color: t.textColorSecondary },
|
|
88
|
+
splitLine: { show: true, lineStyle: { color: t.borderColor, type: "dashed", opacity: 0.6 } },
|
|
89
|
+
};
|
|
90
|
+
return merge(base, overrides);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Title
|
|
94
|
+
export function chartTitle(text: string, overrides?: { subtitle?: string; left?: string | number }): Pick<EChartsOption, "title"> {
|
|
95
|
+
const t = tokens();
|
|
96
|
+
const base = {
|
|
97
|
+
text, subtext: overrides?.subtitle ?? "", left: overrides?.left ?? 0, top: 0, padding: [0, 0, 8, 0],
|
|
98
|
+
textStyle: { fontFamily: t.fontFamily, fontSize: 16, fontWeight: 600 as const, color: t.textColor },
|
|
99
|
+
subtextStyle: { fontFamily: t.fontFamily, fontSize: 12, color: t.textColorSecondary },
|
|
100
|
+
};
|
|
101
|
+
return { title: base };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Grid
|
|
105
|
+
export function chartGrid(overrides?: Record<string, any>): Pick<EChartsOption, "grid"> {
|
|
106
|
+
const base = { top: 48, right: 16, bottom: 48, left: 16, containLabel: true };
|
|
107
|
+
return { grid: merge(base, overrides as any) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Animation
|
|
111
|
+
export function chartAnimation(overrides?: Record<string, any>): Record<string, any> {
|
|
112
|
+
return {
|
|
113
|
+
animation: true,
|
|
114
|
+
animationDuration: overrides?.duration ?? 800,
|
|
115
|
+
animationEasing: overrides?.easing ?? "cubicInOut",
|
|
116
|
+
animationDelay: overrides?.delay ?? ((idx: number) => idx * 60),
|
|
117
|
+
animationDurationUpdate: 400,
|
|
118
|
+
animationEasingUpdate: "cubicInOut",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Colors
|
|
123
|
+
export function chartColors(): string[] {
|
|
124
|
+
return tokens().colors;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function chartColor(index: number): string {
|
|
128
|
+
const colors = chartColors();
|
|
129
|
+
return colors[index % colors.length];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Formatters
|
|
133
|
+
export function formatChartValue(value: number, decimals = 1): string {
|
|
134
|
+
if (Math.abs(value) >= 1e9) return (value / 1e9).toFixed(decimals) + "B";
|
|
135
|
+
if (Math.abs(value) >= 1e6) return (value / 1e6).toFixed(decimals) + "M";
|
|
136
|
+
if (Math.abs(value) >= 1e3) return (value / 1e3).toFixed(decimals) + "K";
|
|
137
|
+
return value.toFixed(decimals);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function formatChartCurrency(value: number, currency = "USD", decimals = 0): string {
|
|
141
|
+
return new Intl.NumberFormat("en-US", { style: "currency", currency, minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(value);
|
|
142
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { BaseChart } from "./base-chart";
|
|
3
|
+
import { chartTooltip, chartLegend, chartAnimation, chartColors } from "./chart-utils";
|
|
4
|
+
import { resolveChartTokens } from "./chart-theme";
|
|
5
|
+
|
|
6
|
+
export interface DonutChartData {
|
|
7
|
+
items: { name: string; value: number; color?: string }[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DonutChartProps {
|
|
11
|
+
data: DonutChartData;
|
|
12
|
+
title?: string;
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
height?: string | number;
|
|
15
|
+
className?: string;
|
|
16
|
+
showLegend?: boolean;
|
|
17
|
+
showLabels?: boolean;
|
|
18
|
+
centerText?: string;
|
|
19
|
+
centerSubText?: string;
|
|
20
|
+
thickness?: number;
|
|
21
|
+
loading?: boolean;
|
|
22
|
+
empty?: boolean;
|
|
23
|
+
onSliceClick?: (params: any) => void;
|
|
24
|
+
ariaLabel?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function DonutChart({
|
|
28
|
+
data, title, subtitle, height = 300, className, showLegend = true, showLabels = false,
|
|
29
|
+
centerText, centerSubText, thickness = 60, loading = false, empty = false, onSliceClick, ariaLabel,
|
|
30
|
+
}: DonutChartProps) {
|
|
31
|
+
const option = useMemo(() => {
|
|
32
|
+
const colors = chartColors();
|
|
33
|
+
const tokens = resolveChartTokens();
|
|
34
|
+
const innerRadius = `${Math.max(100 - thickness, 20)}%`;
|
|
35
|
+
const outerRadius = "70%";
|
|
36
|
+
|
|
37
|
+
const graphic: any[] = [];
|
|
38
|
+
if (centerText) {
|
|
39
|
+
graphic.push({
|
|
40
|
+
type: "text", left: "center", top: "42%",
|
|
41
|
+
style: { text: centerText, fontSize: 24, fontWeight: 700, fontFamily: tokens.fontFamily, fill: tokens.textColor, textAlign: "center" },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (centerSubText) {
|
|
45
|
+
graphic.push({
|
|
46
|
+
type: "text", left: "center", top: "54%",
|
|
47
|
+
style: { text: centerSubText, fontSize: 12, fontFamily: tokens.fontFamily, fill: tokens.textColorSecondary, textAlign: "center" },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...(title ? {
|
|
53
|
+
title: {
|
|
54
|
+
text: title, subtext: subtitle || "", left: 0, top: 0,
|
|
55
|
+
textStyle: { fontSize: 16, fontWeight: 600, fontFamily: tokens.fontFamily, color: tokens.textColor },
|
|
56
|
+
subtextStyle: { fontSize: 12, fontFamily: tokens.fontFamily, color: tokens.textColorSecondary },
|
|
57
|
+
},
|
|
58
|
+
} : {}),
|
|
59
|
+
...chartTooltip({ trigger: "item" }),
|
|
60
|
+
...(showLegend ? chartLegend({ position: "bottom" }) : {}),
|
|
61
|
+
...chartAnimation(),
|
|
62
|
+
graphic,
|
|
63
|
+
series: [{
|
|
64
|
+
type: "pie" as const,
|
|
65
|
+
radius: [innerRadius, outerRadius],
|
|
66
|
+
center: ["50%", "50%"],
|
|
67
|
+
avoidLabelOverlap: true,
|
|
68
|
+
padAngle: 2,
|
|
69
|
+
itemStyle: { borderColor: tokens.backgroundColor, borderWidth: 2, borderRadius: 4 },
|
|
70
|
+
label: { show: showLabels, formatter: "{b}: {d}%", fontSize: 12, fontFamily: tokens.fontFamily },
|
|
71
|
+
labelLine: { show: showLabels },
|
|
72
|
+
emphasis: {
|
|
73
|
+
focus: "self",
|
|
74
|
+
scale: false, scaleSize: 0,
|
|
75
|
+
itemStyle: { shadowBlur: 16, shadowOffsetY: 4, shadowColor: "rgba(0,0,0,0.25)", borderWidth: 3, borderColor: "rgba(255,255,255,0.6)" },
|
|
76
|
+
},
|
|
77
|
+
data: data.items.map((item, i) => ({
|
|
78
|
+
name: item.name, value: item.value,
|
|
79
|
+
itemStyle: item.color ? { color: item.color } : { color: colors[i % colors.length] },
|
|
80
|
+
})),
|
|
81
|
+
animationType: "expansion",
|
|
82
|
+
animationEasing: "cubicInOut",
|
|
83
|
+
animationDelay: (idx: number) => idx * 60,
|
|
84
|
+
}],
|
|
85
|
+
};
|
|
86
|
+
}, [data, title, subtitle, showLegend, showLabels, centerText, centerSubText, thickness]);
|
|
87
|
+
|
|
88
|
+
const dataSummary = useMemo(() => {
|
|
89
|
+
const total = data.items.reduce((sum, item) => sum + item.value, 0);
|
|
90
|
+
return data.items.map((item) => {
|
|
91
|
+
const pct = total > 0 ? ((item.value / total) * 100).toFixed(1) : "0";
|
|
92
|
+
return `${item.name}: ${item.value.toLocaleString()} (${pct}%)`;
|
|
93
|
+
}).join(", ");
|
|
94
|
+
}, [data]);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<BaseChart
|
|
98
|
+
option={option}
|
|
99
|
+
height={height}
|
|
100
|
+
className={className}
|
|
101
|
+
loading={loading}
|
|
102
|
+
empty={empty || data.items.length === 0}
|
|
103
|
+
onEvents={onSliceClick ? { click: onSliceClick } : undefined}
|
|
104
|
+
ariaLabel={ariaLabel || `Donut chart${title ? `: ${title}` : ""}`}
|
|
105
|
+
dataSummary={dataSummary}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default DonutChart;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ChartProvider, useChartContext } from "./chart-provider";
|
|
2
|
+
export { BaseChart, type BaseChartProps, type BaseChartRef } from "./base-chart";
|
|
3
|
+
export { BarChart, type BarChartProps, type BarChartData, type BarChartSeries } from "./bar-chart";
|
|
4
|
+
export { DonutChart, type DonutChartProps, type DonutChartData } from "./donut-chart";
|
|
5
|
+
export { chartColors, chartColor, formatChartValue, formatChartCurrency } from "./chart-utils";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Outlet } from "react-router-dom";
|
|
2
|
+
import { AppSidebar } from "~/components/layouts/app-sidebar";
|
|
3
|
+
import { SiteHeader } from "~/components/layouts/site-header";
|
|
4
|
+
import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar";
|
|
5
|
+
|
|
6
|
+
export function AppLayout() {
|
|
7
|
+
return (
|
|
8
|
+
<SidebarProvider
|
|
9
|
+
style={
|
|
10
|
+
{ "--sidebar-width": "16rem" } as React.CSSProperties
|
|
11
|
+
}
|
|
12
|
+
>
|
|
13
|
+
<AppSidebar />
|
|
14
|
+
<SidebarInset>
|
|
15
|
+
<SiteHeader />
|
|
16
|
+
<div className="copa:flex copa:flex-1 copa:flex-col copa:gap-6 copa:px-6 copa:py-6">
|
|
17
|
+
<Outlet />
|
|
18
|
+
</div>
|
|
19
|
+
</SidebarInset>
|
|
20
|
+
</SidebarProvider>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
IconChartBar,
|
|
4
|
+
IconCreditCard,
|
|
5
|
+
IconDashboard,
|
|
6
|
+
IconFileText,
|
|
7
|
+
IconFolder,
|
|
8
|
+
IconLayoutList,
|
|
9
|
+
IconReceipt,
|
|
10
|
+
IconSettings,
|
|
11
|
+
IconTransfer,
|
|
12
|
+
IconUsers,
|
|
13
|
+
} from "@tabler/icons-react";
|
|
14
|
+
|
|
15
|
+
import { BluecopaLogo } from "~/components/bluecopa-logo";
|
|
16
|
+
import { NavMain, type NavSection } from "~/components/layouts/nav-main";
|
|
17
|
+
import { NavFooter } from "~/components/layouts/nav-user";
|
|
18
|
+
import { ROUTES } from "~/constants";
|
|
19
|
+
import {
|
|
20
|
+
Sidebar,
|
|
21
|
+
SidebarContent,
|
|
22
|
+
SidebarFooter,
|
|
23
|
+
SidebarHeader,
|
|
24
|
+
SidebarMenu,
|
|
25
|
+
SidebarMenuButton,
|
|
26
|
+
SidebarMenuItem,
|
|
27
|
+
SidebarSeparator,
|
|
28
|
+
} from "~/components/ui/sidebar";
|
|
29
|
+
|
|
30
|
+
const navSections: NavSection[] = [
|
|
31
|
+
{
|
|
32
|
+
label: "Overview",
|
|
33
|
+
items: [
|
|
34
|
+
{ title: "Dashboard", url: ROUTES.DASHBOARD, icon: IconDashboard },
|
|
35
|
+
{ title: "Analytics", url: "/analytics", icon: IconChartBar },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: "Finance",
|
|
40
|
+
items: [
|
|
41
|
+
{ title: "Transactions", url: "/transactions", icon: IconTransfer },
|
|
42
|
+
{ title: "Invoices", url: "/invoices", icon: IconReceipt },
|
|
43
|
+
{ title: "Payments", url: ROUTES.PAYMENTS, icon: IconCreditCard },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
label: "Workspace",
|
|
48
|
+
items: [
|
|
49
|
+
{ title: "Reports", url: "/reports", icon: IconFileText },
|
|
50
|
+
{ title: "Projects", url: "/projects", icon: IconFolder },
|
|
51
|
+
{ title: "Workbooks", url: "/workbooks", icon: IconLayoutList },
|
|
52
|
+
{ title: "Team", url: "/team", icon: IconUsers },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
items: [{ title: "Settings", url: ROUTES.SETTINGS, icon: IconSettings }],
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
61
|
+
return (
|
|
62
|
+
<Sidebar collapsible="offcanvas" {...props}>
|
|
63
|
+
<SidebarHeader>
|
|
64
|
+
<SidebarMenu>
|
|
65
|
+
<SidebarMenuItem>
|
|
66
|
+
<SidebarMenuButton
|
|
67
|
+
asChild
|
|
68
|
+
className="copa:data-[slot=sidebar-menu-button]:!p-1.5"
|
|
69
|
+
>
|
|
70
|
+
<a href="https://bluecopa.com">
|
|
71
|
+
<BluecopaLogo className="copa:size-8" />
|
|
72
|
+
<span className="copa:text-base copa:font-semibold">Bluecopa</span>
|
|
73
|
+
</a>
|
|
74
|
+
</SidebarMenuButton>
|
|
75
|
+
</SidebarMenuItem>
|
|
76
|
+
</SidebarMenu>
|
|
77
|
+
</SidebarHeader>
|
|
78
|
+
<SidebarSeparator className="copa:mx-0" />
|
|
79
|
+
<SidebarContent>
|
|
80
|
+
<NavMain sections={navSections} />
|
|
81
|
+
</SidebarContent>
|
|
82
|
+
<SidebarSeparator className="copa:mx-0" />
|
|
83
|
+
<SidebarFooter>
|
|
84
|
+
<NavFooter />
|
|
85
|
+
</SidebarFooter>
|
|
86
|
+
</Sidebar>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Icon } from "@tabler/icons-react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
SidebarGroup,
|
|
6
|
+
SidebarGroupContent,
|
|
7
|
+
SidebarGroupLabel,
|
|
8
|
+
SidebarMenu,
|
|
9
|
+
SidebarMenuButton,
|
|
10
|
+
SidebarMenuItem,
|
|
11
|
+
} from "~/components/ui/sidebar";
|
|
12
|
+
|
|
13
|
+
export type NavItem = {
|
|
14
|
+
title: string;
|
|
15
|
+
url: string;
|
|
16
|
+
icon?: Icon;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type NavSection = {
|
|
20
|
+
label?: string;
|
|
21
|
+
items: NavItem[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function NavMain({ sections }: { sections: NavSection[] }) {
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
{sections.map((section, i) => (
|
|
28
|
+
<SidebarGroup key={section.label ?? i}>
|
|
29
|
+
{section.label && (
|
|
30
|
+
<SidebarGroupLabel>{section.label}</SidebarGroupLabel>
|
|
31
|
+
)}
|
|
32
|
+
<SidebarGroupContent>
|
|
33
|
+
<SidebarMenu>
|
|
34
|
+
{section.items.map((item) => (
|
|
35
|
+
<SidebarMenuItem key={item.title}>
|
|
36
|
+
<SidebarMenuButton tooltip={item.title} asChild>
|
|
37
|
+
<Link to={item.url}>
|
|
38
|
+
{item.icon && <item.icon />}
|
|
39
|
+
<span>{item.title}</span>
|
|
40
|
+
</Link>
|
|
41
|
+
</SidebarMenuButton>
|
|
42
|
+
</SidebarMenuItem>
|
|
43
|
+
))}
|
|
44
|
+
</SidebarMenu>
|
|
45
|
+
</SidebarGroupContent>
|
|
46
|
+
</SidebarGroup>
|
|
47
|
+
))}
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { IconHelpCircle, IconMessageCircle } from "@tabler/icons-react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
SidebarMenu,
|
|
5
|
+
SidebarMenuButton,
|
|
6
|
+
SidebarMenuItem,
|
|
7
|
+
} from "~/components/ui/sidebar";
|
|
8
|
+
|
|
9
|
+
export function NavFooter() {
|
|
10
|
+
return (
|
|
11
|
+
<SidebarMenu>
|
|
12
|
+
<SidebarMenuItem>
|
|
13
|
+
<SidebarMenuButton asChild>
|
|
14
|
+
<a
|
|
15
|
+
href="https://docs.bluecopa.com"
|
|
16
|
+
target="_blank"
|
|
17
|
+
rel="noopener noreferrer"
|
|
18
|
+
>
|
|
19
|
+
<IconHelpCircle className="copa:size-4" />
|
|
20
|
+
<span>Help & Docs</span>
|
|
21
|
+
</a>
|
|
22
|
+
</SidebarMenuButton>
|
|
23
|
+
</SidebarMenuItem>
|
|
24
|
+
<SidebarMenuItem>
|
|
25
|
+
<SidebarMenuButton asChild>
|
|
26
|
+
<a
|
|
27
|
+
href="https://bluecopa.com/support"
|
|
28
|
+
target="_blank"
|
|
29
|
+
rel="noopener noreferrer"
|
|
30
|
+
>
|
|
31
|
+
<IconMessageCircle className="copa:size-4" />
|
|
32
|
+
<span>Support</span>
|
|
33
|
+
</a>
|
|
34
|
+
</SidebarMenuButton>
|
|
35
|
+
</SidebarMenuItem>
|
|
36
|
+
</SidebarMenu>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IconChevronDown,
|
|
3
|
+
IconLogout,
|
|
4
|
+
IconSettings,
|
|
5
|
+
IconUserCircle,
|
|
6
|
+
} from "@tabler/icons-react";
|
|
7
|
+
|
|
8
|
+
import { BluecopaLogo } from "~/components/bluecopa-logo";
|
|
9
|
+
import {
|
|
10
|
+
DropdownMenu,
|
|
11
|
+
DropdownMenuContent,
|
|
12
|
+
DropdownMenuItem,
|
|
13
|
+
DropdownMenuLabel,
|
|
14
|
+
DropdownMenuSeparator,
|
|
15
|
+
DropdownMenuTrigger,
|
|
16
|
+
} from "~/components/ui/dropdown-menu";
|
|
17
|
+
import { Separator } from "~/components/ui/separator";
|
|
18
|
+
import { SidebarTrigger } from "~/components/ui/sidebar";
|
|
19
|
+
import { useAppContext } from "~/contexts/app-context";
|
|
20
|
+
|
|
21
|
+
function UserInitials({ name }: { name?: string }) {
|
|
22
|
+
const initials = (name ?? "")
|
|
23
|
+
.split(" ")
|
|
24
|
+
.map((w) => w[0])
|
|
25
|
+
.join("")
|
|
26
|
+
.slice(0, 2)
|
|
27
|
+
.toUpperCase();
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<span className="copa:flex copa:size-8 copa:shrink-0 copa:items-center copa:justify-center copa:rounded-full copa:bg-primary/10 copa:text-xs copa:font-semibold copa:text-primary">
|
|
31
|
+
{initials || "?"}
|
|
32
|
+
</span>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function SiteHeader() {
|
|
37
|
+
const { user } = useAppContext();
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<header className="copa:flex copa:h-14 copa:shrink-0 copa:items-center copa:gap-2 copa:px-6 copa:bg-sidebar copa:border-b copa:border-sidebar-border">
|
|
41
|
+
<SidebarTrigger className="copa:-ml-1" />
|
|
42
|
+
<Separator
|
|
43
|
+
orientation="vertical"
|
|
44
|
+
className="copa:mx-2 copa:data-[orientation=vertical]:h-4"
|
|
45
|
+
/>
|
|
46
|
+
<BluecopaLogo className="copa:size-7" />
|
|
47
|
+
<span className="copa:text-sm copa:font-semibold">Bluecopa</span>
|
|
48
|
+
|
|
49
|
+
<div className="copa:ml-auto">
|
|
50
|
+
<DropdownMenu>
|
|
51
|
+
<DropdownMenuTrigger asChild>
|
|
52
|
+
<button className="copa:flex copa:items-center copa:gap-2.5 copa:rounded-full copa:border copa:border-border copa:py-1 copa:pl-1 copa:pr-3 copa:text-sm copa:shadow-sm copa:transition-colors copa:hover:bg-accent copa:focus-visible:outline-none copa:focus-visible:ring-2 copa:focus-visible:ring-ring">
|
|
53
|
+
<UserInitials name={user?.name} />
|
|
54
|
+
<span className="copa:hidden copa:sm:inline copa:font-medium copa:text-foreground">
|
|
55
|
+
{user?.name}
|
|
56
|
+
</span>
|
|
57
|
+
<IconChevronDown className="copa:size-3.5 copa:text-muted-foreground" />
|
|
58
|
+
</button>
|
|
59
|
+
</DropdownMenuTrigger>
|
|
60
|
+
<DropdownMenuContent align="end" sideOffset={8} className="copa:w-60 copa:rounded-lg copa:p-1.5 copa:shadow-lg">
|
|
61
|
+
<DropdownMenuLabel className="copa:px-2 copa:py-2 copa:font-normal">
|
|
62
|
+
<div className="copa:flex copa:items-center copa:gap-2.5">
|
|
63
|
+
<UserInitials name={user?.name} />
|
|
64
|
+
<div className="copa:min-w-0 copa:flex copa:flex-col">
|
|
65
|
+
<span className="copa:truncate copa:text-sm copa:font-medium">
|
|
66
|
+
{user?.name}
|
|
67
|
+
</span>
|
|
68
|
+
<span className="copa:truncate copa:text-xs copa:text-muted-foreground">
|
|
69
|
+
{user?.email}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</DropdownMenuLabel>
|
|
74
|
+
<DropdownMenuSeparator className="copa:my-1.5" />
|
|
75
|
+
<DropdownMenuItem className="copa:gap-2 copa:px-2 copa:py-1.5 copa:text-sm">
|
|
76
|
+
<IconUserCircle className="copa:size-4 copa:text-muted-foreground" />
|
|
77
|
+
Profile
|
|
78
|
+
</DropdownMenuItem>
|
|
79
|
+
<DropdownMenuItem className="copa:gap-2 copa:px-2 copa:py-1.5 copa:text-sm">
|
|
80
|
+
<IconSettings className="copa:size-4 copa:text-muted-foreground" />
|
|
81
|
+
Settings
|
|
82
|
+
</DropdownMenuItem>
|
|
83
|
+
<DropdownMenuSeparator className="copa:my-1.5" />
|
|
84
|
+
<DropdownMenuItem className="copa:gap-2 copa:px-2 copa:py-1.5 copa:text-sm copa:text-destructive">
|
|
85
|
+
<IconLogout className="copa:size-4" />
|
|
86
|
+
Log out
|
|
87
|
+
</DropdownMenuItem>
|
|
88
|
+
</DropdownMenuContent>
|
|
89
|
+
</DropdownMenu>
|
|
90
|
+
</div>
|
|
91
|
+
</header>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-screen loading indicator shown while the app is initializing
|
|
3
|
+
* (config, CSS, user data). Uses inline styles so it renders correctly
|
|
4
|
+
* even before Tailwind CSS is fully loaded.
|
|
5
|
+
*/
|
|
6
|
+
export function LoadingScreen({ message = "Loading..." }: { message?: string }) {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
style={{
|
|
10
|
+
display: "flex",
|
|
11
|
+
alignItems: "center",
|
|
12
|
+
justifyContent: "center",
|
|
13
|
+
height: "100vh",
|
|
14
|
+
width: "100%",
|
|
15
|
+
background: "#f8f8fa",
|
|
16
|
+
fontFamily: "'Satoshi', system-ui, sans-serif",
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
<div
|
|
20
|
+
style={{
|
|
21
|
+
display: "flex",
|
|
22
|
+
flexDirection: "column",
|
|
23
|
+
alignItems: "center",
|
|
24
|
+
gap: "16px",
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<div
|
|
28
|
+
style={{
|
|
29
|
+
width: "40px",
|
|
30
|
+
height: "40px",
|
|
31
|
+
border: "3px solid #e5e7eb",
|
|
32
|
+
borderTopColor: "#3548ff",
|
|
33
|
+
borderRadius: "50%",
|
|
34
|
+
animation: "init-spin 0.7s linear infinite",
|
|
35
|
+
}}
|
|
36
|
+
/>
|
|
37
|
+
<span style={{ color: "#6b7280", fontSize: "0.835rem" }}>{message}</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { AgGridReact } from "ag-grid-react";
|
|
3
|
+
import type { ColDef, GridOptions } from "ag-grid-community";
|
|
4
|
+
import {
|
|
5
|
+
AllCommunityModule,
|
|
6
|
+
ClientSideRowModelModule,
|
|
7
|
+
ModuleRegistry,
|
|
8
|
+
NumberEditorModule,
|
|
9
|
+
NumberFilterModule,
|
|
10
|
+
TextFilterModule,
|
|
11
|
+
ValidationModule,
|
|
12
|
+
} from "ag-grid-community";
|
|
13
|
+
import { AgChartsEnterpriseModule } from "ag-charts-enterprise";
|
|
14
|
+
import {
|
|
15
|
+
MasterDetailModule,
|
|
16
|
+
ServerSideRowModelModule,
|
|
17
|
+
SetFilterModule,
|
|
18
|
+
SparklinesModule,
|
|
19
|
+
ContextMenuModule,
|
|
20
|
+
} from "ag-grid-enterprise";
|
|
21
|
+
import { gridTheme } from "~/utils/ag-grid-theme";
|
|
22
|
+
|
|
23
|
+
ModuleRegistry.registerModules([
|
|
24
|
+
AllCommunityModule,
|
|
25
|
+
ClientSideRowModelModule,
|
|
26
|
+
MasterDetailModule,
|
|
27
|
+
SetFilterModule,
|
|
28
|
+
ServerSideRowModelModule,
|
|
29
|
+
SparklinesModule.with(AgChartsEnterpriseModule),
|
|
30
|
+
ContextMenuModule,
|
|
31
|
+
ValidationModule,
|
|
32
|
+
NumberFilterModule,
|
|
33
|
+
NumberEditorModule,
|
|
34
|
+
TextFilterModule,
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
export interface AgGridTableProps<TData extends object = any> {
|
|
38
|
+
columnDefs: ColDef<TData>[];
|
|
39
|
+
rowData?: TData[];
|
|
40
|
+
gridOptions?: GridOptions<TData>;
|
|
41
|
+
height?: number | string;
|
|
42
|
+
className?: string;
|
|
43
|
+
header?: ReactNode;
|
|
44
|
+
quickFilterText?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function AgGridTable<TData extends object = any>({
|
|
48
|
+
columnDefs,
|
|
49
|
+
rowData,
|
|
50
|
+
gridOptions,
|
|
51
|
+
height = 500,
|
|
52
|
+
className = "",
|
|
53
|
+
header,
|
|
54
|
+
quickFilterText,
|
|
55
|
+
}: AgGridTableProps<TData>) {
|
|
56
|
+
const isInfiniteOrServerSide =
|
|
57
|
+
gridOptions?.rowModelType === "infinite" ||
|
|
58
|
+
gridOptions?.rowModelType === "serverSide";
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className={`copa:flex copa:flex-col copa:gap-2 ${className}`}>
|
|
62
|
+
{header ? <div>{header}</div> : null}
|
|
63
|
+
<div
|
|
64
|
+
style={{
|
|
65
|
+
height: typeof height === "number" ? `${height}px` : height,
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
<AgGridReact<TData>
|
|
69
|
+
theme={gridTheme}
|
|
70
|
+
columnDefs={columnDefs}
|
|
71
|
+
{...(!isInfiniteOrServerSide && rowData ? { rowData } : {})}
|
|
72
|
+
animateRows
|
|
73
|
+
quickFilterText={quickFilterText}
|
|
74
|
+
{...gridOptions}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|