@carlonicora/nextjs-jsonapi 6.10.2 → 6.11.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 (108) hide show
  1. package/dist/{BlockNoteEditor-7GQWHXBC.mjs → BlockNoteEditor-5WM5DX3J.mjs} +4 -4
  2. package/dist/{BlockNoteEditor-LX7NMBV6.js → BlockNoteEditor-GY5R25YI.js} +19 -19
  3. package/dist/{BlockNoteEditor-LX7NMBV6.js.map → BlockNoteEditor-GY5R25YI.js.map} +1 -1
  4. package/dist/{BlockNoteViewer-NXV5PKR4.js → BlockNoteViewer-FLUHEXE6.js} +3 -3
  5. package/dist/{BlockNoteViewer-NXV5PKR4.js.map → BlockNoteViewer-FLUHEXE6.js.map} +1 -1
  6. package/dist/{BlockNoteViewer-D3Y4FUST.mjs → BlockNoteViewer-UCA3LV3N.mjs} +2 -2
  7. package/dist/billing/index.js +491 -491
  8. package/dist/billing/index.mjs +3 -3
  9. package/dist/{chunk-LDUTS7SJ.mjs → chunk-3PRLRCMF.mjs} +1281 -603
  10. package/dist/chunk-3PRLRCMF.mjs.map +1 -0
  11. package/dist/{chunk-QB2XU64X.mjs → chunk-6RLATJ2L.mjs} +2 -2
  12. package/dist/{chunk-62P4KIR2.mjs → chunk-BGLOFZ3K.mjs} +282 -2
  13. package/dist/chunk-BGLOFZ3K.mjs.map +1 -0
  14. package/dist/{chunk-62VVBUIL.js → chunk-FNPXCYHO.js} +8 -8
  15. package/dist/{chunk-62VVBUIL.js.map → chunk-FNPXCYHO.js.map} +1 -1
  16. package/dist/{chunk-3I5JCFPY.js → chunk-MP3DV2I5.js} +1647 -969
  17. package/dist/chunk-MP3DV2I5.js.map +1 -0
  18. package/dist/{chunk-TOPGUZSX.js → chunk-OHJSI6L2.js} +4 -4
  19. package/dist/{chunk-TOPGUZSX.js.map → chunk-OHJSI6L2.js.map} +1 -1
  20. package/dist/{chunk-Z4OVEA3N.js → chunk-YGEOX2XX.js} +283 -3
  21. package/dist/chunk-YGEOX2XX.js.map +1 -0
  22. package/dist/{chunk-GHAZOQTT.mjs → chunk-YP6W4AB5.mjs} +2 -2
  23. package/dist/client/index.js +4 -4
  24. package/dist/client/index.mjs +3 -3
  25. package/dist/components/index.d.mts +67 -29
  26. package/dist/components/index.d.ts +67 -29
  27. package/dist/components/index.js +10 -4
  28. package/dist/components/index.js.map +1 -1
  29. package/dist/components/index.mjs +9 -3
  30. package/dist/contexts/index.js +4 -4
  31. package/dist/contexts/index.mjs +3 -3
  32. package/dist/core/index.d.mts +159 -3
  33. package/dist/core/index.d.ts +159 -3
  34. package/dist/core/index.js +14 -2
  35. package/dist/core/index.js.map +1 -1
  36. package/dist/core/index.mjs +13 -1
  37. package/dist/email-automations/index.js +93 -93
  38. package/dist/email-automations/index.mjs +3 -3
  39. package/dist/features/analytics/index.js +120 -122
  40. package/dist/features/analytics/index.js.map +1 -1
  41. package/dist/features/analytics/index.mjs +7 -9
  42. package/dist/features/analytics/index.mjs.map +1 -1
  43. package/dist/features/analytics/server.js +4 -4
  44. package/dist/features/analytics/server.mjs +2 -2
  45. package/dist/features/blog/index.js +150 -150
  46. package/dist/features/blog/index.mjs +3 -3
  47. package/dist/features/blog/server.js +16 -16
  48. package/dist/features/blog/server.mjs +1 -1
  49. package/dist/features/help/index.js +37 -37
  50. package/dist/features/help/index.mjs +3 -3
  51. package/dist/features/tokenusage/index.js +126 -128
  52. package/dist/features/tokenusage/index.js.map +1 -1
  53. package/dist/features/tokenusage/index.mjs +14 -16
  54. package/dist/features/tokenusage/index.mjs.map +1 -1
  55. package/dist/index.d.mts +2 -2
  56. package/dist/index.d.ts +2 -2
  57. package/dist/index.js +15 -3
  58. package/dist/index.js.map +1 -1
  59. package/dist/index.mjs +14 -2
  60. package/dist/{HandbookSectionInterface-5I57rC1b.d.mts → referral-admin.interface-C87ApGMR.d.mts} +41 -1
  61. package/dist/{HandbookSectionInterface-IDYEzQMi.d.ts → referral-admin.interface-CMwl5NiV.d.ts} +41 -1
  62. package/dist/server/index.js +3 -3
  63. package/dist/server/index.mjs +2 -2
  64. package/messages/en.json +78 -1
  65. package/messages/it.json +78 -1
  66. package/package.json +1 -1
  67. package/src/core/index.ts +2 -0
  68. package/src/core/registry/ModuleRegistry.ts +5 -0
  69. package/src/features/referral/__tests__/referral-admin-models.spec.ts +122 -0
  70. package/src/features/referral/__tests__/referral-admin-service.spec.ts +70 -0
  71. package/src/features/referral/__tests__/referral-code-capture.spec.tsx +105 -0
  72. package/src/features/referral/__tests__/referral-invite-errors.spec.tsx +82 -0
  73. package/src/features/referral/__tests__/referral-visit-service.spec.ts +47 -0
  74. package/src/features/referral/__tests__/referrals-admin-container.spec.tsx +230 -0
  75. package/src/features/referral/components/ReferralCodeCapture.tsx +3 -0
  76. package/src/features/referral/components/ReferralDialog.tsx +9 -13
  77. package/src/features/referral/components/ReferralWidget.tsx +75 -45
  78. package/src/features/referral/components/ReferralsAdminFunnel.tsx +74 -0
  79. package/src/features/referral/components/ReferralsAdminTiles.tsx +128 -0
  80. package/src/features/referral/components/containers/ReferralsAdminContainer.tsx +205 -0
  81. package/src/features/referral/components/index.ts +3 -0
  82. package/src/features/referral/data/ReferralAdminReferral.ts +65 -0
  83. package/src/features/referral/data/ReferralAdminReferrer.ts +65 -0
  84. package/src/features/referral/data/ReferralAdminService.ts +51 -0
  85. package/src/features/referral/data/ReferralAdminSummary.ts +60 -0
  86. package/src/features/referral/data/ReferralService.ts +20 -0
  87. package/src/features/referral/data/ReferralVisit.ts +23 -0
  88. package/src/features/referral/data/index.ts +5 -0
  89. package/src/features/referral/hooks/index.ts +2 -0
  90. package/src/features/referral/hooks/useReferralAdminReferralTableStructure.tsx +127 -0
  91. package/src/features/referral/hooks/useReferralAdminReferrerTableStructure.tsx +229 -0
  92. package/src/features/referral/hooks/useReferralInvite.ts +2 -0
  93. package/src/features/referral/index.ts +1 -0
  94. package/src/features/referral/interfaces/index.ts +1 -0
  95. package/src/features/referral/interfaces/referral-admin.interface.ts +45 -0
  96. package/src/features/referral/referral-admin.modules.ts +32 -0
  97. package/dist/chunk-3I5JCFPY.js.map +0 -1
  98. package/dist/chunk-62P4KIR2.mjs.map +0 -1
  99. package/dist/chunk-D6UX37IA.mjs +0 -115
  100. package/dist/chunk-D6UX37IA.mjs.map +0 -1
  101. package/dist/chunk-KEHP2OBQ.js +0 -115
  102. package/dist/chunk-KEHP2OBQ.js.map +0 -1
  103. package/dist/chunk-LDUTS7SJ.mjs.map +0 -1
  104. package/dist/chunk-Z4OVEA3N.js.map +0 -1
  105. /package/dist/{BlockNoteEditor-7GQWHXBC.mjs.map → BlockNoteEditor-5WM5DX3J.mjs.map} +0 -0
  106. /package/dist/{BlockNoteViewer-D3Y4FUST.mjs.map → BlockNoteViewer-UCA3LV3N.mjs.map} +0 -0
  107. /package/dist/{chunk-QB2XU64X.mjs.map → chunk-6RLATJ2L.mjs.map} +0 -0
  108. /package/dist/{chunk-GHAZOQTT.mjs.map → chunk-YP6W4AB5.mjs.map} +0 -0
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { MicroLabel } from "../../../components/typography/MicroLabel";
5
+ import { Card, CardContent } from "../../../shadcnui";
6
+ import { useUsageFormatters } from "../../tokenusage/lib/formatters";
7
+ import { percentageDelta } from "../../tokenusage/lib/metrics";
8
+ import { ReferralAdminSummaryInterface } from "../interfaces/referral-admin.interface";
9
+
10
+ type Props = {
11
+ /** The summary rows: `current`, plus `previous` unless the period is `all`. */
12
+ summary: ReferralAdminSummaryInterface[];
13
+ };
14
+
15
+ type Metric =
16
+ "visits" | "invitesSent" | "signups" | "paying" | "creditsGiven" | "sharingCompanies" | "eligibleCompanies";
17
+
18
+ const ZERO: Record<Metric, number> = {
19
+ visits: 0,
20
+ invitesSent: 0,
21
+ signups: 0,
22
+ paying: 0,
23
+ creditsGiven: 0,
24
+ sharingCompanies: 0,
25
+ eligibleCompanies: 0,
26
+ };
27
+
28
+ /**
29
+ * The KPI row of the administrative referral page. Each tile reads the
30
+ * `current` window; the change line compares it with the `previous` one and is
31
+ * left out when there is no previous window (the `all` period).
32
+ */
33
+ export function ReferralsAdminTiles({ summary }: Props) {
34
+ const t = useTranslations();
35
+ const { decimal } = useUsageFormatters();
36
+
37
+ const current = summary.find((row) => row.window === "current") ?? ZERO;
38
+ const previous = summary.find((row) => row.window === "previous");
39
+
40
+ const change = (metric: Metric): string | undefined => {
41
+ if (!previous) return undefined;
42
+ const delta = percentageDelta(current[metric], previous[metric]);
43
+ const formatted = delta === undefined ? "—" : `${delta > 0 ? "+" : ""}${decimal(delta, 0)}%`;
44
+ return t("referral.admin.tiles.change", { delta: formatted });
45
+ };
46
+
47
+ return (
48
+ <div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(0,4fr)]">
49
+ <LeadTile
50
+ id="sharing"
51
+ label={t("referral.admin.tiles.sharing")}
52
+ value={decimal(current.sharingCompanies, 0)}
53
+ detail={t("referral.admin.tiles.sharingOf", {
54
+ count: decimal(current.sharingCompanies, 0),
55
+ total: decimal(current.eligibleCompanies, 0),
56
+ })}
57
+ />
58
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
59
+ <SupportingTile
60
+ id="visits"
61
+ label={t("referral.admin.tiles.visits")}
62
+ value={decimal(current.visits, 0)}
63
+ change={change("visits")}
64
+ />
65
+ <SupportingTile
66
+ id="invites"
67
+ label={t("referral.admin.tiles.invites")}
68
+ value={decimal(current.invitesSent, 0)}
69
+ change={change("invitesSent")}
70
+ />
71
+ <SupportingTile
72
+ id="signups"
73
+ label={t("referral.admin.tiles.signups")}
74
+ value={decimal(current.signups, 0)}
75
+ change={change("signups")}
76
+ />
77
+ <SupportingTile
78
+ id="paying"
79
+ label={t("referral.admin.tiles.paying")}
80
+ value={decimal(current.paying, 0)}
81
+ change={change("paying")}
82
+ />
83
+ <SupportingTile
84
+ id="credits"
85
+ label={t("referral.admin.tiles.credits")}
86
+ value={decimal(current.creditsGiven, 0)}
87
+ change={change("creditsGiven")}
88
+ />
89
+ </div>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ /** The lead tile: the same card and recipe as AnalyticsAdminTiles' LeadTile. */
95
+ function LeadTile({ id, label, value, detail }: { id: string; label: string; value: string; detail: string }) {
96
+ return (
97
+ <Card data-testid={`referrals-tile-${id}`}>
98
+ <CardContent className="grid gap-1">
99
+ <MicroLabel>{label}</MicroLabel>
100
+ <span data-testid={`referrals-tile-${id}-value`} className="text-primary text-xl font-semibold tabular-nums">
101
+ {value}
102
+ </span>
103
+ <span className="text-muted-foreground text-xs">{detail}</span>
104
+ </CardContent>
105
+ </Card>
106
+ );
107
+ }
108
+
109
+ /** A supporting tile: the same card and recipe as AnalyticsAdminTiles' SupportingTile. */
110
+ function SupportingTile({ id, label, value, change }: { id: string; label: string; value: string; change?: string }) {
111
+ return (
112
+ <Card data-testid={`referrals-tile-${id}`} size="sm">
113
+ <CardContent className="grid gap-1">
114
+ <MicroLabel>{label}</MicroLabel>
115
+ <span className="flex items-center gap-2">
116
+ <span data-testid={`referrals-tile-${id}-value`} className="text-sm font-medium tabular-nums">
117
+ {value}
118
+ </span>
119
+ {change && (
120
+ <span data-testid={`referrals-tile-${id}-change`} className="text-muted-foreground text-xs">
121
+ {change}
122
+ </span>
123
+ )}
124
+ </span>
125
+ </CardContent>
126
+ </Card>
127
+ );
128
+ }
@@ -0,0 +1,205 @@
1
+ "use client";
2
+
3
+ import { CopyIcon } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+ import { useEffect, useMemo, useRef, useState } from "react";
6
+ import { ContentListTable, RoundPageContainer } from "../../../../components";
7
+ import { SharedProvider } from "../../../../contexts/SharedContext";
8
+ import { Modules } from "../../../../core";
9
+ import { DataListRetriever, useDataListRetriever } from "../../../../hooks";
10
+ import { BreadcrumbItemData } from "../../../../interfaces";
11
+ import { Button } from "../../../../shadcnui";
12
+ import { ReferralAdminService } from "../../data/ReferralAdminService";
13
+ // Importing the field enums (runtime values) also runs the hook modules, which
14
+ // is what registers the table generators ContentListTable looks up.
15
+ import { ReferralAdminReferralFields } from "../../hooks/useReferralAdminReferralTableStructure";
16
+ import {
17
+ copyReferrerEmails,
18
+ ReferralAdminReferrerFields,
19
+ ReferralAdminReferrerTableContext,
20
+ } from "../../hooks/useReferralAdminReferrerTableStructure";
21
+ import {
22
+ ReferralAdminPeriod,
23
+ ReferralAdminReferralInterface,
24
+ ReferralAdminReferrerInterface,
25
+ ReferralAdminSort,
26
+ ReferralAdminSummaryInterface,
27
+ } from "../../interfaces/referral-admin.interface";
28
+ import { ReferralsAdminFunnel } from "../ReferralsAdminFunnel";
29
+ import { ReferralsAdminTiles } from "../ReferralsAdminTiles";
30
+
31
+ const PERIODS: ReferralAdminPeriod[] = ["30d", "90d", "all"];
32
+
33
+ const REFERRER_FIELDS = [
34
+ ReferralAdminReferrerFields.rank,
35
+ ReferralAdminReferrerFields.referrer,
36
+ ReferralAdminReferrerFields.visits,
37
+ ReferralAdminReferrerFields.invites,
38
+ ReferralAdminReferrerFields.signups,
39
+ ReferralAdminReferrerFields.paying,
40
+ ReferralAdminReferrerFields.conversion,
41
+ ReferralAdminReferrerFields.credits,
42
+ ReferralAdminReferrerFields.lastInvite,
43
+ ];
44
+
45
+ const REFERRAL_FIELDS = [
46
+ ReferralAdminReferralFields.account,
47
+ ReferralAdminReferralFields.referrer,
48
+ ReferralAdminReferralFields.source,
49
+ ReferralAdminReferralFields.date,
50
+ ReferralAdminReferralFields.status,
51
+ ReferralAdminReferralFields.credits,
52
+ ];
53
+
54
+ /**
55
+ * Pushes a changed parameter into a list retriever. The retriever reads its
56
+ * `retrieverParams` only on the first load; a later change goes through
57
+ * `addAdditionalParameter`, which re-fetches and keeps the old rows on screen
58
+ * until the new ones arrive. The first render is skipped: the initial load
59
+ * already carries the value.
60
+ */
61
+ function useRefetchOnChange(retriever: DataListRetriever<unknown>, key: string, value: string) {
62
+ const previous = useRef(value);
63
+ useEffect(() => {
64
+ if (previous.current === value) return;
65
+ previous.current = value;
66
+ retriever.addAdditionalParameter(key, value);
67
+ }, [retriever, key, value]);
68
+ }
69
+
70
+ /**
71
+ * Administrative referral report: period buttons in the title bar, the KPI
72
+ * tiles, the visit-to-paying funnel, the referrers ranking and every referral.
73
+ * Client component by necessity: `module={Modules.ReferralAdminSummary}` is a
74
+ * registry entry carrying an icon component, which a Server Component cannot
75
+ * pass across the boundary.
76
+ */
77
+ export function ReferralsAdminContainer() {
78
+ const t = useTranslations();
79
+ const [period, setPeriod] = useState<ReferralAdminPeriod>("30d");
80
+ const [sort, setSort] = useState<ReferralAdminSort>("paying");
81
+ const [summary, setSummary] = useState<ReferralAdminSummaryInterface[]>([]);
82
+
83
+ useEffect(() => {
84
+ let cancelled = false;
85
+ ReferralAdminService.getSummary({ period })
86
+ .then((rows) => {
87
+ if (!cancelled) setSummary(rows ?? []);
88
+ })
89
+ .catch(() => {
90
+ if (!cancelled) setSummary([]);
91
+ });
92
+ return () => {
93
+ cancelled = true;
94
+ };
95
+ }, [period]);
96
+
97
+ const referrers: DataListRetriever<ReferralAdminReferrerInterface> = useDataListRetriever({
98
+ module: Modules.ReferralAdminReferrer,
99
+ retriever: (params: { period: ReferralAdminPeriod; sort: ReferralAdminSort }) =>
100
+ ReferralAdminService.getReferrers({ period: params.period, sort: params.sort }),
101
+ retrieverParams: { period, sort },
102
+ });
103
+
104
+ const referrals: DataListRetriever<ReferralAdminReferralInterface> = useDataListRetriever({
105
+ module: Modules.ReferralAdminReferral,
106
+ retriever: (params: { period: ReferralAdminPeriod }) =>
107
+ ReferralAdminService.getReferrals({ period: params.period }),
108
+ retrieverParams: { period },
109
+ });
110
+
111
+ useRefetchOnChange(referrers as DataListRetriever<unknown>, "period", period);
112
+ useRefetchOnChange(referrers as DataListRetriever<unknown>, "sort", sort);
113
+ useRefetchOnChange(referrals as DataListRetriever<unknown>, "period", period);
114
+
115
+ const referrerContext = useMemo<ReferralAdminReferrerTableContext>(() => ({ sort, onSort: setSort }), [sort]);
116
+
117
+ const periodLabels: Record<ReferralAdminPeriod, string> = {
118
+ "30d": t("referral.admin.period.30d"),
119
+ "90d": t("referral.admin.period.90d"),
120
+ all: t("referral.admin.period.all"),
121
+ };
122
+
123
+ const periodButtons = (
124
+ <div
125
+ key="referralsAdminPeriod"
126
+ role="group"
127
+ className="border-border inline-flex items-center gap-0.5 rounded-md border p-0.5"
128
+ >
129
+ {PERIODS.map((value) => {
130
+ const selected = value === period;
131
+ return (
132
+ <Button
133
+ key={value}
134
+ type="button"
135
+ size="sm"
136
+ variant={selected ? "default" : "ghost"}
137
+ aria-pressed={selected}
138
+ data-testid={`referrals-period-${value}`}
139
+ onClick={() => setPeriod(value)}
140
+ >
141
+ {periodLabels[value]}
142
+ </Button>
143
+ );
144
+ })}
145
+ </div>
146
+ );
147
+
148
+ const shownEmails = (referrers.data ?? []).map((entry) => entry.email).filter((email) => !!email);
149
+
150
+ const copyAll = (
151
+ <Button
152
+ type="button"
153
+ variant="outline"
154
+ size="sm"
155
+ data-testid="referrals-copy-all-emails"
156
+ disabled={shownEmails.length === 0}
157
+ onClick={() => copyReferrerEmails(shownEmails.join(", "), t)}
158
+ >
159
+ <CopyIcon />
160
+ {t("referral.admin.referrers.copyAll")}
161
+ </Button>
162
+ );
163
+
164
+ const breadcrumbs: BreadcrumbItemData[] = [{ name: t("referral.admin.title") }];
165
+
166
+ return (
167
+ <SharedProvider value={{ breadcrumbs, title: { type: t("referral.admin.title"), functions: periodButtons } }}>
168
+ <RoundPageContainer module={Modules.ReferralAdminSummary} fullWidth forceHeader testId="page-admin-referrals">
169
+ <div className="flex w-full flex-col gap-6 p-4">
170
+ <p className="text-muted-foreground text-xs">{t("referral.admin.description")}</p>
171
+
172
+ <ReferralsAdminTiles summary={summary} />
173
+
174
+ <ReferralsAdminFunnel summary={summary} />
175
+
176
+ <section data-testid="referrals-referrers">
177
+ <ContentListTable
178
+ data={referrers}
179
+ fields={REFERRER_FIELDS}
180
+ tableGeneratorType={Modules.ReferralAdminReferrer}
181
+ context={referrerContext}
182
+ title={t("referral.admin.referrers.title")}
183
+ titleActions={copyAll}
184
+ hidePagination
185
+ allowSearch={false}
186
+ emptyState={t("referral.admin.referrers.empty")}
187
+ />
188
+ </section>
189
+
190
+ <section data-testid="referrals-referrals">
191
+ <ContentListTable
192
+ data={referrals}
193
+ fields={REFERRAL_FIELDS}
194
+ tableGeneratorType={Modules.ReferralAdminReferral}
195
+ title={t("referral.admin.referrals.title")}
196
+ hidePagination
197
+ allowSearch={false}
198
+ emptyState={t("referral.admin.referrals.empty")}
199
+ />
200
+ </section>
201
+ </div>
202
+ </RoundPageContainer>
203
+ </SharedProvider>
204
+ );
205
+ }
@@ -1,3 +1,6 @@
1
1
  export { ReferralCodeCapture } from "./ReferralCodeCapture";
2
2
  export { ReferralDialog, type ReferralDialogProps, type ReferralDialogTranslations } from "./ReferralDialog";
3
3
  export { ReferralWidget, type ReferralWidgetProps, type ReferralWidgetTranslations } from "./ReferralWidget";
4
+ export { ReferralsAdminContainer } from "./containers/ReferralsAdminContainer";
5
+ export { ReferralsAdminTiles } from "./ReferralsAdminTiles";
6
+ export { ReferralsAdminFunnel } from "./ReferralsAdminFunnel";
@@ -0,0 +1,65 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
2
+ import { ReferralAdminReferralInterface } from "../interfaces/referral-admin.interface";
3
+
4
+ /** One referral in the administrative list: the new account and who brought it. Read-only. */
5
+ export class ReferralAdminReferral extends AbstractApiData implements ReferralAdminReferralInterface {
6
+ private _name?: string;
7
+ private _email?: string;
8
+ private _referrerName?: string;
9
+ private _referrerEmail?: string;
10
+ private _source?: string;
11
+ private _status?: string;
12
+ private _referredAt?: Date;
13
+ private _completedAt?: Date;
14
+ private _tokensAwarded?: number;
15
+
16
+ get name(): string {
17
+ return this._name ?? "";
18
+ }
19
+ get email(): string {
20
+ return this._email ?? "";
21
+ }
22
+ get referrerName(): string {
23
+ return this._referrerName ?? "";
24
+ }
25
+ get referrerEmail(): string {
26
+ return this._referrerEmail ?? "";
27
+ }
28
+ get source(): string {
29
+ return this._source ?? "";
30
+ }
31
+ get status(): string {
32
+ return this._status ?? "";
33
+ }
34
+ get referredAt(): Date {
35
+ return this._referredAt ?? new Date(0);
36
+ }
37
+ get completedAt(): Date | undefined {
38
+ return this._completedAt;
39
+ }
40
+ get tokensAwarded(): number | undefined {
41
+ return this._tokensAwarded;
42
+ }
43
+
44
+ rehydrate(data: JsonApiHydratedDataInterface): this {
45
+ super.rehydrate(data);
46
+
47
+ const attrs = data.jsonApi.attributes;
48
+ this._name = attrs.name;
49
+ this._email = attrs.email;
50
+ this._referrerName = attrs.referrerName;
51
+ this._referrerEmail = attrs.referrerEmail;
52
+ this._source = attrs.source;
53
+ this._status = attrs.status;
54
+ this._referredAt = attrs.referredAt ? new Date(attrs.referredAt) : undefined;
55
+ this._completedAt = attrs.completedAt ? new Date(attrs.completedAt) : undefined;
56
+ this._tokensAwarded = attrs.tokensAwarded ?? undefined;
57
+
58
+ return this;
59
+ }
60
+
61
+ /** The resource is read-only: there are no attributes to send. */
62
+ createJsonApi(data: { id: string }) {
63
+ return { data: { type: Modules.ReferralAdminReferral.name, id: data.id, attributes: {} } };
64
+ }
65
+ }
@@ -0,0 +1,65 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
2
+ import { ReferralAdminReferrerInterface } from "../interfaces/referral-admin.interface";
3
+
4
+ /** One referrer company in the referral ranking. Read-only. */
5
+ export class ReferralAdminReferrer extends AbstractApiData implements ReferralAdminReferrerInterface {
6
+ private _name?: string;
7
+ private _email?: string;
8
+ private _visits?: number;
9
+ private _invitesSent?: number;
10
+ private _signupsLink?: number;
11
+ private _signupsDirect?: number;
12
+ private _paying?: number;
13
+ private _creditsEarned?: number;
14
+ private _lastInviteAt?: Date;
15
+
16
+ get name(): string {
17
+ return this._name ?? "";
18
+ }
19
+ get email(): string {
20
+ return this._email ?? "";
21
+ }
22
+ get visits(): number {
23
+ return this._visits ?? 0;
24
+ }
25
+ get invitesSent(): number {
26
+ return this._invitesSent ?? 0;
27
+ }
28
+ get signupsLink(): number {
29
+ return this._signupsLink ?? 0;
30
+ }
31
+ get signupsDirect(): number {
32
+ return this._signupsDirect ?? 0;
33
+ }
34
+ get paying(): number {
35
+ return this._paying ?? 0;
36
+ }
37
+ get creditsEarned(): number {
38
+ return this._creditsEarned ?? 0;
39
+ }
40
+ get lastInviteAt(): Date | undefined {
41
+ return this._lastInviteAt;
42
+ }
43
+
44
+ rehydrate(data: JsonApiHydratedDataInterface): this {
45
+ super.rehydrate(data);
46
+
47
+ const attrs = data.jsonApi.attributes;
48
+ this._name = attrs.name;
49
+ this._email = attrs.email;
50
+ this._visits = attrs.visits;
51
+ this._invitesSent = attrs.invitesSent;
52
+ this._signupsLink = attrs.signupsLink;
53
+ this._signupsDirect = attrs.signupsDirect;
54
+ this._paying = attrs.paying;
55
+ this._creditsEarned = attrs.creditsEarned;
56
+ this._lastInviteAt = attrs.lastInviteAt ? new Date(attrs.lastInviteAt) : undefined;
57
+
58
+ return this;
59
+ }
60
+
61
+ /** The resource is read-only: there are no attributes to send. */
62
+ createJsonApi(data: { id: string }) {
63
+ return { data: { type: Modules.ReferralAdminReferrer.name, id: data.id, attributes: {} } };
64
+ }
65
+ }
@@ -0,0 +1,51 @@
1
+ import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
2
+ import {
3
+ ReferralAdminPeriod,
4
+ ReferralAdminReferralInterface,
5
+ ReferralAdminReferrerInterface,
6
+ ReferralAdminSort,
7
+ ReferralAdminSummaryInterface,
8
+ } from "../interfaces/referral-admin.interface";
9
+
10
+ /** Reads of the administrative referral report. Every route is administrator-only. */
11
+ export class ReferralAdminService extends AbstractService {
12
+ /** Headline tiles: one row per window (`current`, plus `previous` unless the period is `all`). */
13
+ static async getSummary(params: { period: ReferralAdminPeriod }): Promise<ReferralAdminSummaryInterface[]> {
14
+ const endpoint = new EndpointCreator({ endpoint: Modules.ReferralAdminSummary });
15
+ endpoint.addAdditionalParam("period", params.period);
16
+
17
+ return this.callApi<ReferralAdminSummaryInterface[]>({
18
+ type: Modules.ReferralAdminSummary,
19
+ method: HttpMethod.GET,
20
+ endpoint: endpoint.generate(),
21
+ });
22
+ }
23
+
24
+ /** The referrers ranking, ordered by the allowlisted sort. */
25
+ static async getReferrers(params: {
26
+ period: ReferralAdminPeriod;
27
+ sort: ReferralAdminSort;
28
+ }): Promise<ReferralAdminReferrerInterface[]> {
29
+ const endpoint = new EndpointCreator({ endpoint: Modules.ReferralAdminReferrer });
30
+ endpoint.addAdditionalParam("period", params.period);
31
+ endpoint.addAdditionalParam("sort", params.sort);
32
+
33
+ return this.callApi<ReferralAdminReferrerInterface[]>({
34
+ type: Modules.ReferralAdminReferrer,
35
+ method: HttpMethod.GET,
36
+ endpoint: endpoint.generate(),
37
+ });
38
+ }
39
+
40
+ /** Every referral in the period, newest first. */
41
+ static async getReferrals(params: { period: ReferralAdminPeriod }): Promise<ReferralAdminReferralInterface[]> {
42
+ const endpoint = new EndpointCreator({ endpoint: Modules.ReferralAdminReferral });
43
+ endpoint.addAdditionalParam("period", params.period);
44
+
45
+ return this.callApi<ReferralAdminReferralInterface[]>({
46
+ type: Modules.ReferralAdminReferral,
47
+ method: HttpMethod.GET,
48
+ endpoint: endpoint.generate(),
49
+ });
50
+ }
51
+ }
@@ -0,0 +1,60 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
2
+ import { ReferralAdminSummaryInterface } from "../interfaces/referral-admin.interface";
3
+
4
+ /** One window of the referral report tiles: `current` or `previous`. Read-only. */
5
+ export class ReferralAdminSummary extends AbstractApiData implements ReferralAdminSummaryInterface {
6
+ private _window?: string;
7
+ private _visits?: number;
8
+ private _invitesSent?: number;
9
+ private _signups?: number;
10
+ private _paying?: number;
11
+ private _creditsGiven?: number;
12
+ private _sharingCompanies?: number;
13
+ private _eligibleCompanies?: number;
14
+
15
+ get window(): string {
16
+ return this._window ?? "";
17
+ }
18
+ get visits(): number {
19
+ return this._visits ?? 0;
20
+ }
21
+ get invitesSent(): number {
22
+ return this._invitesSent ?? 0;
23
+ }
24
+ get signups(): number {
25
+ return this._signups ?? 0;
26
+ }
27
+ get paying(): number {
28
+ return this._paying ?? 0;
29
+ }
30
+ get creditsGiven(): number {
31
+ return this._creditsGiven ?? 0;
32
+ }
33
+ get sharingCompanies(): number {
34
+ return this._sharingCompanies ?? 0;
35
+ }
36
+ get eligibleCompanies(): number {
37
+ return this._eligibleCompanies ?? 0;
38
+ }
39
+
40
+ rehydrate(data: JsonApiHydratedDataInterface): this {
41
+ super.rehydrate(data);
42
+
43
+ const attrs = data.jsonApi.attributes;
44
+ this._window = attrs.window;
45
+ this._visits = attrs.visits;
46
+ this._invitesSent = attrs.invitesSent;
47
+ this._signups = attrs.signups;
48
+ this._paying = attrs.paying;
49
+ this._creditsGiven = attrs.creditsGiven;
50
+ this._sharingCompanies = attrs.sharingCompanies;
51
+ this._eligibleCompanies = attrs.eligibleCompanies;
52
+
53
+ return this;
54
+ }
55
+
56
+ /** The resource is read-only: there are no attributes to send. */
57
+ createJsonApi(data: { id: string }) {
58
+ return { data: { type: Modules.ReferralAdminSummary.name, id: data.id, attributes: {} } };
59
+ }
60
+ }
@@ -33,6 +33,26 @@ export class ReferralService extends AbstractService {
33
33
  endpoint: endpoint.generate(),
34
34
  input: { email },
35
35
  overridesJsonApiCreation: true,
36
+ // The widget turns the error code into a translated message; the global
37
+ // handler would show the API's English text and swallow the failure.
38
+ suppressGlobalError: true,
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Count one visit to a referral link. Answers 204 whatever the code.
44
+ */
45
+ static async recordVisit(params: { referralCode: string }): Promise<void> {
46
+ const endpoint = new EndpointCreator({ endpoint: Modules.ReferralVisit });
47
+
48
+ await this.callApi<void>({
49
+ type: Modules.ReferralVisit,
50
+ method: HttpMethod.POST,
51
+ endpoint: endpoint.generate(),
52
+ // The request layer builds the body through ReferralVisit.createJsonApi().
53
+ input: { referralCode: params.referralCode },
54
+ // A failed beacon must never show an error to a visitor.
55
+ suppressGlobalError: true,
36
56
  });
37
57
  }
38
58
  }
@@ -0,0 +1,23 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
2
+
3
+ /**
4
+ * One visit to a referral link, posted once per browser when the referral
5
+ * cookie is first set. The API answers 204, so there is nothing to read back.
6
+ * The body carries no id: the server generates the visit id.
7
+ */
8
+ export class ReferralVisit extends AbstractApiData {
9
+ rehydrate(data: JsonApiHydratedDataInterface): this {
10
+ super.rehydrate(data);
11
+
12
+ return this;
13
+ }
14
+
15
+ createJsonApi(data: { referralCode: string }) {
16
+ return {
17
+ data: {
18
+ type: Modules.ReferralVisit.name,
19
+ attributes: { referralCode: data.referralCode },
20
+ },
21
+ };
22
+ }
23
+ }
@@ -1,2 +1,7 @@
1
1
  export * from "./ReferralStats";
2
2
  export * from "./ReferralService";
3
+ export * from "./ReferralVisit";
4
+ export * from "./ReferralAdminSummary";
5
+ export * from "./ReferralAdminReferrer";
6
+ export * from "./ReferralAdminReferral";
7
+ export * from "./ReferralAdminService";
@@ -1,2 +1,4 @@
1
1
  export * from "./useReferralStats";
2
2
  export * from "./useReferralInvite";
3
+ export * from "./useReferralAdminReferrerTableStructure";
4
+ export * from "./useReferralAdminReferralTableStructure";