create-quadrokit 0.2.10 → 0.2.12
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/README.md +4 -4
- package/templates/admin-shell/src/components/AppShell.tsx +4 -4
- package/templates/admin-shell/src/locales/en.json +8 -1
- package/templates/admin-shell/src/pages/SampleDataPage.tsx +38 -0
- package/templates/admin-shell/src/router.tsx +2 -2
- package/templates/dashboard/package.json +1 -0
- package/templates/dashboard/src/components/AppShell.tsx +2 -2
- package/templates/dashboard/src/components/dashboard/ChartCard.tsx +29 -0
- package/templates/dashboard/src/components/dashboard/DashboardOverview.tsx +79 -0
- package/templates/dashboard/src/components/dashboard/KpiCard.tsx +31 -0
- package/templates/dashboard/src/components/dashboard/RegionsBarChart.tsx +45 -0
- package/templates/dashboard/src/components/dashboard/SeriesBarChart.tsx +55 -0
- package/templates/dashboard/src/components/dashboard/TrendLineChart.tsx +55 -0
- package/templates/dashboard/src/components/dashboard/chartTheme.ts +18 -0
- package/templates/dashboard/src/components/dashboard/dashboardMockData.ts +51 -0
- package/templates/dashboard/src/components/dashboard/index.ts +1 -0
- package/templates/dashboard/src/locales/en.json +26 -1
- package/templates/dashboard/src/pages/HomePage.tsx +16 -0
- package/templates/dashboard/src/pages/SampleDataPage.tsx +38 -0
- package/templates/dashboard/src/router.tsx +2 -2
- package/templates/ecommerce/src/components/AppShell.tsx +2 -2
- package/templates/ecommerce/src/locales/en.json +8 -1
- package/templates/ecommerce/src/pages/SampleDataPage.tsx +38 -0
- package/templates/ecommerce/src/router.tsx +2 -2
- package/templates/website/src/components/AppShell.tsx +2 -2
- package/templates/website/src/locales/en.json +8 -1
- package/templates/website/src/pages/HomePage.tsx +1 -1
- package/templates/website/src/pages/SampleDataPage.tsx +38 -0
- package/templates/website/src/router.tsx +2 -2
- package/templates/admin-shell/src/pages/AgenciesPage.tsx +0 -77
- package/templates/dashboard/src/pages/AgenciesPage.tsx +0 -77
- package/templates/ecommerce/src/pages/AgenciesPage.tsx +0 -77
- package/templates/website/src/pages/AgenciesPage.tsx +0 -77
package/package.json
CHANGED
package/templates/README.md
CHANGED
|
@@ -4,10 +4,10 @@ Vite + React 19 + React Router 7 + **Tailwind 3.4** + **i18next** + **react-hook
|
|
|
4
4
|
|
|
5
5
|
| Template | Use case |
|
|
6
6
|
|----------|----------|
|
|
7
|
-
| `dashboard` | Default shell,
|
|
8
|
-
| `website` | Marketing-style hero + same stack |
|
|
9
|
-
| `ecommerce` | Product-card style placeholder |
|
|
10
|
-
| `admin-shell` | Sidebar + main
|
|
7
|
+
| `dashboard` | Default shell, **Recharts** overview (static KPIs + graphs), sample-data route, form demo |
|
|
8
|
+
| `website` | Marketing-style hero + same stack (static `/sample-data`) |
|
|
9
|
+
| `ecommerce` | Product-card style placeholder + static `/sample-data` |
|
|
10
|
+
| `admin-shell` | Sidebar + main layout + static `/sample-data` |
|
|
11
11
|
|
|
12
12
|
## Monorepo
|
|
13
13
|
|
|
@@ -23,12 +23,12 @@ export function AppShell() {
|
|
|
23
23
|
{t('app.nav_home')}
|
|
24
24
|
</NavLink>
|
|
25
25
|
<NavLink
|
|
26
|
-
to="/
|
|
26
|
+
to="/sample-data"
|
|
27
27
|
className={({ isActive }) =>
|
|
28
28
|
isActive ? 'font-medium text-foreground' : 'hover:text-foreground'
|
|
29
29
|
}
|
|
30
30
|
>
|
|
31
|
-
{t('app.
|
|
31
|
+
{t('app.nav_sample_data')}
|
|
32
32
|
</NavLink>
|
|
33
33
|
</nav>
|
|
34
34
|
</aside>
|
|
@@ -47,10 +47,10 @@ export function AppShell() {
|
|
|
47
47
|
{t('app.nav_home')}
|
|
48
48
|
</NavLink>
|
|
49
49
|
<NavLink
|
|
50
|
-
to="/
|
|
50
|
+
to="/sample-data"
|
|
51
51
|
className={({ isActive }) => (isActive ? 'text-foreground' : '')}
|
|
52
52
|
>
|
|
53
|
-
{t('app.
|
|
53
|
+
{t('app.nav_sample_data')}
|
|
54
54
|
</NavLink>
|
|
55
55
|
</nav>
|
|
56
56
|
</div>
|
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
"title": "QuadroKit",
|
|
4
4
|
"tagline": "Admin shell with sidebar — same stack, denser layout.",
|
|
5
5
|
"nav_home": "Home",
|
|
6
|
-
"
|
|
6
|
+
"nav_sample_data": "Sample data",
|
|
7
7
|
"session_cookie": "Expected 4D session cookie: {{name}}"
|
|
8
8
|
},
|
|
9
|
+
"sample_data": {
|
|
10
|
+
"title": "Sample data",
|
|
11
|
+
"intro": "This page uses static placeholder rows. It does not assume any particular 4D table.",
|
|
12
|
+
"hint": "After `quadrokit-client generate`, ask your coding agent to load the right entities from your catalog (e.g. `quadro.<Entity>.all({ ... })`).",
|
|
13
|
+
"card_title": "Example rows",
|
|
14
|
+
"count": "{{count}} placeholder rows"
|
|
15
|
+
},
|
|
9
16
|
"form": {
|
|
10
17
|
"demo_title": "react-hook-form + zod",
|
|
11
18
|
"label": "Label",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@quadrokit/ui'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
|
|
4
|
+
/** Static placeholder — swap for `quadro.<YourEntity>.all(...)` wired to your catalog. */
|
|
5
|
+
const PLACEHOLDER_ROWS = [
|
|
6
|
+
{ id: '1', label: 'Example row A' },
|
|
7
|
+
{ id: '2', label: 'Example row B' },
|
|
8
|
+
{ id: '3', label: 'Example row C' },
|
|
9
|
+
] as const
|
|
10
|
+
|
|
11
|
+
export function SampleDataPage() {
|
|
12
|
+
const { t } = useTranslation()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="space-y-6">
|
|
16
|
+
<div>
|
|
17
|
+
<h1 className="text-2xl font-semibold">{t('sample_data.title')}</h1>
|
|
18
|
+
<p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.intro')}</p>
|
|
19
|
+
<p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.hint')}</p>
|
|
20
|
+
</div>
|
|
21
|
+
<Card>
|
|
22
|
+
<CardHeader>
|
|
23
|
+
<CardTitle>{t('sample_data.card_title')}</CardTitle>
|
|
24
|
+
<CardDescription>
|
|
25
|
+
{t('sample_data.count', { count: PLACEHOLDER_ROWS.length })}
|
|
26
|
+
</CardDescription>
|
|
27
|
+
</CardHeader>
|
|
28
|
+
<CardContent>
|
|
29
|
+
<ul className="list-inside list-disc space-y-1 text-sm">
|
|
30
|
+
{PLACEHOLDER_ROWS.map((row) => (
|
|
31
|
+
<li key={row.id}>{row.label}</li>
|
|
32
|
+
))}
|
|
33
|
+
</ul>
|
|
34
|
+
</CardContent>
|
|
35
|
+
</Card>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createBrowserRouter } from 'react-router-dom'
|
|
2
2
|
import { AppShell } from './components/AppShell'
|
|
3
|
-
import {
|
|
3
|
+
import { SampleDataPage } from './pages/SampleDataPage'
|
|
4
4
|
import { HomePage } from './pages/HomePage'
|
|
5
5
|
|
|
6
6
|
export const router = createBrowserRouter([
|
|
@@ -9,7 +9,7 @@ export const router = createBrowserRouter([
|
|
|
9
9
|
element: <AppShell />,
|
|
10
10
|
children: [
|
|
11
11
|
{ index: true, element: <HomePage /> },
|
|
12
|
-
{ path: '
|
|
12
|
+
{ path: 'sample-data', element: <SampleDataPage /> },
|
|
13
13
|
],
|
|
14
14
|
},
|
|
15
15
|
])
|
|
@@ -24,12 +24,12 @@ export function AppShell() {
|
|
|
24
24
|
{t('app.nav_home')}
|
|
25
25
|
</NavLink>
|
|
26
26
|
<NavLink
|
|
27
|
-
to="/
|
|
27
|
+
to="/sample-data"
|
|
28
28
|
className={({ isActive }) =>
|
|
29
29
|
isActive ? 'font-medium text-foreground' : 'hover:text-foreground'
|
|
30
30
|
}
|
|
31
31
|
>
|
|
32
|
-
{t('app.
|
|
32
|
+
{t('app.nav_sample_data')}
|
|
33
33
|
</NavLink>
|
|
34
34
|
</nav>
|
|
35
35
|
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle, cn } from '@quadrokit/ui'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
export type ChartCardProps = {
|
|
5
|
+
title: string
|
|
6
|
+
description: string
|
|
7
|
+
/** Chart area (e.g. `h-[260px]`) */
|
|
8
|
+
chartClassName: string
|
|
9
|
+
children: ReactNode
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ChartCard({
|
|
14
|
+
title,
|
|
15
|
+
description,
|
|
16
|
+
chartClassName,
|
|
17
|
+
children,
|
|
18
|
+
className,
|
|
19
|
+
}: ChartCardProps) {
|
|
20
|
+
return (
|
|
21
|
+
<Card className={className}>
|
|
22
|
+
<CardHeader>
|
|
23
|
+
<CardTitle className="text-base">{title}</CardTitle>
|
|
24
|
+
<CardDescription>{description}</CardDescription>
|
|
25
|
+
</CardHeader>
|
|
26
|
+
<CardContent className={cn('pt-0', chartClassName)}>{children}</CardContent>
|
|
27
|
+
</Card>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { ChartCard } from './ChartCard'
|
|
4
|
+
import { MOCK_BARS, MOCK_KPI, MOCK_LINES, MOCK_REGIONS } from './dashboardMockData'
|
|
5
|
+
import { KpiCard } from './KpiCard'
|
|
6
|
+
import { RegionsBarChart } from './RegionsBarChart'
|
|
7
|
+
import { SeriesBarChart } from './SeriesBarChart'
|
|
8
|
+
import { TrendLineChart } from './TrendLineChart'
|
|
9
|
+
|
|
10
|
+
export function DashboardOverview() {
|
|
11
|
+
const { t } = useTranslation()
|
|
12
|
+
|
|
13
|
+
const kpiCards = useMemo(
|
|
14
|
+
() =>
|
|
15
|
+
MOCK_KPI.map((row) => ({
|
|
16
|
+
key: row.key,
|
|
17
|
+
title: t(`dashboard.kpi_${row.key}`),
|
|
18
|
+
value: row.value,
|
|
19
|
+
deltaLine: `${row.delta} · ${t('dashboard.trend_vs')}`,
|
|
20
|
+
trendPositive: row.positive,
|
|
21
|
+
})),
|
|
22
|
+
[t]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="space-y-6">
|
|
27
|
+
<p className="text-sm text-muted-foreground">{t('dashboard.footnote')}</p>
|
|
28
|
+
|
|
29
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
30
|
+
{kpiCards.map((row) => (
|
|
31
|
+
<KpiCard
|
|
32
|
+
key={row.key}
|
|
33
|
+
title={row.title}
|
|
34
|
+
value={row.value}
|
|
35
|
+
deltaLine={row.deltaLine}
|
|
36
|
+
trendPositive={row.trendPositive}
|
|
37
|
+
/>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div className="grid gap-6 lg:grid-cols-2">
|
|
42
|
+
<ChartCard
|
|
43
|
+
className="min-h-[320px] shadow-sm"
|
|
44
|
+
title={t('dashboard.chart_bars_title')}
|
|
45
|
+
description={t('dashboard.chart_subtitle')}
|
|
46
|
+
chartClassName="h-[260px]"
|
|
47
|
+
>
|
|
48
|
+
<SeriesBarChart
|
|
49
|
+
data={MOCK_BARS}
|
|
50
|
+
seriesA={{ key: 'a', label: t('dashboard.series_a') }}
|
|
51
|
+
seriesB={{ key: 'b', label: t('dashboard.series_b') }}
|
|
52
|
+
/>
|
|
53
|
+
</ChartCard>
|
|
54
|
+
|
|
55
|
+
<ChartCard
|
|
56
|
+
className="min-h-[320px] shadow-sm"
|
|
57
|
+
title={t('dashboard.chart_lines_title')}
|
|
58
|
+
description={t('dashboard.chart_subtitle')}
|
|
59
|
+
chartClassName="h-[260px]"
|
|
60
|
+
>
|
|
61
|
+
<TrendLineChart
|
|
62
|
+
data={MOCK_LINES}
|
|
63
|
+
lineU={{ key: 'u', label: t('dashboard.series_u') }}
|
|
64
|
+
lineV={{ key: 'v', label: t('dashboard.series_v') }}
|
|
65
|
+
/>
|
|
66
|
+
</ChartCard>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<ChartCard
|
|
70
|
+
className="shadow-sm"
|
|
71
|
+
title={t('dashboard.chart_regions_title')}
|
|
72
|
+
description={t('dashboard.chart_subtitle')}
|
|
73
|
+
chartClassName="h-[220px]"
|
|
74
|
+
>
|
|
75
|
+
<RegionsBarChart data={MOCK_REGIONS} valueLabel={t('dashboard.count')} />
|
|
76
|
+
</ChartCard>
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@quadrokit/ui'
|
|
2
|
+
|
|
3
|
+
export type KpiCardProps = {
|
|
4
|
+
title: string
|
|
5
|
+
value: string
|
|
6
|
+
/** Full second line, e.g. "+12% · vs. last period" */
|
|
7
|
+
deltaLine: string
|
|
8
|
+
trendPositive: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function KpiCard({ title, value, deltaLine, trendPositive }: KpiCardProps) {
|
|
12
|
+
return (
|
|
13
|
+
<Card className="shadow-sm">
|
|
14
|
+
<CardHeader className="pb-2">
|
|
15
|
+
<CardDescription>{title}</CardDescription>
|
|
16
|
+
<CardTitle className="text-2xl font-semibold tabular-nums">{value}</CardTitle>
|
|
17
|
+
</CardHeader>
|
|
18
|
+
<CardContent className="pt-0">
|
|
19
|
+
<p
|
|
20
|
+
className={
|
|
21
|
+
trendPositive
|
|
22
|
+
? 'text-xs font-medium text-emerald-600 dark:text-emerald-400'
|
|
23
|
+
: 'text-xs font-medium text-rose-600 dark:text-rose-400'
|
|
24
|
+
}
|
|
25
|
+
>
|
|
26
|
+
{deltaLine}
|
|
27
|
+
</p>
|
|
28
|
+
</CardContent>
|
|
29
|
+
</Card>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
|
|
2
|
+
import { chartAxisTick, chartGridStroke, chartTooltipContentStyle } from './chartTheme'
|
|
3
|
+
import type { RegionDatum } from './dashboardMockData'
|
|
4
|
+
|
|
5
|
+
export type RegionsBarChartProps = {
|
|
6
|
+
data: readonly RegionDatum[]
|
|
7
|
+
categoryKey?: keyof RegionDatum & string
|
|
8
|
+
valueKey?: keyof RegionDatum & string
|
|
9
|
+
valueLabel: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function RegionsBarChart({
|
|
13
|
+
data,
|
|
14
|
+
categoryKey = 'region',
|
|
15
|
+
valueKey = 'n',
|
|
16
|
+
valueLabel,
|
|
17
|
+
}: RegionsBarChartProps) {
|
|
18
|
+
return (
|
|
19
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
20
|
+
<BarChart
|
|
21
|
+
layout="vertical"
|
|
22
|
+
data={[...data]}
|
|
23
|
+
margin={{ top: 8, right: 24, left: 8, bottom: 8 }}
|
|
24
|
+
>
|
|
25
|
+
<CartesianGrid strokeDasharray="3 3" stroke={chartGridStroke} horizontal={false} />
|
|
26
|
+
<XAxis type="number" tick={chartAxisTick} axisLine={{ stroke: chartGridStroke }} />
|
|
27
|
+
<YAxis
|
|
28
|
+
type="category"
|
|
29
|
+
dataKey={categoryKey}
|
|
30
|
+
width={72}
|
|
31
|
+
tick={chartAxisTick}
|
|
32
|
+
axisLine={{ stroke: chartGridStroke }}
|
|
33
|
+
/>
|
|
34
|
+
<Tooltip contentStyle={chartTooltipContentStyle} />
|
|
35
|
+
<Bar
|
|
36
|
+
dataKey={valueKey}
|
|
37
|
+
name={valueLabel}
|
|
38
|
+
fill="hsl(var(--primary))"
|
|
39
|
+
radius={[0, 4, 4, 0]}
|
|
40
|
+
maxBarSize={18}
|
|
41
|
+
/>
|
|
42
|
+
</BarChart>
|
|
43
|
+
</ResponsiveContainer>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Bar,
|
|
3
|
+
BarChart,
|
|
4
|
+
CartesianGrid,
|
|
5
|
+
Legend,
|
|
6
|
+
ResponsiveContainer,
|
|
7
|
+
Tooltip,
|
|
8
|
+
XAxis,
|
|
9
|
+
YAxis,
|
|
10
|
+
} from 'recharts'
|
|
11
|
+
import {
|
|
12
|
+
chartAxisTick,
|
|
13
|
+
chartGridStroke,
|
|
14
|
+
chartLegendStyle,
|
|
15
|
+
chartTooltipContentStyle,
|
|
16
|
+
} from './chartTheme'
|
|
17
|
+
import type { BarDatum } from './dashboardMockData'
|
|
18
|
+
|
|
19
|
+
export type SeriesBarChartProps = {
|
|
20
|
+
data: readonly BarDatum[]
|
|
21
|
+
seriesA: { key: 'a'; label: string }
|
|
22
|
+
seriesB: { key: 'b'; label: string }
|
|
23
|
+
xKey?: keyof BarDatum & string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function SeriesBarChart({ data, seriesA, seriesB, xKey = 'name' }: SeriesBarChartProps) {
|
|
27
|
+
return (
|
|
28
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
29
|
+
<BarChart data={[...data]} margin={{ top: 8, right: 8, left: -8, bottom: 0 }}>
|
|
30
|
+
<CartesianGrid strokeDasharray="3 3" stroke={chartGridStroke} vertical={false} />
|
|
31
|
+
<XAxis dataKey={xKey} tick={chartAxisTick} axisLine={{ stroke: chartGridStroke }} />
|
|
32
|
+
<YAxis tick={chartAxisTick} axisLine={{ stroke: chartGridStroke }} width={32} />
|
|
33
|
+
<Tooltip
|
|
34
|
+
cursor={{ fill: 'hsl(var(--muted) / 0.35)' }}
|
|
35
|
+
contentStyle={chartTooltipContentStyle}
|
|
36
|
+
/>
|
|
37
|
+
<Legend wrapperStyle={chartLegendStyle} />
|
|
38
|
+
<Bar
|
|
39
|
+
dataKey={seriesA.key}
|
|
40
|
+
name={seriesA.label}
|
|
41
|
+
fill="hsl(var(--primary))"
|
|
42
|
+
radius={[4, 4, 0, 0]}
|
|
43
|
+
maxBarSize={40}
|
|
44
|
+
/>
|
|
45
|
+
<Bar
|
|
46
|
+
dataKey={seriesB.key}
|
|
47
|
+
name={seriesB.label}
|
|
48
|
+
fill="hsl(var(--muted-foreground) / 0.35)"
|
|
49
|
+
radius={[4, 4, 0, 0]}
|
|
50
|
+
maxBarSize={40}
|
|
51
|
+
/>
|
|
52
|
+
</BarChart>
|
|
53
|
+
</ResponsiveContainer>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CartesianGrid,
|
|
3
|
+
Legend,
|
|
4
|
+
Line,
|
|
5
|
+
LineChart,
|
|
6
|
+
ResponsiveContainer,
|
|
7
|
+
Tooltip,
|
|
8
|
+
XAxis,
|
|
9
|
+
YAxis,
|
|
10
|
+
} from 'recharts'
|
|
11
|
+
import {
|
|
12
|
+
chartAxisTick,
|
|
13
|
+
chartGridStroke,
|
|
14
|
+
chartLegendStyle,
|
|
15
|
+
chartTooltipContentStyle,
|
|
16
|
+
} from './chartTheme'
|
|
17
|
+
import type { LineDatum } from './dashboardMockData'
|
|
18
|
+
|
|
19
|
+
export type TrendLineChartProps = {
|
|
20
|
+
data: readonly LineDatum[]
|
|
21
|
+
xKey?: keyof LineDatum & string
|
|
22
|
+
lineU: { key: 'u'; label: string }
|
|
23
|
+
lineV: { key: 'v'; label: string }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function TrendLineChart({ data, xKey = 'x', lineU, lineV }: TrendLineChartProps) {
|
|
27
|
+
return (
|
|
28
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
29
|
+
<LineChart data={[...data]} margin={{ top: 8, right: 8, left: -8, bottom: 0 }}>
|
|
30
|
+
<CartesianGrid strokeDasharray="3 3" stroke={chartGridStroke} />
|
|
31
|
+
<XAxis dataKey={xKey} tick={chartAxisTick} axisLine={{ stroke: chartGridStroke }} />
|
|
32
|
+
<YAxis tick={chartAxisTick} axisLine={{ stroke: chartGridStroke }} width={32} />
|
|
33
|
+
<Tooltip contentStyle={chartTooltipContentStyle} />
|
|
34
|
+
<Legend wrapperStyle={chartLegendStyle} />
|
|
35
|
+
<Line
|
|
36
|
+
type="monotone"
|
|
37
|
+
dataKey={lineU.key}
|
|
38
|
+
name={lineU.label}
|
|
39
|
+
stroke="hsl(var(--primary))"
|
|
40
|
+
strokeWidth={2}
|
|
41
|
+
dot={{ r: 3 }}
|
|
42
|
+
activeDot={{ r: 5 }}
|
|
43
|
+
/>
|
|
44
|
+
<Line
|
|
45
|
+
type="monotone"
|
|
46
|
+
dataKey={lineV.key}
|
|
47
|
+
name={lineV.label}
|
|
48
|
+
stroke="hsl(var(--muted-foreground))"
|
|
49
|
+
strokeWidth={2}
|
|
50
|
+
dot={{ r: 3 }}
|
|
51
|
+
/>
|
|
52
|
+
</LineChart>
|
|
53
|
+
</ResponsiveContainer>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react'
|
|
2
|
+
|
|
3
|
+
/** Shared Recharts styling aligned with QuadroKit CSS variables (light/dark). */
|
|
4
|
+
export const chartAxisTick = {
|
|
5
|
+
fill: 'hsl(var(--muted-foreground))',
|
|
6
|
+
fontSize: 11,
|
|
7
|
+
} as const
|
|
8
|
+
|
|
9
|
+
export const chartGridStroke = 'hsl(var(--border))'
|
|
10
|
+
|
|
11
|
+
export const chartTooltipContentStyle: CSSProperties = {
|
|
12
|
+
background: 'hsl(var(--card))',
|
|
13
|
+
border: '1px solid hsl(var(--border))',
|
|
14
|
+
borderRadius: '8px',
|
|
15
|
+
fontSize: '12px',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const chartLegendStyle = { fontSize: '12px' } as const
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Static demo data — replace with props from your API / `quadro` client. */
|
|
2
|
+
|
|
3
|
+
export type KpiDatum = {
|
|
4
|
+
key: string
|
|
5
|
+
value: string
|
|
6
|
+
delta: string
|
|
7
|
+
positive: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const MOCK_KPI: readonly KpiDatum[] = [
|
|
11
|
+
{ key: 'revenue', value: '$587.54', delta: '+12%', positive: true },
|
|
12
|
+
{ key: 'sales', value: '4,500', delta: '+8%', positive: true },
|
|
13
|
+
{ key: 'customers', value: '2,242', delta: '-3%', positive: false },
|
|
14
|
+
{ key: 'orders', value: '1,128', delta: '+15%', positive: true },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export type BarDatum = {
|
|
18
|
+
name: string
|
|
19
|
+
a: number
|
|
20
|
+
b: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const MOCK_BARS: readonly BarDatum[] = [
|
|
24
|
+
{ name: 'Jan', a: 40, b: 24 },
|
|
25
|
+
{ name: 'Feb', a: 30, b: 38 },
|
|
26
|
+
{ name: 'Mar', a: 55, b: 42 },
|
|
27
|
+
{ name: 'Apr', a: 45, b: 30 },
|
|
28
|
+
{ name: 'May', a: 60, b: 48 },
|
|
29
|
+
{ name: 'Jun', a: 52, b: 55 },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
export type LineDatum = { x: number; u: number; v: number }
|
|
33
|
+
|
|
34
|
+
export const MOCK_LINES: readonly LineDatum[] = [
|
|
35
|
+
{ x: 1, u: 12, v: 8 },
|
|
36
|
+
{ x: 2, u: 18, v: 14 },
|
|
37
|
+
{ x: 3, u: 15, v: 20 },
|
|
38
|
+
{ x: 4, u: 22, v: 16 },
|
|
39
|
+
{ x: 5, u: 28, v: 24 },
|
|
40
|
+
{ x: 6, u: 25, v: 30 },
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
export type RegionDatum = { region: string; n: number }
|
|
44
|
+
|
|
45
|
+
export const MOCK_REGIONS: readonly RegionDatum[] = [
|
|
46
|
+
{ region: 'North', n: 14 },
|
|
47
|
+
{ region: 'South', n: 19 },
|
|
48
|
+
{ region: 'East', n: 12 },
|
|
49
|
+
{ region: 'West', n: 17 },
|
|
50
|
+
{ region: 'Other', n: 9 },
|
|
51
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DashboardOverview } from './DashboardOverview'
|
|
@@ -3,9 +3,34 @@
|
|
|
3
3
|
"title": "QuadroKit",
|
|
4
4
|
"tagline": "Dashboard template — Vite, React, 4D REST, themes, i18n.",
|
|
5
5
|
"nav_home": "Home",
|
|
6
|
-
"
|
|
6
|
+
"nav_sample_data": "Sample data",
|
|
7
7
|
"session_cookie": "Expected 4D session cookie: {{name}}"
|
|
8
8
|
},
|
|
9
|
+
"dashboard": {
|
|
10
|
+
"loading": "Loading charts…",
|
|
11
|
+
"footnote": "Charts use static sample data — wire your 4D aggregates or entity sets when you are ready.",
|
|
12
|
+
"kpi_revenue": "Revenue",
|
|
13
|
+
"kpi_sales": "Sales",
|
|
14
|
+
"kpi_customers": "Customers",
|
|
15
|
+
"kpi_orders": "Orders",
|
|
16
|
+
"trend_vs": "vs. last period",
|
|
17
|
+
"chart_bars_title": "Series comparison",
|
|
18
|
+
"chart_lines_title": "Trend lines",
|
|
19
|
+
"chart_regions_title": "By region",
|
|
20
|
+
"chart_subtitle": "Demo data only",
|
|
21
|
+
"series_a": "Series A",
|
|
22
|
+
"series_b": "Series B",
|
|
23
|
+
"series_u": "Signal U",
|
|
24
|
+
"series_v": "Signal V",
|
|
25
|
+
"count": "Count"
|
|
26
|
+
},
|
|
27
|
+
"sample_data": {
|
|
28
|
+
"title": "Sample data",
|
|
29
|
+
"intro": "This page uses static placeholder rows. It does not assume any particular 4D table.",
|
|
30
|
+
"hint": "After `quadrokit-client generate`, ask your coding agent to load the right entities from your catalog (e.g. `quadro.<Entity>.all({ ... })`).",
|
|
31
|
+
"card_title": "Example rows",
|
|
32
|
+
"count": "{{count}} placeholder rows"
|
|
33
|
+
},
|
|
9
34
|
"form": {
|
|
10
35
|
"demo_title": "react-hook-form + zod",
|
|
11
36
|
"label": "Label",
|
|
@@ -9,12 +9,18 @@ import {
|
|
|
9
9
|
Input,
|
|
10
10
|
Label,
|
|
11
11
|
} from '@quadrokit/ui'
|
|
12
|
+
import { lazy, Suspense } from 'react'
|
|
12
13
|
import { useForm } from 'react-hook-form'
|
|
13
14
|
import { useTranslation } from 'react-i18next'
|
|
14
15
|
import { z } from 'zod'
|
|
15
16
|
import { quadro } from '@/lib/quadro-client'
|
|
16
17
|
import { useSidebarHint } from '@/stores/useSidebarHint'
|
|
17
18
|
|
|
19
|
+
const DashboardOverview = lazy(async () => {
|
|
20
|
+
const m = await import('@/components/dashboard')
|
|
21
|
+
return { default: m.DashboardOverview }
|
|
22
|
+
})
|
|
23
|
+
|
|
18
24
|
const schema = z.object({
|
|
19
25
|
label: z.string().min(1, 'Required'),
|
|
20
26
|
})
|
|
@@ -41,6 +47,16 @@ export function HomePage() {
|
|
|
41
47
|
</p>
|
|
42
48
|
</div>
|
|
43
49
|
|
|
50
|
+
<Suspense
|
|
51
|
+
fallback={
|
|
52
|
+
<div className="flex min-h-[200px] items-center justify-center rounded-xl border border-dashed border-border text-sm text-muted-foreground">
|
|
53
|
+
{t('dashboard.loading')}
|
|
54
|
+
</div>
|
|
55
|
+
}
|
|
56
|
+
>
|
|
57
|
+
<DashboardOverview />
|
|
58
|
+
</Suspense>
|
|
59
|
+
|
|
44
60
|
<Card>
|
|
45
61
|
<CardHeader>
|
|
46
62
|
<CardTitle>{t('form.demo_title')}</CardTitle>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@quadrokit/ui'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
|
|
4
|
+
/** Static placeholder — swap for `quadro.<YourEntity>.all(...)` wired to your catalog. */
|
|
5
|
+
const PLACEHOLDER_ROWS = [
|
|
6
|
+
{ id: '1', label: 'Example row A' },
|
|
7
|
+
{ id: '2', label: 'Example row B' },
|
|
8
|
+
{ id: '3', label: 'Example row C' },
|
|
9
|
+
] as const
|
|
10
|
+
|
|
11
|
+
export function SampleDataPage() {
|
|
12
|
+
const { t } = useTranslation()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="space-y-6">
|
|
16
|
+
<div>
|
|
17
|
+
<h1 className="text-2xl font-semibold">{t('sample_data.title')}</h1>
|
|
18
|
+
<p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.intro')}</p>
|
|
19
|
+
<p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.hint')}</p>
|
|
20
|
+
</div>
|
|
21
|
+
<Card>
|
|
22
|
+
<CardHeader>
|
|
23
|
+
<CardTitle>{t('sample_data.card_title')}</CardTitle>
|
|
24
|
+
<CardDescription>
|
|
25
|
+
{t('sample_data.count', { count: PLACEHOLDER_ROWS.length })}
|
|
26
|
+
</CardDescription>
|
|
27
|
+
</CardHeader>
|
|
28
|
+
<CardContent>
|
|
29
|
+
<ul className="list-inside list-disc space-y-1 text-sm">
|
|
30
|
+
{PLACEHOLDER_ROWS.map((row) => (
|
|
31
|
+
<li key={row.id}>{row.label}</li>
|
|
32
|
+
))}
|
|
33
|
+
</ul>
|
|
34
|
+
</CardContent>
|
|
35
|
+
</Card>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createBrowserRouter } from 'react-router-dom'
|
|
2
2
|
import { AppShell } from './components/AppShell'
|
|
3
|
-
import { AgenciesPage } from './pages/AgenciesPage'
|
|
4
3
|
import { HomePage } from './pages/HomePage'
|
|
4
|
+
import { SampleDataPage } from './pages/SampleDataPage'
|
|
5
5
|
|
|
6
6
|
export const router = createBrowserRouter([
|
|
7
7
|
{
|
|
@@ -9,7 +9,7 @@ export const router = createBrowserRouter([
|
|
|
9
9
|
element: <AppShell />,
|
|
10
10
|
children: [
|
|
11
11
|
{ index: true, element: <HomePage /> },
|
|
12
|
-
{ path: '
|
|
12
|
+
{ path: 'sample-data', element: <SampleDataPage /> },
|
|
13
13
|
],
|
|
14
14
|
},
|
|
15
15
|
])
|
|
@@ -24,12 +24,12 @@ export function AppShell() {
|
|
|
24
24
|
{t('app.nav_home')}
|
|
25
25
|
</NavLink>
|
|
26
26
|
<NavLink
|
|
27
|
-
to="/
|
|
27
|
+
to="/sample-data"
|
|
28
28
|
className={({ isActive }) =>
|
|
29
29
|
isActive ? 'font-medium text-foreground' : 'hover:text-foreground'
|
|
30
30
|
}
|
|
31
31
|
>
|
|
32
|
-
{t('app.
|
|
32
|
+
{t('app.nav_sample_data')}
|
|
33
33
|
</NavLink>
|
|
34
34
|
</nav>
|
|
35
35
|
</div>
|
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
"title": "QuadroKit",
|
|
4
4
|
"tagline": "Storefront shell — swap cards for real 4D entities.",
|
|
5
5
|
"nav_home": "Home",
|
|
6
|
-
"
|
|
6
|
+
"nav_sample_data": "Sample data",
|
|
7
7
|
"session_cookie": "Expected 4D session cookie: {{name}}"
|
|
8
8
|
},
|
|
9
|
+
"sample_data": {
|
|
10
|
+
"title": "Sample data",
|
|
11
|
+
"intro": "This page uses static placeholder rows. It does not assume any particular 4D table.",
|
|
12
|
+
"hint": "After `quadrokit-client generate`, ask your coding agent to load the right entities from your catalog (e.g. `quadro.<Entity>.all({ ... })`).",
|
|
13
|
+
"card_title": "Example rows",
|
|
14
|
+
"count": "{{count}} placeholder rows"
|
|
15
|
+
},
|
|
9
16
|
"store": {
|
|
10
17
|
"title": "Storefront",
|
|
11
18
|
"sub": "Placeholder tiles — connect `quadro` to your catalog entities when you are ready.",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@quadrokit/ui'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
|
|
4
|
+
/** Static placeholder — swap for `quadro.<YourEntity>.all(...)` wired to your catalog. */
|
|
5
|
+
const PLACEHOLDER_ROWS = [
|
|
6
|
+
{ id: '1', label: 'Example row A' },
|
|
7
|
+
{ id: '2', label: 'Example row B' },
|
|
8
|
+
{ id: '3', label: 'Example row C' },
|
|
9
|
+
] as const
|
|
10
|
+
|
|
11
|
+
export function SampleDataPage() {
|
|
12
|
+
const { t } = useTranslation()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="space-y-6">
|
|
16
|
+
<div>
|
|
17
|
+
<h1 className="text-2xl font-semibold">{t('sample_data.title')}</h1>
|
|
18
|
+
<p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.intro')}</p>
|
|
19
|
+
<p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.hint')}</p>
|
|
20
|
+
</div>
|
|
21
|
+
<Card>
|
|
22
|
+
<CardHeader>
|
|
23
|
+
<CardTitle>{t('sample_data.card_title')}</CardTitle>
|
|
24
|
+
<CardDescription>
|
|
25
|
+
{t('sample_data.count', { count: PLACEHOLDER_ROWS.length })}
|
|
26
|
+
</CardDescription>
|
|
27
|
+
</CardHeader>
|
|
28
|
+
<CardContent>
|
|
29
|
+
<ul className="list-inside list-disc space-y-1 text-sm">
|
|
30
|
+
{PLACEHOLDER_ROWS.map((row) => (
|
|
31
|
+
<li key={row.id}>{row.label}</li>
|
|
32
|
+
))}
|
|
33
|
+
</ul>
|
|
34
|
+
</CardContent>
|
|
35
|
+
</Card>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createBrowserRouter } from 'react-router-dom'
|
|
2
2
|
import { AppShell } from './components/AppShell'
|
|
3
|
-
import {
|
|
3
|
+
import { SampleDataPage } from './pages/SampleDataPage'
|
|
4
4
|
import { HomePage } from './pages/HomePage'
|
|
5
5
|
|
|
6
6
|
export const router = createBrowserRouter([
|
|
@@ -9,7 +9,7 @@ export const router = createBrowserRouter([
|
|
|
9
9
|
element: <AppShell />,
|
|
10
10
|
children: [
|
|
11
11
|
{ index: true, element: <HomePage /> },
|
|
12
|
-
{ path: '
|
|
12
|
+
{ path: 'sample-data', element: <SampleDataPage /> },
|
|
13
13
|
],
|
|
14
14
|
},
|
|
15
15
|
])
|
|
@@ -24,12 +24,12 @@ export function AppShell() {
|
|
|
24
24
|
{t('app.nav_home')}
|
|
25
25
|
</NavLink>
|
|
26
26
|
<NavLink
|
|
27
|
-
to="/
|
|
27
|
+
to="/sample-data"
|
|
28
28
|
className={({ isActive }) =>
|
|
29
29
|
isActive ? 'font-medium text-foreground' : 'hover:text-foreground'
|
|
30
30
|
}
|
|
31
31
|
>
|
|
32
|
-
{t('app.
|
|
32
|
+
{t('app.nav_sample_data')}
|
|
33
33
|
</NavLink>
|
|
34
34
|
</nav>
|
|
35
35
|
</div>
|
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
"title": "QuadroKit",
|
|
4
4
|
"tagline": "Marketing-style template — same stack, lighter chrome.",
|
|
5
5
|
"nav_home": "Home",
|
|
6
|
-
"
|
|
6
|
+
"nav_sample_data": "Sample data",
|
|
7
7
|
"session_cookie": "Expected 4D session cookie: {{name}}"
|
|
8
8
|
},
|
|
9
|
+
"sample_data": {
|
|
10
|
+
"title": "Sample data",
|
|
11
|
+
"intro": "This page uses static placeholder rows. It does not assume any particular 4D table.",
|
|
12
|
+
"hint": "After `quadrokit-client generate`, ask your coding agent to load the right entities from your catalog (e.g. `quadro.<Entity>.all({ ... })`).",
|
|
13
|
+
"card_title": "Example rows",
|
|
14
|
+
"count": "{{count}} placeholder rows"
|
|
15
|
+
},
|
|
9
16
|
"hero": {
|
|
10
17
|
"headline": "Ship a public site on top of your 4D datastore.",
|
|
11
18
|
"sub": "QuadroKit keeps REST, auth cookies, and typed clients consistent across templates.",
|
|
@@ -46,7 +46,7 @@ export function HomePage() {
|
|
|
46
46
|
<a href="#contact">{t('hero.cta_primary')}</a>
|
|
47
47
|
</Button>
|
|
48
48
|
<Button variant="outline" asChild>
|
|
49
|
-
<a href="/
|
|
49
|
+
<a href="/sample-data">{t('hero.cta_secondary')}</a>
|
|
50
50
|
</Button>
|
|
51
51
|
</div>
|
|
52
52
|
</section>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@quadrokit/ui'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
|
|
4
|
+
/** Static placeholder — swap for `quadro.<YourEntity>.all(...)` wired to your catalog. */
|
|
5
|
+
const PLACEHOLDER_ROWS = [
|
|
6
|
+
{ id: '1', label: 'Example row A' },
|
|
7
|
+
{ id: '2', label: 'Example row B' },
|
|
8
|
+
{ id: '3', label: 'Example row C' },
|
|
9
|
+
] as const
|
|
10
|
+
|
|
11
|
+
export function SampleDataPage() {
|
|
12
|
+
const { t } = useTranslation()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="space-y-6">
|
|
16
|
+
<div>
|
|
17
|
+
<h1 className="text-2xl font-semibold">{t('sample_data.title')}</h1>
|
|
18
|
+
<p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.intro')}</p>
|
|
19
|
+
<p className="mt-2 max-w-2xl text-sm text-muted-foreground">{t('sample_data.hint')}</p>
|
|
20
|
+
</div>
|
|
21
|
+
<Card>
|
|
22
|
+
<CardHeader>
|
|
23
|
+
<CardTitle>{t('sample_data.card_title')}</CardTitle>
|
|
24
|
+
<CardDescription>
|
|
25
|
+
{t('sample_data.count', { count: PLACEHOLDER_ROWS.length })}
|
|
26
|
+
</CardDescription>
|
|
27
|
+
</CardHeader>
|
|
28
|
+
<CardContent>
|
|
29
|
+
<ul className="list-inside list-disc space-y-1 text-sm">
|
|
30
|
+
{PLACEHOLDER_ROWS.map((row) => (
|
|
31
|
+
<li key={row.id}>{row.label}</li>
|
|
32
|
+
))}
|
|
33
|
+
</ul>
|
|
34
|
+
</CardContent>
|
|
35
|
+
</Card>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createBrowserRouter } from 'react-router-dom'
|
|
2
2
|
import { AppShell } from './components/AppShell'
|
|
3
|
-
import {
|
|
3
|
+
import { SampleDataPage } from './pages/SampleDataPage'
|
|
4
4
|
import { HomePage } from './pages/HomePage'
|
|
5
5
|
|
|
6
6
|
export const router = createBrowserRouter([
|
|
@@ -9,7 +9,7 @@ export const router = createBrowserRouter([
|
|
|
9
9
|
element: <AppShell />,
|
|
10
10
|
children: [
|
|
11
11
|
{ index: true, element: <HomePage /> },
|
|
12
|
-
{ path: '
|
|
12
|
+
{ path: 'sample-data', element: <SampleDataPage /> },
|
|
13
13
|
],
|
|
14
14
|
},
|
|
15
15
|
])
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@quadrokit/ui'
|
|
2
|
-
import { useEffect, useRef, useState } from 'react'
|
|
3
|
-
import { quadro } from '@/lib/quadro-client'
|
|
4
|
-
|
|
5
|
-
const PAGE_SIZE = 20
|
|
6
|
-
|
|
7
|
-
export function AgenciesPage() {
|
|
8
|
-
const [names, setNames] = useState<string[]>([])
|
|
9
|
-
const [error, setError] = useState<string | null>(null)
|
|
10
|
-
const loadSeq = useRef(0)
|
|
11
|
-
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
const seq = ++loadSeq.current
|
|
14
|
-
let cancelled = false
|
|
15
|
-
;(async () => {
|
|
16
|
-
try {
|
|
17
|
-
const list = quadro.Agency.all({
|
|
18
|
-
page: 1,
|
|
19
|
-
pageSize: PAGE_SIZE,
|
|
20
|
-
select: ['name'],
|
|
21
|
-
maxItems: PAGE_SIZE,
|
|
22
|
-
})
|
|
23
|
-
const acc: string[] = []
|
|
24
|
-
for await (const a of list) {
|
|
25
|
-
if (cancelled || seq !== loadSeq.current) {
|
|
26
|
-
break
|
|
27
|
-
}
|
|
28
|
-
acc.push(a.name ?? '')
|
|
29
|
-
}
|
|
30
|
-
if (!cancelled && seq === loadSeq.current) {
|
|
31
|
-
setNames(acc)
|
|
32
|
-
}
|
|
33
|
-
} catch (e) {
|
|
34
|
-
if (!cancelled && seq === loadSeq.current) {
|
|
35
|
-
setError(e instanceof Error ? e.message : String(e))
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
})()
|
|
39
|
-
return () => {
|
|
40
|
-
cancelled = true
|
|
41
|
-
}
|
|
42
|
-
}, [])
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div className="space-y-6">
|
|
46
|
-
<div>
|
|
47
|
-
<h1 className="text-2xl font-semibold">Agencies</h1>
|
|
48
|
-
<p className="text-sm text-muted-foreground">
|
|
49
|
-
Sample list from 4D REST (requires running server + auth cookie).
|
|
50
|
-
</p>
|
|
51
|
-
</div>
|
|
52
|
-
{error && (
|
|
53
|
-
<Card className="border-destructive/50">
|
|
54
|
-
<CardHeader>
|
|
55
|
-
<CardTitle className="text-destructive">Request failed</CardTitle>
|
|
56
|
-
<CardDescription>{error}</CardDescription>
|
|
57
|
-
</CardHeader>
|
|
58
|
-
</Card>
|
|
59
|
-
)}
|
|
60
|
-
<Card>
|
|
61
|
-
<CardHeader>
|
|
62
|
-
<CardTitle>Names</CardTitle>
|
|
63
|
-
<CardDescription>
|
|
64
|
-
{names.length ? `${names.length} loaded` : 'No data yet'}
|
|
65
|
-
</CardDescription>
|
|
66
|
-
</CardHeader>
|
|
67
|
-
<CardContent>
|
|
68
|
-
<ul className="list-inside list-disc space-y-1 text-sm">
|
|
69
|
-
{names.map((n) => (
|
|
70
|
-
<li key={n}>{n || '(empty)'}</li>
|
|
71
|
-
))}
|
|
72
|
-
</ul>
|
|
73
|
-
</CardContent>
|
|
74
|
-
</Card>
|
|
75
|
-
</div>
|
|
76
|
-
)
|
|
77
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@quadrokit/ui'
|
|
2
|
-
import { useEffect, useRef, useState } from 'react'
|
|
3
|
-
import { quadro } from '@/lib/quadro-client'
|
|
4
|
-
|
|
5
|
-
const PAGE_SIZE = 20
|
|
6
|
-
|
|
7
|
-
export function AgenciesPage() {
|
|
8
|
-
const [names, setNames] = useState<string[]>([])
|
|
9
|
-
const [error, setError] = useState<string | null>(null)
|
|
10
|
-
const loadSeq = useRef(0)
|
|
11
|
-
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
const seq = ++loadSeq.current
|
|
14
|
-
let cancelled = false
|
|
15
|
-
;(async () => {
|
|
16
|
-
try {
|
|
17
|
-
const list = quadro.Agency.all({
|
|
18
|
-
page: 1,
|
|
19
|
-
pageSize: PAGE_SIZE,
|
|
20
|
-
select: ['name'],
|
|
21
|
-
maxItems: PAGE_SIZE,
|
|
22
|
-
})
|
|
23
|
-
const acc: string[] = []
|
|
24
|
-
for await (const a of list) {
|
|
25
|
-
if (cancelled || seq !== loadSeq.current) {
|
|
26
|
-
break
|
|
27
|
-
}
|
|
28
|
-
acc.push(a.name ?? '')
|
|
29
|
-
}
|
|
30
|
-
if (!cancelled && seq === loadSeq.current) {
|
|
31
|
-
setNames(acc)
|
|
32
|
-
}
|
|
33
|
-
} catch (e) {
|
|
34
|
-
if (!cancelled && seq === loadSeq.current) {
|
|
35
|
-
setError(e instanceof Error ? e.message : String(e))
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
})()
|
|
39
|
-
return () => {
|
|
40
|
-
cancelled = true
|
|
41
|
-
}
|
|
42
|
-
}, [])
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div className="space-y-6">
|
|
46
|
-
<div>
|
|
47
|
-
<h1 className="text-2xl font-semibold">Agencies</h1>
|
|
48
|
-
<p className="text-sm text-muted-foreground">
|
|
49
|
-
Sample list from 4D REST (requires running server + auth cookie).
|
|
50
|
-
</p>
|
|
51
|
-
</div>
|
|
52
|
-
{error && (
|
|
53
|
-
<Card className="border-destructive/50">
|
|
54
|
-
<CardHeader>
|
|
55
|
-
<CardTitle className="text-destructive">Request failed</CardTitle>
|
|
56
|
-
<CardDescription>{error}</CardDescription>
|
|
57
|
-
</CardHeader>
|
|
58
|
-
</Card>
|
|
59
|
-
)}
|
|
60
|
-
<Card>
|
|
61
|
-
<CardHeader>
|
|
62
|
-
<CardTitle>Names</CardTitle>
|
|
63
|
-
<CardDescription>
|
|
64
|
-
{names.length ? `${names.length} loaded` : 'No data yet'}
|
|
65
|
-
</CardDescription>
|
|
66
|
-
</CardHeader>
|
|
67
|
-
<CardContent>
|
|
68
|
-
<ul className="list-inside list-disc space-y-1 text-sm">
|
|
69
|
-
{names.map((n) => (
|
|
70
|
-
<li key={n}>{n || '(empty)'}</li>
|
|
71
|
-
))}
|
|
72
|
-
</ul>
|
|
73
|
-
</CardContent>
|
|
74
|
-
</Card>
|
|
75
|
-
</div>
|
|
76
|
-
)
|
|
77
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@quadrokit/ui'
|
|
2
|
-
import { useEffect, useRef, useState } from 'react'
|
|
3
|
-
import { quadro } from '@/lib/quadro-client'
|
|
4
|
-
|
|
5
|
-
const PAGE_SIZE = 20
|
|
6
|
-
|
|
7
|
-
export function AgenciesPage() {
|
|
8
|
-
const [names, setNames] = useState<string[]>([])
|
|
9
|
-
const [error, setError] = useState<string | null>(null)
|
|
10
|
-
const loadSeq = useRef(0)
|
|
11
|
-
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
const seq = ++loadSeq.current
|
|
14
|
-
let cancelled = false
|
|
15
|
-
;(async () => {
|
|
16
|
-
try {
|
|
17
|
-
const list = quadro.Agency.all({
|
|
18
|
-
page: 1,
|
|
19
|
-
pageSize: PAGE_SIZE,
|
|
20
|
-
select: ['name'],
|
|
21
|
-
maxItems: PAGE_SIZE,
|
|
22
|
-
})
|
|
23
|
-
const acc: string[] = []
|
|
24
|
-
for await (const a of list) {
|
|
25
|
-
if (cancelled || seq !== loadSeq.current) {
|
|
26
|
-
break
|
|
27
|
-
}
|
|
28
|
-
acc.push(a.name ?? '')
|
|
29
|
-
}
|
|
30
|
-
if (!cancelled && seq === loadSeq.current) {
|
|
31
|
-
setNames(acc)
|
|
32
|
-
}
|
|
33
|
-
} catch (e) {
|
|
34
|
-
if (!cancelled && seq === loadSeq.current) {
|
|
35
|
-
setError(e instanceof Error ? e.message : String(e))
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
})()
|
|
39
|
-
return () => {
|
|
40
|
-
cancelled = true
|
|
41
|
-
}
|
|
42
|
-
}, [])
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div className="space-y-6">
|
|
46
|
-
<div>
|
|
47
|
-
<h1 className="text-2xl font-semibold">Agencies</h1>
|
|
48
|
-
<p className="text-sm text-muted-foreground">
|
|
49
|
-
Sample list from 4D REST (requires running server + auth cookie).
|
|
50
|
-
</p>
|
|
51
|
-
</div>
|
|
52
|
-
{error && (
|
|
53
|
-
<Card className="border-destructive/50">
|
|
54
|
-
<CardHeader>
|
|
55
|
-
<CardTitle className="text-destructive">Request failed</CardTitle>
|
|
56
|
-
<CardDescription>{error}</CardDescription>
|
|
57
|
-
</CardHeader>
|
|
58
|
-
</Card>
|
|
59
|
-
)}
|
|
60
|
-
<Card>
|
|
61
|
-
<CardHeader>
|
|
62
|
-
<CardTitle>Names</CardTitle>
|
|
63
|
-
<CardDescription>
|
|
64
|
-
{names.length ? `${names.length} loaded` : 'No data yet'}
|
|
65
|
-
</CardDescription>
|
|
66
|
-
</CardHeader>
|
|
67
|
-
<CardContent>
|
|
68
|
-
<ul className="list-inside list-disc space-y-1 text-sm">
|
|
69
|
-
{names.map((n) => (
|
|
70
|
-
<li key={n}>{n || '(empty)'}</li>
|
|
71
|
-
))}
|
|
72
|
-
</ul>
|
|
73
|
-
</CardContent>
|
|
74
|
-
</Card>
|
|
75
|
-
</div>
|
|
76
|
-
)
|
|
77
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@quadrokit/ui'
|
|
2
|
-
import { useEffect, useRef, useState } from 'react'
|
|
3
|
-
import { quadro } from '@/lib/quadro-client'
|
|
4
|
-
|
|
5
|
-
const PAGE_SIZE = 20
|
|
6
|
-
|
|
7
|
-
export function AgenciesPage() {
|
|
8
|
-
const [names, setNames] = useState<string[]>([])
|
|
9
|
-
const [error, setError] = useState<string | null>(null)
|
|
10
|
-
const loadSeq = useRef(0)
|
|
11
|
-
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
const seq = ++loadSeq.current
|
|
14
|
-
let cancelled = false
|
|
15
|
-
;(async () => {
|
|
16
|
-
try {
|
|
17
|
-
const list = quadro.Agency.all({
|
|
18
|
-
page: 1,
|
|
19
|
-
pageSize: PAGE_SIZE,
|
|
20
|
-
select: ['name'],
|
|
21
|
-
maxItems: PAGE_SIZE,
|
|
22
|
-
})
|
|
23
|
-
const acc: string[] = []
|
|
24
|
-
for await (const a of list) {
|
|
25
|
-
if (cancelled || seq !== loadSeq.current) {
|
|
26
|
-
break
|
|
27
|
-
}
|
|
28
|
-
acc.push(a.name ?? '')
|
|
29
|
-
}
|
|
30
|
-
if (!cancelled && seq === loadSeq.current) {
|
|
31
|
-
setNames(acc)
|
|
32
|
-
}
|
|
33
|
-
} catch (e) {
|
|
34
|
-
if (!cancelled && seq === loadSeq.current) {
|
|
35
|
-
setError(e instanceof Error ? e.message : String(e))
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
})()
|
|
39
|
-
return () => {
|
|
40
|
-
cancelled = true
|
|
41
|
-
}
|
|
42
|
-
}, [])
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div className="space-y-6">
|
|
46
|
-
<div>
|
|
47
|
-
<h1 className="text-2xl font-semibold">Agencies</h1>
|
|
48
|
-
<p className="text-sm text-muted-foreground">
|
|
49
|
-
Sample list from 4D REST (requires running server + auth cookie).
|
|
50
|
-
</p>
|
|
51
|
-
</div>
|
|
52
|
-
{error && (
|
|
53
|
-
<Card className="border-destructive/50">
|
|
54
|
-
<CardHeader>
|
|
55
|
-
<CardTitle className="text-destructive">Request failed</CardTitle>
|
|
56
|
-
<CardDescription>{error}</CardDescription>
|
|
57
|
-
</CardHeader>
|
|
58
|
-
</Card>
|
|
59
|
-
)}
|
|
60
|
-
<Card>
|
|
61
|
-
<CardHeader>
|
|
62
|
-
<CardTitle>Names</CardTitle>
|
|
63
|
-
<CardDescription>
|
|
64
|
-
{names.length ? `${names.length} loaded` : 'No data yet'}
|
|
65
|
-
</CardDescription>
|
|
66
|
-
</CardHeader>
|
|
67
|
-
<CardContent>
|
|
68
|
-
<ul className="list-inside list-disc space-y-1 text-sm">
|
|
69
|
-
{names.map((n) => (
|
|
70
|
-
<li key={n}>{n || '(empty)'}</li>
|
|
71
|
-
))}
|
|
72
|
-
</ul>
|
|
73
|
-
</CardContent>
|
|
74
|
-
</Card>
|
|
75
|
-
</div>
|
|
76
|
-
)
|
|
77
|
-
}
|