@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,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ColumnDef } from "@tanstack/react-table";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
import { cellDate } from "../../../components/tables/cells/cell.date";
|
|
7
|
+
import { registerTableGenerator, TableContent, UseTableStructureHook } from "../../../hooks";
|
|
8
|
+
import { Badge } from "../../../shadcnui";
|
|
9
|
+
import { useUsageFormatters } from "../../tokenusage/lib/formatters";
|
|
10
|
+
import { ReferralAdminReferralInterface } from "../interfaces/referral-admin.interface";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Columns of "Every referral". The values double as the TanStack column ids:
|
|
14
|
+
* ContentListTable builds its `columnVisibility` from the `fields` array.
|
|
15
|
+
*/
|
|
16
|
+
export enum ReferralAdminReferralFields {
|
|
17
|
+
account = "account",
|
|
18
|
+
referrer = "referrer",
|
|
19
|
+
source = "source",
|
|
20
|
+
date = "date",
|
|
21
|
+
status = "status",
|
|
22
|
+
credits = "credits",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type Row = { row: { original: TableContent<ReferralAdminReferralInterface> } };
|
|
26
|
+
|
|
27
|
+
export const useReferralAdminReferralTableStructure: UseTableStructureHook<
|
|
28
|
+
ReferralAdminReferralInterface,
|
|
29
|
+
ReferralAdminReferralFields
|
|
30
|
+
> = (params) => {
|
|
31
|
+
const t = useTranslations();
|
|
32
|
+
const { decimal } = useUsageFormatters();
|
|
33
|
+
|
|
34
|
+
const tableData = useMemo(() => {
|
|
35
|
+
return params.data.map((entry: ReferralAdminReferralInterface) => {
|
|
36
|
+
const row: TableContent<ReferralAdminReferralInterface> = { jsonApiData: entry };
|
|
37
|
+
// cellDate reads the value from the row by the column name.
|
|
38
|
+
row[ReferralAdminReferralFields.date] = entry.referredAt;
|
|
39
|
+
return row;
|
|
40
|
+
});
|
|
41
|
+
}, [params.data]);
|
|
42
|
+
|
|
43
|
+
const fieldColumnMap: Record<ReferralAdminReferralFields, () => any> = {
|
|
44
|
+
[ReferralAdminReferralFields.account]: () => ({
|
|
45
|
+
id: ReferralAdminReferralFields.account,
|
|
46
|
+
header: t("referral.admin.referrals.columns.account"),
|
|
47
|
+
cell: ({ row }: Row) => {
|
|
48
|
+
const entry = row.original.jsonApiData;
|
|
49
|
+
return (
|
|
50
|
+
<div className="flex flex-col">
|
|
51
|
+
<span className="text-sm">{entry.name}</span>
|
|
52
|
+
<span className="text-muted-foreground text-xs">{entry.email}</span>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
enableSorting: false,
|
|
57
|
+
enableHiding: false,
|
|
58
|
+
}),
|
|
59
|
+
[ReferralAdminReferralFields.referrer]: () => ({
|
|
60
|
+
id: ReferralAdminReferralFields.referrer,
|
|
61
|
+
header: t("referral.admin.referrals.columns.referrer"),
|
|
62
|
+
cell: ({ row }: Row) => {
|
|
63
|
+
const entry = row.original.jsonApiData;
|
|
64
|
+
return (
|
|
65
|
+
<div className="flex flex-col">
|
|
66
|
+
<span className="text-sm">{entry.referrerName}</span>
|
|
67
|
+
<span className="text-muted-foreground text-xs">{entry.referrerEmail}</span>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
enableSorting: false,
|
|
72
|
+
enableHiding: false,
|
|
73
|
+
}),
|
|
74
|
+
[ReferralAdminReferralFields.source]: () => ({
|
|
75
|
+
id: ReferralAdminReferralFields.source,
|
|
76
|
+
header: t("referral.admin.referrals.columns.source"),
|
|
77
|
+
cell: ({ row }: Row) => {
|
|
78
|
+
const source = row.original.jsonApiData.source;
|
|
79
|
+
if (source === "link") return <Badge variant="softBlue">{t("referral.admin.source.link")}</Badge>;
|
|
80
|
+
if (source === "direct") return <Badge variant="softGray">{t("referral.admin.source.direct")}</Badge>;
|
|
81
|
+
// A referral written before the source was recorded: unknown, not guessed.
|
|
82
|
+
return <span className="text-muted-foreground text-xs">—</span>;
|
|
83
|
+
},
|
|
84
|
+
enableSorting: false,
|
|
85
|
+
enableHiding: false,
|
|
86
|
+
}),
|
|
87
|
+
[ReferralAdminReferralFields.date]: () =>
|
|
88
|
+
cellDate({ name: ReferralAdminReferralFields.date, title: t("referral.admin.referrals.columns.date") }),
|
|
89
|
+
[ReferralAdminReferralFields.status]: () => ({
|
|
90
|
+
id: ReferralAdminReferralFields.status,
|
|
91
|
+
header: t("referral.admin.referrals.columns.status"),
|
|
92
|
+
cell: ({ row }: Row) =>
|
|
93
|
+
row.original.jsonApiData.status === "completed" ? (
|
|
94
|
+
<Badge variant="softGreen">{t("referral.admin.status.completed")}</Badge>
|
|
95
|
+
) : (
|
|
96
|
+
<Badge variant="softYellow">{t("referral.admin.status.pending")}</Badge>
|
|
97
|
+
),
|
|
98
|
+
enableSorting: false,
|
|
99
|
+
enableHiding: false,
|
|
100
|
+
}),
|
|
101
|
+
[ReferralAdminReferralFields.credits]: () => ({
|
|
102
|
+
id: ReferralAdminReferralFields.credits,
|
|
103
|
+
header: t("referral.admin.referrals.columns.credits"),
|
|
104
|
+
cell: ({ row }: Row) => {
|
|
105
|
+
const entry = row.original.jsonApiData;
|
|
106
|
+
// Both sides receive the same award: shown as "200 + 200", as in the approved mock.
|
|
107
|
+
if (entry.status !== "completed" || !entry.tokensAwarded)
|
|
108
|
+
return <span className="block text-muted-foreground text-xs text-end">—</span>;
|
|
109
|
+
const award = decimal(entry.tokensAwarded, 0);
|
|
110
|
+
return <span className="block text-xs tabular-nums text-end">{`${award} + ${award}`}</span>;
|
|
111
|
+
},
|
|
112
|
+
meta: { className: "text-end" },
|
|
113
|
+
enableSorting: false,
|
|
114
|
+
enableHiding: false,
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const columns = useMemo(() => {
|
|
119
|
+
return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
|
|
120
|
+
TableContent<ReferralAdminReferralInterface>
|
|
121
|
+
>[];
|
|
122
|
+
}, [params.fields, fieldColumnMap, t, decimal]);
|
|
123
|
+
|
|
124
|
+
return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
registerTableGenerator("referrals/administration/referrals", useReferralAdminReferralTableStructure);
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ColumnDef } from "@tanstack/react-table";
|
|
4
|
+
import { CopyIcon } from "lucide-react";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import { cellDate } from "../../../components/tables/cells/cell.date";
|
|
8
|
+
import { registerTableGenerator, TableContent, UseTableStructureHook } from "../../../hooks";
|
|
9
|
+
import { Button } from "../../../shadcnui";
|
|
10
|
+
import { showError, showToast } from "../../../utils";
|
|
11
|
+
import { useUsageFormatters } from "../../tokenusage/lib/formatters";
|
|
12
|
+
import { ReferralAdminReferrerInterface, ReferralAdminSort } from "../interfaces/referral-admin.interface";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Columns of the referrers ranking. The values double as the TanStack column
|
|
16
|
+
* ids: ContentListTable builds its `columnVisibility` from the `fields` array,
|
|
17
|
+
* so a column whose id differs from its field is invisible.
|
|
18
|
+
*/
|
|
19
|
+
export enum ReferralAdminReferrerFields {
|
|
20
|
+
rank = "rank",
|
|
21
|
+
referrer = "referrer",
|
|
22
|
+
visits = "visits",
|
|
23
|
+
invites = "invites",
|
|
24
|
+
signups = "signups",
|
|
25
|
+
paying = "paying",
|
|
26
|
+
conversion = "conversion",
|
|
27
|
+
credits = "credits",
|
|
28
|
+
lastInvite = "lastInvite",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What the container passes through ContentListTable's `context`: the active sort and its setter. */
|
|
32
|
+
export type ReferralAdminReferrerTableContext = {
|
|
33
|
+
sort: ReferralAdminSort;
|
|
34
|
+
onSort: (sort: ReferralAdminSort) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Copies text to the clipboard from inside a click handler, with the toast on
|
|
39
|
+
* success and the error toast on refusal (a denied permission or an insecure
|
|
40
|
+
* context). Shared by the per-row button and the container's "Copy all emails".
|
|
41
|
+
*/
|
|
42
|
+
export async function copyReferrerEmails(
|
|
43
|
+
text: string,
|
|
44
|
+
t: (key: string, values?: Record<string, string | number>) => string,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
try {
|
|
47
|
+
await navigator.clipboard.writeText(text);
|
|
48
|
+
showToast(t("referral.admin.referrers.copied"));
|
|
49
|
+
} catch {
|
|
50
|
+
showError(t("referral.admin.referrers.copyFailed"));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const NUMERIC = { className: "text-end" };
|
|
55
|
+
|
|
56
|
+
export const useReferralAdminReferrerTableStructure: UseTableStructureHook<
|
|
57
|
+
ReferralAdminReferrerInterface,
|
|
58
|
+
ReferralAdminReferrerFields
|
|
59
|
+
> = (params) => {
|
|
60
|
+
const t = useTranslations();
|
|
61
|
+
const { decimal } = useUsageFormatters();
|
|
62
|
+
const context = params.context as ReferralAdminReferrerTableContext | undefined;
|
|
63
|
+
|
|
64
|
+
const tableData = useMemo(() => {
|
|
65
|
+
return params.data.map((entry: ReferralAdminReferrerInterface, index: number) => {
|
|
66
|
+
const row: TableContent<ReferralAdminReferrerInterface> = { jsonApiData: entry };
|
|
67
|
+
row[ReferralAdminReferrerFields.rank] = index + 1;
|
|
68
|
+
// cellDate reads the value from the row by the column name.
|
|
69
|
+
row[ReferralAdminReferrerFields.lastInvite] = entry.lastInviteAt;
|
|
70
|
+
return row;
|
|
71
|
+
});
|
|
72
|
+
}, [params.data]);
|
|
73
|
+
|
|
74
|
+
/** Header of a sortable column: a button that sets the sort the API reads (D8). */
|
|
75
|
+
const sortableHeader = (sort: ReferralAdminSort, label: string) => () => {
|
|
76
|
+
const active = context?.sort === sort;
|
|
77
|
+
return (
|
|
78
|
+
<Button
|
|
79
|
+
type="button"
|
|
80
|
+
variant="ghost"
|
|
81
|
+
size="xs"
|
|
82
|
+
className={active ? "text-primary" : "text-muted-foreground"}
|
|
83
|
+
title={t("referral.admin.referrers.sortBy")}
|
|
84
|
+
aria-sort={active ? "descending" : undefined}
|
|
85
|
+
data-testid={active ? "referrals-referrers-sort" : undefined}
|
|
86
|
+
onClick={() => context?.onSort(sort)}
|
|
87
|
+
>
|
|
88
|
+
{label}
|
|
89
|
+
</Button>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Typography role 15 (numeric): tabular-nums, end-aligned.
|
|
94
|
+
const numericCell = (read: (entry: ReferralAdminReferrerInterface) => number) => {
|
|
95
|
+
return ({ row }: { row: { original: TableContent<ReferralAdminReferrerInterface> } }) => (
|
|
96
|
+
<span className="block text-xs tabular-nums text-end">{decimal(read(row.original.jsonApiData), 0)}</span>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const fieldColumnMap: Record<ReferralAdminReferrerFields, () => any> = {
|
|
101
|
+
[ReferralAdminReferrerFields.rank]: () => ({
|
|
102
|
+
id: ReferralAdminReferrerFields.rank,
|
|
103
|
+
header: "#",
|
|
104
|
+
cell: ({ row }: { row: { original: TableContent<ReferralAdminReferrerInterface> } }) => (
|
|
105
|
+
<span className="text-muted-foreground text-xs tabular-nums">
|
|
106
|
+
{row.original[ReferralAdminReferrerFields.rank]}
|
|
107
|
+
</span>
|
|
108
|
+
),
|
|
109
|
+
enableSorting: false,
|
|
110
|
+
enableHiding: false,
|
|
111
|
+
}),
|
|
112
|
+
[ReferralAdminReferrerFields.referrer]: () => ({
|
|
113
|
+
id: ReferralAdminReferrerFields.referrer,
|
|
114
|
+
header: t("referral.admin.referrers.columns.referrer"),
|
|
115
|
+
cell: ({ row }: { row: { original: TableContent<ReferralAdminReferrerInterface> } }) => {
|
|
116
|
+
const entry = row.original.jsonApiData;
|
|
117
|
+
return (
|
|
118
|
+
<div className="flex items-center gap-2">
|
|
119
|
+
<div className="flex flex-col">
|
|
120
|
+
<span className="text-sm">{entry.name}</span>
|
|
121
|
+
<span className="text-muted-foreground text-xs">{entry.email}</span>
|
|
122
|
+
</div>
|
|
123
|
+
{entry.email && (
|
|
124
|
+
<Button
|
|
125
|
+
type="button"
|
|
126
|
+
variant="ghost"
|
|
127
|
+
size="icon-xs"
|
|
128
|
+
aria-label={t("referral.admin.referrers.copyEmail")}
|
|
129
|
+
title={t("referral.admin.referrers.copyEmail")}
|
|
130
|
+
data-testid={`referrals-copy-email-${entry.id}`}
|
|
131
|
+
onClick={() => copyReferrerEmails(entry.email, t)}
|
|
132
|
+
>
|
|
133
|
+
<CopyIcon />
|
|
134
|
+
</Button>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
enableSorting: false,
|
|
140
|
+
enableHiding: false,
|
|
141
|
+
}),
|
|
142
|
+
[ReferralAdminReferrerFields.visits]: () => ({
|
|
143
|
+
id: ReferralAdminReferrerFields.visits,
|
|
144
|
+
header: t("referral.admin.referrers.columns.visits"),
|
|
145
|
+
cell: numericCell((entry) => entry.visits),
|
|
146
|
+
meta: NUMERIC,
|
|
147
|
+
enableSorting: false,
|
|
148
|
+
enableHiding: false,
|
|
149
|
+
}),
|
|
150
|
+
[ReferralAdminReferrerFields.invites]: () => ({
|
|
151
|
+
id: ReferralAdminReferrerFields.invites,
|
|
152
|
+
header: sortableHeader("invites", t("referral.admin.referrers.columns.invites")),
|
|
153
|
+
cell: numericCell((entry) => entry.invitesSent),
|
|
154
|
+
meta: NUMERIC,
|
|
155
|
+
enableSorting: false,
|
|
156
|
+
enableHiding: false,
|
|
157
|
+
}),
|
|
158
|
+
[ReferralAdminReferrerFields.signups]: () => ({
|
|
159
|
+
id: ReferralAdminReferrerFields.signups,
|
|
160
|
+
header: sortableHeader("signups", t("referral.admin.referrers.columns.signups")),
|
|
161
|
+
cell: ({ row }: { row: { original: TableContent<ReferralAdminReferrerInterface> } }) => {
|
|
162
|
+
const entry = row.original.jsonApiData;
|
|
163
|
+
return (
|
|
164
|
+
<div className="flex flex-col items-end">
|
|
165
|
+
<span className="text-xs tabular-nums">{decimal(entry.signupsLink + entry.signupsDirect, 0)}</span>
|
|
166
|
+
<span className="text-muted-foreground text-xs">
|
|
167
|
+
{t("referral.admin.referrers.split", { link: entry.signupsLink, direct: entry.signupsDirect })}
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
},
|
|
172
|
+
meta: NUMERIC,
|
|
173
|
+
enableSorting: false,
|
|
174
|
+
enableHiding: false,
|
|
175
|
+
}),
|
|
176
|
+
[ReferralAdminReferrerFields.paying]: () => ({
|
|
177
|
+
id: ReferralAdminReferrerFields.paying,
|
|
178
|
+
header: sortableHeader("paying", t("referral.admin.referrers.columns.paying")),
|
|
179
|
+
cell: numericCell((entry) => entry.paying),
|
|
180
|
+
meta: NUMERIC,
|
|
181
|
+
enableSorting: false,
|
|
182
|
+
enableHiding: false,
|
|
183
|
+
}),
|
|
184
|
+
[ReferralAdminReferrerFields.conversion]: () => ({
|
|
185
|
+
id: ReferralAdminReferrerFields.conversion,
|
|
186
|
+
header: sortableHeader("conversion", t("referral.admin.referrers.columns.conversion")),
|
|
187
|
+
cell: ({ row }: { row: { original: TableContent<ReferralAdminReferrerInterface> } }) => {
|
|
188
|
+
const entry = row.original.jsonApiData;
|
|
189
|
+
// Conversion = paying / emails sent; a referrer who sent none has no rate.
|
|
190
|
+
if (!entry.invitesSent) return <span className="block text-muted-foreground text-xs text-end">—</span>;
|
|
191
|
+
const rate = Math.round((entry.paying / entry.invitesSent) * 100);
|
|
192
|
+
return (
|
|
193
|
+
<span className="flex items-center justify-end gap-1.5">
|
|
194
|
+
<span className="bg-primary/15 relative h-1.5 w-12 overflow-hidden rounded-full">
|
|
195
|
+
<span className="bg-primary absolute inset-y-0 start-0" style={{ width: `${Math.min(rate, 100)}%` }} />
|
|
196
|
+
</span>
|
|
197
|
+
<span className="text-xs tabular-nums">{`${decimal(rate, 0)}%`}</span>
|
|
198
|
+
</span>
|
|
199
|
+
);
|
|
200
|
+
},
|
|
201
|
+
meta: NUMERIC,
|
|
202
|
+
enableSorting: false,
|
|
203
|
+
enableHiding: false,
|
|
204
|
+
}),
|
|
205
|
+
[ReferralAdminReferrerFields.credits]: () => ({
|
|
206
|
+
id: ReferralAdminReferrerFields.credits,
|
|
207
|
+
header: sortableHeader("credits", t("referral.admin.referrers.columns.credits")),
|
|
208
|
+
cell: numericCell((entry) => entry.creditsEarned),
|
|
209
|
+
meta: NUMERIC,
|
|
210
|
+
enableSorting: false,
|
|
211
|
+
enableHiding: false,
|
|
212
|
+
}),
|
|
213
|
+
[ReferralAdminReferrerFields.lastInvite]: () =>
|
|
214
|
+
cellDate({
|
|
215
|
+
name: ReferralAdminReferrerFields.lastInvite,
|
|
216
|
+
title: t("referral.admin.referrers.columns.lastInvite"),
|
|
217
|
+
}),
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const columns = useMemo(() => {
|
|
221
|
+
return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
|
|
222
|
+
TableContent<ReferralAdminReferrerInterface>
|
|
223
|
+
>[];
|
|
224
|
+
}, [params.fields, fieldColumnMap, t, decimal, context?.sort]);
|
|
225
|
+
|
|
226
|
+
return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
registerTableGenerator("referrals/administration/referrers", useReferralAdminReferrerTableStructure);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
|
|
3
|
+
/** Report window of the administrative referral page. */
|
|
4
|
+
export type ReferralAdminPeriod = "30d" | "90d" | "all";
|
|
5
|
+
|
|
6
|
+
/** The referrers ranking order the API allowlists. */
|
|
7
|
+
export type ReferralAdminSort = "paying" | "signups" | "invites" | "conversion" | "credits";
|
|
8
|
+
|
|
9
|
+
/** One window (`current` or `previous`) of the headline tiles. */
|
|
10
|
+
export interface ReferralAdminSummaryInterface extends ApiDataInterface {
|
|
11
|
+
get window(): string;
|
|
12
|
+
get visits(): number;
|
|
13
|
+
get invitesSent(): number;
|
|
14
|
+
get signups(): number;
|
|
15
|
+
get paying(): number;
|
|
16
|
+
get creditsGiven(): number;
|
|
17
|
+
get sharingCompanies(): number;
|
|
18
|
+
get eligibleCompanies(): number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** One referrer company in the ranking; the id is the company id. */
|
|
22
|
+
export interface ReferralAdminReferrerInterface extends ApiDataInterface {
|
|
23
|
+
get name(): string;
|
|
24
|
+
get email(): string;
|
|
25
|
+
get visits(): number;
|
|
26
|
+
get invitesSent(): number;
|
|
27
|
+
get signupsLink(): number;
|
|
28
|
+
get signupsDirect(): number;
|
|
29
|
+
get paying(): number;
|
|
30
|
+
get creditsEarned(): number;
|
|
31
|
+
get lastInviteAt(): Date | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One referral (new account and who brought it); the id is the Referral id. */
|
|
35
|
+
export interface ReferralAdminReferralInterface extends ApiDataInterface {
|
|
36
|
+
get name(): string;
|
|
37
|
+
get email(): string;
|
|
38
|
+
get referrerName(): string;
|
|
39
|
+
get referrerEmail(): string;
|
|
40
|
+
get source(): string;
|
|
41
|
+
get status(): string;
|
|
42
|
+
get referredAt(): Date;
|
|
43
|
+
get completedAt(): Date | undefined;
|
|
44
|
+
get tokensAwarded(): number | undefined;
|
|
45
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { GiftIcon } from "lucide-react";
|
|
2
|
+
import { ModuleFactory, ModuleWithPermissions } from "../../permissions";
|
|
3
|
+
import { ReferralAdminReferral } from "./data/ReferralAdminReferral";
|
|
4
|
+
import { ReferralAdminReferrer } from "./data/ReferralAdminReferrer";
|
|
5
|
+
import { ReferralAdminSummary } from "./data/ReferralAdminSummary";
|
|
6
|
+
import { ReferralVisit } from "./data/ReferralVisit";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The referral visit and administrative report resources, as one object a
|
|
10
|
+
* consuming app spreads into its `allModules` (same rationale as
|
|
11
|
+
* `analyticsModules`). An app records link visits only once it spreads this.
|
|
12
|
+
* None has a pageUrl: these are not navigable resources.
|
|
13
|
+
*/
|
|
14
|
+
export const referralAdminModules = (factory: ModuleFactory) =>
|
|
15
|
+
({
|
|
16
|
+
ReferralVisit: factory({ name: "referrals/visits", model: ReferralVisit }),
|
|
17
|
+
ReferralAdminSummary: factory({
|
|
18
|
+
name: "referrals/administration/summary",
|
|
19
|
+
model: ReferralAdminSummary,
|
|
20
|
+
icon: GiftIcon,
|
|
21
|
+
}),
|
|
22
|
+
ReferralAdminReferrer: factory({
|
|
23
|
+
name: "referrals/administration/referrers",
|
|
24
|
+
model: ReferralAdminReferrer,
|
|
25
|
+
icon: GiftIcon,
|
|
26
|
+
}),
|
|
27
|
+
ReferralAdminReferral: factory({
|
|
28
|
+
name: "referrals/administration/referrals",
|
|
29
|
+
model: ReferralAdminReferral,
|
|
30
|
+
icon: GiftIcon,
|
|
31
|
+
}),
|
|
32
|
+
}) satisfies Record<string, ModuleWithPermissions>;
|