@carlonicora/nextjs-jsonapi 1.131.2 → 1.133.0

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 (78) hide show
  1. package/dist/{BlockNoteEditor-5RUXGWLR.js → BlockNoteEditor-H626BS5Y.js} +19 -19
  2. package/dist/{BlockNoteEditor-5RUXGWLR.js.map → BlockNoteEditor-H626BS5Y.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-F4AKH532.mjs → BlockNoteEditor-VXK7INMR.mjs} +4 -4
  4. package/dist/billing/index.js +357 -357
  5. package/dist/billing/index.mjs +3 -3
  6. package/dist/{chunk-LBMNRFCY.js → chunk-5IE6DZ2D.js} +153 -77
  7. package/dist/chunk-5IE6DZ2D.js.map +1 -0
  8. package/dist/{chunk-5MNE72ZE.mjs → chunk-DZL2G7NB.mjs} +77 -1
  9. package/dist/chunk-DZL2G7NB.mjs.map +1 -0
  10. package/dist/{chunk-XTLTQQ7X.js → chunk-F6KBHC2X.js} +7 -7
  11. package/dist/{chunk-XTLTQQ7X.js.map → chunk-F6KBHC2X.js.map} +1 -1
  12. package/dist/{chunk-T7F2EVPR.js → chunk-TVJFCWST.js} +908 -929
  13. package/dist/chunk-TVJFCWST.js.map +1 -0
  14. package/dist/{chunk-NATPK5ME.mjs → chunk-VSAAVVHY.mjs} +2 -2
  15. package/dist/{chunk-BBBQGJEG.mjs → chunk-WEDLGKSS.mjs} +104 -125
  16. package/dist/chunk-WEDLGKSS.mjs.map +1 -0
  17. package/dist/client/index.js +4 -4
  18. package/dist/client/index.mjs +3 -3
  19. package/dist/components/index.d.mts +6 -2
  20. package/dist/components/index.d.ts +6 -2
  21. package/dist/components/index.js +4 -4
  22. package/dist/components/index.mjs +3 -3
  23. package/dist/contexts/index.d.mts +9 -2
  24. package/dist/contexts/index.d.ts +9 -2
  25. package/dist/contexts/index.js +6 -4
  26. package/dist/contexts/index.js.map +1 -1
  27. package/dist/contexts/index.mjs +5 -3
  28. package/dist/core/index.d.mts +11 -1
  29. package/dist/core/index.d.ts +11 -1
  30. package/dist/core/index.js +12 -2
  31. package/dist/core/index.js.map +1 -1
  32. package/dist/core/index.mjs +13 -3
  33. package/dist/features/help/index.js +39 -39
  34. package/dist/features/help/index.js.map +1 -1
  35. package/dist/features/help/index.mjs +5 -5
  36. package/dist/features/help/index.mjs.map +1 -1
  37. package/dist/index.d.mts +1 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.js +13 -3
  40. package/dist/index.js.map +1 -1
  41. package/dist/index.mjs +14 -4
  42. package/dist/server/index.js +3 -3
  43. package/dist/server/index.mjs +1 -1
  44. package/package.json +1 -1
  45. package/src/components/containers/ReactMarkdownContainer.tsx +1 -1
  46. package/src/components/containers/RoundPageContainer.tsx +7 -0
  47. package/src/components/forms/EditorSheet.tsx +24 -1
  48. package/src/components/forms/FormDateTime.tsx +5 -0
  49. package/src/components/navigations/Breadcrumb.tsx +13 -11
  50. package/src/components/navigations/Header.tsx +3 -1
  51. package/src/components/tables/ContentListTable.tsx +33 -19
  52. package/src/components/tables/__tests__/ContentListTable.test.tsx +77 -0
  53. package/src/contexts/HeaderChildrenContext.tsx +17 -2
  54. package/src/features/assistant-message/components/MessageItem.tsx +1 -1
  55. package/src/features/auth/components/GdprConsentSection.tsx +2 -2
  56. package/src/features/auth/components/forms/Register.tsx +2 -2
  57. package/src/features/help/components/HelpArticleBody.tsx +2 -2
  58. package/src/features/notification/components/common/NotificationErrorBoundary.tsx +1 -4
  59. package/src/features/oauth/components/consent/OAuthConsentScreen.tsx +2 -2
  60. package/src/features/waitlist/components/forms/WaitlistForm.tsx +2 -2
  61. package/src/features/waitlist/components/lists/WaitlistList.tsx +13 -2
  62. package/src/shadcnui/custom/link.tsx +1 -1
  63. package/src/shadcnui/ui/accordion.tsx +1 -1
  64. package/src/shadcnui/ui/alert-dialog.tsx +1 -1
  65. package/src/shadcnui/ui/alert.tsx +2 -5
  66. package/src/shadcnui/ui/badge.tsx +1 -1
  67. package/src/shadcnui/ui/button.tsx +1 -1
  68. package/src/shadcnui/ui/dialog.tsx +1 -4
  69. package/src/shadcnui/ui/field.tsx +1 -1
  70. package/src/utils/__tests__/italian-validators.test.ts +63 -0
  71. package/src/utils/index.ts +8 -0
  72. package/src/utils/italian-validators.ts +18 -5
  73. package/dist/chunk-5MNE72ZE.mjs.map +0 -1
  74. package/dist/chunk-BBBQGJEG.mjs.map +0 -1
  75. package/dist/chunk-LBMNRFCY.js.map +0 -1
  76. package/dist/chunk-T7F2EVPR.js.map +0 -1
  77. /package/dist/{BlockNoteEditor-F4AKH532.mjs.map → BlockNoteEditor-VXK7INMR.mjs.map} +0 -0
  78. /package/dist/{chunk-NATPK5ME.mjs.map → chunk-VSAAVVHY.mjs.map} +0 -0
@@ -19,18 +19,18 @@ import {
19
19
  Breadcrumb as UIBreadcrumb,
20
20
  } from "../../shadcnui";
21
21
 
22
- type BreadcrumbProps = { items: BreadcrumbItemData[] };
22
+ type BreadcrumbProps = { items: BreadcrumbItemData[]; rootLabel?: string };
23
23
 
24
24
  const ITEMS_TO_DISPLAY = 4;
25
25
 
26
26
  function BreadcrumbDesktop({
27
27
  items,
28
28
  generateUrl,
29
- t,
29
+ rootLabel,
30
30
  }: {
31
31
  items: BreadcrumbItemData[];
32
32
  generateUrl: ReturnType<typeof usePageUrlGenerator>;
33
- t: ReturnType<typeof useTranslations>;
33
+ rootLabel: string;
34
34
  }) {
35
35
  const [open, setOpen] = useState<boolean>(false);
36
36
 
@@ -38,7 +38,7 @@ function BreadcrumbDesktop({
38
38
  <UIBreadcrumb>
39
39
  <BreadcrumbList>
40
40
  <BreadcrumbItem>
41
- <Link href={generateUrl({ page: `/` })}>{t(`common.home`)}</Link>
41
+ <Link href={generateUrl({ page: `/` })}>{rootLabel}</Link>
42
42
  </BreadcrumbItem>
43
43
  {items.length > 0 && <BreadcrumbSeparator />}
44
44
 
@@ -112,23 +112,23 @@ function BreadcrumbDesktop({
112
112
  function BreadcrumbMobile({
113
113
  items,
114
114
  generateUrl,
115
- t,
115
+ rootLabel,
116
116
  }: {
117
117
  items: BreadcrumbItemData[];
118
118
  generateUrl: ReturnType<typeof usePageUrlGenerator>;
119
- t: ReturnType<typeof useTranslations>;
119
+ rootLabel: string;
120
120
  }) {
121
121
  const [open, setOpen] = useState<boolean>(false);
122
122
 
123
123
  const lastItem = items[items.length - 1];
124
- const allItems = [{ name: t(`common.home`), href: generateUrl({ page: `/` }) }, ...items];
124
+ const allItems = [{ name: rootLabel, href: generateUrl({ page: `/` }) }, ...items];
125
125
 
126
126
  if (!lastItem && items.length === 0) {
127
127
  return (
128
128
  <UIBreadcrumb>
129
129
  <BreadcrumbList>
130
130
  <BreadcrumbItem>
131
- <Link href={generateUrl({ page: `/` })}>{t(`common.home`)}</Link>
131
+ <Link href={generateUrl({ page: `/` })}>{rootLabel}</Link>
132
132
  </BreadcrumbItem>
133
133
  </BreadcrumbList>
134
134
  </UIBreadcrumb>
@@ -158,14 +158,16 @@ function BreadcrumbMobile({
158
158
  );
159
159
  }
160
160
 
161
- export function BreadcrumbNavigation({ items }: BreadcrumbProps) {
161
+ export function BreadcrumbNavigation({ items, rootLabel }: BreadcrumbProps) {
162
162
  const generateUrl = usePageUrlGenerator();
163
163
  const t = useTranslations();
164
164
  const isMobile = useIsMobile();
165
165
 
166
+ const root = rootLabel?.trim() ? rootLabel : t(`common.home`);
167
+
166
168
  if (isMobile) {
167
- return <BreadcrumbMobile items={items} generateUrl={generateUrl} t={t} />;
169
+ return <BreadcrumbMobile items={items} generateUrl={generateUrl} rootLabel={root} />;
168
170
  }
169
171
 
170
- return <BreadcrumbDesktop items={items} generateUrl={generateUrl} t={t} />;
172
+ return <BreadcrumbDesktop items={items} generateUrl={generateUrl} rootLabel={root} />;
171
173
  }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useIsMobile } from "@/utils";
4
+ import { useHeaderRootLabel } from "../../contexts/HeaderChildrenContext";
4
5
  import { useSharedContext } from "../../contexts/SharedContext";
5
6
  import { SidebarTrigger } from "../../shadcnui";
6
7
  import { BreadcrumbNavigation } from "./Breadcrumb";
@@ -13,6 +14,7 @@ type HeaderProps = {
13
14
 
14
15
  export function Header({ children, leftContent, className }: HeaderProps) {
15
16
  const { breadcrumbs } = useSharedContext();
17
+ const rootLabel = useHeaderRootLabel();
16
18
  const isMobile = useIsMobile();
17
19
 
18
20
  return (
@@ -21,7 +23,7 @@ export function Header({ children, leftContent, className }: HeaderProps) {
21
23
  <SidebarTrigger aria-label="Toggle sidebar" id="sidebar-trigger" />
22
24
  {leftContent}
23
25
  <div className="flex w-full flex-row items-center justify-start">
24
- <BreadcrumbNavigation items={breadcrumbs} />
26
+ <BreadcrumbNavigation items={breadcrumbs} rootLabel={rootLabel ?? undefined} />
25
27
  </div>
26
28
  {!isMobile && children && (
27
29
  <div className="flex w-64 flex-row items-center justify-end gap-x-4 whitespace-nowrap">
@@ -10,6 +10,7 @@ import React, { ReactNode, memo, useMemo, useState } from "react";
10
10
  import { DataListRetriever, TableContent, useTableGenerator } from "../../hooks";
11
11
  import { ModuleWithPermissions } from "../../permissions";
12
12
  import { Button, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "../../shadcnui";
13
+ import { MicroLabel } from "../typography";
13
14
  import { ContentTableSearch } from "./ContentTableSearch";
14
15
 
15
16
  const EMPTY_ARRAY: any[] = [];
@@ -49,6 +50,9 @@ type ContentListTableProps = {
49
50
  defaultExpanded?: boolean | ExpandedState;
50
51
  fullWidth?: boolean;
51
52
  groupBy?: string;
53
+ groupLabel?: (key: string) => ReactNode;
54
+ groupOrder?: string[];
55
+ hideHeader?: boolean;
52
56
  emptyState?: ReactNode;
53
57
  onRowClick?: (rowData: any) => void;
54
58
  };
@@ -118,12 +122,24 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
118
122
  }
119
123
  }
120
124
 
121
- const sortedKeys = [...groupMap.keys()].sort((a, b) => a.localeCompare(b));
125
+ const order = props.groupOrder;
126
+ const sortedKeys = [...groupMap.keys()].sort((a, b) => {
127
+ if (order) {
128
+ const ia = order.indexOf(a);
129
+ const ib = order.indexOf(b);
130
+ // keys absent from groupOrder sort last, alphabetically among themselves
131
+ if (ia === -1 && ib === -1) return a.localeCompare(b);
132
+ if (ia === -1) return 1;
133
+ if (ib === -1) return -1;
134
+ return ia - ib;
135
+ }
136
+ return a.localeCompare(b);
137
+ });
122
138
  return sortedKeys.map((groupKey) => ({
123
139
  groupKey,
124
140
  rows: groupMap.get(groupKey)!,
125
141
  }));
126
- }, [props.groupBy, rowModel]);
142
+ }, [props.groupBy, props.groupOrder, rowModel]);
127
143
 
128
144
  const showFooter = !!(data.next || data.previous);
129
145
 
@@ -168,18 +184,19 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
168
184
  </TableHead>
169
185
  </TableRow>
170
186
  )}
171
- {table.getHeaderGroups().map((headerGroup) => (
172
- <TableRow key={headerGroup.id}>
173
- {headerGroup.headers.map((header) => {
174
- const meta = header.column.columnDef.meta as { className?: string } | undefined;
175
- return (
176
- <TableHead key={header.id} className={meta?.className}>
177
- {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
178
- </TableHead>
179
- );
180
- })}
181
- </TableRow>
182
- ))}
187
+ {!props.hideHeader &&
188
+ table.getHeaderGroups().map((headerGroup) => (
189
+ <TableRow key={headerGroup.id}>
190
+ {headerGroup.headers.map((header) => {
191
+ const meta = header.column.columnDef.meta as { className?: string } | undefined;
192
+ return (
193
+ <TableHead key={header.id} className={meta?.className}>
194
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
195
+ </TableHead>
196
+ );
197
+ })}
198
+ </TableRow>
199
+ ))}
183
200
  </TableHeader>
184
201
  <TableBody>
185
202
  {rowModel && rowModel.rows?.length ? (
@@ -187,11 +204,8 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
187
204
  groupedRows.map((group) => (
188
205
  <React.Fragment key={group.groupKey}>
189
206
  <TableRow>
190
- <TableCell
191
- colSpan={tableColumns.length}
192
- className="bg-muted text-muted-foreground px-4 py-2 text-sm font-semibold"
193
- >
194
- {group.groupKey}
207
+ <TableCell colSpan={tableColumns.length} className="bg-muted px-4 py-2">
208
+ <MicroLabel>{props.groupLabel?.(group.groupKey) ?? group.groupKey}</MicroLabel>
195
209
  </TableCell>
196
210
  </TableRow>
197
211
  {group.rows.map((row) => (
@@ -338,6 +338,37 @@ describe("ContentListTable", () => {
338
338
  });
339
339
  });
340
340
 
341
+ describe("hideHeader", () => {
342
+ it("should render the column header row by default", () => {
343
+ const dataRetriever = createMockDataRetriever({
344
+ data: [{ id: "1", title: "Article 1" }],
345
+ });
346
+
347
+ render(<ContentListTable data={dataRetriever} tableGeneratorType={mockModule as any} fields={["id", "title"]} />);
348
+
349
+ expect(screen.getByText("Title")).toBeInTheDocument();
350
+ });
351
+
352
+ it("should not render the column header row when hideHeader is set", () => {
353
+ const dataRetriever = createMockDataRetriever({
354
+ data: [{ id: "1", title: "Article 1" }],
355
+ });
356
+
357
+ render(
358
+ <ContentListTable
359
+ data={dataRetriever}
360
+ tableGeneratorType={mockModule as any}
361
+ fields={["id", "title"]}
362
+ hideHeader
363
+ />,
364
+ );
365
+
366
+ expect(screen.queryByText("Title")).not.toBeInTheDocument();
367
+ // Body rows must still render — only the header row is suppressed.
368
+ expect(screen.getByText("Article 1")).toBeInTheDocument();
369
+ });
370
+ });
371
+
341
372
  describe("grouping", () => {
342
373
  it("should not group rows when groupBy is not provided", () => {
343
374
  const dataRetriever = createMockDataRetriever({
@@ -511,6 +542,52 @@ describe("ContentListTable", () => {
511
542
  const article3Count = allText.filter((t) => t === "Article 3").length;
512
543
  expect(article3Count).toBe(2);
513
544
  });
545
+
546
+ it("should render group headers through groupLabel when provided", () => {
547
+ const dataRetriever = createMockDataRetriever({
548
+ data: [
549
+ { id: "1", title: "Article 1", category: "CIVIL" },
550
+ { id: "2", title: "Article 2", category: "CRIMINAL" },
551
+ ],
552
+ });
553
+
554
+ render(
555
+ <ContentListTable
556
+ data={dataRetriever}
557
+ tableGeneratorType={mockModule as any}
558
+ fields={["id", "title"]}
559
+ groupBy="category"
560
+ groupLabel={(key) => `Branch: ${key}`}
561
+ />,
562
+ );
563
+
564
+ expect(screen.getByText("Branch: CIVIL")).toBeInTheDocument();
565
+ expect(screen.getByText("Branch: CRIMINAL")).toBeInTheDocument();
566
+ expect(screen.queryByText("CIVIL")).not.toBeInTheDocument();
567
+ });
568
+
569
+ it("should order groups by groupOrder instead of alphabetically", () => {
570
+ const dataRetriever = createMockDataRetriever({
571
+ data: [
572
+ { id: "1", title: "A Article", category: "A" },
573
+ { id: "2", title: "B Article", category: "B" },
574
+ { id: "3", title: "C Article", category: "C" },
575
+ ],
576
+ });
577
+
578
+ render(
579
+ <ContentListTable
580
+ data={dataRetriever}
581
+ tableGeneratorType={mockModule as any}
582
+ fields={["id", "title"]}
583
+ groupBy="category"
584
+ groupOrder={["C", "A", "B"]}
585
+ />,
586
+ );
587
+
588
+ const headings = screen.getAllByRole("heading", { level: 4 });
589
+ expect(headings.map((h) => h.textContent)).toEqual(["C", "A", "B"]);
590
+ });
514
591
  });
515
592
 
516
593
  describe("onRowClick", () => {
@@ -4,24 +4,30 @@ import { createContext, useContext, ReactNode } from "react";
4
4
 
5
5
  interface HeaderChildrenContextType {
6
6
  headerChildren: ReactNode | null;
7
+ headerRootLabel: string | null;
7
8
  }
8
9
 
9
10
  const HeaderChildrenContext = createContext<HeaderChildrenContextType>({
10
11
  headerChildren: null,
12
+ headerRootLabel: null,
11
13
  });
12
14
 
13
15
  interface HeaderChildrenProviderProps {
14
16
  children: ReactNode;
15
17
  content: ReactNode;
18
+ rootLabel?: string;
16
19
  }
17
20
 
18
21
  /**
19
22
  * Provider to supply custom content to be rendered in the Header component.
20
23
  * Wrap your layout with this provider and pass the content you want in the header.
24
+ * Optionally pass `rootLabel` to override the first breadcrumb entry (defaults to `common.home`).
21
25
  */
22
- export function HeaderChildrenProvider({ children, content }: HeaderChildrenProviderProps) {
26
+ export function HeaderChildrenProvider({ children, content, rootLabel }: HeaderChildrenProviderProps) {
23
27
  return (
24
- <HeaderChildrenContext.Provider value={{ headerChildren: content }}>{children}</HeaderChildrenContext.Provider>
28
+ <HeaderChildrenContext.Provider value={{ headerChildren: content, headerRootLabel: rootLabel ?? null }}>
29
+ {children}
30
+ </HeaderChildrenContext.Provider>
25
31
  );
26
32
  }
27
33
 
@@ -33,3 +39,12 @@ export function useHeaderChildren(): ReactNode | null {
33
39
  const context = useContext(HeaderChildrenContext);
34
40
  return context.headerChildren;
35
41
  }
42
+
43
+ /**
44
+ * Hook to get the label of the first breadcrumb entry, when the application supplies one.
45
+ * Returns null when no override was provided, in which case the Header falls back to `common.home`.
46
+ */
47
+ export function useHeaderRootLabel(): string | null {
48
+ const context = useContext(HeaderChildrenContext);
49
+ return context.headerRootLabel;
50
+ }
@@ -46,7 +46,7 @@ export function MessageItem({
46
46
  <div className="text-destructive flex items-center gap-2 text-xs">
47
47
  <AlertCircle className="h-3.5 w-3.5" />
48
48
  <span>{t("features.assistant.send_failed")}</span>
49
- <button type="button" className="underline" onClick={() => onRetry?.(message.id)}>
49
+ <button type="button" onClick={() => onRetry?.(message.id)}>
50
50
  {t("features.assistant.retry")}
51
51
  </button>
52
52
  </div>
@@ -21,11 +21,11 @@ export function GdprConsentSection<T extends FieldValues>({
21
21
  const termsLabel = (
22
22
  <>
23
23
  {t("terms_prefix")}{" "}
24
- <Link href="/terms" target="_blank" rel="noopener" className="underline">
24
+ <Link href="/terms" target="_blank" rel="noopener">
25
25
  {t("terms_of_service")}
26
26
  </Link>{" "}
27
27
  {t("and")}{" "}
28
- <Link href="/privacy" target="_blank" rel="noopener" className="underline">
28
+ <Link href="/privacy" target="_blank" rel="noopener">
29
29
  {t("privacy_policy")}
30
30
  </Link>
31
31
  </>
@@ -194,7 +194,7 @@ export default function Register() {
194
194
  <CardContent className="text-center">
195
195
  <p className="text-destructive mb-4">{inviteError}</p>
196
196
  <p className="mb-4">{t("waitlist.invite.join_prompt")}</p>
197
- <Link href="/waitlist" className="text-primary underline">
197
+ <Link href="/waitlist" className="text-primary">
198
198
  {t("waitlist.invite.join_link")}
199
199
  </Link>
200
200
  </CardContent>
@@ -215,7 +215,7 @@ export default function Register() {
215
215
  <CardContent className="text-center">
216
216
  <p className="mb-4">{t("waitlist.invite.registration_description")}</p>
217
217
  <p className="mb-4">{t("waitlist.invite.registration_hint")}</p>
218
- <Link href="/waitlist" className="text-primary underline">
218
+ <Link href="/waitlist" className="text-primary">
219
219
  {t("waitlist.invite.join_link")}
220
220
  </Link>
221
221
  </CardContent>
@@ -43,14 +43,14 @@ export function HelpArticleBody(props: {
43
43
  ) : null}
44
44
  <div className="mt-4 flex justify-between text-sm">
45
45
  {prev ? (
46
- <Link href={generateUrl({ page: `/help/${prev.howToType}/${prev.slug}` })} className="hover:underline">
46
+ <Link href={generateUrl({ page: `/help/${prev.howToType}/${prev.slug}` })}>
47
47
  ← {t("help.article.previous")}: {prev.title}
48
48
  </Link>
49
49
  ) : (
50
50
  <span />
51
51
  )}
52
52
  {next ? (
53
- <Link href={generateUrl({ page: `/help/${next.howToType}/${next.slug}` })} className="hover:underline">
53
+ <Link href={generateUrl({ page: `/help/${next.howToType}/${next.slug}` })}>
54
54
  {t("help.article.next")}: {next.title} →
55
55
  </Link>
56
56
  ) : (
@@ -34,10 +34,7 @@ export class NotificationErrorBoundary extends Component<Props, State> {
34
34
  <div className="flex items-center justify-center p-4 text-center">
35
35
  <div className="text-muted-foreground text-sm">
36
36
  <p>Something went wrong with notifications.</p>
37
- <button
38
- onClick={() => this.setState({ hasError: false })}
39
- className="text-primary mt-2 underline hover:no-underline"
40
- >
37
+ <button onClick={() => this.setState({ hasError: false })} className="text-primary mt-2">
41
38
  Try again
42
39
  </button>
43
40
  </div>
@@ -111,11 +111,11 @@ export function OAuthConsentScreen({
111
111
  <CardFooter className="justify-center">
112
112
  <p className="text-xs text-center text-muted-foreground">
113
113
  By authorizing, you agree to the app's{" "}
114
- <a href={termsUrl} className="underline hover:text-foreground" target="_blank" rel="noopener">
114
+ <a href={termsUrl} className="hover:text-foreground" target="_blank" rel="noopener">
115
115
  Terms of Service
116
116
  </a>{" "}
117
117
  and{" "}
118
- <a href={privacyUrl} className="underline hover:text-foreground" target="_blank" rel="noopener">
118
+ <a href={privacyUrl} className="hover:text-foreground" target="_blank" rel="noopener">
119
119
  Privacy Policy
120
120
  </a>
121
121
  .
@@ -159,11 +159,11 @@ export function WaitlistForm({ onSuccess }: WaitlistFormProps) {
159
159
  label={
160
160
  <>
161
161
  {t("auth.gdpr.terms_prefix")}
162
- <Link href="/terms" target="_blank" rel="noopener" className="underline">
162
+ <Link href="/terms" target="_blank" rel="noopener">
163
163
  {t("auth.gdpr.terms_of_service")}
164
164
  </Link>
165
165
  {t("auth.gdpr.and")}
166
- <Link href="/privacy" target="_blank" rel="noopener" className="underline">
166
+ <Link href="/privacy" target="_blank" rel="noopener">
167
167
  {t("auth.gdpr.privacy_policy")}
168
168
  </Link>
169
169
  </>
@@ -3,7 +3,7 @@
3
3
  import { flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
4
4
  import { RefreshCw, Users } from "lucide-react";
5
5
  import { useTranslations } from "next-intl";
6
- import { useCallback, useEffect, useState } from "react";
6
+ import { useCallback, useEffect, useMemo, useState } from "react";
7
7
  import { errorToast } from "../../../../components";
8
8
  import { SectionHeader } from "../../../../components/typography";
9
9
  import {
@@ -64,6 +64,17 @@ export function WaitlistList() {
64
64
 
65
65
  const columns = useWaitlistTableStructure({ onInvite: handleInvite });
66
66
 
67
+ const statusItems = useMemo(
68
+ () => ({
69
+ all: t("waitlist.admin.all_statuses"),
70
+ pending: t("waitlist.admin.status.pending"),
71
+ confirmed: t("waitlist.admin.status.confirmed"),
72
+ invited: t("waitlist.admin.status.invited"),
73
+ registered: t("waitlist.admin.status.registered"),
74
+ }),
75
+ [t],
76
+ );
77
+
67
78
  const table = useReactTable({
68
79
  data: entries,
69
80
  columns,
@@ -82,7 +93,7 @@ export function WaitlistList() {
82
93
 
83
94
  <div className="flex items-center gap-4">
84
95
  {/* Status Filter */}
85
- <Select value={statusFilter} onValueChange={(value) => setStatusFilter(value ?? "all")}>
96
+ <Select items={statusItems} value={statusFilter} onValueChange={(value) => setStatusFilter(value ?? "all")}>
86
97
  <SelectTrigger className="w-40">
87
98
  <SelectValue placeholder={t("waitlist.admin.filter_placeholder")} />
88
99
  </SelectTrigger>
@@ -21,7 +21,7 @@ const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(({ className, href,
21
21
  prefetch={false}
22
22
  ref={ref}
23
23
  href={href}
24
- className={cn("text-primary font-medium hover:underline", className)}
24
+ className={cn("text-primary font-medium", className)}
25
25
  {...props}
26
26
  >
27
27
  {children}
@@ -59,7 +59,7 @@ function AccordionContent({ className, children, ...props }: AccordionPrimitive.
59
59
  >
60
60
  <div
61
61
  className={cn(
62
- "pt-0 pb-4 [&_a]:hover:text-foreground h-(--accordion-panel-height) data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4",
62
+ "pt-0 pb-4 [&_a]:hover:text-foreground h-(--accordion-panel-height) data-ending-style:h-0 data-starting-style:h-0 [&_p:not(:last-child)]:mb-4",
63
63
  className,
64
64
  )}
65
65
  >
@@ -114,7 +114,7 @@ function AlertDialogDescription({
114
114
  <AlertDialogPrimitive.Description
115
115
  data-slot="alert-dialog-description"
116
116
  className={cn(
117
- "text-muted-foreground *:[a]:hover:text-foreground text-xs/relaxed text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3",
117
+ "text-muted-foreground *:[a]:hover:text-foreground text-xs/relaxed text-balance md:text-pretty",
118
118
  className,
119
119
  )}
120
120
  {...props}
@@ -27,10 +27,7 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
27
27
  return (
28
28
  <div
29
29
  data-slot="alert-title"
30
- className={cn(
31
- "text-sm font-medium group-has-[>svg]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3",
32
- className,
33
- )}
30
+ className={cn("text-sm font-medium group-has-[>svg]/alert:col-start-2 [&_a]:hover:text-foreground", className)}
34
31
  {...props}
35
32
  />
36
33
  );
@@ -41,7 +38,7 @@ function AlertDescription({ className, ...props }: React.ComponentProps<"div">)
41
38
  <div
42
39
  data-slot="alert-description"
43
40
  className={cn(
44
- "text-muted-foreground text-xs/relaxed text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3",
41
+ "text-muted-foreground text-xs/relaxed text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground",
45
42
  className,
46
43
  )}
47
44
  {...props}
@@ -16,7 +16,7 @@ const badgeVariants = cva(
16
16
  outline:
17
17
  "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground bg-input/20 dark:bg-input/30",
18
18
  ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
19
- link: "text-primary underline-offset-4 hover:underline",
19
+ link: "text-primary",
20
20
  warning: "bg-warning text-warning-foreground [a]:hover:bg-warning/80",
21
21
  softGreen: "bg-green-500/10 text-green-700 dark:bg-green-500/15 dark:text-green-400",
22
22
  softRed: "bg-red-500/10 text-red-700 dark:bg-red-500/15 dark:text-red-400",
@@ -17,7 +17,7 @@ const buttonVariants = cva(
17
17
  "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
18
18
  destructive:
19
19
  "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
20
- link: "text-primary underline-offset-4 hover:underline",
20
+ link: "text-primary",
21
21
  },
22
22
  size: {
23
23
  default:
@@ -102,10 +102,7 @@ function DialogDescription({ className, ...props }: DialogPrimitive.Description.
102
102
  return (
103
103
  <DialogPrimitive.Description
104
104
  data-slot="dialog-description"
105
- className={cn(
106
- "text-muted-foreground *:[a]:hover:text-foreground text-xs/relaxed *:[a]:underline *:[a]:underline-offset-3",
107
- className,
108
- )}
105
+ className={cn("text-muted-foreground *:[a]:hover:text-foreground text-xs/relaxed", className)}
109
106
  {...props}
110
107
  />
111
108
  );
@@ -123,7 +123,7 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
123
123
  className={cn(
124
124
  "text-muted-foreground text-left text-xs/relaxed [[data-variant=legend]+&]:-mt-1.5 leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
125
125
  "last:mt-0 nth-last-2:-mt-1",
126
- "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
126
+ "[&>a:hover]:text-primary",
127
127
  className,
128
128
  )}
129
129
  {...props}
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { validateCodiceFiscale, validateItalianTaxCode, validatePartitaIva } from "../italian-validators";
4
+
5
+ const VALID_CODICE_FISCALE = "RSSMRA85M01H501Q";
6
+ const VALID_PARTITA_IVA = "00743110157";
7
+
8
+ describe("validatePartitaIva", () => {
9
+ it("accepts a valid partita iva", () => {
10
+ expect(validatePartitaIva(VALID_PARTITA_IVA)).toBe(true);
11
+ });
12
+
13
+ it("rejects a partita iva with a wrong check digit", () => {
14
+ expect(validatePartitaIva("00743110158")).toBe(false);
15
+ });
16
+
17
+ it("rejects a codice fiscale", () => {
18
+ expect(validatePartitaIva(VALID_CODICE_FISCALE)).toBe(false);
19
+ });
20
+ });
21
+
22
+ describe("validateCodiceFiscale", () => {
23
+ it("accepts a valid codice fiscale", () => {
24
+ expect(validateCodiceFiscale(VALID_CODICE_FISCALE)).toBe(true);
25
+ });
26
+
27
+ it("rejects a partita iva by default", () => {
28
+ expect(validateCodiceFiscale(VALID_PARTITA_IVA)).toBe(false);
29
+ });
30
+
31
+ it("accepts a partita iva when allowPartitaIva is set", () => {
32
+ expect(validateCodiceFiscale(VALID_PARTITA_IVA, { allowPartitaIva: true })).toBe(true);
33
+ });
34
+
35
+ it("still accepts a codice fiscale when allowPartitaIva is set", () => {
36
+ expect(validateCodiceFiscale(VALID_CODICE_FISCALE, { allowPartitaIva: true })).toBe(true);
37
+ });
38
+
39
+ it("rejects a value that is neither, even when allowPartitaIva is set", () => {
40
+ expect(validateCodiceFiscale("NOT-A-CODE", { allowPartitaIva: true })).toBe(false);
41
+ expect(validateCodiceFiscale("00743110158", { allowPartitaIva: true })).toBe(false);
42
+ });
43
+
44
+ it("ignores whitespace", () => {
45
+ expect(validateCodiceFiscale("0074 3110 157", { allowPartitaIva: true })).toBe(true);
46
+ });
47
+
48
+ it("rejects empty values", () => {
49
+ expect(validateCodiceFiscale("", { allowPartitaIva: true })).toBe(false);
50
+ });
51
+ });
52
+
53
+ describe("validateItalianTaxCode", () => {
54
+ it("forwards the options to the codice fiscale validator", () => {
55
+ expect(validateItalianTaxCode(VALID_PARTITA_IVA, "codiceFiscale")).toBe(false);
56
+ expect(validateItalianTaxCode(VALID_PARTITA_IVA, "codiceFiscale", { allowPartitaIva: true })).toBe(true);
57
+ });
58
+
59
+ it("leaves partita iva validation unchanged", () => {
60
+ expect(validateItalianTaxCode(VALID_PARTITA_IVA, "partitaIva")).toBe(true);
61
+ expect(validateItalianTaxCode(VALID_CODICE_FISCALE, "partitaIva")).toBe(false);
62
+ });
63
+ });
@@ -12,6 +12,14 @@ export { getTableComponents, getTableOptions, TableOptions } from "./table-optio
12
12
  export { entityObjectSchema, userObjectSchema, type EntityObject, type UserObject } from "./schemas";
13
13
 
14
14
  export { getInitials } from "./getInitials";
15
+ export {
16
+ validatePartitaIva,
17
+ validateCodiceFiscale,
18
+ validateItalianTaxCode,
19
+ formatPartitaIva,
20
+ formatCodiceFiscale,
21
+ type CodiceFiscaleValidationOptions,
22
+ } from "./italian-validators";
15
23
  export * from "./blocknote-diff.util";
16
24
  export * from "./blocknote-word-diff-renderer.util";
17
25
  export * from "./icons";