@carlonicora/nextjs-jsonapi 1.39.2 → 1.40.1
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-WQXQPLMX.js → BlockNoteEditor-4G3L3LSF.js} +14 -14
- package/dist/{BlockNoteEditor-WQXQPLMX.js.map → BlockNoteEditor-4G3L3LSF.js.map} +1 -1
- package/dist/{BlockNoteEditor-CITC7I2Z.mjs → BlockNoteEditor-EKY4AHVK.mjs} +4 -4
- package/dist/billing/index.js +346 -346
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-LDH2FGJY.mjs → chunk-BAOP6PTD.mjs} +689 -34
- package/dist/chunk-BAOP6PTD.mjs.map +1 -0
- package/dist/{chunk-2RBYXY6T.js → chunk-GKY5DAIH.js} +1228 -573
- package/dist/chunk-GKY5DAIH.js.map +1 -0
- package/dist/{chunk-TQ5GRRTM.mjs → chunk-GVN7XC3U.mjs} +278 -2
- package/dist/chunk-GVN7XC3U.mjs.map +1 -0
- package/dist/{chunk-XLMJPA4N.mjs → chunk-RRIYLEY6.mjs} +22 -2
- package/dist/chunk-RRIYLEY6.mjs.map +1 -0
- package/dist/{chunk-2PHWAL6Q.js → chunk-T5YYOT4Z.js} +22 -2
- package/dist/chunk-T5YYOT4Z.js.map +1 -0
- package/dist/{chunk-3EZX4G2E.js → chunk-ZNGEVB5M.js} +279 -3
- package/dist/chunk-ZNGEVB5M.js.map +1 -0
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +28 -4
- package/dist/components/index.d.ts +28 -4
- package/dist/components/index.js +16 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +15 -3
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +127 -3
- package/dist/core/index.d.ts +127 -3
- package/dist/core/index.js +12 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +11 -1
- package/dist/index.d.mts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +17 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +16 -2
- package/dist/{s3.service-hnTPVTm2.d.mts → s3.service-BoOF5-ln.d.mts} +1 -0
- package/dist/{s3.service-DXkDoMf1.d.ts → s3.service-Mxo-7wQ6.d.ts} +1 -0
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/waitlist.config-kPfjImle.d.mts +26 -0
- package/dist/waitlist.config-kPfjImle.d.ts +26 -0
- package/package.json +1 -1
- package/src/components/forms/FormCheckbox.tsx +1 -1
- package/src/components/forms/FormSelect.tsx +1 -1
- package/src/components/index.ts +1 -0
- package/src/core/index.ts +3 -0
- package/src/core/registry/ModuleRegistry.ts +3 -0
- package/src/features/auth/components/forms/Register.tsx +180 -1
- package/src/features/auth/data/auth.interface.ts +1 -0
- package/src/features/auth/data/auth.ts +1 -0
- package/src/features/index.ts +1 -0
- package/src/features/waitlist/components/forms/WaitlistForm.tsx +186 -0
- package/src/features/waitlist/components/forms/WaitlistQuestionnaireRenderer.tsx +110 -0
- package/src/features/waitlist/components/forms/index.ts +2 -0
- package/src/features/waitlist/components/index.ts +3 -0
- package/src/features/waitlist/components/lists/WaitlistList.tsx +145 -0
- package/src/features/waitlist/components/lists/index.ts +1 -0
- package/src/features/waitlist/components/sections/WaitlistConfirmation.tsx +68 -0
- package/src/features/waitlist/components/sections/WaitlistHeroSection.tsx +49 -0
- package/src/features/waitlist/components/sections/WaitlistSuccessState.tsx +19 -0
- package/src/features/waitlist/components/sections/index.ts +3 -0
- package/src/features/waitlist/config/waitlist.config.ts +35 -0
- package/src/features/waitlist/data/Waitlist.ts +104 -0
- package/src/features/waitlist/data/WaitlistInterface.ts +32 -0
- package/src/features/waitlist/data/WaitlistService.ts +153 -0
- package/src/features/waitlist/data/index.ts +5 -0
- package/src/features/waitlist/data/waitlist-stats.interface.ts +9 -0
- package/src/features/waitlist/data/waitlist-stats.ts +47 -0
- package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +121 -0
- package/src/features/waitlist/index.ts +28 -0
- package/src/features/waitlist/waitlist-stats.module.ts +8 -0
- package/src/features/waitlist/waitlist.module.ts +9 -0
- package/src/index.ts +9 -0
- package/src/login/config.ts +9 -0
- package/dist/chunk-2PHWAL6Q.js.map +0 -1
- package/dist/chunk-2RBYXY6T.js.map +0 -1
- package/dist/chunk-3EZX4G2E.js.map +0 -1
- package/dist/chunk-LDH2FGJY.mjs.map +0 -1
- package/dist/chunk-TQ5GRRTM.mjs.map +0 -1
- package/dist/chunk-XLMJPA4N.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-CITC7I2Z.mjs.map → BlockNoteEditor-EKY4AHVK.mjs.map} +0 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import { CheckCircle, Mail } from "lucide-react";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { SubmitHandler, useForm } from "react-hook-form";
|
|
8
|
+
import { v4 } from "uuid";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { errorToast, FormInput, GdprConsentCheckbox } from "../../../../components";
|
|
11
|
+
import { Button, Form, Link } from "../../../../shadcnui";
|
|
12
|
+
import { getWaitlistConfig } from "../../config/waitlist.config";
|
|
13
|
+
import { WaitlistService } from "../../data/WaitlistService";
|
|
14
|
+
import { WaitlistQuestionnaireRenderer } from "./WaitlistQuestionnaireRenderer";
|
|
15
|
+
|
|
16
|
+
interface WaitlistFormProps {
|
|
17
|
+
onSuccess?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function WaitlistForm({ onSuccess }: WaitlistFormProps) {
|
|
21
|
+
const t = useTranslations();
|
|
22
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
23
|
+
const [isSuccess, setIsSuccess] = useState(false);
|
|
24
|
+
|
|
25
|
+
const config = getWaitlistConfig();
|
|
26
|
+
const questionnaireFields = config.questionnaire ?? [];
|
|
27
|
+
|
|
28
|
+
// Build dynamic questionnaire schema based on config
|
|
29
|
+
const questionnaireSchema = questionnaireFields.reduce(
|
|
30
|
+
(acc, field) => {
|
|
31
|
+
if (field.type === "checkbox" && field.options && field.options.length > 0) {
|
|
32
|
+
// Multiple checkboxes - each option is a boolean
|
|
33
|
+
const optionsSchema = field.options.reduce(
|
|
34
|
+
(optAcc, opt) => {
|
|
35
|
+
optAcc[opt.value] = z.boolean().optional();
|
|
36
|
+
return optAcc;
|
|
37
|
+
},
|
|
38
|
+
{} as Record<string, z.ZodOptional<z.ZodBoolean>>,
|
|
39
|
+
);
|
|
40
|
+
acc[field.id] = z.object(optionsSchema).optional();
|
|
41
|
+
} else if (field.type === "checkbox") {
|
|
42
|
+
// Single checkbox
|
|
43
|
+
acc[field.id] = field.required ? z.literal(true) : z.boolean().optional();
|
|
44
|
+
} else if (field.type === "select") {
|
|
45
|
+
acc[field.id] = field.required ? z.string().min(1) : z.string().optional();
|
|
46
|
+
} else {
|
|
47
|
+
// text, textarea
|
|
48
|
+
acc[field.id] = field.required ? z.string().min(1) : z.string().optional();
|
|
49
|
+
}
|
|
50
|
+
return acc;
|
|
51
|
+
},
|
|
52
|
+
{} as Record<string, z.ZodTypeAny>,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const formSchema = z.object({
|
|
56
|
+
id: z.uuidv4(),
|
|
57
|
+
email: z.email({ message: t("common.errors.invalid_email") }),
|
|
58
|
+
gdprConsent: z.literal(true, {
|
|
59
|
+
message: t("auth.gdpr.terms_required"),
|
|
60
|
+
}),
|
|
61
|
+
marketingConsent: z.boolean().optional(),
|
|
62
|
+
questionnaire: z.object(questionnaireSchema).optional(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
type FormValues = z.infer<typeof formSchema>;
|
|
66
|
+
|
|
67
|
+
// Build default values for questionnaire fields
|
|
68
|
+
const questionnaireDefaults = questionnaireFields.reduce(
|
|
69
|
+
(acc, field) => {
|
|
70
|
+
if (field.type === "checkbox" && field.options && field.options.length > 0) {
|
|
71
|
+
acc[field.id] = field.options.reduce(
|
|
72
|
+
(optAcc, opt) => {
|
|
73
|
+
optAcc[opt.value] = false;
|
|
74
|
+
return optAcc;
|
|
75
|
+
},
|
|
76
|
+
{} as Record<string, boolean>,
|
|
77
|
+
);
|
|
78
|
+
} else if (field.type === "checkbox") {
|
|
79
|
+
acc[field.id] = false;
|
|
80
|
+
} else {
|
|
81
|
+
acc[field.id] = "";
|
|
82
|
+
}
|
|
83
|
+
return acc;
|
|
84
|
+
},
|
|
85
|
+
{} as Record<string, any>,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const form = useForm<FormValues>({
|
|
89
|
+
resolver: zodResolver(formSchema),
|
|
90
|
+
defaultValues: {
|
|
91
|
+
id: v4(),
|
|
92
|
+
email: "",
|
|
93
|
+
gdprConsent: false as unknown as true,
|
|
94
|
+
marketingConsent: false,
|
|
95
|
+
questionnaire: questionnaireDefaults,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const onSubmit: SubmitHandler<FormValues> = async (values) => {
|
|
100
|
+
setIsSubmitting(true);
|
|
101
|
+
try {
|
|
102
|
+
const now = new Date().toISOString();
|
|
103
|
+
|
|
104
|
+
await WaitlistService.submit({
|
|
105
|
+
id: values.id,
|
|
106
|
+
email: values.email,
|
|
107
|
+
gdprConsent: values.gdprConsent,
|
|
108
|
+
gdprConsentAt: now,
|
|
109
|
+
marketingConsent: values.marketingConsent ?? false,
|
|
110
|
+
marketingConsentAt: values.marketingConsent ? now : undefined,
|
|
111
|
+
questionnaire: values.questionnaire,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
setIsSuccess(true);
|
|
115
|
+
onSuccess?.();
|
|
116
|
+
} catch (e) {
|
|
117
|
+
errorToast({ error: e });
|
|
118
|
+
} finally {
|
|
119
|
+
setIsSubmitting(false);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (isSuccess) {
|
|
124
|
+
return (
|
|
125
|
+
<div className="space-y-6 text-center">
|
|
126
|
+
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-green-100">
|
|
127
|
+
<CheckCircle className="h-8 w-8 text-green-600" />
|
|
128
|
+
</div>
|
|
129
|
+
<div className="space-y-2">
|
|
130
|
+
<h3 className="text-xl font-semibold">{t("waitlist.success.title")}</h3>
|
|
131
|
+
<p className="text-muted-foreground">{t("waitlist.success.description")}</p>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="flex items-center justify-center gap-2 text-muted-foreground text-sm">
|
|
134
|
+
<Mail className="h-4 w-4" />
|
|
135
|
+
<span>{t("waitlist.success.hint")}</span>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<Form {...form}>
|
|
143
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
144
|
+
<FormInput
|
|
145
|
+
isRequired
|
|
146
|
+
form={form}
|
|
147
|
+
id="email"
|
|
148
|
+
name={t("common.fields.email.label")}
|
|
149
|
+
placeholder={t("common.fields.email.placeholder")}
|
|
150
|
+
/>
|
|
151
|
+
|
|
152
|
+
{questionnaireFields.length > 0 && <WaitlistQuestionnaireRenderer form={form} fields={questionnaireFields} />}
|
|
153
|
+
|
|
154
|
+
<div className="space-y-4 py-4">
|
|
155
|
+
<GdprConsentCheckbox
|
|
156
|
+
form={form}
|
|
157
|
+
id="gdprConsent"
|
|
158
|
+
label={
|
|
159
|
+
<>
|
|
160
|
+
{t("auth.gdpr.terms_prefix")}
|
|
161
|
+
<Link href="/terms" target="_blank" rel="noopener" className="underline">
|
|
162
|
+
{t("auth.gdpr.terms_of_service")}
|
|
163
|
+
</Link>
|
|
164
|
+
{t("auth.gdpr.and")}
|
|
165
|
+
<Link href="/privacy" target="_blank" rel="noopener" className="underline">
|
|
166
|
+
{t("auth.gdpr.privacy_policy")}
|
|
167
|
+
</Link>
|
|
168
|
+
</>
|
|
169
|
+
}
|
|
170
|
+
required
|
|
171
|
+
/>
|
|
172
|
+
<GdprConsentCheckbox
|
|
173
|
+
form={form}
|
|
174
|
+
id="marketingConsent"
|
|
175
|
+
label={t("auth.gdpr.marketing_consent")}
|
|
176
|
+
description={t("auth.gdpr.marketing_description")}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
|
181
|
+
{isSubmitting ? t("common.actions.submitting") : t("waitlist.buttons.join")}
|
|
182
|
+
</Button>
|
|
183
|
+
</form>
|
|
184
|
+
</Form>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
|
4
|
+
import { FormInput, FormTextarea, FormSelect, FormCheckbox } from "../../../../components";
|
|
5
|
+
import { QuestionnaireField } from "../../config/waitlist.config";
|
|
6
|
+
|
|
7
|
+
interface WaitlistQuestionnaireRendererProps<T extends FieldValues> {
|
|
8
|
+
form: UseFormReturn<T>;
|
|
9
|
+
fields: QuestionnaireField[];
|
|
10
|
+
fieldPrefix?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function WaitlistQuestionnaireRenderer<T extends FieldValues>({
|
|
14
|
+
form,
|
|
15
|
+
fields,
|
|
16
|
+
fieldPrefix = "questionnaire",
|
|
17
|
+
}: WaitlistQuestionnaireRendererProps<T>) {
|
|
18
|
+
if (!fields || fields.length === 0) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="space-y-4">
|
|
24
|
+
{fields.map((field) => {
|
|
25
|
+
const fieldId = `${fieldPrefix}.${field.id}` as Path<T>;
|
|
26
|
+
|
|
27
|
+
switch (field.type) {
|
|
28
|
+
case "text":
|
|
29
|
+
return (
|
|
30
|
+
<FormInput
|
|
31
|
+
key={field.id}
|
|
32
|
+
form={form}
|
|
33
|
+
id={fieldId}
|
|
34
|
+
name={field.label}
|
|
35
|
+
placeholder={field.placeholder}
|
|
36
|
+
isRequired={field.required}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
case "textarea":
|
|
41
|
+
return (
|
|
42
|
+
<FormTextarea
|
|
43
|
+
key={field.id}
|
|
44
|
+
form={form}
|
|
45
|
+
id={fieldId}
|
|
46
|
+
name={field.label}
|
|
47
|
+
placeholder={field.placeholder}
|
|
48
|
+
className="min-h-24"
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
case "select":
|
|
53
|
+
if (!field.options || field.options.length === 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return (
|
|
57
|
+
<FormSelect
|
|
58
|
+
key={field.id}
|
|
59
|
+
form={form}
|
|
60
|
+
id={fieldId}
|
|
61
|
+
name={field.label}
|
|
62
|
+
values={field.options.map((opt) => ({
|
|
63
|
+
id: opt.value,
|
|
64
|
+
text: opt.label,
|
|
65
|
+
}))}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
case "checkbox":
|
|
70
|
+
if (!field.options || field.options.length === 0) {
|
|
71
|
+
return (
|
|
72
|
+
<FormCheckbox
|
|
73
|
+
key={field.id}
|
|
74
|
+
form={form}
|
|
75
|
+
id={fieldId}
|
|
76
|
+
name={field.label}
|
|
77
|
+
description={field.description}
|
|
78
|
+
isRequired={field.required}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
// Multiple checkboxes for options
|
|
83
|
+
return (
|
|
84
|
+
<div key={field.id} className="space-y-2">
|
|
85
|
+
<span className="text-sm font-medium">
|
|
86
|
+
{field.label}
|
|
87
|
+
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
88
|
+
</span>
|
|
89
|
+
{field.description && <p className="text-muted-foreground text-xs">{field.description}</p>}
|
|
90
|
+
<div className="space-y-2">
|
|
91
|
+
{field.options.map((option) => (
|
|
92
|
+
<FormCheckbox
|
|
93
|
+
key={option.value}
|
|
94
|
+
form={form}
|
|
95
|
+
id={`${fieldId}.${option.value}` as Path<T>}
|
|
96
|
+
name={option.label}
|
|
97
|
+
description={option.description}
|
|
98
|
+
/>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
default:
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
})}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
|
4
|
+
import { RefreshCw, Users } from "lucide-react";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import { useCallback, useEffect, useState } from "react";
|
|
7
|
+
import { errorToast } from "../../../../components";
|
|
8
|
+
import {
|
|
9
|
+
Button,
|
|
10
|
+
Select,
|
|
11
|
+
SelectContent,
|
|
12
|
+
SelectItem,
|
|
13
|
+
SelectTrigger,
|
|
14
|
+
SelectValue,
|
|
15
|
+
Table,
|
|
16
|
+
TableBody,
|
|
17
|
+
TableCell,
|
|
18
|
+
TableHead,
|
|
19
|
+
TableHeader,
|
|
20
|
+
TableRow,
|
|
21
|
+
} from "../../../../shadcnui";
|
|
22
|
+
import { showToast } from "../../../../utils/toast";
|
|
23
|
+
import { WaitlistInterface } from "../../data/WaitlistInterface";
|
|
24
|
+
import { WaitlistService } from "../../data/WaitlistService";
|
|
25
|
+
import { useWaitlistTableStructure } from "../../hooks/useWaitlistTableStructure";
|
|
26
|
+
|
|
27
|
+
export function WaitlistList() {
|
|
28
|
+
const t = useTranslations();
|
|
29
|
+
const [entries, setEntries] = useState<WaitlistInterface[]>([]);
|
|
30
|
+
const [total, setTotal] = useState(0);
|
|
31
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
32
|
+
const [statusFilter, setStatusFilter] = useState<string>("all");
|
|
33
|
+
|
|
34
|
+
const loadEntries = useCallback(async () => {
|
|
35
|
+
setIsLoading(true);
|
|
36
|
+
try {
|
|
37
|
+
const result = await WaitlistService.findMany({
|
|
38
|
+
status: statusFilter === "all" ? undefined : statusFilter,
|
|
39
|
+
fetchAll: true,
|
|
40
|
+
});
|
|
41
|
+
setEntries(result);
|
|
42
|
+
setTotal(result.length);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
errorToast({ error });
|
|
45
|
+
} finally {
|
|
46
|
+
setIsLoading(false);
|
|
47
|
+
}
|
|
48
|
+
}, [statusFilter]);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
loadEntries();
|
|
52
|
+
}, [loadEntries]);
|
|
53
|
+
|
|
54
|
+
const handleInvite = async (entry: WaitlistInterface) => {
|
|
55
|
+
try {
|
|
56
|
+
await WaitlistService.invite(entry.id);
|
|
57
|
+
showToast(t("waitlist.admin.invite_sent", { email: entry.email }));
|
|
58
|
+
loadEntries();
|
|
59
|
+
} catch (error) {
|
|
60
|
+
errorToast({ error });
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const columns = useWaitlistTableStructure({ onInvite: handleInvite });
|
|
65
|
+
|
|
66
|
+
const table = useReactTable({
|
|
67
|
+
data: entries,
|
|
68
|
+
columns,
|
|
69
|
+
getCoreRowModel: getCoreRowModel(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="space-y-4">
|
|
74
|
+
{/* Header */}
|
|
75
|
+
<div className="flex items-center justify-between">
|
|
76
|
+
<div className="flex items-center gap-2">
|
|
77
|
+
<Users className="h-5 w-5" />
|
|
78
|
+
<h2 className="text-xl font-semibold">{t("waitlist.admin.title")}</h2>
|
|
79
|
+
<span className="text-muted-foreground">({t("waitlist.admin.entries_count", { count: total })})</span>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div className="flex items-center gap-4">
|
|
83
|
+
{/* Status Filter */}
|
|
84
|
+
<Select value={statusFilter} onValueChange={(value) => setStatusFilter(value ?? "all")}>
|
|
85
|
+
<SelectTrigger className="w-40">
|
|
86
|
+
<SelectValue placeholder={t("waitlist.admin.filter_placeholder")} />
|
|
87
|
+
</SelectTrigger>
|
|
88
|
+
<SelectContent>
|
|
89
|
+
<SelectItem value="all">{t("waitlist.admin.all_statuses")}</SelectItem>
|
|
90
|
+
<SelectItem value="pending">{t("waitlist.admin.status.pending")}</SelectItem>
|
|
91
|
+
<SelectItem value="confirmed">{t("waitlist.admin.status.confirmed")}</SelectItem>
|
|
92
|
+
<SelectItem value="invited">{t("waitlist.admin.status.invited")}</SelectItem>
|
|
93
|
+
<SelectItem value="registered">{t("waitlist.admin.status.registered")}</SelectItem>
|
|
94
|
+
</SelectContent>
|
|
95
|
+
</Select>
|
|
96
|
+
|
|
97
|
+
{/* Refresh Button */}
|
|
98
|
+
<Button variant="outline" size="icon" onClick={loadEntries} disabled={isLoading}>
|
|
99
|
+
<RefreshCw className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`} />
|
|
100
|
+
</Button>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Table */}
|
|
105
|
+
<div className="rounded-md border">
|
|
106
|
+
<Table>
|
|
107
|
+
<TableHeader>
|
|
108
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
109
|
+
<TableRow key={headerGroup.id}>
|
|
110
|
+
{headerGroup.headers.map((header) => (
|
|
111
|
+
<TableHead key={header.id}>
|
|
112
|
+
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
|
113
|
+
</TableHead>
|
|
114
|
+
))}
|
|
115
|
+
</TableRow>
|
|
116
|
+
))}
|
|
117
|
+
</TableHeader>
|
|
118
|
+
<TableBody>
|
|
119
|
+
{isLoading ? (
|
|
120
|
+
<TableRow>
|
|
121
|
+
<TableCell colSpan={columns.length} className="h-24 text-center">
|
|
122
|
+
{t("waitlist.admin.loading")}
|
|
123
|
+
</TableCell>
|
|
124
|
+
</TableRow>
|
|
125
|
+
) : entries.length === 0 ? (
|
|
126
|
+
<TableRow>
|
|
127
|
+
<TableCell colSpan={columns.length} className="h-24 text-center">
|
|
128
|
+
{t("waitlist.admin.empty")}
|
|
129
|
+
</TableCell>
|
|
130
|
+
</TableRow>
|
|
131
|
+
) : (
|
|
132
|
+
table.getRowModel().rows.map((row) => (
|
|
133
|
+
<TableRow key={row.id}>
|
|
134
|
+
{row.getVisibleCells().map((cell) => (
|
|
135
|
+
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
|
|
136
|
+
))}
|
|
137
|
+
</TableRow>
|
|
138
|
+
))
|
|
139
|
+
)}
|
|
140
|
+
</TableBody>
|
|
141
|
+
</Table>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./WaitlistList";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CheckCircle, Loader2, XCircle } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { buttonVariants, Link } from "../../../../shadcnui";
|
|
7
|
+
import { WaitlistService } from "../../data/WaitlistService";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
code: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type ConfirmationState = "loading" | "success" | "error";
|
|
14
|
+
|
|
15
|
+
export function WaitlistConfirmation({ code }: Props) {
|
|
16
|
+
const t = useTranslations();
|
|
17
|
+
const [state, setState] = useState<ConfirmationState>("loading");
|
|
18
|
+
const [errorMessage, setErrorMessage] = useState<string>("");
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
async function confirmEmail() {
|
|
22
|
+
try {
|
|
23
|
+
await WaitlistService.confirm(code);
|
|
24
|
+
setState("success");
|
|
25
|
+
} catch (error) {
|
|
26
|
+
setState("error");
|
|
27
|
+
setErrorMessage(error instanceof Error ? error.message : t("waitlist.confirmation.error_default"));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
confirmEmail();
|
|
32
|
+
}, [code, t]);
|
|
33
|
+
|
|
34
|
+
if (state === "loading") {
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex flex-col items-center justify-center space-y-4 py-16 text-center">
|
|
37
|
+
<Loader2 className="text-primary h-12 w-12 animate-spin" />
|
|
38
|
+
<p className="text-muted-foreground">{t("waitlist.confirmation.loading")}</p>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (state === "error") {
|
|
44
|
+
return (
|
|
45
|
+
<div className="flex flex-col items-center justify-center space-y-4 py-16 text-center">
|
|
46
|
+
<div className="bg-destructive/10 rounded-full p-4">
|
|
47
|
+
<XCircle className="text-destructive h-12 w-12" />
|
|
48
|
+
</div>
|
|
49
|
+
<h2 className="text-2xl font-bold">{t("waitlist.confirmation.error_title")}</h2>
|
|
50
|
+
<p className="text-muted-foreground max-w-md">{errorMessage}</p>
|
|
51
|
+
<Link href="/waitlist" className={buttonVariants({ variant: "outline" })}>
|
|
52
|
+
{t("waitlist.buttons.return")}
|
|
53
|
+
</Link>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="flex flex-col items-center justify-center space-y-4 py-16 text-center">
|
|
60
|
+
<div className="bg-primary/10 rounded-full p-4">
|
|
61
|
+
<CheckCircle className="text-primary h-12 w-12" />
|
|
62
|
+
</div>
|
|
63
|
+
<h2 className="text-2xl font-bold">{t("waitlist.confirmation.success_title")}</h2>
|
|
64
|
+
<p className="text-muted-foreground max-w-md">{t("waitlist.confirmation.success_description")}</p>
|
|
65
|
+
<p className="text-muted-foreground text-sm">{t("waitlist.confirmation.success_hint")}</p>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CheckCircle } from "lucide-react";
|
|
4
|
+
import { getWaitlistConfig } from "../../config/waitlist.config";
|
|
5
|
+
import { WaitlistForm } from "../forms/WaitlistForm";
|
|
6
|
+
|
|
7
|
+
export function WaitlistHeroSection() {
|
|
8
|
+
const config = getWaitlistConfig();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<section className="relative overflow-hidden py-16 md:py-24">
|
|
12
|
+
<div className="container mx-auto px-4">
|
|
13
|
+
<div className="grid gap-12 lg:grid-cols-2 lg:items-center">
|
|
14
|
+
{/* Left Column - Content */}
|
|
15
|
+
<div className="space-y-8">
|
|
16
|
+
<div className="space-y-4">
|
|
17
|
+
{config.heroTitle && (
|
|
18
|
+
<h1 className="text-4xl font-bold tracking-tight md:text-5xl lg:text-6xl">{config.heroTitle}</h1>
|
|
19
|
+
)}
|
|
20
|
+
{config.heroSubtitle && (
|
|
21
|
+
<p className="text-muted-foreground text-xl md:text-2xl">{config.heroSubtitle}</p>
|
|
22
|
+
)}
|
|
23
|
+
{config.heroDescription && <p className="text-muted-foreground">{config.heroDescription}</p>}
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
{/* Benefits */}
|
|
27
|
+
{config.benefits && config.benefits.length > 0 && (
|
|
28
|
+
<ul className="space-y-3">
|
|
29
|
+
{config.benefits.map((benefit, index) => (
|
|
30
|
+
<li key={index} className="flex items-start gap-3">
|
|
31
|
+
<CheckCircle className="text-primary mt-0.5 h-5 w-5 shrink-0" />
|
|
32
|
+
<span>{benefit}</span>
|
|
33
|
+
</li>
|
|
34
|
+
))}
|
|
35
|
+
</ul>
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
{/* Right Column - Form */}
|
|
40
|
+
<div className="lg:pl-8">
|
|
41
|
+
<div className="bg-card rounded-lg border p-6 shadow-lg md:p-8">
|
|
42
|
+
<WaitlistForm />
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CheckCircle } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
|
|
6
|
+
export function WaitlistSuccessState() {
|
|
7
|
+
const t = useTranslations();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex flex-col items-center justify-center space-y-4 py-8 text-center">
|
|
11
|
+
<div className="bg-primary/10 rounded-full p-4">
|
|
12
|
+
<CheckCircle className="text-primary h-12 w-12" />
|
|
13
|
+
</div>
|
|
14
|
+
<h2 className="text-2xl font-bold">{t("waitlist.success.title")}</h2>
|
|
15
|
+
<p className="text-muted-foreground max-w-md">{t("waitlist.success.description")}</p>
|
|
16
|
+
<p className="text-muted-foreground text-sm">{t("waitlist.success.hint")}</p>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type QuestionnaireFieldType = "text" | "textarea" | "select" | "checkbox";
|
|
2
|
+
|
|
3
|
+
export interface QuestionnaireOption {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface QuestionnaireField {
|
|
10
|
+
id: string;
|
|
11
|
+
type: QuestionnaireFieldType;
|
|
12
|
+
label: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
required?: boolean;
|
|
16
|
+
options?: QuestionnaireOption[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WaitlistConfig {
|
|
20
|
+
questionnaire?: QuestionnaireField[];
|
|
21
|
+
heroTitle?: string;
|
|
22
|
+
heroSubtitle?: string;
|
|
23
|
+
heroDescription?: string;
|
|
24
|
+
benefits?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let _waitlistConfig: WaitlistConfig = {};
|
|
28
|
+
|
|
29
|
+
export function configureWaitlist(config: WaitlistConfig): void {
|
|
30
|
+
_waitlistConfig = config;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getWaitlistConfig(): WaitlistConfig {
|
|
34
|
+
return _waitlistConfig;
|
|
35
|
+
}
|