@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.
- package/dist/{BlockNoteEditor-7GQWHXBC.mjs → BlockNoteEditor-5WM5DX3J.mjs} +4 -4
- package/dist/{BlockNoteEditor-LX7NMBV6.js → BlockNoteEditor-GY5R25YI.js} +19 -19
- package/dist/{BlockNoteEditor-LX7NMBV6.js.map → BlockNoteEditor-GY5R25YI.js.map} +1 -1
- package/dist/{BlockNoteViewer-NXV5PKR4.js → BlockNoteViewer-FLUHEXE6.js} +3 -3
- package/dist/{BlockNoteViewer-NXV5PKR4.js.map → BlockNoteViewer-FLUHEXE6.js.map} +1 -1
- package/dist/{BlockNoteViewer-D3Y4FUST.mjs → BlockNoteViewer-UCA3LV3N.mjs} +2 -2
- package/dist/billing/index.js +491 -491
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-LDUTS7SJ.mjs → chunk-3PRLRCMF.mjs} +1281 -603
- package/dist/chunk-3PRLRCMF.mjs.map +1 -0
- package/dist/{chunk-QB2XU64X.mjs → chunk-6RLATJ2L.mjs} +2 -2
- package/dist/{chunk-62P4KIR2.mjs → chunk-BGLOFZ3K.mjs} +282 -2
- package/dist/chunk-BGLOFZ3K.mjs.map +1 -0
- package/dist/{chunk-62VVBUIL.js → chunk-FNPXCYHO.js} +8 -8
- package/dist/{chunk-62VVBUIL.js.map → chunk-FNPXCYHO.js.map} +1 -1
- package/dist/{chunk-3I5JCFPY.js → chunk-MP3DV2I5.js} +1647 -969
- package/dist/chunk-MP3DV2I5.js.map +1 -0
- package/dist/{chunk-TOPGUZSX.js → chunk-OHJSI6L2.js} +4 -4
- package/dist/{chunk-TOPGUZSX.js.map → chunk-OHJSI6L2.js.map} +1 -1
- package/dist/{chunk-Z4OVEA3N.js → chunk-YGEOX2XX.js} +283 -3
- package/dist/chunk-YGEOX2XX.js.map +1 -0
- package/dist/{chunk-GHAZOQTT.mjs → chunk-YP6W4AB5.mjs} +2 -2
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +67 -29
- package/dist/components/index.d.ts +67 -29
- package/dist/components/index.js +10 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +9 -3
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +159 -3
- package/dist/core/index.d.ts +159 -3
- package/dist/core/index.js +14 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +13 -1
- package/dist/email-automations/index.js +93 -93
- package/dist/email-automations/index.mjs +3 -3
- package/dist/features/analytics/index.js +120 -122
- package/dist/features/analytics/index.js.map +1 -1
- package/dist/features/analytics/index.mjs +7 -9
- package/dist/features/analytics/index.mjs.map +1 -1
- package/dist/features/analytics/server.js +4 -4
- package/dist/features/analytics/server.mjs +2 -2
- package/dist/features/blog/index.js +150 -150
- package/dist/features/blog/index.mjs +3 -3
- package/dist/features/blog/server.js +16 -16
- package/dist/features/blog/server.mjs +1 -1
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.js +126 -128
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +14 -16
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +14 -2
- package/dist/{HandbookSectionInterface-5I57rC1b.d.mts → referral-admin.interface-C87ApGMR.d.mts} +41 -1
- package/dist/{HandbookSectionInterface-IDYEzQMi.d.ts → referral-admin.interface-CMwl5NiV.d.ts} +41 -1
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +2 -2
- package/messages/en.json +78 -1
- package/messages/it.json +78 -1
- package/package.json +1 -1
- package/src/core/index.ts +2 -0
- package/src/core/registry/ModuleRegistry.ts +5 -0
- package/src/features/referral/__tests__/referral-admin-models.spec.ts +122 -0
- package/src/features/referral/__tests__/referral-admin-service.spec.ts +70 -0
- package/src/features/referral/__tests__/referral-code-capture.spec.tsx +105 -0
- package/src/features/referral/__tests__/referral-invite-errors.spec.tsx +82 -0
- package/src/features/referral/__tests__/referral-visit-service.spec.ts +47 -0
- package/src/features/referral/__tests__/referrals-admin-container.spec.tsx +230 -0
- package/src/features/referral/components/ReferralCodeCapture.tsx +3 -0
- package/src/features/referral/components/ReferralDialog.tsx +9 -13
- package/src/features/referral/components/ReferralWidget.tsx +75 -45
- package/src/features/referral/components/ReferralsAdminFunnel.tsx +74 -0
- package/src/features/referral/components/ReferralsAdminTiles.tsx +128 -0
- package/src/features/referral/components/containers/ReferralsAdminContainer.tsx +205 -0
- package/src/features/referral/components/index.ts +3 -0
- package/src/features/referral/data/ReferralAdminReferral.ts +65 -0
- package/src/features/referral/data/ReferralAdminReferrer.ts +65 -0
- package/src/features/referral/data/ReferralAdminService.ts +51 -0
- package/src/features/referral/data/ReferralAdminSummary.ts +60 -0
- package/src/features/referral/data/ReferralService.ts +20 -0
- package/src/features/referral/data/ReferralVisit.ts +23 -0
- package/src/features/referral/data/index.ts +5 -0
- package/src/features/referral/hooks/index.ts +2 -0
- package/src/features/referral/hooks/useReferralAdminReferralTableStructure.tsx +127 -0
- package/src/features/referral/hooks/useReferralAdminReferrerTableStructure.tsx +229 -0
- package/src/features/referral/hooks/useReferralInvite.ts +2 -0
- package/src/features/referral/index.ts +1 -0
- package/src/features/referral/interfaces/index.ts +1 -0
- package/src/features/referral/interfaces/referral-admin.interface.ts +45 -0
- package/src/features/referral/referral-admin.modules.ts +32 -0
- package/dist/chunk-3I5JCFPY.js.map +0 -1
- package/dist/chunk-62P4KIR2.mjs.map +0 -1
- package/dist/chunk-D6UX37IA.mjs +0 -115
- package/dist/chunk-D6UX37IA.mjs.map +0 -1
- package/dist/chunk-KEHP2OBQ.js +0 -115
- package/dist/chunk-KEHP2OBQ.js.map +0 -1
- package/dist/chunk-LDUTS7SJ.mjs.map +0 -1
- package/dist/chunk-Z4OVEA3N.js.map +0 -1
- /package/dist/{BlockNoteEditor-7GQWHXBC.mjs.map → BlockNoteEditor-5WM5DX3J.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-D3Y4FUST.mjs.map → BlockNoteViewer-UCA3LV3N.mjs.map} +0 -0
- /package/dist/{chunk-QB2XU64X.mjs.map → chunk-6RLATJ2L.mjs.map} +0 -0
- /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";
|