@hed-hog/catalog 0.0.276

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.
Files changed (65) hide show
  1. package/dist/catalog-resource.config.d.ts +12 -0
  2. package/dist/catalog-resource.config.d.ts.map +1 -0
  3. package/dist/catalog-resource.config.js +209 -0
  4. package/dist/catalog-resource.config.js.map +1 -0
  5. package/dist/catalog.controller.d.ts +29 -0
  6. package/dist/catalog.controller.d.ts.map +1 -0
  7. package/dist/catalog.controller.js +117 -0
  8. package/dist/catalog.controller.js.map +1 -0
  9. package/dist/catalog.module.d.ts +3 -0
  10. package/dist/catalog.module.d.ts.map +1 -0
  11. package/dist/catalog.module.js +25 -0
  12. package/dist/catalog.module.js.map +1 -0
  13. package/dist/catalog.service.d.ts +36 -0
  14. package/dist/catalog.service.d.ts.map +1 -0
  15. package/dist/catalog.service.js +142 -0
  16. package/dist/catalog.service.js.map +1 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +18 -0
  20. package/dist/index.js.map +1 -0
  21. package/hedhog/data/menu.yaml +233 -0
  22. package/hedhog/data/role.yaml +7 -0
  23. package/hedhog/data/route.yaml +56 -0
  24. package/hedhog/frontend/app/[resource]/page.tsx.ejs +684 -0
  25. package/hedhog/frontend/app/_components/catalog-nav.tsx.ejs +85 -0
  26. package/hedhog/frontend/app/_lib/catalog-resources.tsx.ejs +569 -0
  27. package/hedhog/frontend/app/dashboard/page.tsx.ejs +566 -0
  28. package/hedhog/frontend/app/page.tsx.ejs +5 -0
  29. package/hedhog/frontend/messages/en.json +293 -0
  30. package/hedhog/frontend/messages/pt.json +293 -0
  31. package/hedhog/table/catalog_affiliate_program.yaml +41 -0
  32. package/hedhog/table/catalog_attribute.yaml +53 -0
  33. package/hedhog/table/catalog_attribute_group.yaml +18 -0
  34. package/hedhog/table/catalog_brand.yaml +34 -0
  35. package/hedhog/table/catalog_category_attribute.yaml +36 -0
  36. package/hedhog/table/catalog_click_event.yaml +50 -0
  37. package/hedhog/table/catalog_comparison.yaml +65 -0
  38. package/hedhog/table/catalog_comparison_highlight.yaml +39 -0
  39. package/hedhog/table/catalog_comparison_item.yaml +30 -0
  40. package/hedhog/table/catalog_content_relation.yaml +42 -0
  41. package/hedhog/table/catalog_import_run.yaml +33 -0
  42. package/hedhog/table/catalog_import_source.yaml +24 -0
  43. package/hedhog/table/catalog_merchant.yaml +29 -0
  44. package/hedhog/table/catalog_offer.yaml +83 -0
  45. package/hedhog/table/catalog_price_history.yaml +34 -0
  46. package/hedhog/table/catalog_product.yaml +76 -0
  47. package/hedhog/table/catalog_product_attribute_value.yaml +60 -0
  48. package/hedhog/table/catalog_product_category.yaml +26 -0
  49. package/hedhog/table/catalog_product_image.yaml +34 -0
  50. package/hedhog/table/catalog_product_score.yaml +38 -0
  51. package/hedhog/table/catalog_product_site.yaml +47 -0
  52. package/hedhog/table/catalog_product_tag.yaml +19 -0
  53. package/hedhog/table/catalog_score_criterion.yaml +37 -0
  54. package/hedhog/table/catalog_seo_page_rule.yaml +51 -0
  55. package/hedhog/table/catalog_similarity_rule.yaml +28 -0
  56. package/hedhog/table/catalog_site.yaml +40 -0
  57. package/hedhog/table/catalog_site_category.yaml +26 -0
  58. package/package.json +40 -0
  59. package/src/catalog-resource.config.ts +218 -0
  60. package/src/catalog.controller.ts +82 -0
  61. package/src/catalog.module.ts +12 -0
  62. package/src/catalog.service.ts +167 -0
  63. package/src/index.ts +1 -0
  64. package/src/language/en.json +4 -0
  65. package/src/language/pt.json +4 -0
@@ -0,0 +1,566 @@
1
+ 'use client';
2
+
3
+ import { CatalogNav } from '../_components/catalog-nav';
4
+ import {
5
+ catalogKpiResources,
6
+ catalogQuickActionResources,
7
+ catalogResources,
8
+ } from '../_lib/catalog-resources';
9
+ import { Page, PageHeader } from '@/components/entity-list';
10
+ import { Badge } from '@/components/ui/badge';
11
+ import { Button } from '@/components/ui/button';
12
+ import {
13
+ Card,
14
+ CardContent,
15
+ CardDescription,
16
+ CardHeader,
17
+ CardTitle,
18
+ } from '@/components/ui/card';
19
+ import { Skeleton } from '@/components/ui/skeleton';
20
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
21
+ import {
22
+ Activity,
23
+ ArrowUpRight,
24
+ Boxes,
25
+ ChartNoAxesCombined,
26
+ RefreshCcw,
27
+ ShieldCheck,
28
+ Sparkles,
29
+ } from 'lucide-react';
30
+ import Link from 'next/link';
31
+ import { useTranslations } from 'next-intl';
32
+ import {
33
+ Bar,
34
+ BarChart,
35
+ CartesianGrid,
36
+ Cell,
37
+ Pie,
38
+ PieChart,
39
+ ResponsiveContainer,
40
+ Tooltip,
41
+ XAxis,
42
+ YAxis,
43
+ } from 'recharts';
44
+
45
+ type CatalogStats = {
46
+ resource: string;
47
+ total: number;
48
+ active?: number;
49
+ };
50
+
51
+ const chartPalette = [
52
+ '#f97316',
53
+ '#14b8a6',
54
+ '#0ea5e9',
55
+ '#84cc16',
56
+ '#f59e0b',
57
+ '#ef4444',
58
+ ];
59
+
60
+ const chartTooltipStyle = {
61
+ backgroundColor: 'hsl(var(--card))',
62
+ border: '1px solid hsl(var(--border))',
63
+ borderRadius: '12px',
64
+ fontSize: '12px',
65
+ };
66
+
67
+ export default function CatalogDashboardPage() {
68
+ const t = useTranslations('catalog');
69
+ const { request } = useApp();
70
+
71
+ const { data, isLoading, refetch } = useQuery<CatalogStats[]>({
72
+ queryKey: ['catalog-dashboard-stats'],
73
+ queryFn: async () => {
74
+ const responses = await Promise.allSettled(
75
+ catalogResources.map(async (resource) => {
76
+ const response = await request({
77
+ url: `/catalog/${resource.resource}/stats`,
78
+ });
79
+ const statsData = response.data as {
80
+ total?: number;
81
+ active?: number;
82
+ };
83
+
84
+ return {
85
+ resource: resource.resource,
86
+ total: Number(statsData.total ?? 0),
87
+ active:
88
+ statsData.active !== undefined
89
+ ? Number(statsData.active)
90
+ : undefined,
91
+ } satisfies CatalogStats;
92
+ })
93
+ );
94
+
95
+ return responses.map((response, index) =>
96
+ response.status === 'fulfilled'
97
+ ? response.value
98
+ : {
99
+ resource:
100
+ catalogResources[index]?.resource ?? `fallback-${index}`,
101
+ total: 0,
102
+ }
103
+ );
104
+ },
105
+ initialData: [],
106
+ });
107
+
108
+ const dashboardStats = data as CatalogStats[];
109
+
110
+ const statsMap = new Map(dashboardStats.map((item) => [item.resource, item]));
111
+ const totalRecords = dashboardStats.reduce(
112
+ (sum, item) => sum + item.total,
113
+ 0
114
+ );
115
+ const totalActive = dashboardStats.reduce(
116
+ (sum, item) => sum + (typeof item.active === 'number' ? item.active : 0),
117
+ 0
118
+ );
119
+ const monitoredResources = dashboardStats.filter(
120
+ (item) => item.total > 0
121
+ ).length;
122
+ const statusAwareResources = dashboardStats.filter(
123
+ (item) => typeof item.active === 'number'
124
+ ).length;
125
+
126
+ const kpiCards = catalogKpiResources.map((resourceKey) => {
127
+ const resource = catalogResources.find(
128
+ (item) => item.resource === resourceKey
129
+ )!;
130
+ const stats = statsMap.get(resourceKey);
131
+ return { resource, total: stats?.total ?? 0, active: stats?.active };
132
+ });
133
+
134
+ const volumeChartData = [...catalogResources]
135
+ .map((resource) => ({
136
+ name: t(`resources.${resource.translationKey}.shortTitle`),
137
+ total: statsMap.get(resource.resource)?.total ?? 0,
138
+ }))
139
+ .sort((left, right) => right.total - left.total)
140
+ .slice(0, 6);
141
+
142
+ const statusChartData = catalogResources
143
+ .filter((resource) => resource.hasActiveStats)
144
+ .map((resource) => {
145
+ const stats = statsMap.get(resource.resource);
146
+ const active = Number(stats?.active ?? 0);
147
+ const total = Number(stats?.total ?? 0);
148
+
149
+ return {
150
+ name: t(`resources.${resource.translationKey}.shortTitle`),
151
+ active,
152
+ inactive: Math.max(total - active, 0),
153
+ };
154
+ });
155
+
156
+ const distributionChartData = volumeChartData.map((item, index) => ({
157
+ ...item,
158
+ fill: chartPalette[index % chartPalette.length],
159
+ }));
160
+
161
+ const leaderboard = [...catalogResources]
162
+ .map((resource) => ({
163
+ resource,
164
+ total: statsMap.get(resource.resource)?.total ?? 0,
165
+ active: statsMap.get(resource.resource)?.active,
166
+ }))
167
+ .sort((left, right) => right.total - left.total)
168
+ .slice(0, 5);
169
+
170
+ return (
171
+ <Page>
172
+ <PageHeader
173
+ title={t('dashboard.title')}
174
+ description={t('dashboard.subtitle')}
175
+ breadcrumbs={[
176
+ { label: t('breadcrumbs.home'), href: '/' },
177
+ { label: t('breadcrumbs.catalog') },
178
+ ]}
179
+ actions={[
180
+ {
181
+ label: t('refresh'),
182
+ onClick: () => {
183
+ void refetch();
184
+ },
185
+ variant: 'outline',
186
+ icon: <RefreshCcw className="size-4" />,
187
+ },
188
+ ]}
189
+ />
190
+
191
+ <div className="min-w-0 space-y-6 overflow-x-hidden">
192
+ <Card className="overflow-hidden border-orange-200/70 bg-gradient-to-br from-orange-50 via-background to-amber-50 py-0">
193
+ <CardContent className="grid min-w-0 gap-6 px-6 py-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(280px,0.9fr)]">
194
+ <div className="min-w-0 space-y-4">
195
+ <Badge className="w-fit rounded-full bg-orange-500/10 px-3 py-1 text-orange-700 hover:bg-orange-500/10">
196
+ <Sparkles className="mr-2 size-3.5" />
197
+ {t('dashboard.heroBadge')}
198
+ </Badge>
199
+ <div className="space-y-2">
200
+ <h2 className="text-3xl font-semibold tracking-tight text-balance">
201
+ {t('dashboard.heroTitle')}
202
+ </h2>
203
+ <p className="max-w-2xl text-sm leading-6 text-muted-foreground">
204
+ {t('dashboard.heroDescription')}
205
+ </p>
206
+ </div>
207
+ <div className="flex flex-wrap gap-3">
208
+ <Button asChild>
209
+ <Link href="/catalog/products">
210
+ {t('dashboard.primaryAction')}
211
+ </Link>
212
+ </Button>
213
+ <Button asChild variant="outline">
214
+ <Link href="/catalog/import-sources">
215
+ {t('dashboard.secondaryAction')}
216
+ </Link>
217
+ </Button>
218
+ </div>
219
+ </div>
220
+
221
+ <div className="grid gap-3 sm:grid-cols-3 lg:grid-cols-1">
222
+ {[
223
+ {
224
+ icon: Boxes,
225
+ label: t('dashboard.summary.totalRecords'),
226
+ value: totalRecords,
227
+ },
228
+ {
229
+ icon: ShieldCheck,
230
+ label: t('dashboard.summary.activeRecords'),
231
+ value: totalActive,
232
+ },
233
+ {
234
+ icon: Activity,
235
+ label: t('dashboard.summary.monitoredResources'),
236
+ value: monitoredResources,
237
+ },
238
+ ].map((item) => (
239
+ <div
240
+ key={item.label}
241
+ className="rounded-2xl border border-white/70 bg-white/80 p-4 shadow-sm backdrop-blur"
242
+ >
243
+ <div className="mb-3 flex items-center gap-2 text-muted-foreground">
244
+ <item.icon className="size-4" />
245
+ <span className="text-xs uppercase tracking-[0.2em]">
246
+ {item.label}
247
+ </span>
248
+ </div>
249
+ <div className="text-3xl font-semibold tracking-tight">
250
+ {item.value}
251
+ </div>
252
+ </div>
253
+ ))}
254
+ </div>
255
+ </CardContent>
256
+ </Card>
257
+
258
+ <CatalogNav currentHref="/catalog/dashboard" />
259
+
260
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
261
+ {kpiCards.map(({ resource, total, active }) => {
262
+ const Icon = resource.icon;
263
+
264
+ return (
265
+ <Card
266
+ key={resource.resource}
267
+ className="overflow-hidden border-border/70 py-0"
268
+ >
269
+ <div
270
+ className={`h-1 w-full bg-gradient-to-r ${resource.colorClass}`}
271
+ />
272
+ <CardContent className="space-y-4 px-6 py-5">
273
+ <div className="flex items-start justify-between gap-3">
274
+ <div>
275
+ <p className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
276
+ {t(`resources.${resource.translationKey}.shortTitle`)}
277
+ </p>
278
+ <p className="mt-2 text-3xl font-semibold tracking-tight">
279
+ {isLoading ? '-' : total}
280
+ </p>
281
+ </div>
282
+ <div className={`rounded-2xl p-3 ${resource.glowClass}`}>
283
+ <Icon className="size-5" />
284
+ </div>
285
+ </div>
286
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
287
+ <span>{t('dashboard.card.totalLabel')}</span>
288
+ <span>
289
+ {typeof active === 'number'
290
+ ? t('dashboard.card.activeLabel', { count: active })
291
+ : t('dashboard.card.noActiveLabel')}
292
+ </span>
293
+ </div>
294
+ </CardContent>
295
+ </Card>
296
+ );
297
+ })}
298
+ </div>
299
+
300
+ <div className="grid min-w-0 gap-6 xl:grid-cols-[minmax(0,1.3fr)_minmax(320px,0.9fr)]">
301
+ <Card className="min-w-0">
302
+ <CardHeader>
303
+ <CardTitle>{t('dashboard.charts.volume.title')}</CardTitle>
304
+ <CardDescription>
305
+ {t('dashboard.charts.volume.description')}
306
+ </CardDescription>
307
+ </CardHeader>
308
+ <CardContent className="h-[320px]">
309
+ {isLoading ? (
310
+ <Skeleton className="h-full w-full rounded-xl" />
311
+ ) : (
312
+ <ResponsiveContainer width="100%" height="100%">
313
+ <BarChart data={volumeChartData}>
314
+ <CartesianGrid
315
+ strokeDasharray="3 3"
316
+ stroke="hsl(var(--border))"
317
+ vertical={false}
318
+ />
319
+ <XAxis
320
+ dataKey="name"
321
+ tickLine={false}
322
+ axisLine={false}
323
+ fontSize={12}
324
+ />
325
+ <YAxis tickLine={false} axisLine={false} fontSize={12} />
326
+ <Tooltip contentStyle={chartTooltipStyle} />
327
+ <Bar dataKey="total" radius={[8, 8, 0, 0]}>
328
+ {volumeChartData.map((entry, index) => (
329
+ <Cell
330
+ key={`${entry.name}-${index}`}
331
+ fill={chartPalette[index % chartPalette.length]}
332
+ />
333
+ ))}
334
+ </Bar>
335
+ </BarChart>
336
+ </ResponsiveContainer>
337
+ )}
338
+ </CardContent>
339
+ </Card>
340
+
341
+ <Card className="min-w-0">
342
+ <CardHeader>
343
+ <CardTitle>{t('dashboard.ranking.title')}</CardTitle>
344
+ <CardDescription>
345
+ {t('dashboard.ranking.description')}
346
+ </CardDescription>
347
+ </CardHeader>
348
+ <CardContent className="space-y-3">
349
+ {isLoading
350
+ ? Array.from({ length: 5 }).map((_, index) => (
351
+ <Skeleton
352
+ key={index}
353
+ className="h-[72px] w-full rounded-2xl"
354
+ />
355
+ ))
356
+ : leaderboard.map((item, index) => {
357
+ const Icon = item.resource.icon;
358
+
359
+ return (
360
+ <Link
361
+ key={item.resource.resource}
362
+ href={item.resource.href}
363
+ className="flex min-w-0 items-center justify-between gap-3 rounded-2xl border border-border/70 px-4 py-3 transition-colors hover:bg-muted/40"
364
+ >
365
+ <div className="flex min-w-0 items-center gap-3">
366
+ <div className="flex size-10 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
367
+ <Icon className="size-4" />
368
+ </div>
369
+ <div className="min-w-0">
370
+ <p className="truncate font-medium">
371
+ {index + 1}.{' '}
372
+ {t(
373
+ `resources.${item.resource.translationKey}.title`
374
+ )}
375
+ </p>
376
+ <p className="truncate text-xs text-muted-foreground">
377
+ {typeof item.active === 'number'
378
+ ? t('dashboard.ranking.activeCount', {
379
+ count: item.active,
380
+ })
381
+ : t('dashboard.card.noActiveLabel')}
382
+ </p>
383
+ </div>
384
+ </div>
385
+ <div className="shrink-0 text-right">
386
+ <p className="text-2xl font-semibold">{item.total}</p>
387
+ <p className="text-xs text-muted-foreground">
388
+ {t('dashboard.card.totalLabel')}
389
+ </p>
390
+ </div>
391
+ </Link>
392
+ );
393
+ })}
394
+ </CardContent>
395
+ </Card>
396
+ </div>
397
+
398
+ <div className="grid gap-6 xl:grid-cols-2">
399
+ <Card className="min-w-0">
400
+ <CardHeader>
401
+ <CardTitle>{t('dashboard.charts.status.title')}</CardTitle>
402
+ <CardDescription>
403
+ {t('dashboard.charts.status.description', {
404
+ count: statusAwareResources,
405
+ })}
406
+ </CardDescription>
407
+ </CardHeader>
408
+ <CardContent className="h-[320px]">
409
+ {isLoading ? (
410
+ <Skeleton className="h-full w-full rounded-xl" />
411
+ ) : (
412
+ <ResponsiveContainer width="100%" height="100%">
413
+ <BarChart data={statusChartData}>
414
+ <CartesianGrid
415
+ strokeDasharray="3 3"
416
+ stroke="hsl(var(--border))"
417
+ vertical={false}
418
+ />
419
+ <XAxis
420
+ dataKey="name"
421
+ tickLine={false}
422
+ axisLine={false}
423
+ fontSize={12}
424
+ />
425
+ <YAxis tickLine={false} axisLine={false} fontSize={12} />
426
+ <Tooltip contentStyle={chartTooltipStyle} />
427
+ <Bar
428
+ dataKey="active"
429
+ stackId="catalog-status"
430
+ fill="#f97316"
431
+ radius={[8, 8, 0, 0]}
432
+ />
433
+ <Bar
434
+ dataKey="inactive"
435
+ stackId="catalog-status"
436
+ fill="#fed7aa"
437
+ radius={[8, 8, 0, 0]}
438
+ />
439
+ </BarChart>
440
+ </ResponsiveContainer>
441
+ )}
442
+ </CardContent>
443
+ </Card>
444
+
445
+ <Card className="min-w-0">
446
+ <CardHeader>
447
+ <CardTitle>{t('dashboard.charts.distribution.title')}</CardTitle>
448
+ <CardDescription>
449
+ {t('dashboard.charts.distribution.description')}
450
+ </CardDescription>
451
+ </CardHeader>
452
+ <CardContent className="grid min-w-0 h-auto gap-4 lg:h-[320px] lg:grid-cols-[minmax(220px,0.9fr)_minmax(0,1.1fr)]">
453
+ {isLoading ? (
454
+ <>
455
+ <Skeleton className="h-full w-full rounded-full" />
456
+ <Skeleton className="h-full w-full rounded-2xl" />
457
+ </>
458
+ ) : (
459
+ <>
460
+ <ResponsiveContainer width="100%" height="100%">
461
+ <PieChart>
462
+ <Pie
463
+ data={distributionChartData}
464
+ dataKey="total"
465
+ nameKey="name"
466
+ innerRadius={58}
467
+ outerRadius={96}
468
+ paddingAngle={2}
469
+ >
470
+ {distributionChartData.map((entry) => (
471
+ <Cell key={entry.name} fill={entry.fill} />
472
+ ))}
473
+ </Pie>
474
+ <Tooltip contentStyle={chartTooltipStyle} />
475
+ </PieChart>
476
+ </ResponsiveContainer>
477
+ <div className="min-w-0 space-y-3">
478
+ {distributionChartData.map((item) => (
479
+ <div
480
+ key={item.name}
481
+ className="flex min-w-0 items-center justify-between gap-3 rounded-2xl border border-border/70 px-4 py-3"
482
+ >
483
+ <div className="flex min-w-0 items-center gap-3">
484
+ <span
485
+ className="inline-block size-3 rounded-full"
486
+ style={{ backgroundColor: item.fill }}
487
+ />
488
+ <span className="truncate font-medium">{item.name}</span>
489
+ </div>
490
+ <span className="shrink-0 text-sm text-muted-foreground">
491
+ {item.total}
492
+ </span>
493
+ </div>
494
+ ))}
495
+ </div>
496
+ </>
497
+ )}
498
+ </CardContent>
499
+ </Card>
500
+ </div>
501
+
502
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
503
+ {catalogQuickActionResources.map((resource) => {
504
+ const Icon = resource.icon;
505
+
506
+ return (
507
+ <Card key={resource.resource} className="min-w-0 overflow-hidden py-0">
508
+ <div
509
+ className={`h-full bg-gradient-to-br ${resource.colorClass} px-6 py-5`}
510
+ >
511
+ <div className="min-w-0 rounded-2xl border border-white/70 bg-background/90 p-5 shadow-sm backdrop-blur">
512
+ <div className="mb-4 flex items-center justify-between">
513
+ <div className={`rounded-2xl p-3 ${resource.glowClass}`}>
514
+ <Icon className="size-5" />
515
+ </div>
516
+ <ArrowUpRight className="size-4 text-muted-foreground" />
517
+ </div>
518
+ <h3 className="text-lg font-semibold">
519
+ {t(`resources.${resource.translationKey}.title`)}
520
+ </h3>
521
+ <p className="mt-2 min-h-10 break-words text-sm text-muted-foreground">
522
+ {t(`resources.${resource.translationKey}.description`)}
523
+ </p>
524
+ <div className="mt-5 flex items-center justify-between">
525
+ <span className="text-sm font-medium text-muted-foreground">
526
+ {t('dashboard.quickActions.itemHint')}
527
+ </span>
528
+ <Button asChild size="sm">
529
+ <Link href={resource.href}>{t('openResource')}</Link>
530
+ </Button>
531
+ </div>
532
+ </div>
533
+ </div>
534
+ </Card>
535
+ );
536
+ })}
537
+ </div>
538
+
539
+ <Card className="min-w-0 border-dashed">
540
+ <CardContent className="flex flex-wrap items-center justify-between gap-4 px-6 py-4">
541
+ <div className="space-y-1">
542
+ <div className="flex items-center gap-2 text-sm font-medium">
543
+ <ChartNoAxesCombined className="size-4 text-orange-600" />
544
+ {t('dashboard.footer.title')}
545
+ </div>
546
+ <p className="text-sm text-muted-foreground">
547
+ {t('dashboard.footer.description')}
548
+ </p>
549
+ </div>
550
+ <div className="flex flex-wrap gap-2">
551
+ <Badge variant="secondary">
552
+ {t('dashboard.footer.realData')}
553
+ </Badge>
554
+ <Badge variant="outline">{t('dashboard.footer.multiView')}</Badge>
555
+ <Badge variant="outline">
556
+ {t('dashboard.footer.resourceCount', {
557
+ count: catalogResources.length,
558
+ })}
559
+ </Badge>
560
+ </div>
561
+ </CardContent>
562
+ </Card>
563
+ </div>
564
+ </Page>
565
+ );
566
+ }
@@ -0,0 +1,5 @@
1
+ import { redirect } from 'next/navigation';
2
+
3
+ export default function CatalogPage() {
4
+ redirect('/catalog/dashboard');
5
+ }