@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.
Files changed (84) hide show
  1. package/dist/{BlockNoteEditor-WQXQPLMX.js → BlockNoteEditor-4G3L3LSF.js} +14 -14
  2. package/dist/{BlockNoteEditor-WQXQPLMX.js.map → BlockNoteEditor-4G3L3LSF.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-CITC7I2Z.mjs → BlockNoteEditor-EKY4AHVK.mjs} +4 -4
  4. package/dist/billing/index.js +346 -346
  5. package/dist/billing/index.mjs +3 -3
  6. package/dist/{chunk-LDH2FGJY.mjs → chunk-BAOP6PTD.mjs} +689 -34
  7. package/dist/chunk-BAOP6PTD.mjs.map +1 -0
  8. package/dist/{chunk-2RBYXY6T.js → chunk-GKY5DAIH.js} +1228 -573
  9. package/dist/chunk-GKY5DAIH.js.map +1 -0
  10. package/dist/{chunk-TQ5GRRTM.mjs → chunk-GVN7XC3U.mjs} +278 -2
  11. package/dist/chunk-GVN7XC3U.mjs.map +1 -0
  12. package/dist/{chunk-XLMJPA4N.mjs → chunk-RRIYLEY6.mjs} +22 -2
  13. package/dist/chunk-RRIYLEY6.mjs.map +1 -0
  14. package/dist/{chunk-2PHWAL6Q.js → chunk-T5YYOT4Z.js} +22 -2
  15. package/dist/chunk-T5YYOT4Z.js.map +1 -0
  16. package/dist/{chunk-3EZX4G2E.js → chunk-ZNGEVB5M.js} +279 -3
  17. package/dist/chunk-ZNGEVB5M.js.map +1 -0
  18. package/dist/client/index.js +4 -4
  19. package/dist/client/index.mjs +3 -3
  20. package/dist/components/index.d.mts +28 -4
  21. package/dist/components/index.d.ts +28 -4
  22. package/dist/components/index.js +16 -4
  23. package/dist/components/index.js.map +1 -1
  24. package/dist/components/index.mjs +15 -3
  25. package/dist/contexts/index.js +4 -4
  26. package/dist/contexts/index.mjs +3 -3
  27. package/dist/core/index.d.mts +127 -3
  28. package/dist/core/index.d.ts +127 -3
  29. package/dist/core/index.js +12 -2
  30. package/dist/core/index.js.map +1 -1
  31. package/dist/core/index.mjs +11 -1
  32. package/dist/index.d.mts +5 -2
  33. package/dist/index.d.ts +5 -2
  34. package/dist/index.js +17 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +16 -2
  37. package/dist/{s3.service-hnTPVTm2.d.mts → s3.service-BoOF5-ln.d.mts} +1 -0
  38. package/dist/{s3.service-DXkDoMf1.d.ts → s3.service-Mxo-7wQ6.d.ts} +1 -0
  39. package/dist/server/index.d.mts +1 -1
  40. package/dist/server/index.d.ts +1 -1
  41. package/dist/server/index.js +3 -3
  42. package/dist/server/index.mjs +1 -1
  43. package/dist/waitlist.config-kPfjImle.d.mts +26 -0
  44. package/dist/waitlist.config-kPfjImle.d.ts +26 -0
  45. package/package.json +1 -1
  46. package/src/components/forms/FormCheckbox.tsx +1 -1
  47. package/src/components/forms/FormSelect.tsx +1 -1
  48. package/src/components/index.ts +1 -0
  49. package/src/core/index.ts +3 -0
  50. package/src/core/registry/ModuleRegistry.ts +3 -0
  51. package/src/features/auth/components/forms/Register.tsx +180 -1
  52. package/src/features/auth/data/auth.interface.ts +1 -0
  53. package/src/features/auth/data/auth.ts +1 -0
  54. package/src/features/index.ts +1 -0
  55. package/src/features/waitlist/components/forms/WaitlistForm.tsx +186 -0
  56. package/src/features/waitlist/components/forms/WaitlistQuestionnaireRenderer.tsx +110 -0
  57. package/src/features/waitlist/components/forms/index.ts +2 -0
  58. package/src/features/waitlist/components/index.ts +3 -0
  59. package/src/features/waitlist/components/lists/WaitlistList.tsx +145 -0
  60. package/src/features/waitlist/components/lists/index.ts +1 -0
  61. package/src/features/waitlist/components/sections/WaitlistConfirmation.tsx +68 -0
  62. package/src/features/waitlist/components/sections/WaitlistHeroSection.tsx +49 -0
  63. package/src/features/waitlist/components/sections/WaitlistSuccessState.tsx +19 -0
  64. package/src/features/waitlist/components/sections/index.ts +3 -0
  65. package/src/features/waitlist/config/waitlist.config.ts +35 -0
  66. package/src/features/waitlist/data/Waitlist.ts +104 -0
  67. package/src/features/waitlist/data/WaitlistInterface.ts +32 -0
  68. package/src/features/waitlist/data/WaitlistService.ts +153 -0
  69. package/src/features/waitlist/data/index.ts +5 -0
  70. package/src/features/waitlist/data/waitlist-stats.interface.ts +9 -0
  71. package/src/features/waitlist/data/waitlist-stats.ts +47 -0
  72. package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +121 -0
  73. package/src/features/waitlist/index.ts +28 -0
  74. package/src/features/waitlist/waitlist-stats.module.ts +8 -0
  75. package/src/features/waitlist/waitlist.module.ts +9 -0
  76. package/src/index.ts +9 -0
  77. package/src/login/config.ts +9 -0
  78. package/dist/chunk-2PHWAL6Q.js.map +0 -1
  79. package/dist/chunk-2RBYXY6T.js.map +0 -1
  80. package/dist/chunk-3EZX4G2E.js.map +0 -1
  81. package/dist/chunk-LDH2FGJY.mjs.map +0 -1
  82. package/dist/chunk-TQ5GRRTM.mjs.map +0 -1
  83. package/dist/chunk-XLMJPA4N.mjs.map +0 -1
  84. /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,2 @@
1
+ export * from "./WaitlistForm";
2
+ export * from "./WaitlistQuestionnaireRenderer";
@@ -0,0 +1,3 @@
1
+ export * from "./forms";
2
+ export * from "./sections";
3
+ export * from "./lists";
@@ -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,3 @@
1
+ export * from "./WaitlistHeroSection";
2
+ export * from "./WaitlistSuccessState";
3
+ export * from "./WaitlistConfirmation";
@@ -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
+ }