@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
package/dist/index.mjs CHANGED
@@ -3,13 +3,15 @@ import {
3
3
  configureJsonApi,
4
4
  configureLogin,
5
5
  configureRoles,
6
+ configureWaitlist,
6
7
  getApiUrl,
7
8
  getAppUrl,
8
9
  getRoleId,
9
10
  getStripePublishableKey,
10
11
  getTrackablePages,
12
+ getWaitlistConfig,
11
13
  isRolesConfigured
12
- } from "./chunk-XLMJPA4N.mjs";
14
+ } from "./chunk-RRIYLEY6.mjs";
13
15
  import {
14
16
  AVAILABLE_OAUTH_SCOPES,
15
17
  AbstractApiData,
@@ -95,6 +97,11 @@ import {
95
97
  UserFields,
96
98
  UserModule,
97
99
  UserService,
100
+ Waitlist,
101
+ WaitlistModule,
102
+ WaitlistService,
103
+ WaitlistStats,
104
+ WaitlistStatsModule,
98
105
  checkPermissions,
99
106
  checkPermissionsFromServer,
100
107
  clearLastApiTotal,
@@ -129,7 +136,7 @@ import {
129
136
  useComposedRefs,
130
137
  useIsMobile,
131
138
  userObjectSchema
132
- } from "./chunk-TQ5GRRTM.mjs";
139
+ } from "./chunk-GVN7XC3U.mjs";
133
140
  import "./chunk-AUXK7QSA.mjs";
134
141
  import "./chunk-C7C7VY4F.mjs";
135
142
  import {
@@ -232,6 +239,11 @@ export {
232
239
  UserFields,
233
240
  UserModule,
234
241
  UserService,
242
+ Waitlist,
243
+ WaitlistModule,
244
+ WaitlistService,
245
+ WaitlistStats,
246
+ WaitlistStatsModule,
235
247
  checkPermissions,
236
248
  checkPermissionsFromServer,
237
249
  clearLastApiTotal,
@@ -242,6 +254,7 @@ export {
242
254
  configureJsonApi,
243
255
  configureLogin,
244
256
  configureRoles,
257
+ configureWaitlist,
245
258
  createJsonApiInclusion,
246
259
  dismissToast,
247
260
  entityObjectSchema,
@@ -266,6 +279,7 @@ export {
266
279
  getTokenHandler,
267
280
  getTrackablePages,
268
281
  getValueFromPath,
282
+ getWaitlistConfig,
269
283
  hasBootstrapper,
270
284
  isRolesConfigured,
271
285
  rehydrate,
@@ -243,6 +243,7 @@ type AuthInput = {
243
243
  termsAcceptedAt?: string;
244
244
  marketingConsent?: boolean;
245
245
  marketingConsentAt?: string | null;
246
+ inviteCode?: string;
246
247
  };
247
248
  type AuthQuery = {
248
249
  userId?: string;
@@ -243,6 +243,7 @@ type AuthInput = {
243
243
  termsAcceptedAt?: string;
244
244
  marketingConsent?: boolean;
245
245
  marketingConsentAt?: string | null;
246
+ inviteCode?: string;
246
247
  };
247
248
  type AuthQuery = {
248
249
  userId?: string;
@@ -2,7 +2,7 @@ import { A as ApiData } from '../ApiData-DPKNfY-9.mjs';
2
2
  import { M as ModuleWithPermissions, A as Action } from '../notification.interface-DIln2r7X.mjs';
3
3
  import { A as ApiRequestDataTypeInterface } from '../ApiRequestDataTypeInterface-CUKFDBx2.mjs';
4
4
  import { A as ApiResponseInterface } from '../ApiResponseInterface-zeewugD7.mjs';
5
- export { f as ServerAuthService, C as ServerCompanyService, h as ServerContentService, F as ServerFeatureService, i as ServerNotificationService, j as ServerPushService, R as ServerRoleService, m as ServerS3Service, U as ServerUserService } from '../s3.service-hnTPVTm2.mjs';
5
+ export { f as ServerAuthService, C as ServerCompanyService, h as ServerContentService, F as ServerFeatureService, i as ServerNotificationService, j as ServerPushService, R as ServerRoleService, m as ServerS3Service, U as ServerUserService } from '../s3.service-BoOF5-ln.mjs';
6
6
  import 'lucide-react';
7
7
  import '../ApiDataInterface-DPP8s46n.mjs';
8
8
  import '../feature.interface-BxFFOPNq.mjs';
@@ -2,7 +2,7 @@ import { A as ApiData } from '../ApiData-DPKNfY-9.js';
2
2
  import { M as ModuleWithPermissions, A as Action } from '../notification.interface-XARGKJAq.js';
3
3
  import { A as ApiRequestDataTypeInterface } from '../ApiRequestDataTypeInterface-CUKFDBx2.js';
4
4
  import { A as ApiResponseInterface } from '../ApiResponseInterface-CAIAeP5d.js';
5
- export { f as ServerAuthService, C as ServerCompanyService, h as ServerContentService, F as ServerFeatureService, i as ServerNotificationService, j as ServerPushService, R as ServerRoleService, m as ServerS3Service, U as ServerUserService } from '../s3.service-DXkDoMf1.js';
5
+ export { f as ServerAuthService, C as ServerCompanyService, h as ServerContentService, F as ServerFeatureService, i as ServerNotificationService, j as ServerPushService, R as ServerRoleService, m as ServerS3Service, U as ServerUserService } from '../s3.service-Mxo-7wQ6.js';
6
6
  import 'lucide-react';
7
7
  import '../ApiDataInterface-DPP8s46n.js';
8
8
  import '../feature.interface-CIWxo8NP.js';
@@ -15,7 +15,7 @@ var _chunk3ZPK4QOBjs = require('../chunk-3ZPK4QOB.js');
15
15
 
16
16
 
17
17
 
18
- var _chunk3EZX4G2Ejs = require('../chunk-3EZX4G2E.js');
18
+ var _chunkZNGEVB5Mjs = require('../chunk-ZNGEVB5M.js');
19
19
  require('../chunk-LXKSUWAV.js');
20
20
  require('../chunk-IBS6NI7D.js');
21
21
 
@@ -86,7 +86,7 @@ var ServerSession = class {
86
86
  if (!rawModules) return false;
87
87
  const modules = JSON.parse(_pako2.default.ungzip(Buffer.from(rawModules, "base64"), { to: "string" }));
88
88
  const selectedModule = modules.find((module) => module.id === params.module.moduleId);
89
- return _chunk3EZX4G2Ejs.checkPermissionsFromServer.call(void 0, {
89
+ return _chunkZNGEVB5Mjs.checkPermissionsFromServer.call(void 0, {
90
90
  module: params.module,
91
91
  action: params.action,
92
92
  data: params.data,
@@ -296,5 +296,5 @@ _chunk7QVYU63Ejs.__name.call(void 0, ServerJsonApiDelete, "ServerJsonApiDelete")
296
296
 
297
297
 
298
298
 
299
- exports.ServerAuthService = _chunk3EZX4G2Ejs.AuthService; exports.ServerCompanyService = _chunk3EZX4G2Ejs.CompanyService; exports.ServerContentService = _chunk3EZX4G2Ejs.ContentService; exports.ServerFeatureService = _chunk3EZX4G2Ejs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunk3EZX4G2Ejs.NotificationService; exports.ServerPushService = _chunk3EZX4G2Ejs.PushService; exports.ServerRoleService = _chunk3EZX4G2Ejs.RoleService; exports.ServerS3Service = _chunk3EZX4G2Ejs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunk3EZX4G2Ejs.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
299
+ exports.ServerAuthService = _chunkZNGEVB5Mjs.AuthService; exports.ServerCompanyService = _chunkZNGEVB5Mjs.CompanyService; exports.ServerContentService = _chunkZNGEVB5Mjs.ContentService; exports.ServerFeatureService = _chunkZNGEVB5Mjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunkZNGEVB5Mjs.NotificationService; exports.ServerPushService = _chunkZNGEVB5Mjs.PushService; exports.ServerRoleService = _chunkZNGEVB5Mjs.RoleService; exports.ServerS3Service = _chunkZNGEVB5Mjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunkZNGEVB5Mjs.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
300
300
  //# sourceMappingURL=index.js.map
@@ -15,7 +15,7 @@ import {
15
15
  S3Service,
16
16
  UserService,
17
17
  checkPermissionsFromServer
18
- } from "../chunk-TQ5GRRTM.mjs";
18
+ } from "../chunk-GVN7XC3U.mjs";
19
19
  import "../chunk-AUXK7QSA.mjs";
20
20
  import "../chunk-C7C7VY4F.mjs";
21
21
  import {
@@ -0,0 +1,26 @@
1
+ type QuestionnaireFieldType = "text" | "textarea" | "select" | "checkbox";
2
+ interface QuestionnaireOption {
3
+ value: string;
4
+ label: string;
5
+ description?: string;
6
+ }
7
+ interface QuestionnaireField {
8
+ id: string;
9
+ type: QuestionnaireFieldType;
10
+ label: string;
11
+ description?: string;
12
+ placeholder?: string;
13
+ required?: boolean;
14
+ options?: QuestionnaireOption[];
15
+ }
16
+ interface WaitlistConfig {
17
+ questionnaire?: QuestionnaireField[];
18
+ heroTitle?: string;
19
+ heroSubtitle?: string;
20
+ heroDescription?: string;
21
+ benefits?: string[];
22
+ }
23
+ declare function configureWaitlist(config: WaitlistConfig): void;
24
+ declare function getWaitlistConfig(): WaitlistConfig;
25
+
26
+ export { type QuestionnaireField as Q, type WaitlistConfig as W, type QuestionnaireFieldType as a, type QuestionnaireOption as b, configureWaitlist as c, getWaitlistConfig as g };
@@ -0,0 +1,26 @@
1
+ type QuestionnaireFieldType = "text" | "textarea" | "select" | "checkbox";
2
+ interface QuestionnaireOption {
3
+ value: string;
4
+ label: string;
5
+ description?: string;
6
+ }
7
+ interface QuestionnaireField {
8
+ id: string;
9
+ type: QuestionnaireFieldType;
10
+ label: string;
11
+ description?: string;
12
+ placeholder?: string;
13
+ required?: boolean;
14
+ options?: QuestionnaireOption[];
15
+ }
16
+ interface WaitlistConfig {
17
+ questionnaire?: QuestionnaireField[];
18
+ heroTitle?: string;
19
+ heroSubtitle?: string;
20
+ heroDescription?: string;
21
+ benefits?: string[];
22
+ }
23
+ declare function configureWaitlist(config: WaitlistConfig): void;
24
+ declare function getWaitlistConfig(): WaitlistConfig;
25
+
26
+ export { type QuestionnaireField as Q, type WaitlistConfig as W, type QuestionnaireFieldType as a, type QuestionnaireOption as b, configureWaitlist as c, getWaitlistConfig as g };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "1.39.2",
3
+ "version": "1.40.1",
4
4
  "description": "Next.js JSON:API client with server/client support and caching",
5
5
  "author": "Carlo Nicora",
6
6
  "license": "GPL-3.0-or-later",
@@ -39,7 +39,7 @@ export function FormCheckbox({ form, id, name, labelBefore, description, isRequi
39
39
  <div className="flex gap-x-4">
40
40
  {labelBefore && label()}
41
41
  {labelBefore && isRequired && <span className="text-destructive ml-2 font-semibold">*</span>}
42
- <Checkbox id={id} defaultChecked={field.value} onCheckedChange={field.onChange} />
42
+ <Checkbox id={id} checked={field.value ?? false} onCheckedChange={field.onChange} />
43
43
  {!labelBefore && label()}
44
44
  {!labelBefore && isRequired && <span className="text-destructive ml-2 font-semibold">*</span>}
45
45
  </div>
@@ -44,7 +44,7 @@ export function FormSelect({
44
44
  data-testid={testId}
45
45
  >
46
46
  <SelectTrigger className="w-full">
47
- <SelectValue />
47
+ <SelectValue>{values.find((v) => v.id === field.value)?.text}</SelectValue>
48
48
  </SelectTrigger>
49
49
  <SelectContent>
50
50
  {values.map((type: { id: string; text: string }) => (
@@ -19,6 +19,7 @@ export * from "../features/onboarding/components";
19
19
  export * from "../features/role/components";
20
20
  export * from "../features/user/components";
21
21
  export * from "../features/oauth/components";
22
+ export * from "../features/waitlist/components";
22
23
 
23
24
  // shadcn/ui components (merged from /shadcnui entry point)
24
25
  export * from "../shadcnui";
package/src/core/index.ts CHANGED
@@ -62,3 +62,6 @@ export * from "../features/user/user.module";
62
62
  export * from "../features/oauth/oauth.module";
63
63
  export * from "../features/oauth/data";
64
64
  export * from "../features/oauth/interfaces";
65
+ export * from "../features/waitlist/data";
66
+ export * from "../features/waitlist/waitlist.module";
67
+ export * from "../features/waitlist/waitlist-stats.module";
@@ -29,6 +29,9 @@ export interface FoundationModuleDefinitions {
29
29
  StripePromotionCode: ModuleWithPermissions;
30
30
  // OAuth modules
31
31
  OAuth: ModuleWithPermissions;
32
+ // Waitlist modules
33
+ Waitlist: ModuleWithPermissions;
34
+ WaitlistStats: ModuleWithPermissions;
32
35
  }
33
36
 
34
37
  // App-specific modules - apps will augment this interface ONLY
@@ -1,13 +1,17 @@
1
1
  "use client";
2
2
 
3
3
  import { zodResolver } from "@hookform/resolvers/zod";
4
+ import { Loader2 } from "lucide-react";
4
5
  import { useTranslations } from "next-intl";
5
6
  import Image from "next/image";
6
- import { useState } from "react";
7
+ import { useSearchParams } from "next/navigation";
8
+ import { useEffect, useState } from "react";
7
9
  import { SubmitHandler, useForm } from "react-hook-form";
8
10
  import { v4 } from "uuid";
9
11
  import { z } from "zod";
12
+ import { getApiUrl } from "../../../../client/config";
10
13
  import { errorToast, FormInput, FormPassword } from "../../../../components";
14
+ import { getRegistrationMode, isDiscordAuthEnabled, isGoogleAuthEnabled } from "../../../../login/config";
11
15
  import { GdprConsentSection } from "../GdprConsentSection";
12
16
  import {
13
17
  Button,
@@ -22,12 +26,22 @@ import {
22
26
  import { useAuthContext } from "../../contexts";
23
27
  import { AuthService } from "../../data/auth.service";
24
28
  import { AuthComponent } from "../../enums";
29
+ import { WaitlistService } from "../../../waitlist/data/WaitlistService";
25
30
 
26
31
  export default function Register() {
27
32
  const t = useTranslations();
28
33
  const { setComponentType } = useAuthContext();
34
+ const searchParams = useSearchParams();
35
+ const inviteCode = searchParams.get("invite");
36
+ const registrationMode = getRegistrationMode();
29
37
 
30
38
  const [showConfirmation, setShowConfirmation] = useState<boolean>(false);
39
+ const [inviteValidated, setInviteValidated] = useState<boolean>(false);
40
+ const [inviteError, setInviteError] = useState<string>("");
41
+ // Initialize loading state to true if we have an invite code in waitlist mode
42
+ const [isValidatingInvite, setIsValidatingInvite] = useState<boolean>(
43
+ registrationMode === "waitlist" && !!inviteCode,
44
+ );
31
45
 
32
46
  const formSchema = z.object({
33
47
  company: z.string().min(1, {
@@ -63,6 +77,41 @@ export default function Register() {
63
77
  },
64
78
  });
65
79
 
80
+ useEffect(() => {
81
+ async function validateInvite() {
82
+ console.log("[Register] validateInvite called. registrationMode:", registrationMode, "inviteCode:", inviteCode);
83
+
84
+ if (registrationMode !== "waitlist" || !inviteCode) {
85
+ console.log("[Register] Skipping validation - not in waitlist mode or no invite code");
86
+ return;
87
+ }
88
+
89
+ setIsValidatingInvite(true);
90
+ try {
91
+ console.log("[Register] Calling WaitlistService.validateInvite...");
92
+ const result = await WaitlistService.validateInvite(inviteCode);
93
+ console.log("[Register] Validation result:", JSON.stringify(result));
94
+
95
+ if (result && result.valid) {
96
+ console.log("[Register] Invite valid! Email:", result.email);
97
+ setInviteValidated(true);
98
+ form.setValue("email", result.email);
99
+ } else {
100
+ const errorMsg = result ? t("waitlist.invite.error_expired") : t("waitlist.invite.error_invalid");
101
+ console.log("[Register] Invite invalid. result:", result, "errorMsg:", errorMsg);
102
+ setInviteError(errorMsg);
103
+ }
104
+ } catch (error) {
105
+ console.error("[Register] Validation exception:", error);
106
+ setInviteError(t("waitlist.invite.error_validation_failed"));
107
+ } finally {
108
+ setIsValidatingInvite(false);
109
+ }
110
+ }
111
+
112
+ validateInvite();
113
+ }, [registrationMode, inviteCode, form, t]);
114
+
66
115
  const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = async (values: z.infer<typeof formSchema>) => {
67
116
  try {
68
117
  const payload = {
@@ -74,6 +123,7 @@ export default function Register() {
74
123
  termsAcceptedAt: new Date().toISOString(),
75
124
  marketingConsent: values.marketingConsent ?? false,
76
125
  marketingConsentAt: values.marketingConsent ? new Date().toISOString() : null,
126
+ inviteCode: inviteCode ?? undefined,
77
127
  };
78
128
 
79
129
  await AuthService.register(payload);
@@ -83,6 +133,71 @@ export default function Register() {
83
133
  }
84
134
  };
85
135
 
136
+ // Show loading state while validating invite
137
+ if (registrationMode === "waitlist" && inviteCode && isValidatingInvite) {
138
+ return (
139
+ <>
140
+ <CardHeader>
141
+ <CardTitle className="text-primary flex flex-col items-center pb-10 text-4xl">
142
+ <Image src="/logo.webp" alt="Logo" width={100} height={100} priority />
143
+ {t("waitlist.invite.validating_title")}
144
+ </CardTitle>
145
+ </CardHeader>
146
+ <CardContent className="flex flex-col items-center justify-center space-y-4 py-8">
147
+ <Loader2 className="h-8 w-8 animate-spin text-primary" />
148
+ <p className="text-muted-foreground">{t("waitlist.invite.validating_description")}</p>
149
+ </CardContent>
150
+ </>
151
+ );
152
+ }
153
+
154
+ // Show error if invite validation failed
155
+ if (registrationMode === "waitlist" && inviteCode && inviteError) {
156
+ return (
157
+ <>
158
+ <CardHeader>
159
+ <CardTitle className="text-primary flex flex-col items-center pb-10 text-4xl">
160
+ <Image src="/logo.webp" alt="Logo" width={100} height={100} priority />
161
+ {t("waitlist.invite.invalid_title")}
162
+ </CardTitle>
163
+ </CardHeader>
164
+ <CardContent className="text-center">
165
+ <p className="text-destructive mb-4">{inviteError}</p>
166
+ <p className="mb-4">{t("waitlist.invite.join_prompt")}</p>
167
+ <Link href="/waitlist" className="text-primary underline">
168
+ {t("waitlist.invite.join_link")}
169
+ </Link>
170
+ </CardContent>
171
+ </>
172
+ );
173
+ }
174
+
175
+ // Show waitlist message if in waitlist mode without invite
176
+ if (registrationMode === "waitlist" && !inviteCode) {
177
+ return (
178
+ <>
179
+ <CardHeader>
180
+ <CardTitle className="text-primary flex flex-col items-center pb-10 text-4xl">
181
+ <Image src="/logo.webp" alt="Logo" width={100} height={100} priority />
182
+ {t("waitlist.invite.registration_title")}
183
+ </CardTitle>
184
+ </CardHeader>
185
+ <CardContent className="text-center">
186
+ <p className="mb-4">{t("waitlist.invite.registration_description")}</p>
187
+ <p className="mb-4">{t("waitlist.invite.registration_hint")}</p>
188
+ <Link href="/waitlist" className="text-primary underline">
189
+ {t("waitlist.invite.join_link")}
190
+ </Link>
191
+ </CardContent>
192
+ <CardFooter className="flex w-full flex-row justify-between">
193
+ <Link href="#" className="flex w-full justify-start" onClick={() => setComponentType(AuthComponent.Login)}>
194
+ {t("auth.buttons.login")}
195
+ </Link>
196
+ </CardFooter>
197
+ </>
198
+ );
199
+ }
200
+
86
201
  return (
87
202
  <>
88
203
  <CardHeader>
@@ -134,6 +249,70 @@ export default function Register() {
134
249
  <Button className="mt-4 w-full" type={"submit"}>
135
250
  {t(`auth.buttons.register`)}
136
251
  </Button>
252
+
253
+ {/* OAuth options when invite code is validated */}
254
+ {registrationMode === "waitlist" &&
255
+ inviteValidated &&
256
+ (isGoogleAuthEnabled() || isDiscordAuthEnabled()) && (
257
+ <div className="space-y-4 pt-4">
258
+ <div className="relative">
259
+ <div className="absolute inset-0 flex items-center">
260
+ <span className="w-full border-t" />
261
+ </div>
262
+ <div className="relative flex justify-center text-xs uppercase">
263
+ <span className="bg-card px-2 text-muted-foreground">{t("auth.buttons.or")}</span>
264
+ </div>
265
+ </div>
266
+
267
+ <div className="space-y-2">
268
+ {isGoogleAuthEnabled() && (
269
+ <Link
270
+ href={`${getApiUrl()}auth/google${inviteCode ? `?invite=${inviteCode}` : ""}`}
271
+ className="flex w-full"
272
+ >
273
+ <Button
274
+ className="w-full bg-white hover:bg-gray-50 text-gray-700 border border-gray-300"
275
+ variant="outline"
276
+ type="button"
277
+ >
278
+ <svg className="mr-2 h-5 w-5" viewBox="0 0 24 24">
279
+ <path
280
+ fill="#4285F4"
281
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
282
+ />
283
+ <path
284
+ fill="#34A853"
285
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
286
+ />
287
+ <path
288
+ fill="#FBBC05"
289
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
290
+ />
291
+ <path
292
+ fill="#EA4335"
293
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
294
+ />
295
+ </svg>
296
+ {t("waitlist.buttons.register_with_google")}
297
+ </Button>
298
+ </Link>
299
+ )}
300
+ {isDiscordAuthEnabled() && (
301
+ <Link
302
+ href={`${getApiUrl()}auth/discord${inviteCode ? `?invite=${inviteCode}` : ""}`}
303
+ className="flex w-full"
304
+ >
305
+ <Button className="w-full" variant="outline" type="button">
306
+ <svg className="mr-2 h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
307
+ <path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
308
+ </svg>
309
+ {t("waitlist.buttons.register_with_discord")}
310
+ </Button>
311
+ </Link>
312
+ )}
313
+ </div>
314
+ </div>
315
+ )}
137
316
  </CardContent>
138
317
  <CardFooter className="flex w-full flex-row justify-between">
139
318
  <Link
@@ -12,6 +12,7 @@ export type AuthInput = {
12
12
  termsAcceptedAt?: string;
13
13
  marketingConsent?: boolean;
14
14
  marketingConsentAt?: string | null;
15
+ inviteCode?: string;
15
16
  };
16
17
 
17
18
  export type AuthQuery = {
@@ -49,6 +49,7 @@ export class Auth extends AbstractApiData implements AuthInterface {
49
49
  if (data.termsAcceptedAt !== undefined) response.data.attributes.termsAcceptedAt = data.termsAcceptedAt;
50
50
  if (data.marketingConsent !== undefined) response.data.attributes.marketingConsent = data.marketingConsent;
51
51
  if (data.marketingConsentAt !== undefined) response.data.attributes.marketingConsentAt = data.marketingConsentAt;
52
+ if (data.inviteCode !== undefined) response.data.attributes.inviteCode = data.inviteCode;
52
53
 
53
54
  return response;
54
55
  }
@@ -19,3 +19,4 @@ export * from "./s3";
19
19
  export * from "./search";
20
20
  export * from "./user";
21
21
  export * from "./oauth";
22
+ export * from "./waitlist";