gymmonk-schema 0.4.0 → 0.5.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/common.d.ts +6 -0
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +8 -2
- package/dist/common.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/messages.d.ts +184 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +162 -0
- package/dist/messages.js.map +1 -0
- package/dist/onboarding-form.d.ts +256 -0
- package/dist/onboarding-form.d.ts.map +1 -0
- package/dist/onboarding-form.js +317 -0
- package/dist/onboarding-form.js.map +1 -0
- package/package.json +1 -1
package/dist/common.d.ts
CHANGED
|
@@ -24,6 +24,12 @@ export declare const paginationSchema: z.ZodObject<{
|
|
|
24
24
|
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
25
25
|
}, z.core.$strip>;
|
|
26
26
|
export type Pagination = z.infer<typeof paginationSchema>;
|
|
27
|
+
/**
|
|
28
|
+
* The raw patterns, exported so the onboarding FORM schemas validate a typed
|
|
29
|
+
* value exactly the way the API schemas below do — one rule, both sides.
|
|
30
|
+
*/
|
|
31
|
+
export declare const PHONE_REGEX: RegExp;
|
|
32
|
+
export declare const PINCODE_REGEX: RegExp;
|
|
27
33
|
/**
|
|
28
34
|
* A bare 10-digit Indian mobile number. Stored RAW (no `+91`) app-wide — the
|
|
29
35
|
* country code is display-only. Mirrors the jansathi phone-number standard.
|
package/dist/common.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,OAAO,KAAG,OAGvD,CAAC;AAEF,yEAAyE;AACzE,eAAO,MAAM,qBAAqB,GAAI,kBAAe,gDAC8B,CAAC;AAIpF,kDAAkD;AAClD,eAAO,MAAM,cAAc,aAGgB,CAAC;AAC5C,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAI5D,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB;;;iBAG3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D;;;GAGG;AACH,eAAO,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,OAAO,KAAG,OAGvD,CAAC;AAEF,yEAAyE;AACzE,eAAO,MAAM,qBAAqB,GAAI,kBAAe,gDAC8B,CAAC;AAIpF,kDAAkD;AAClD,eAAO,MAAM,cAAc,aAGgB,CAAC;AAC5C,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAI5D,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB;;;iBAG3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D;;;GAGG;AACH,eAAO,MAAM,WAAW,QAAiB,CAAC;AAC1C,eAAO,MAAM,aAAa,QAAY,CAAC;AAEvC;;;GAGG;AACH,eAAO,MAAM,WAAW,aAGqC,CAAC;AAC9D,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD,6DAA6D;AAC7D,eAAO,MAAM,mBAAmB,6CAA+D,CAAC;AAEhG,wCAAwC;AACxC,eAAO,MAAM,aAAa,aAG8B,CAAC;AACzD,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD,6EAA6E;AAC7E,eAAO,MAAM,mBAAmB,4CAG/B,CAAC;AAIF,4DAA4D;AAC5D,eAAO,MAAM,aAAa,aAGoC,CAAC;AAC/D,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD,kEAAkE;AAClE,eAAO,MAAM,iBAAiB,kBAAmC,CAAC;AAClE,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,sCAAsC;AACtC,eAAO,MAAM,eAAe,aAGmC,CAAC;AAChE,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAIxD;;;GAGG;AACH,eAAO,MAAM,cAAc,aAA0B,CAAC;AACtD,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC"}
|
package/dist/common.js
CHANGED
|
@@ -33,6 +33,12 @@ export const paginationSchema = z.object({
|
|
|
33
33
|
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
34
34
|
});
|
|
35
35
|
// ─── Contact primitives (India) ──────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* The raw patterns, exported so the onboarding FORM schemas validate a typed
|
|
38
|
+
* value exactly the way the API schemas below do — one rule, both sides.
|
|
39
|
+
*/
|
|
40
|
+
export const PHONE_REGEX = /^[6-9]\d{9}$/;
|
|
41
|
+
export const PINCODE_REGEX = /^\d{6}$/;
|
|
36
42
|
/**
|
|
37
43
|
* A bare 10-digit Indian mobile number. Stored RAW (no `+91`) app-wide — the
|
|
38
44
|
* country code is display-only. Mirrors the jansathi phone-number standard.
|
|
@@ -40,14 +46,14 @@ export const paginationSchema = z.object({
|
|
|
40
46
|
export const phoneSchema = z
|
|
41
47
|
.string()
|
|
42
48
|
.trim()
|
|
43
|
-
.regex(
|
|
49
|
+
.regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number');
|
|
44
50
|
/** Optional phone — empty/whitespace becomes `undefined`. */
|
|
45
51
|
export const optionalPhoneSchema = z.preprocess(emptyStringToUndefined, phoneSchema.optional());
|
|
46
52
|
/** A 6-digit Indian postal PIN code. */
|
|
47
53
|
export const pincodeSchema = z
|
|
48
54
|
.string()
|
|
49
55
|
.trim()
|
|
50
|
-
.regex(
|
|
56
|
+
.regex(PINCODE_REGEX, 'Enter a valid 6-digit pincode');
|
|
51
57
|
/** Optional email — empty/whitespace becomes `undefined`, else validated. */
|
|
52
58
|
export const optionalEmailSchema = z.preprocess(emptyStringToUndefined, z.email('Enter a valid email').optional());
|
|
53
59
|
// ─── Date & time primitives ──────────────────────────────────────────────────
|
package/dist/common.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.js","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,KAAc,EAAW,EAAE;IAChE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IACvE,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,yEAAyE;AACzE,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,SAAS,GAAG,GAAG,EAAE,EAAE,CACvD,CAAC,CAAC,UAAU,CAAC,sBAAsB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;AAEpF,gFAAgF;AAEhF,kDAAkD;AAClD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC;KAC5B,MAAM,EAAE;KACR,IAAI,EAAE;KACN,KAAK,CAAC,mBAAmB,EAAE,YAAY,CAAC,CAAC;AAG5C,gFAAgF;AAEhF,4DAA4D;AAC5D,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/C,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;CAC3D,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC;KACzB,MAAM,EAAE;KACR,IAAI,EAAE;KACN,KAAK,CAAC,
|
|
1
|
+
{"version":3,"file":"common.js","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Shared helpers and primitives\n * ==============================================\n * The stable building blocks reused across every GymMonk domain schema.\n * Mirrors the `dating-schema` / `near-me-schema` idiom: pure Zod, const-object\n * enums fed to `z.enum(...)`, NodeNext `.js` import specifiers.\n *\n * @module gymmonk-schema/common\n */\n\nimport { z } from 'zod';\n\n// ─── String helpers ──────────────────────────────────────────────────────────\n\n/**\n * Converts empty / whitespace-only strings to `undefined`\n * so Zod `.optional()` treats them as \"not provided\".\n */\nexport const emptyStringToUndefined = (value: unknown): unknown => {\n if (typeof value === 'string' && value.trim() === '') return undefined;\n return value;\n};\n\n/** Optional trimmed string. Empty strings are coerced to `undefined`. */\nexport const optionalTrimmedString = (maxLength = 100) =>\n z.preprocess(emptyStringToUndefined, z.string().trim().max(maxLength).optional());\n\n// ─── Identifiers ─────────────────────────────────────────────────────────────\n\n/** A 24-character hex MongoDB ObjectId string. */\nexport const objectIdSchema = z\n .string()\n .trim()\n .regex(/^[0-9a-fA-F]{24}$/, 'Invalid id');\nexport type ObjectIdString = z.infer<typeof objectIdSchema>;\n\n// ─── Pagination ──────────────────────────────────────────────────────────────\n\n/** Pagination query params shared across list endpoints. */\nexport const paginationSchema = z.object({\n page: z.coerce.number().int().min(1).default(1),\n limit: z.coerce.number().int().min(1).max(100).default(20),\n});\nexport type Pagination = z.infer<typeof paginationSchema>;\n\n// ─── Contact primitives (India) ──────────────────────────────────────────────\n\n/**\n * The raw patterns, exported so the onboarding FORM schemas validate a typed\n * value exactly the way the API schemas below do — one rule, both sides.\n */\nexport const PHONE_REGEX = /^[6-9]\\d{9}$/;\nexport const PINCODE_REGEX = /^\\d{6}$/;\n\n/**\n * A bare 10-digit Indian mobile number. Stored RAW (no `+91`) app-wide — the\n * country code is display-only. Mirrors the jansathi phone-number standard.\n */\nexport const phoneSchema = z\n .string()\n .trim()\n .regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number');\nexport type Phone = z.infer<typeof phoneSchema>;\n\n/** Optional phone — empty/whitespace becomes `undefined`. */\nexport const optionalPhoneSchema = z.preprocess(emptyStringToUndefined, phoneSchema.optional());\n\n/** A 6-digit Indian postal PIN code. */\nexport const pincodeSchema = z\n .string()\n .trim()\n .regex(PINCODE_REGEX, 'Enter a valid 6-digit pincode');\nexport type Pincode = z.infer<typeof pincodeSchema>;\n\n/** Optional email — empty/whitespace becomes `undefined`, else validated. */\nexport const optionalEmailSchema = z.preprocess(\n emptyStringToUndefined,\n z.email('Enter a valid email').optional(),\n);\n\n// ─── Date & time primitives ──────────────────────────────────────────────────\n\n/** A calendar date with no time component, `YYYY-MM-DD`. */\nexport const isoDateSchema = z\n .string()\n .trim()\n .regex(/^\\d{4}-\\d{2}-\\d{2}$/, 'Expected an YYYY-MM-DD date');\nexport type IsoDate = z.infer<typeof isoDateSchema>;\n\n/** A full ISO-8601 timestamp, e.g. `2026-08-06T09:30:00.000Z`. */\nexport const isoDateTimeSchema = z.iso.datetime({ offset: true });\nexport type IsoDateTime = z.infer<typeof isoDateTimeSchema>;\n\n/** Time of day in 24-hour `HH:MM`. */\nexport const timeOfDaySchema = z\n .string()\n .trim()\n .regex(/^([01]\\d|2[0-3]):[0-5]\\d$/, 'Expected an HH:MM time');\nexport type TimeOfDay = z.infer<typeof timeOfDaySchema>;\n\n// ─── Money ───────────────────────────────────────────────────────────────────\n\n/**\n * A whole-rupee (INR) amount. GymMonk prices are whole rupees; format with a\n * `₹` and grouping at the edge, never store the formatted string.\n */\nexport const priceInrSchema = z.number().int().min(0);\nexport type PriceInr = z.infer<typeof priceInrSchema>;\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -21,8 +21,10 @@ export * from './member.js';
|
|
|
21
21
|
export * from './member-profile.js';
|
|
22
22
|
export * from './membership.js';
|
|
23
23
|
export * from './membership-plan.js';
|
|
24
|
+
export * from './messages.js';
|
|
24
25
|
export * from './notification.js';
|
|
25
26
|
export * from './onboarding.js';
|
|
27
|
+
export * from './onboarding-form.js';
|
|
26
28
|
export * from './organisation.js';
|
|
27
29
|
export * from './owner-profile.js';
|
|
28
30
|
export * from './pricing.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAE5B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAE/B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAE9B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAE9B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAEhC,cAAc,sBAAsB,CAAC;AAErC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAEhC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAE5B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAE/B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAE9B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAE9B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAEhC,cAAc,sBAAsB,CAAC;AAErC,cAAc,eAAe,CAAC;AAE9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAEhC,cAAc,sBAAsB,CAAC;AAErC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -26,9 +26,13 @@ export * from './member-profile.js';
|
|
|
26
26
|
export * from './membership.js';
|
|
27
27
|
// ─── Membership ──────────────────────────────────────────────────────────────
|
|
28
28
|
export * from './membership-plan.js';
|
|
29
|
+
// ─── API result messages + error codes ───────────────────────────────────────
|
|
30
|
+
export * from './messages.js';
|
|
29
31
|
// ─── Engagement & analytics ──────────────────────────────────────────────────
|
|
30
32
|
export * from './notification.js';
|
|
31
33
|
export * from './onboarding.js';
|
|
34
|
+
// ─── Onboarding wizard FORM validation (client-side working shapes) ──────────
|
|
35
|
+
export * from './onboarding-form.js';
|
|
32
36
|
// ─── Gym structure & onboarding ──────────────────────────────────────────────
|
|
33
37
|
export * from './organisation.js';
|
|
34
38
|
export * from './owner-profile.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,gFAAgF;AAChF,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC","sourcesContent":["/**\n * gymmonk-schema — Public API\n * ===========================\n * Shared Zod schemas, enums and domain types for GymMonk (fitness SaaS).\n * Single source of truth (SSOT) for data shapes, TypeScript types and\n * validation, consumed by both gymmonk-backend and gymmonk-web-client.\n *\n * Every domain module is re-exported through this barrel. Import names are\n * globally unique across modules, so `export *` composes without collisions.\n */\n\nexport * from './announcement.js';\nexport * from './center.js';\n// ─── Foundation ──────────────────────────────────────────────────────────────\nexport * from './common.js';\nexport * from './equipment.js';\n// ─── Training ────────────────────────────────────────────────────────────────\nexport * from './exercise.js';\nexport * from './feedback.js';\n// ─── Uploads ─────────────────────────────────────────────────────────────────\nexport * from './file.js';\nexport * from './home.js';\nexport * from './insights.js';\n// ─── People ──────────────────────────────────────────────────────────────────\nexport * from './member.js';\nexport * from './member-profile.js';\nexport * from './membership.js';\n// ─── Membership ──────────────────────────────────────────────────────────────\nexport * from './membership-plan.js';\n// ─── Engagement & analytics ──────────────────────────────────────────────────\nexport * from './notification.js';\nexport * from './onboarding.js';\n// ─── Gym structure & onboarding ──────────────────────────────────────────────\nexport * from './organisation.js';\nexport * from './owner-profile.js';\nexport * from './pricing.js';\nexport * from './session.js';\nexport * from './shared.js';\nexport * from './staff.js';\nexport * from './subscription.js';\nexport * from './user.js';\nexport * from './workout-plan.js';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,gFAAgF;AAChF,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC","sourcesContent":["/**\n * gymmonk-schema — Public API\n * ===========================\n * Shared Zod schemas, enums and domain types for GymMonk (fitness SaaS).\n * Single source of truth (SSOT) for data shapes, TypeScript types and\n * validation, consumed by both gymmonk-backend and gymmonk-web-client.\n *\n * Every domain module is re-exported through this barrel. Import names are\n * globally unique across modules, so `export *` composes without collisions.\n */\n\nexport * from './announcement.js';\nexport * from './center.js';\n// ─── Foundation ──────────────────────────────────────────────────────────────\nexport * from './common.js';\nexport * from './equipment.js';\n// ─── Training ────────────────────────────────────────────────────────────────\nexport * from './exercise.js';\nexport * from './feedback.js';\n// ─── Uploads ─────────────────────────────────────────────────────────────────\nexport * from './file.js';\nexport * from './home.js';\nexport * from './insights.js';\n// ─── People ──────────────────────────────────────────────────────────────────\nexport * from './member.js';\nexport * from './member-profile.js';\nexport * from './membership.js';\n// ─── Membership ──────────────────────────────────────────────────────────────\nexport * from './membership-plan.js';\n// ─── API result messages + error codes ───────────────────────────────────────\nexport * from './messages.js';\n// ─── Engagement & analytics ──────────────────────────────────────────────────\nexport * from './notification.js';\nexport * from './onboarding.js';\n// ─── Onboarding wizard FORM validation (client-side working shapes) ──────────\nexport * from './onboarding-form.js';\n// ─── Gym structure & onboarding ──────────────────────────────────────────────\nexport * from './organisation.js';\nexport * from './owner-profile.js';\nexport * from './pricing.js';\nexport * from './session.js';\nexport * from './shared.js';\nexport * from './staff.js';\nexport * from './subscription.js';\nexport * from './user.js';\nexport * from './workout-plan.js';\n"]}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gymmonk-schema — API result messages
|
|
3
|
+
* ====================================
|
|
4
|
+
* Single source of truth for every user-facing SUCCESS and ERROR message the
|
|
5
|
+
* product shows, plus the API error codes both sides key off.
|
|
6
|
+
*
|
|
7
|
+
* Why it lives here: the schema package is already the SSOT for data shapes,
|
|
8
|
+
* types and validation. Result copy is part of that contract — the backend
|
|
9
|
+
* raises a message, the client shows it, and neither should invent its own
|
|
10
|
+
* wording. Both repos import from here, so copy changes land in one place and
|
|
11
|
+
* can never drift between server and client.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* backend → `throw new BadRequestError(ERROR_MESSAGE.centerAddressRequired)`
|
|
15
|
+
* frontend → `showSuccess(...toast(SUCCESS_MESSAGE.profileUpdated))`
|
|
16
|
+
*
|
|
17
|
+
* @module gymmonk-schema/messages
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Machine-readable error codes returned in the API envelope
|
|
21
|
+
* (`{ success: false, error: { code, message } }`). The client can branch on a
|
|
22
|
+
* code; the message is what a human reads.
|
|
23
|
+
*/
|
|
24
|
+
export declare const ERROR_CODE: {
|
|
25
|
+
readonly INTERNAL_ERROR: "INTERNAL_ERROR";
|
|
26
|
+
readonly VALIDATION_ERROR: "VALIDATION_ERROR";
|
|
27
|
+
readonly NOT_FOUND: "NOT_FOUND";
|
|
28
|
+
readonly INVALID_TOKEN: "INVALID_TOKEN";
|
|
29
|
+
readonly TOKEN_EXPIRED: "TOKEN_EXPIRED";
|
|
30
|
+
readonly UNAUTHORIZED: "UNAUTHORIZED";
|
|
31
|
+
readonly FORBIDDEN: "FORBIDDEN";
|
|
32
|
+
readonly SESSION_INVALIDATED: "SESSION_INVALIDATED";
|
|
33
|
+
readonly USER_NOT_FOUND: "USER_NOT_FOUND";
|
|
34
|
+
readonly USER_ALREADY_EXISTS: "USER_ALREADY_EXISTS";
|
|
35
|
+
readonly INVALID_ROLE: "INVALID_ROLE";
|
|
36
|
+
readonly RATE_LIMITED: "RATE_LIMITED";
|
|
37
|
+
readonly RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED";
|
|
38
|
+
readonly SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE";
|
|
39
|
+
};
|
|
40
|
+
export type ErrorCode = (typeof ERROR_CODE)[keyof typeof ERROR_CODE];
|
|
41
|
+
/** A result message: a headline and an optional quieter second line. */
|
|
42
|
+
export interface ResultMessage {
|
|
43
|
+
title: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Confirmation shown after a write succeeds, keyed by action. Interpolating
|
|
48
|
+
* entries are functions so the caller supplies the value without re-wording.
|
|
49
|
+
*/
|
|
50
|
+
export declare const SUCCESS_MESSAGE: {
|
|
51
|
+
readonly memberOnboardingComplete: {
|
|
52
|
+
readonly title: "You're all set";
|
|
53
|
+
readonly description: "Welcome to GymMonk.";
|
|
54
|
+
};
|
|
55
|
+
readonly ownerOnboardingComplete: {
|
|
56
|
+
readonly title: "Your gym is ready";
|
|
57
|
+
readonly description: "Welcome to GymMonk.";
|
|
58
|
+
};
|
|
59
|
+
readonly profileUpdated: {
|
|
60
|
+
readonly title: "Profile updated";
|
|
61
|
+
};
|
|
62
|
+
readonly organisationUpdated: {
|
|
63
|
+
readonly title: "Organisation updated";
|
|
64
|
+
};
|
|
65
|
+
readonly centerAdded: {
|
|
66
|
+
readonly title: "Center added";
|
|
67
|
+
};
|
|
68
|
+
readonly centerUpdated: {
|
|
69
|
+
readonly title: "Center updated";
|
|
70
|
+
};
|
|
71
|
+
readonly membershipPlanCreated: {
|
|
72
|
+
readonly title: "Membership plan created";
|
|
73
|
+
};
|
|
74
|
+
readonly membershipUpdated: {
|
|
75
|
+
readonly title: "Membership updated";
|
|
76
|
+
};
|
|
77
|
+
readonly membershipRenewed: {
|
|
78
|
+
readonly title: "Membership renewed";
|
|
79
|
+
};
|
|
80
|
+
readonly membershipCancelled: {
|
|
81
|
+
readonly title: "Membership cancelled";
|
|
82
|
+
};
|
|
83
|
+
readonly membershipPauseRequested: {
|
|
84
|
+
readonly title: "Pause request sent";
|
|
85
|
+
readonly description: "Your gym will confirm the pause.";
|
|
86
|
+
};
|
|
87
|
+
readonly subscriptionUpdated: {
|
|
88
|
+
readonly title: "Subscription updated";
|
|
89
|
+
};
|
|
90
|
+
readonly workoutPlanCreated: {
|
|
91
|
+
readonly title: "Workout plan created";
|
|
92
|
+
};
|
|
93
|
+
readonly workoutCreated: {
|
|
94
|
+
readonly title: "Workout created";
|
|
95
|
+
};
|
|
96
|
+
readonly workoutPlanAssigned: {
|
|
97
|
+
readonly title: "Workout plan assigned";
|
|
98
|
+
};
|
|
99
|
+
readonly checkedIn: {
|
|
100
|
+
readonly title: "Checked in";
|
|
101
|
+
readonly description: "Your session has started.";
|
|
102
|
+
};
|
|
103
|
+
readonly checkedOut: {
|
|
104
|
+
readonly title: "Checked out";
|
|
105
|
+
readonly description: "Session saved.";
|
|
106
|
+
};
|
|
107
|
+
readonly feedbackSubmitted: {
|
|
108
|
+
readonly title: "Feedback sent";
|
|
109
|
+
readonly description: "Thank you for helping us improve.";
|
|
110
|
+
};
|
|
111
|
+
readonly complaintSubmitted: {
|
|
112
|
+
readonly title: "Complaint submitted";
|
|
113
|
+
readonly description: "Your gym will look into it.";
|
|
114
|
+
};
|
|
115
|
+
readonly announcementSent: {
|
|
116
|
+
readonly title: "Announcement sent";
|
|
117
|
+
};
|
|
118
|
+
readonly announcementScheduled: {
|
|
119
|
+
readonly title: "Announcement scheduled";
|
|
120
|
+
};
|
|
121
|
+
readonly announcementDraftSaved: {
|
|
122
|
+
readonly title: "Draft saved";
|
|
123
|
+
};
|
|
124
|
+
readonly mpinSet: {
|
|
125
|
+
readonly title: "PIN set";
|
|
126
|
+
};
|
|
127
|
+
readonly mpinReset: {
|
|
128
|
+
readonly title: "Your PIN has been reset.";
|
|
129
|
+
};
|
|
130
|
+
readonly mpinResetLinkSent: {
|
|
131
|
+
readonly title: "We've emailed you a reset link";
|
|
132
|
+
readonly description: "Check your inbox.";
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
export type SuccessMessageKey = keyof typeof SUCCESS_MESSAGE;
|
|
136
|
+
/** Success messages that name the thing they acted on. */
|
|
137
|
+
export declare const SUCCESS_MESSAGE_FOR: {
|
|
138
|
+
readonly memberAdded: (name: string) => ResultMessage;
|
|
139
|
+
readonly staffAdded: (name: string) => ResultMessage;
|
|
140
|
+
readonly inviteClaimed: (gymName: string) => ResultMessage;
|
|
141
|
+
readonly roleSwitched: (roleLabel: string) => ResultMessage;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Failure copy. Transport entries describe a request that never reached the
|
|
145
|
+
* API; the rest are domain rules the backend enforces and the client can also
|
|
146
|
+
* check up-front — same wording either way.
|
|
147
|
+
*/
|
|
148
|
+
export declare const ERROR_MESSAGE: {
|
|
149
|
+
readonly generic: "Something went wrong. Please try again.";
|
|
150
|
+
readonly network: "Network error. Check your connection and try again.";
|
|
151
|
+
readonly timeout: "That took too long. Please try again.";
|
|
152
|
+
readonly sessionExpired: "Your session expired. Please sign in again.";
|
|
153
|
+
readonly formInvalid: "Please check the highlighted fields.";
|
|
154
|
+
readonly roleRequired: "Choose a role to continue";
|
|
155
|
+
readonly memberOnboardingFailed: "We could not save your details. Please try again.";
|
|
156
|
+
readonly ownerOnboardingFailed: "We could not create your gym. Please try again.";
|
|
157
|
+
readonly centerRequiredForPlan: "Pick the center this plan belongs to.";
|
|
158
|
+
readonly organisationAddressRequired: "Add your organisation address before creating a center.";
|
|
159
|
+
readonly noCenterForPlans: "No gym center found. Add a center before creating plans.";
|
|
160
|
+
readonly noCenterForWorkouts: "No gym center found. Add a center before creating workouts.";
|
|
161
|
+
readonly noCenterForAnnouncements: "No gym center found. Add a center before sending announcements.";
|
|
162
|
+
readonly planUnavailable: "That plan is not available right now.";
|
|
163
|
+
readonly workoutPlanRequired: "Choose a plan to assign";
|
|
164
|
+
readonly checkInFailed: "Could not check you in. Please try again.";
|
|
165
|
+
readonly checkOutFailed: "Could not check you out. Please try again.";
|
|
166
|
+
readonly feedbackRatingRequired: "Choose a rating to submit your feedback";
|
|
167
|
+
readonly complaintIssueRequired: "Choose the issue you are reporting";
|
|
168
|
+
readonly mpinIncorrect: "Incorrect PIN.";
|
|
169
|
+
readonly mpinVerifyFailed: "Couldn't verify. Try again.";
|
|
170
|
+
readonly mpinSetFailed: "Couldn't set your PIN. Try again.";
|
|
171
|
+
readonly mpinMismatch: "Those didn't match. Try again.";
|
|
172
|
+
readonly mpinResetLinkFailed: "Couldn't send the reset link. Try again.";
|
|
173
|
+
readonly mpinResetFailed: "Couldn't reset your PIN. Try again.";
|
|
174
|
+
readonly imageUploadFailed: "Could not upload that image. Please try again.";
|
|
175
|
+
readonly imageProcessFailed: "Could not process that image. Please try again.";
|
|
176
|
+
};
|
|
177
|
+
export type ErrorMessageKey = keyof typeof ERROR_MESSAGE;
|
|
178
|
+
/** Error messages that name a limit or value. */
|
|
179
|
+
export declare const ERROR_MESSAGE_FOR: {
|
|
180
|
+
readonly maxPhotos: (max: number) => string;
|
|
181
|
+
/** Role-switch PIN lockout, in whole minutes remaining. */
|
|
182
|
+
readonly mpinLockedOut: (minutes: number) => string;
|
|
183
|
+
};
|
|
184
|
+
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH;;;;GAIG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;CAwBb,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAIrE,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiDsB,CAAC;AAEnD,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,eAAe,CAAC;AAE7D,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB;iCACV,MAAM,KAAG,aAAa;gCAIvB,MAAM,KAAG,aAAa;sCAIhB,MAAM,KAAG,aAAa;uCAIrB,MAAM,KAAG,aAAa;CAGxC,CAAC;AAIX;;;;GAIG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+CiB,CAAC;AAE5C,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,aAAa,CAAC;AAEzD,iDAAiD;AACjD,eAAO,MAAM,iBAAiB;8BACX,MAAM,KAAG,MAAM;IAChC,2DAA2D;sCAClC,MAAM,KAAG,MAAM;CAEhC,CAAC"}
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gymmonk-schema — API result messages
|
|
3
|
+
* ====================================
|
|
4
|
+
* Single source of truth for every user-facing SUCCESS and ERROR message the
|
|
5
|
+
* product shows, plus the API error codes both sides key off.
|
|
6
|
+
*
|
|
7
|
+
* Why it lives here: the schema package is already the SSOT for data shapes,
|
|
8
|
+
* types and validation. Result copy is part of that contract — the backend
|
|
9
|
+
* raises a message, the client shows it, and neither should invent its own
|
|
10
|
+
* wording. Both repos import from here, so copy changes land in one place and
|
|
11
|
+
* can never drift between server and client.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* backend → `throw new BadRequestError(ERROR_MESSAGE.centerAddressRequired)`
|
|
15
|
+
* frontend → `showSuccess(...toast(SUCCESS_MESSAGE.profileUpdated))`
|
|
16
|
+
*
|
|
17
|
+
* @module gymmonk-schema/messages
|
|
18
|
+
*/
|
|
19
|
+
// ─── API error codes ─────────────────────────────────────────────────────────
|
|
20
|
+
/**
|
|
21
|
+
* Machine-readable error codes returned in the API envelope
|
|
22
|
+
* (`{ success: false, error: { code, message } }`). The client can branch on a
|
|
23
|
+
* code; the message is what a human reads.
|
|
24
|
+
*/
|
|
25
|
+
export const ERROR_CODE = {
|
|
26
|
+
// General
|
|
27
|
+
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
|
28
|
+
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
29
|
+
NOT_FOUND: 'NOT_FOUND',
|
|
30
|
+
// Auth
|
|
31
|
+
INVALID_TOKEN: 'INVALID_TOKEN',
|
|
32
|
+
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
|
|
33
|
+
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
34
|
+
FORBIDDEN: 'FORBIDDEN',
|
|
35
|
+
SESSION_INVALIDATED: 'SESSION_INVALIDATED',
|
|
36
|
+
// User
|
|
37
|
+
USER_NOT_FOUND: 'USER_NOT_FOUND',
|
|
38
|
+
USER_ALREADY_EXISTS: 'USER_ALREADY_EXISTS',
|
|
39
|
+
INVALID_ROLE: 'INVALID_ROLE',
|
|
40
|
+
// Rate limiting
|
|
41
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
42
|
+
RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
|
|
43
|
+
// Upstream / dependency unavailable
|
|
44
|
+
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
|
|
45
|
+
};
|
|
46
|
+
// ─── Success messages ────────────────────────────────────────────────────────
|
|
47
|
+
/**
|
|
48
|
+
* Confirmation shown after a write succeeds, keyed by action. Interpolating
|
|
49
|
+
* entries are functions so the caller supplies the value without re-wording.
|
|
50
|
+
*/
|
|
51
|
+
export const SUCCESS_MESSAGE = {
|
|
52
|
+
// Onboarding
|
|
53
|
+
memberOnboardingComplete: { title: "You're all set", description: 'Welcome to GymMonk.' },
|
|
54
|
+
ownerOnboardingComplete: { title: 'Your gym is ready', description: 'Welcome to GymMonk.' },
|
|
55
|
+
// Identity & profile
|
|
56
|
+
profileUpdated: { title: 'Profile updated' },
|
|
57
|
+
organisationUpdated: { title: 'Organisation updated' },
|
|
58
|
+
// Gym structure
|
|
59
|
+
centerAdded: { title: 'Center added' },
|
|
60
|
+
centerUpdated: { title: 'Center updated' },
|
|
61
|
+
membershipPlanCreated: { title: 'Membership plan created' },
|
|
62
|
+
// Membership (member-side)
|
|
63
|
+
membershipUpdated: { title: 'Membership updated' },
|
|
64
|
+
membershipRenewed: { title: 'Membership renewed' },
|
|
65
|
+
membershipCancelled: { title: 'Membership cancelled' },
|
|
66
|
+
membershipPauseRequested: {
|
|
67
|
+
title: 'Pause request sent',
|
|
68
|
+
description: 'Your gym will confirm the pause.',
|
|
69
|
+
},
|
|
70
|
+
// Owner SaaS subscription
|
|
71
|
+
subscriptionUpdated: { title: 'Subscription updated' },
|
|
72
|
+
// Training
|
|
73
|
+
workoutPlanCreated: { title: 'Workout plan created' },
|
|
74
|
+
workoutCreated: { title: 'Workout created' },
|
|
75
|
+
workoutPlanAssigned: { title: 'Workout plan assigned' },
|
|
76
|
+
// Sessions
|
|
77
|
+
checkedIn: { title: 'Checked in', description: 'Your session has started.' },
|
|
78
|
+
checkedOut: { title: 'Checked out', description: 'Session saved.' },
|
|
79
|
+
// Engagement
|
|
80
|
+
feedbackSubmitted: { title: 'Feedback sent', description: 'Thank you for helping us improve.' },
|
|
81
|
+
complaintSubmitted: { title: 'Complaint submitted', description: 'Your gym will look into it.' },
|
|
82
|
+
announcementSent: { title: 'Announcement sent' },
|
|
83
|
+
announcementScheduled: { title: 'Announcement scheduled' },
|
|
84
|
+
announcementDraftSaved: { title: 'Draft saved' },
|
|
85
|
+
// Security (role-switch PIN)
|
|
86
|
+
mpinSet: { title: 'PIN set' },
|
|
87
|
+
mpinReset: { title: 'Your PIN has been reset.' },
|
|
88
|
+
mpinResetLinkSent: {
|
|
89
|
+
title: "We've emailed you a reset link",
|
|
90
|
+
description: 'Check your inbox.',
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
/** Success messages that name the thing they acted on. */
|
|
94
|
+
export const SUCCESS_MESSAGE_FOR = {
|
|
95
|
+
memberAdded: (name) => ({
|
|
96
|
+
title: 'Member added',
|
|
97
|
+
description: `Share the invite with ${name}.`,
|
|
98
|
+
}),
|
|
99
|
+
staffAdded: (name) => ({
|
|
100
|
+
title: 'Staff added',
|
|
101
|
+
description: `Share the invite with ${name}.`,
|
|
102
|
+
}),
|
|
103
|
+
inviteClaimed: (gymName) => ({
|
|
104
|
+
title: "You're all set",
|
|
105
|
+
description: `Your account is linked to ${gymName}.`,
|
|
106
|
+
}),
|
|
107
|
+
roleSwitched: (roleLabel) => ({
|
|
108
|
+
title: `Switched to ${roleLabel}`,
|
|
109
|
+
}),
|
|
110
|
+
};
|
|
111
|
+
// ─── Error messages ──────────────────────────────────────────────────────────
|
|
112
|
+
/**
|
|
113
|
+
* Failure copy. Transport entries describe a request that never reached the
|
|
114
|
+
* API; the rest are domain rules the backend enforces and the client can also
|
|
115
|
+
* check up-front — same wording either way.
|
|
116
|
+
*/
|
|
117
|
+
export const ERROR_MESSAGE = {
|
|
118
|
+
// Transport / generic
|
|
119
|
+
generic: 'Something went wrong. Please try again.',
|
|
120
|
+
network: 'Network error. Check your connection and try again.',
|
|
121
|
+
timeout: 'That took too long. Please try again.',
|
|
122
|
+
sessionExpired: 'Your session expired. Please sign in again.',
|
|
123
|
+
// Client-side validation
|
|
124
|
+
formInvalid: 'Please check the highlighted fields.',
|
|
125
|
+
// Onboarding
|
|
126
|
+
roleRequired: 'Choose a role to continue',
|
|
127
|
+
memberOnboardingFailed: 'We could not save your details. Please try again.',
|
|
128
|
+
ownerOnboardingFailed: 'We could not create your gym. Please try again.',
|
|
129
|
+
// Gym structure prerequisites
|
|
130
|
+
centerRequiredForPlan: 'Pick the center this plan belongs to.',
|
|
131
|
+
organisationAddressRequired: 'Add your organisation address before creating a center.',
|
|
132
|
+
noCenterForPlans: 'No gym center found. Add a center before creating plans.',
|
|
133
|
+
noCenterForWorkouts: 'No gym center found. Add a center before creating workouts.',
|
|
134
|
+
noCenterForAnnouncements: 'No gym center found. Add a center before sending announcements.',
|
|
135
|
+
// Membership
|
|
136
|
+
planUnavailable: 'That plan is not available right now.',
|
|
137
|
+
// Training
|
|
138
|
+
workoutPlanRequired: 'Choose a plan to assign',
|
|
139
|
+
// Sessions
|
|
140
|
+
checkInFailed: 'Could not check you in. Please try again.',
|
|
141
|
+
checkOutFailed: 'Could not check you out. Please try again.',
|
|
142
|
+
// Engagement
|
|
143
|
+
feedbackRatingRequired: 'Choose a rating to submit your feedback',
|
|
144
|
+
complaintIssueRequired: 'Choose the issue you are reporting',
|
|
145
|
+
// Security (role-switch PIN)
|
|
146
|
+
mpinIncorrect: 'Incorrect PIN.',
|
|
147
|
+
mpinVerifyFailed: "Couldn't verify. Try again.",
|
|
148
|
+
mpinSetFailed: "Couldn't set your PIN. Try again.",
|
|
149
|
+
mpinMismatch: "Those didn't match. Try again.",
|
|
150
|
+
mpinResetLinkFailed: "Couldn't send the reset link. Try again.",
|
|
151
|
+
mpinResetFailed: "Couldn't reset your PIN. Try again.",
|
|
152
|
+
// Uploads
|
|
153
|
+
imageUploadFailed: 'Could not upload that image. Please try again.',
|
|
154
|
+
imageProcessFailed: 'Could not process that image. Please try again.',
|
|
155
|
+
};
|
|
156
|
+
/** Error messages that name a limit or value. */
|
|
157
|
+
export const ERROR_MESSAGE_FOR = {
|
|
158
|
+
maxPhotos: (max) => `You can add up to ${max} photos`,
|
|
159
|
+
/** Role-switch PIN lockout, in whole minutes remaining. */
|
|
160
|
+
mpinLockedOut: (minutes) => `Too many attempts. Try again in ${minutes} minute${minutes > 1 ? 's' : ''}.`,
|
|
161
|
+
};
|
|
162
|
+
//# sourceMappingURL=messages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — API result messages\n * ====================================\n * Single source of truth for every user-facing SUCCESS and ERROR message the\n * product shows, plus the API error codes both sides key off.\n *\n * Why it lives here: the schema package is already the SSOT for data shapes,\n * types and validation. Result copy is part of that contract — the backend\n * raises a message, the client shows it, and neither should invent its own\n * wording. Both repos import from here, so copy changes land in one place and\n * can never drift between server and client.\n *\n * Usage:\n * backend → `throw new BadRequestError(ERROR_MESSAGE.centerAddressRequired)`\n * frontend → `showSuccess(...toast(SUCCESS_MESSAGE.profileUpdated))`\n *\n * @module gymmonk-schema/messages\n */\n\n// ─── API error codes ─────────────────────────────────────────────────────────\n\n/**\n * Machine-readable error codes returned in the API envelope\n * (`{ success: false, error: { code, message } }`). The client can branch on a\n * code; the message is what a human reads.\n */\nexport const ERROR_CODE = {\n // General\n INTERNAL_ERROR: 'INTERNAL_ERROR',\n VALIDATION_ERROR: 'VALIDATION_ERROR',\n NOT_FOUND: 'NOT_FOUND',\n\n // Auth\n INVALID_TOKEN: 'INVALID_TOKEN',\n TOKEN_EXPIRED: 'TOKEN_EXPIRED',\n UNAUTHORIZED: 'UNAUTHORIZED',\n FORBIDDEN: 'FORBIDDEN',\n SESSION_INVALIDATED: 'SESSION_INVALIDATED',\n\n // User\n USER_NOT_FOUND: 'USER_NOT_FOUND',\n USER_ALREADY_EXISTS: 'USER_ALREADY_EXISTS',\n INVALID_ROLE: 'INVALID_ROLE',\n\n // Rate limiting\n RATE_LIMITED: 'RATE_LIMITED',\n RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',\n\n // Upstream / dependency unavailable\n SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',\n} as const;\n\nexport type ErrorCode = (typeof ERROR_CODE)[keyof typeof ERROR_CODE];\n\n// ─── Message shape ───────────────────────────────────────────────────────────\n\n/** A result message: a headline and an optional quieter second line. */\nexport interface ResultMessage {\n title: string;\n description?: string;\n}\n\n// ─── Success messages ────────────────────────────────────────────────────────\n\n/**\n * Confirmation shown after a write succeeds, keyed by action. Interpolating\n * entries are functions so the caller supplies the value without re-wording.\n */\nexport const SUCCESS_MESSAGE = {\n // Onboarding\n memberOnboardingComplete: { title: \"You're all set\", description: 'Welcome to GymMonk.' },\n ownerOnboardingComplete: { title: 'Your gym is ready', description: 'Welcome to GymMonk.' },\n\n // Identity & profile\n profileUpdated: { title: 'Profile updated' },\n organisationUpdated: { title: 'Organisation updated' },\n\n // Gym structure\n centerAdded: { title: 'Center added' },\n centerUpdated: { title: 'Center updated' },\n membershipPlanCreated: { title: 'Membership plan created' },\n\n // Membership (member-side)\n membershipUpdated: { title: 'Membership updated' },\n membershipRenewed: { title: 'Membership renewed' },\n membershipCancelled: { title: 'Membership cancelled' },\n membershipPauseRequested: {\n title: 'Pause request sent',\n description: 'Your gym will confirm the pause.',\n },\n\n // Owner SaaS subscription\n subscriptionUpdated: { title: 'Subscription updated' },\n\n // Training\n workoutPlanCreated: { title: 'Workout plan created' },\n workoutCreated: { title: 'Workout created' },\n workoutPlanAssigned: { title: 'Workout plan assigned' },\n\n // Sessions\n checkedIn: { title: 'Checked in', description: 'Your session has started.' },\n checkedOut: { title: 'Checked out', description: 'Session saved.' },\n\n // Engagement\n feedbackSubmitted: { title: 'Feedback sent', description: 'Thank you for helping us improve.' },\n complaintSubmitted: { title: 'Complaint submitted', description: 'Your gym will look into it.' },\n announcementSent: { title: 'Announcement sent' },\n announcementScheduled: { title: 'Announcement scheduled' },\n announcementDraftSaved: { title: 'Draft saved' },\n\n // Security (role-switch PIN)\n mpinSet: { title: 'PIN set' },\n mpinReset: { title: 'Your PIN has been reset.' },\n mpinResetLinkSent: {\n title: \"We've emailed you a reset link\",\n description: 'Check your inbox.',\n },\n} as const satisfies Record<string, ResultMessage>;\n\nexport type SuccessMessageKey = keyof typeof SUCCESS_MESSAGE;\n\n/** Success messages that name the thing they acted on. */\nexport const SUCCESS_MESSAGE_FOR = {\n memberAdded: (name: string): ResultMessage => ({\n title: 'Member added',\n description: `Share the invite with ${name}.`,\n }),\n staffAdded: (name: string): ResultMessage => ({\n title: 'Staff added',\n description: `Share the invite with ${name}.`,\n }),\n inviteClaimed: (gymName: string): ResultMessage => ({\n title: \"You're all set\",\n description: `Your account is linked to ${gymName}.`,\n }),\n roleSwitched: (roleLabel: string): ResultMessage => ({\n title: `Switched to ${roleLabel}`,\n }),\n} as const;\n\n// ─── Error messages ──────────────────────────────────────────────────────────\n\n/**\n * Failure copy. Transport entries describe a request that never reached the\n * API; the rest are domain rules the backend enforces and the client can also\n * check up-front — same wording either way.\n */\nexport const ERROR_MESSAGE = {\n // Transport / generic\n generic: 'Something went wrong. Please try again.',\n network: 'Network error. Check your connection and try again.',\n timeout: 'That took too long. Please try again.',\n sessionExpired: 'Your session expired. Please sign in again.',\n\n // Client-side validation\n formInvalid: 'Please check the highlighted fields.',\n\n // Onboarding\n roleRequired: 'Choose a role to continue',\n memberOnboardingFailed: 'We could not save your details. Please try again.',\n ownerOnboardingFailed: 'We could not create your gym. Please try again.',\n\n // Gym structure prerequisites\n centerRequiredForPlan: 'Pick the center this plan belongs to.',\n organisationAddressRequired: 'Add your organisation address before creating a center.',\n noCenterForPlans: 'No gym center found. Add a center before creating plans.',\n noCenterForWorkouts: 'No gym center found. Add a center before creating workouts.',\n noCenterForAnnouncements: 'No gym center found. Add a center before sending announcements.',\n\n // Membership\n planUnavailable: 'That plan is not available right now.',\n\n // Training\n workoutPlanRequired: 'Choose a plan to assign',\n\n // Sessions\n checkInFailed: 'Could not check you in. Please try again.',\n checkOutFailed: 'Could not check you out. Please try again.',\n\n // Engagement\n feedbackRatingRequired: 'Choose a rating to submit your feedback',\n complaintIssueRequired: 'Choose the issue you are reporting',\n\n // Security (role-switch PIN)\n mpinIncorrect: 'Incorrect PIN.',\n mpinVerifyFailed: \"Couldn't verify. Try again.\",\n mpinSetFailed: \"Couldn't set your PIN. Try again.\",\n mpinMismatch: \"Those didn't match. Try again.\",\n mpinResetLinkFailed: \"Couldn't send the reset link. Try again.\",\n mpinResetFailed: \"Couldn't reset your PIN. Try again.\",\n\n // Uploads\n imageUploadFailed: 'Could not upload that image. Please try again.',\n imageProcessFailed: 'Could not process that image. Please try again.',\n} as const satisfies Record<string, string>;\n\nexport type ErrorMessageKey = keyof typeof ERROR_MESSAGE;\n\n/** Error messages that name a limit or value. */\nexport const ERROR_MESSAGE_FOR = {\n maxPhotos: (max: number): string => `You can add up to ${max} photos`,\n /** Role-switch PIN lockout, in whole minutes remaining. */\n mpinLockedOut: (minutes: number): string =>\n `Too many attempts. Try again in ${minutes} minute${minutes > 1 ? 's' : ''}.`,\n} as const;\n"]}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gymmonk-schema — Onboarding FORM schemas
|
|
3
|
+
* ========================================
|
|
4
|
+
* The client-side validation contract for the member and owner onboarding
|
|
5
|
+
* wizards, and the gym-selection step.
|
|
6
|
+
*
|
|
7
|
+
* Why these live here and are distinct from the `*OnboardingBody` API schemas:
|
|
8
|
+
* a half-filled form legitimately holds `null` for a choice the user has not
|
|
9
|
+
* made yet, whereas the API body only ever sees completed values. So these are
|
|
10
|
+
* the SAME rules expressed over the form's working shape — every message,
|
|
11
|
+
* regex and bound is defined once, here, and both sides agree on it.
|
|
12
|
+
*
|
|
13
|
+
* Everything reuses the primitives in `shared.ts` / `common.ts`
|
|
14
|
+
* (`INDIAN_STATES`, `CENTER_TYPES`, `WEEKDAYS`, phone/pincode rules) rather
|
|
15
|
+
* than restating them.
|
|
16
|
+
*
|
|
17
|
+
* @module gymmonk-schema/onboarding-form
|
|
18
|
+
*/
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
import { WEEKDAYS } from './shared.js';
|
|
21
|
+
export declare const onboardingAddressSchema: z.ZodObject<{
|
|
22
|
+
street: z.ZodString;
|
|
23
|
+
city: z.ZodString;
|
|
24
|
+
state: z.ZodNullable<z.ZodString> & z.ZodType<{} | undefined, string | null, z.core.$ZodTypeInternals<{} | undefined, string | null>>;
|
|
25
|
+
pincode: z.ZodString;
|
|
26
|
+
}, z.core.$strip>;
|
|
27
|
+
/** Working shape of an address inside an onboarding form. */
|
|
28
|
+
export type OnboardingAddress = {
|
|
29
|
+
street: string;
|
|
30
|
+
city: string;
|
|
31
|
+
state: string | null;
|
|
32
|
+
pincode: string;
|
|
33
|
+
};
|
|
34
|
+
/** Street + city + state + a 6-digit pincode all present. */
|
|
35
|
+
export declare function isOnboardingAddressComplete(a: OnboardingAddress): boolean;
|
|
36
|
+
export declare const onboardingSocialLinksSchema: z.ZodObject<{
|
|
37
|
+
jansathi: z.ZodString;
|
|
38
|
+
youtube: z.ZodString;
|
|
39
|
+
instagram: z.ZodString;
|
|
40
|
+
facebook: z.ZodString;
|
|
41
|
+
}, z.core.$strip>;
|
|
42
|
+
export type OnboardingSocialLinks = z.infer<typeof onboardingSocialLinksSchema>;
|
|
43
|
+
export declare const onboardingDayHoursSchema: z.ZodObject<{
|
|
44
|
+
open: z.ZodBoolean;
|
|
45
|
+
from: z.ZodString;
|
|
46
|
+
to: z.ZodString;
|
|
47
|
+
}, z.core.$strip>;
|
|
48
|
+
export type OnboardingDayHours = z.infer<typeof onboardingDayHoursSchema>;
|
|
49
|
+
export type OnboardingWeeklyHours = Record<(typeof WEEKDAYS)[number], OnboardingDayHours>;
|
|
50
|
+
/** Age bounds a gym membership realistically spans. */
|
|
51
|
+
export declare const MEMBER_MIN_AGE_YEARS = 10;
|
|
52
|
+
export declare const MEMBER_MAX_AGE_YEARS = 100;
|
|
53
|
+
export declare const memberOnboardingFormSchema: z.ZodObject<{
|
|
54
|
+
firstName: z.ZodString;
|
|
55
|
+
lastName: z.ZodString;
|
|
56
|
+
dobDay: z.ZodString;
|
|
57
|
+
dobMonth: z.ZodString;
|
|
58
|
+
dobYear: z.ZodString;
|
|
59
|
+
gender: z.ZodNullable<z.ZodEnum<{
|
|
60
|
+
male: "male";
|
|
61
|
+
female: "female";
|
|
62
|
+
other: "other";
|
|
63
|
+
}>> & z.ZodType<{} | undefined, "male" | "female" | "other" | null, z.core.$ZodTypeInternals<{} | undefined, "male" | "female" | "other" | null>>;
|
|
64
|
+
heightCm: z.ZodNullable<z.ZodNumber> & z.ZodType<{} | undefined, number | null, z.core.$ZodTypeInternals<{} | undefined, number | null>>;
|
|
65
|
+
weightKg: z.ZodNullable<z.ZodNumber> & z.ZodType<{} | undefined, number | null, z.core.$ZodTypeInternals<{} | undefined, number | null>>;
|
|
66
|
+
neckCm: z.ZodNullable<z.ZodNumber> & z.ZodType<{} | undefined, number | null, z.core.$ZodTypeInternals<{} | undefined, number | null>>;
|
|
67
|
+
waistCm: z.ZodNullable<z.ZodNumber> & z.ZodType<{} | undefined, number | null, z.core.$ZodTypeInternals<{} | undefined, number | null>>;
|
|
68
|
+
hipsCm: z.ZodNullable<z.ZodNumber>;
|
|
69
|
+
phone: z.ZodString;
|
|
70
|
+
emergencyName: z.ZodString;
|
|
71
|
+
emergencyPhone: z.ZodString;
|
|
72
|
+
bloodGroup: z.ZodNullable<z.ZodString> & z.ZodType<{} | undefined, string | null, z.core.$ZodTypeInternals<{} | undefined, string | null>>;
|
|
73
|
+
avatarUrl: z.ZodString;
|
|
74
|
+
}, z.core.$strip>;
|
|
75
|
+
/**
|
|
76
|
+
* The form's working shape. Hand-written (not `z.infer`) so unset numeric /
|
|
77
|
+
* choice fields can hold `null` while the schema's `requiredChoice` rules still
|
|
78
|
+
* drive the field errors — the resolver is cast to this type at `useForm`.
|
|
79
|
+
*/
|
|
80
|
+
export type MemberOnboardingForm = {
|
|
81
|
+
firstName: string;
|
|
82
|
+
lastName: string;
|
|
83
|
+
dobDay: string;
|
|
84
|
+
dobMonth: string;
|
|
85
|
+
dobYear: string;
|
|
86
|
+
gender: 'male' | 'female' | 'other' | null;
|
|
87
|
+
heightCm: number | null;
|
|
88
|
+
weightKg: number | null;
|
|
89
|
+
neckCm: number | null;
|
|
90
|
+
waistCm: number | null;
|
|
91
|
+
hipsCm: number | null;
|
|
92
|
+
phone: string;
|
|
93
|
+
emergencyName: string;
|
|
94
|
+
emergencyPhone: string;
|
|
95
|
+
bloodGroup: string | null;
|
|
96
|
+
avatarUrl: string;
|
|
97
|
+
};
|
|
98
|
+
export declare const EMPTY_MEMBER_ONBOARDING_FORM: MemberOnboardingForm;
|
|
99
|
+
/** Fields validated before advancing each member step. */
|
|
100
|
+
export declare const MEMBER_STEP_FIELDS: (keyof MemberOnboardingForm)[][];
|
|
101
|
+
/** Age bounds for a gym owner (the DOB itself stays optional). */
|
|
102
|
+
export declare const OWNER_MIN_AGE_YEARS = 16;
|
|
103
|
+
export declare const OWNER_MAX_AGE_YEARS = 100;
|
|
104
|
+
export declare const ownerOnboardingFormSchema: z.ZodObject<{
|
|
105
|
+
firstName: z.ZodString;
|
|
106
|
+
lastName: z.ZodString;
|
|
107
|
+
dobDay: z.ZodString;
|
|
108
|
+
dobMonth: z.ZodString;
|
|
109
|
+
dobYear: z.ZodString;
|
|
110
|
+
gender: z.ZodNullable<z.ZodEnum<{
|
|
111
|
+
male: "male";
|
|
112
|
+
female: "female";
|
|
113
|
+
other: "other";
|
|
114
|
+
}>> & z.ZodType<{} | undefined, "male" | "female" | "other" | null, z.core.$ZodTypeInternals<{} | undefined, "male" | "female" | "other" | null>>;
|
|
115
|
+
phone: z.ZodString;
|
|
116
|
+
altEmail: z.ZodString;
|
|
117
|
+
website: z.ZodString;
|
|
118
|
+
noWebsite: z.ZodBoolean;
|
|
119
|
+
address: z.ZodObject<{
|
|
120
|
+
street: z.ZodString;
|
|
121
|
+
city: z.ZodString;
|
|
122
|
+
state: z.ZodNullable<z.ZodString> & z.ZodType<{} | undefined, string | null, z.core.$ZodTypeInternals<{} | undefined, string | null>>;
|
|
123
|
+
pincode: z.ZodString;
|
|
124
|
+
}, z.core.$strip>;
|
|
125
|
+
socials: z.ZodObject<{
|
|
126
|
+
jansathi: z.ZodString;
|
|
127
|
+
youtube: z.ZodString;
|
|
128
|
+
instagram: z.ZodString;
|
|
129
|
+
facebook: z.ZodString;
|
|
130
|
+
}, z.core.$strip>;
|
|
131
|
+
orgName: z.ZodString;
|
|
132
|
+
orgDescription: z.ZodString;
|
|
133
|
+
hqAddress: z.ZodObject<{
|
|
134
|
+
street: z.ZodString;
|
|
135
|
+
city: z.ZodString;
|
|
136
|
+
state: z.ZodNullable<z.ZodString> & z.ZodType<{} | undefined, string | null, z.core.$ZodTypeInternals<{} | undefined, string | null>>;
|
|
137
|
+
pincode: z.ZodString;
|
|
138
|
+
}, z.core.$strip>;
|
|
139
|
+
gstNumber: z.ZodString;
|
|
140
|
+
cinNumber: z.ZodString;
|
|
141
|
+
numberOfCenters: z.ZodNullable<z.ZodNumber> & z.ZodType<{} | undefined, number | null, z.core.$ZodTypeInternals<{} | undefined, number | null>>;
|
|
142
|
+
orgSocials: z.ZodObject<{
|
|
143
|
+
jansathi: z.ZodString;
|
|
144
|
+
youtube: z.ZodString;
|
|
145
|
+
instagram: z.ZodString;
|
|
146
|
+
facebook: z.ZodString;
|
|
147
|
+
}, z.core.$strip>;
|
|
148
|
+
centerName: z.ZodString;
|
|
149
|
+
centerDescription: z.ZodString;
|
|
150
|
+
centerTypes: z.ZodArray<z.ZodString>;
|
|
151
|
+
centerAddress: z.ZodObject<{
|
|
152
|
+
street: z.ZodString;
|
|
153
|
+
city: z.ZodString;
|
|
154
|
+
state: z.ZodNullable<z.ZodString> & z.ZodType<{} | undefined, string | null, z.core.$ZodTypeInternals<{} | undefined, string | null>>;
|
|
155
|
+
pincode: z.ZodString;
|
|
156
|
+
}, z.core.$strip>;
|
|
157
|
+
floorArea: z.ZodNullable<z.ZodNumber>;
|
|
158
|
+
centerPhone1: z.ZodString;
|
|
159
|
+
centerPhone2: z.ZodString;
|
|
160
|
+
centerEmail: z.ZodString;
|
|
161
|
+
hours: z.ZodObject<{
|
|
162
|
+
mon: z.ZodObject<{
|
|
163
|
+
open: z.ZodBoolean;
|
|
164
|
+
from: z.ZodString;
|
|
165
|
+
to: z.ZodString;
|
|
166
|
+
}, z.core.$strip>;
|
|
167
|
+
tue: z.ZodObject<{
|
|
168
|
+
open: z.ZodBoolean;
|
|
169
|
+
from: z.ZodString;
|
|
170
|
+
to: z.ZodString;
|
|
171
|
+
}, z.core.$strip>;
|
|
172
|
+
wed: z.ZodObject<{
|
|
173
|
+
open: z.ZodBoolean;
|
|
174
|
+
from: z.ZodString;
|
|
175
|
+
to: z.ZodString;
|
|
176
|
+
}, z.core.$strip>;
|
|
177
|
+
thu: z.ZodObject<{
|
|
178
|
+
open: z.ZodBoolean;
|
|
179
|
+
from: z.ZodString;
|
|
180
|
+
to: z.ZodString;
|
|
181
|
+
}, z.core.$strip>;
|
|
182
|
+
fri: z.ZodObject<{
|
|
183
|
+
open: z.ZodBoolean;
|
|
184
|
+
from: z.ZodString;
|
|
185
|
+
to: z.ZodString;
|
|
186
|
+
}, z.core.$strip>;
|
|
187
|
+
sat: z.ZodObject<{
|
|
188
|
+
open: z.ZodBoolean;
|
|
189
|
+
from: z.ZodString;
|
|
190
|
+
to: z.ZodString;
|
|
191
|
+
}, z.core.$strip>;
|
|
192
|
+
sun: z.ZodObject<{
|
|
193
|
+
open: z.ZodBoolean;
|
|
194
|
+
from: z.ZodString;
|
|
195
|
+
to: z.ZodString;
|
|
196
|
+
}, z.core.$strip>;
|
|
197
|
+
}, z.core.$strip>;
|
|
198
|
+
photos: z.ZodArray<z.ZodString>;
|
|
199
|
+
avatarUrl: z.ZodString;
|
|
200
|
+
planId: z.ZodNullable<z.ZodString> & z.ZodType<{} | undefined, string | null, z.core.$ZodTypeInternals<{} | undefined, string | null>>;
|
|
201
|
+
tenureId: z.ZodNullable<z.ZodString>;
|
|
202
|
+
}, z.core.$strip>;
|
|
203
|
+
/** Hand-written working shape (nullable "unset" fields); see the member form. */
|
|
204
|
+
export type OwnerOnboardingForm = {
|
|
205
|
+
firstName: string;
|
|
206
|
+
lastName: string;
|
|
207
|
+
dobDay: string;
|
|
208
|
+
dobMonth: string;
|
|
209
|
+
dobYear: string;
|
|
210
|
+
gender: 'male' | 'female' | 'other' | null;
|
|
211
|
+
phone: string;
|
|
212
|
+
altEmail: string;
|
|
213
|
+
website: string;
|
|
214
|
+
noWebsite: boolean;
|
|
215
|
+
address: OnboardingAddress;
|
|
216
|
+
socials: OnboardingSocialLinks;
|
|
217
|
+
orgName: string;
|
|
218
|
+
orgDescription: string;
|
|
219
|
+
hqAddress: OnboardingAddress;
|
|
220
|
+
gstNumber: string;
|
|
221
|
+
cinNumber: string;
|
|
222
|
+
numberOfCenters: number | null;
|
|
223
|
+
orgSocials: OnboardingSocialLinks;
|
|
224
|
+
centerName: string;
|
|
225
|
+
centerDescription: string;
|
|
226
|
+
centerTypes: string[];
|
|
227
|
+
centerAddress: OnboardingAddress;
|
|
228
|
+
floorArea: number | null;
|
|
229
|
+
centerPhone1: string;
|
|
230
|
+
centerPhone2: string;
|
|
231
|
+
centerEmail: string;
|
|
232
|
+
hours: OnboardingWeeklyHours;
|
|
233
|
+
photos: string[];
|
|
234
|
+
avatarUrl: string;
|
|
235
|
+
planId: string | null;
|
|
236
|
+
tenureId: string | null;
|
|
237
|
+
};
|
|
238
|
+
/** The opening hours a new center starts with. */
|
|
239
|
+
export declare const DEFAULT_ONBOARDING_HOURS: OnboardingWeeklyHours;
|
|
240
|
+
export declare const EMPTY_OWNER_ONBOARDING_FORM: OwnerOnboardingForm;
|
|
241
|
+
/**
|
|
242
|
+
* Fields validated (via `form.trigger`) before advancing each owner step.
|
|
243
|
+
* Triggering a parent path (e.g. "address") validates its nested fields too.
|
|
244
|
+
*
|
|
245
|
+
* The DOB cells belong here even though the date is optional: the refine above
|
|
246
|
+
* still rejects a half-typed date, and a field missing from this map would be
|
|
247
|
+
* unreachable twice over — Continue would not surface it, and the final submit
|
|
248
|
+
* (which finds the offending STEP by searching these lists) would find no step
|
|
249
|
+
* and leave the plan screen dead.
|
|
250
|
+
*/
|
|
251
|
+
export declare const OWNER_STEP_FIELDS: (keyof OwnerOnboardingForm)[][];
|
|
252
|
+
export declare const gymSelectionSchema: z.ZodObject<{
|
|
253
|
+
gymId: z.ZodString;
|
|
254
|
+
}, z.core.$strip>;
|
|
255
|
+
export type GymSelection = z.infer<typeof gymSelectionSchema>;
|
|
256
|
+
//# sourceMappingURL=onboarding-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onboarding-form.d.ts","sourceRoot":"","sources":["../src/onboarding-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAyBvC,eAAO,MAAM,uBAAuB;;;;;iBAKlC,CAAC;AAEH,6DAA6D;AAC7D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,6DAA6D;AAC7D,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAIzE;AAID,eAAO,MAAM,2BAA2B;;;;;iBAKtC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAIhF,eAAO,MAAM,wBAAwB;;;;iBAInC,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC1E,MAAM,MAAM,qBAAqB,GAAG,MAAM,CAAC,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC;AAuB1F,uDAAuD;AACvD,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;iBAyDnC,CAAC;AAEL;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAC;IAC3C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,4BAA4B,EAAE,oBAiB1C,CAAC;AAEF,0DAA0D;AAC1D,eAAO,MAAM,kBAAkB,EAAE,CAAC,MAAM,oBAAoB,CAAC,EAAE,EAK9D,CAAC;AAIF,kEAAkE;AAClE,eAAO,MAAM,mBAAmB,KAAK,CAAC;AACtC,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4ElC,CAAC;AAEL,iFAAiF;AACjF,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAC;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,OAAO,EAAE,qBAAqB,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,UAAU,EAAE,qBAAqB,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,aAAa,EAAE,iBAAiB,CAAC;IACjC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,qBAAqB,CAAC;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAeF,kDAAkD;AAClD,eAAO,MAAM,wBAAwB,EAAE,qBAQtC,CAAC;AAEF,eAAO,MAAM,2BAA2B,EAAE,mBAiCzC,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,EAAE,CAAC,MAAM,mBAAmB,CAAC,EAAE,EAM5D,CAAC;AAIF,eAAO,MAAM,kBAAkB;;iBAE7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gymmonk-schema — Onboarding FORM schemas
|
|
3
|
+
* ========================================
|
|
4
|
+
* The client-side validation contract for the member and owner onboarding
|
|
5
|
+
* wizards, and the gym-selection step.
|
|
6
|
+
*
|
|
7
|
+
* Why these live here and are distinct from the `*OnboardingBody` API schemas:
|
|
8
|
+
* a half-filled form legitimately holds `null` for a choice the user has not
|
|
9
|
+
* made yet, whereas the API body only ever sees completed values. So these are
|
|
10
|
+
* the SAME rules expressed over the form's working shape — every message,
|
|
11
|
+
* regex and bound is defined once, here, and both sides agree on it.
|
|
12
|
+
*
|
|
13
|
+
* Everything reuses the primitives in `shared.ts` / `common.ts`
|
|
14
|
+
* (`INDIAN_STATES`, `CENTER_TYPES`, `WEEKDAYS`, phone/pincode rules) rather
|
|
15
|
+
* than restating them.
|
|
16
|
+
*
|
|
17
|
+
* @module gymmonk-schema/onboarding-form
|
|
18
|
+
*/
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
import { PHONE_REGEX, PINCODE_REGEX } from './common.js';
|
|
21
|
+
import { WEEKDAYS } from './shared.js';
|
|
22
|
+
// ─── Field-level rules (shared by both wizards) ──────────────────────────────
|
|
23
|
+
/** A required 10-digit Indian mobile, as typed into a form field. */
|
|
24
|
+
const formPhone = z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number');
|
|
25
|
+
/** An optional phone: blank is allowed, anything else must be valid. */
|
|
26
|
+
const optionalFormPhone = z
|
|
27
|
+
.string()
|
|
28
|
+
.refine((v) => v === '' || PHONE_REGEX.test(v), 'Enter a valid 10-digit mobile number');
|
|
29
|
+
/** An optional email: blank is allowed, anything else must look like an email. */
|
|
30
|
+
const optionalFormEmail = z
|
|
31
|
+
.string()
|
|
32
|
+
.refine((v) => v === '' || /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(v), {
|
|
33
|
+
message: 'Enter a valid email',
|
|
34
|
+
});
|
|
35
|
+
/** A choice the user has not made yet is `null` until they pick one. */
|
|
36
|
+
const requiredChoice = (inner, message) => inner.nullable().refine((v) => v !== null, { message });
|
|
37
|
+
// ─── Address (form shape: `state` is null until picked) ──────────────────────
|
|
38
|
+
export const onboardingAddressSchema = z.object({
|
|
39
|
+
street: z.string().trim().min(1, 'Street address is required'),
|
|
40
|
+
city: z.string().trim().min(1, 'City is required'),
|
|
41
|
+
state: requiredChoice(z.string(), 'Select a state'),
|
|
42
|
+
pincode: z.string().regex(PINCODE_REGEX, 'Enter a valid 6-digit pincode'),
|
|
43
|
+
});
|
|
44
|
+
/** Street + city + state + a 6-digit pincode all present. */
|
|
45
|
+
export function isOnboardingAddressComplete(a) {
|
|
46
|
+
return (a.street.trim().length > 0 && a.city.trim().length > 0 && a.state != null && a.pincode.length === 6);
|
|
47
|
+
}
|
|
48
|
+
// ─── Social links (form shape: every handle is a plain string) ───────────────
|
|
49
|
+
export const onboardingSocialLinksSchema = z.object({
|
|
50
|
+
jansathi: z.string(),
|
|
51
|
+
youtube: z.string(),
|
|
52
|
+
instagram: z.string(),
|
|
53
|
+
facebook: z.string(),
|
|
54
|
+
});
|
|
55
|
+
// ─── Opening hours (form shape) ─────────────────────────────────────────────
|
|
56
|
+
export const onboardingDayHoursSchema = z.object({
|
|
57
|
+
open: z.boolean(),
|
|
58
|
+
from: z.string(),
|
|
59
|
+
to: z.string(),
|
|
60
|
+
});
|
|
61
|
+
// ─── Date-of-birth helpers ──────────────────────────────────────────────────
|
|
62
|
+
/**
|
|
63
|
+
* `Date` rolls overflow forward (Feb 31 → Mar 3), so a real date is one whose
|
|
64
|
+
* parts survive the round-trip.
|
|
65
|
+
*/
|
|
66
|
+
function isRealDate(year, month, day) {
|
|
67
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
68
|
+
return (date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day);
|
|
69
|
+
}
|
|
70
|
+
/** Whole years elapsed since the given Y/M/D, in UTC. */
|
|
71
|
+
function ageInYears(year, month, day) {
|
|
72
|
+
const date = Date.UTC(year, month - 1, day);
|
|
73
|
+
return (Date.now() - date) / (365.25 * 24 * 60 * 60 * 1000);
|
|
74
|
+
}
|
|
75
|
+
// ─── Member onboarding form ─────────────────────────────────────────────────
|
|
76
|
+
/** Age bounds a gym membership realistically spans. */
|
|
77
|
+
export const MEMBER_MIN_AGE_YEARS = 10;
|
|
78
|
+
export const MEMBER_MAX_AGE_YEARS = 100;
|
|
79
|
+
export const memberOnboardingFormSchema = z
|
|
80
|
+
.object({
|
|
81
|
+
firstName: z.string().trim().min(1, 'First name is required'),
|
|
82
|
+
lastName: z.string().trim(),
|
|
83
|
+
dobDay: z.string().min(1, 'Enter the day'),
|
|
84
|
+
dobMonth: z.string().min(1, 'Enter the month'),
|
|
85
|
+
dobYear: z.string().length(4, 'Enter a 4-digit year'),
|
|
86
|
+
gender: requiredChoice(z.enum(['male', 'female', 'other']), 'Select your gender'),
|
|
87
|
+
heightCm: requiredChoice(z.number(), 'Add your height'),
|
|
88
|
+
weightKg: requiredChoice(z.number(), 'Add your weight'),
|
|
89
|
+
neckCm: requiredChoice(z.number(), 'Add your neck'),
|
|
90
|
+
waistCm: requiredChoice(z.number(), 'Add your waist'),
|
|
91
|
+
hipsCm: z.number().nullable(),
|
|
92
|
+
phone: formPhone,
|
|
93
|
+
emergencyName: z.string().trim().min(1, 'Add an emergency contact name'),
|
|
94
|
+
emergencyPhone: formPhone,
|
|
95
|
+
bloodGroup: requiredChoice(z.string(), 'Select your blood group'),
|
|
96
|
+
/** GCS URL of the uploaded photo. Optional — it can be added later. */
|
|
97
|
+
avatarUrl: z.string(),
|
|
98
|
+
})
|
|
99
|
+
/**
|
|
100
|
+
* The three DOB cells are individually bounded by the input, but only the
|
|
101
|
+
* assembled date can catch an impossible one (31 Feb) or an implausible age.
|
|
102
|
+
* The issue is attached to `dobDay` so it renders under the field group.
|
|
103
|
+
*/
|
|
104
|
+
.superRefine((v, ctx) => {
|
|
105
|
+
if (!v.dobDay || !v.dobMonth || v.dobYear.length !== 4)
|
|
106
|
+
return;
|
|
107
|
+
const day = Number(v.dobDay);
|
|
108
|
+
const month = Number(v.dobMonth);
|
|
109
|
+
const year = Number(v.dobYear);
|
|
110
|
+
if (!isRealDate(year, month, day)) {
|
|
111
|
+
ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'That date does not exist' });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const age = ageInYears(year, month, day);
|
|
115
|
+
if (age < MEMBER_MIN_AGE_YEARS) {
|
|
116
|
+
ctx.addIssue({
|
|
117
|
+
code: 'custom',
|
|
118
|
+
path: ['dobDay'],
|
|
119
|
+
message: `You must be at least ${MEMBER_MIN_AGE_YEARS} to join`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
else if (age > MEMBER_MAX_AGE_YEARS) {
|
|
123
|
+
ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Check the year of birth' });
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
/**
|
|
127
|
+
* The API requires the hips measurement for female members (needed for the
|
|
128
|
+
* body-fat estimate), so it fails inline on the measurements step instead of
|
|
129
|
+
* 422-ing at Finish. Own refine — no early-return coupling with the DOB rule.
|
|
130
|
+
*/
|
|
131
|
+
.superRefine((v, ctx) => {
|
|
132
|
+
if (v.gender === 'female' && v.hipsCm == null) {
|
|
133
|
+
ctx.addIssue({ code: 'custom', path: ['hipsCm'], message: 'Hips measurement is required' });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
export const EMPTY_MEMBER_ONBOARDING_FORM = {
|
|
137
|
+
firstName: '',
|
|
138
|
+
lastName: '',
|
|
139
|
+
dobDay: '',
|
|
140
|
+
dobMonth: '',
|
|
141
|
+
dobYear: '',
|
|
142
|
+
gender: null,
|
|
143
|
+
heightCm: null,
|
|
144
|
+
weightKg: null,
|
|
145
|
+
neckCm: null,
|
|
146
|
+
waistCm: null,
|
|
147
|
+
hipsCm: null,
|
|
148
|
+
phone: '',
|
|
149
|
+
emergencyName: '',
|
|
150
|
+
emergencyPhone: '',
|
|
151
|
+
bloodGroup: null,
|
|
152
|
+
avatarUrl: '',
|
|
153
|
+
};
|
|
154
|
+
/** Fields validated before advancing each member step. */
|
|
155
|
+
export const MEMBER_STEP_FIELDS = [
|
|
156
|
+
['firstName', 'dobDay', 'dobMonth', 'dobYear', 'gender'],
|
|
157
|
+
['heightCm', 'weightKg'],
|
|
158
|
+
['neckCm', 'waistCm', 'hipsCm'],
|
|
159
|
+
['phone', 'emergencyName', 'emergencyPhone', 'bloodGroup'],
|
|
160
|
+
];
|
|
161
|
+
// ─── Owner onboarding form ──────────────────────────────────────────────────
|
|
162
|
+
/** Age bounds for a gym owner (the DOB itself stays optional). */
|
|
163
|
+
export const OWNER_MIN_AGE_YEARS = 16;
|
|
164
|
+
export const OWNER_MAX_AGE_YEARS = 100;
|
|
165
|
+
export const ownerOnboardingFormSchema = z
|
|
166
|
+
.object({
|
|
167
|
+
// 1 — basics
|
|
168
|
+
firstName: z.string().trim().min(1, 'First name is required'),
|
|
169
|
+
lastName: z.string().trim(),
|
|
170
|
+
dobDay: z.string(),
|
|
171
|
+
dobMonth: z.string(),
|
|
172
|
+
dobYear: z.string(),
|
|
173
|
+
gender: requiredChoice(z.enum(['male', 'female', 'other']), 'Select your gender'),
|
|
174
|
+
// 2 — contact
|
|
175
|
+
phone: formPhone,
|
|
176
|
+
altEmail: optionalFormEmail,
|
|
177
|
+
website: z.string(),
|
|
178
|
+
noWebsite: z.boolean(),
|
|
179
|
+
address: onboardingAddressSchema,
|
|
180
|
+
socials: onboardingSocialLinksSchema,
|
|
181
|
+
// 3 — organisation
|
|
182
|
+
orgName: z.string().trim().min(1, 'Organisation name is required'),
|
|
183
|
+
orgDescription: z.string(),
|
|
184
|
+
hqAddress: onboardingAddressSchema,
|
|
185
|
+
gstNumber: z.string(),
|
|
186
|
+
cinNumber: z.string(),
|
|
187
|
+
numberOfCenters: requiredChoice(z.number(), 'Add the number of centers'),
|
|
188
|
+
orgSocials: onboardingSocialLinksSchema,
|
|
189
|
+
// 4 — center
|
|
190
|
+
centerName: z.string().trim().min(1, 'Center name is required'),
|
|
191
|
+
centerDescription: z.string(),
|
|
192
|
+
centerTypes: z.array(z.string()).min(1, 'Pick at least one type'),
|
|
193
|
+
centerAddress: onboardingAddressSchema,
|
|
194
|
+
floorArea: z.number().nullable(),
|
|
195
|
+
centerPhone1: formPhone,
|
|
196
|
+
centerPhone2: optionalFormPhone,
|
|
197
|
+
centerEmail: optionalFormEmail,
|
|
198
|
+
hours: z.object({
|
|
199
|
+
mon: onboardingDayHoursSchema,
|
|
200
|
+
tue: onboardingDayHoursSchema,
|
|
201
|
+
wed: onboardingDayHoursSchema,
|
|
202
|
+
thu: onboardingDayHoursSchema,
|
|
203
|
+
fri: onboardingDayHoursSchema,
|
|
204
|
+
sat: onboardingDayHoursSchema,
|
|
205
|
+
sun: onboardingDayHoursSchema,
|
|
206
|
+
}),
|
|
207
|
+
photos: z.array(z.string()),
|
|
208
|
+
/** GCS URL of the owner's uploaded photo. Optional. */
|
|
209
|
+
avatarUrl: z.string(),
|
|
210
|
+
// 5 — plan
|
|
211
|
+
planId: requiredChoice(z.string(), 'Choose a plan'),
|
|
212
|
+
tenureId: z.string().nullable(),
|
|
213
|
+
})
|
|
214
|
+
/**
|
|
215
|
+
* The owner's DOB is optional, so this only engages once any cell is touched:
|
|
216
|
+
* then all three are required and must form a real, plausible date.
|
|
217
|
+
*/
|
|
218
|
+
.superRefine((v, ctx) => {
|
|
219
|
+
const touched = Boolean(v.dobDay || v.dobMonth || v.dobYear);
|
|
220
|
+
if (!touched)
|
|
221
|
+
return;
|
|
222
|
+
const fail = (message) => ctx.addIssue({ code: 'custom', path: ['dobDay'], message });
|
|
223
|
+
if (!v.dobDay || !v.dobMonth || v.dobYear.length !== 4) {
|
|
224
|
+
fail('Enter the full date of birth');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const day = Number(v.dobDay);
|
|
228
|
+
const month = Number(v.dobMonth);
|
|
229
|
+
const year = Number(v.dobYear);
|
|
230
|
+
if (!isRealDate(year, month, day)) {
|
|
231
|
+
fail('That date does not exist');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const age = ageInYears(year, month, day);
|
|
235
|
+
if (age < OWNER_MIN_AGE_YEARS)
|
|
236
|
+
fail(`You must be at least ${OWNER_MIN_AGE_YEARS}`);
|
|
237
|
+
else if (age > OWNER_MAX_AGE_YEARS)
|
|
238
|
+
fail('Check the year of birth');
|
|
239
|
+
});
|
|
240
|
+
const EMPTY_ONBOARDING_ADDRESS = {
|
|
241
|
+
street: '',
|
|
242
|
+
city: '',
|
|
243
|
+
state: null,
|
|
244
|
+
pincode: '',
|
|
245
|
+
};
|
|
246
|
+
const EMPTY_ONBOARDING_SOCIALS = {
|
|
247
|
+
jansathi: '',
|
|
248
|
+
youtube: '',
|
|
249
|
+
instagram: '',
|
|
250
|
+
facebook: '',
|
|
251
|
+
};
|
|
252
|
+
/** The opening hours a new center starts with. */
|
|
253
|
+
export const DEFAULT_ONBOARDING_HOURS = {
|
|
254
|
+
mon: { open: true, from: '06:00', to: '22:00' },
|
|
255
|
+
tue: { open: true, from: '06:00', to: '22:00' },
|
|
256
|
+
wed: { open: true, from: '06:00', to: '22:00' },
|
|
257
|
+
thu: { open: true, from: '06:00', to: '22:00' },
|
|
258
|
+
fri: { open: true, from: '06:00', to: '22:00' },
|
|
259
|
+
sat: { open: true, from: '07:00', to: '21:00' },
|
|
260
|
+
sun: { open: false, from: '07:00', to: '13:00' },
|
|
261
|
+
};
|
|
262
|
+
export const EMPTY_OWNER_ONBOARDING_FORM = {
|
|
263
|
+
firstName: '',
|
|
264
|
+
lastName: '',
|
|
265
|
+
dobDay: '',
|
|
266
|
+
dobMonth: '',
|
|
267
|
+
dobYear: '',
|
|
268
|
+
gender: null,
|
|
269
|
+
phone: '',
|
|
270
|
+
altEmail: '',
|
|
271
|
+
website: '',
|
|
272
|
+
noWebsite: false,
|
|
273
|
+
address: { ...EMPTY_ONBOARDING_ADDRESS },
|
|
274
|
+
socials: { ...EMPTY_ONBOARDING_SOCIALS },
|
|
275
|
+
orgName: '',
|
|
276
|
+
orgDescription: '',
|
|
277
|
+
hqAddress: { ...EMPTY_ONBOARDING_ADDRESS },
|
|
278
|
+
gstNumber: '',
|
|
279
|
+
cinNumber: '',
|
|
280
|
+
numberOfCenters: null,
|
|
281
|
+
orgSocials: { ...EMPTY_ONBOARDING_SOCIALS },
|
|
282
|
+
centerName: '',
|
|
283
|
+
centerDescription: '',
|
|
284
|
+
centerTypes: [],
|
|
285
|
+
centerAddress: { ...EMPTY_ONBOARDING_ADDRESS },
|
|
286
|
+
floorArea: null,
|
|
287
|
+
centerPhone1: '',
|
|
288
|
+
centerPhone2: '',
|
|
289
|
+
centerEmail: '',
|
|
290
|
+
hours: structuredClone(DEFAULT_ONBOARDING_HOURS),
|
|
291
|
+
photos: [],
|
|
292
|
+
avatarUrl: '',
|
|
293
|
+
planId: null,
|
|
294
|
+
tenureId: null,
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* Fields validated (via `form.trigger`) before advancing each owner step.
|
|
298
|
+
* Triggering a parent path (e.g. "address") validates its nested fields too.
|
|
299
|
+
*
|
|
300
|
+
* The DOB cells belong here even though the date is optional: the refine above
|
|
301
|
+
* still rejects a half-typed date, and a field missing from this map would be
|
|
302
|
+
* unreachable twice over — Continue would not surface it, and the final submit
|
|
303
|
+
* (which finds the offending STEP by searching these lists) would find no step
|
|
304
|
+
* and leave the plan screen dead.
|
|
305
|
+
*/
|
|
306
|
+
export const OWNER_STEP_FIELDS = [
|
|
307
|
+
['firstName', 'gender', 'dobDay', 'dobMonth', 'dobYear'],
|
|
308
|
+
['phone', 'altEmail', 'address'],
|
|
309
|
+
['orgName', 'hqAddress', 'numberOfCenters'],
|
|
310
|
+
['centerName', 'centerTypes', 'centerAddress', 'centerPhone1', 'centerPhone2', 'centerEmail'],
|
|
311
|
+
['planId', 'tenureId'],
|
|
312
|
+
];
|
|
313
|
+
// ─── Gym selection (member step 0) ──────────────────────────────────────────
|
|
314
|
+
export const gymSelectionSchema = z.object({
|
|
315
|
+
gymId: z.string().trim().min(1, 'Enter your gym id or pick a gym below'),
|
|
316
|
+
});
|
|
317
|
+
//# sourceMappingURL=onboarding-form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onboarding-form.js","sourceRoot":"","sources":["../src/onboarding-form.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Onboarding FORM schemas\n * ========================================\n * The client-side validation contract for the member and owner onboarding\n * wizards, and the gym-selection step.\n *\n * Why these live here and are distinct from the `*OnboardingBody` API schemas:\n * a half-filled form legitimately holds `null` for a choice the user has not\n * made yet, whereas the API body only ever sees completed values. So these are\n * the SAME rules expressed over the form's working shape — every message,\n * regex and bound is defined once, here, and both sides agree on it.\n *\n * Everything reuses the primitives in `shared.ts` / `common.ts`\n * (`INDIAN_STATES`, `CENTER_TYPES`, `WEEKDAYS`, phone/pincode rules) rather\n * than restating them.\n *\n * @module gymmonk-schema/onboarding-form\n */\n\nimport { z } from 'zod';\nimport { PHONE_REGEX, PINCODE_REGEX } from './common.js';\nimport { WEEKDAYS } from './shared.js';\n\n// ─── Field-level rules (shared by both wizards) ──────────────────────────────\n\n/** A required 10-digit Indian mobile, as typed into a form field. */\nconst formPhone = z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number');\n\n/** An optional phone: blank is allowed, anything else must be valid. */\nconst optionalFormPhone = z\n .string()\n .refine((v) => v === '' || PHONE_REGEX.test(v), 'Enter a valid 10-digit mobile number');\n\n/** An optional email: blank is allowed, anything else must look like an email. */\nconst optionalFormEmail = z\n .string()\n .refine((v) => v === '' || /^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(v), {\n message: 'Enter a valid email',\n });\n\n/** A choice the user has not made yet is `null` until they pick one. */\nconst requiredChoice = <T extends z.ZodTypeAny>(inner: T, message: string) =>\n inner.nullable().refine((v: unknown) => v !== null, { message });\n\n// ─── Address (form shape: `state` is null until picked) ──────────────────────\n\nexport const onboardingAddressSchema = z.object({\n street: z.string().trim().min(1, 'Street address is required'),\n city: z.string().trim().min(1, 'City is required'),\n state: requiredChoice(z.string(), 'Select a state'),\n pincode: z.string().regex(PINCODE_REGEX, 'Enter a valid 6-digit pincode'),\n});\n\n/** Working shape of an address inside an onboarding form. */\nexport type OnboardingAddress = {\n street: string;\n city: string;\n state: string | null;\n pincode: string;\n};\n\n/** Street + city + state + a 6-digit pincode all present. */\nexport function isOnboardingAddressComplete(a: OnboardingAddress): boolean {\n return (\n a.street.trim().length > 0 && a.city.trim().length > 0 && a.state != null && a.pincode.length === 6\n );\n}\n\n// ─── Social links (form shape: every handle is a plain string) ───────────────\n\nexport const onboardingSocialLinksSchema = z.object({\n jansathi: z.string(),\n youtube: z.string(),\n instagram: z.string(),\n facebook: z.string(),\n});\nexport type OnboardingSocialLinks = z.infer<typeof onboardingSocialLinksSchema>;\n\n// ─── Opening hours (form shape) ─────────────────────────────────────────────\n\nexport const onboardingDayHoursSchema = z.object({\n open: z.boolean(),\n from: z.string(),\n to: z.string(),\n});\nexport type OnboardingDayHours = z.infer<typeof onboardingDayHoursSchema>;\nexport type OnboardingWeeklyHours = Record<(typeof WEEKDAYS)[number], OnboardingDayHours>;\n\n// ─── Date-of-birth helpers ──────────────────────────────────────────────────\n\n/**\n * `Date` rolls overflow forward (Feb 31 → Mar 3), so a real date is one whose\n * parts survive the round-trip.\n */\nfunction isRealDate(year: number, month: number, day: number): boolean {\n const date = new Date(Date.UTC(year, month - 1, day));\n return (\n date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day\n );\n}\n\n/** Whole years elapsed since the given Y/M/D, in UTC. */\nfunction ageInYears(year: number, month: number, day: number): number {\n const date = Date.UTC(year, month - 1, day);\n return (Date.now() - date) / (365.25 * 24 * 60 * 60 * 1000);\n}\n\n// ─── Member onboarding form ─────────────────────────────────────────────────\n\n/** Age bounds a gym membership realistically spans. */\nexport const MEMBER_MIN_AGE_YEARS = 10;\nexport const MEMBER_MAX_AGE_YEARS = 100;\n\nexport const memberOnboardingFormSchema = z\n .object({\n firstName: z.string().trim().min(1, 'First name is required'),\n lastName: z.string().trim(),\n dobDay: z.string().min(1, 'Enter the day'),\n dobMonth: z.string().min(1, 'Enter the month'),\n dobYear: z.string().length(4, 'Enter a 4-digit year'),\n gender: requiredChoice(z.enum(['male', 'female', 'other']), 'Select your gender'),\n heightCm: requiredChoice(z.number(), 'Add your height'),\n weightKg: requiredChoice(z.number(), 'Add your weight'),\n neckCm: requiredChoice(z.number(), 'Add your neck'),\n waistCm: requiredChoice(z.number(), 'Add your waist'),\n hipsCm: z.number().nullable(),\n phone: formPhone,\n emergencyName: z.string().trim().min(1, 'Add an emergency contact name'),\n emergencyPhone: formPhone,\n bloodGroup: requiredChoice(z.string(), 'Select your blood group'),\n /** GCS URL of the uploaded photo. Optional — it can be added later. */\n avatarUrl: z.string(),\n })\n /**\n * The three DOB cells are individually bounded by the input, but only the\n * assembled date can catch an impossible one (31 Feb) or an implausible age.\n * The issue is attached to `dobDay` so it renders under the field group.\n */\n .superRefine((v, ctx) => {\n if (!v.dobDay || !v.dobMonth || v.dobYear.length !== 4) return;\n\n const day = Number(v.dobDay);\n const month = Number(v.dobMonth);\n const year = Number(v.dobYear);\n\n if (!isRealDate(year, month, day)) {\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'That date does not exist' });\n return;\n }\n\n const age = ageInYears(year, month, day);\n if (age < MEMBER_MIN_AGE_YEARS) {\n ctx.addIssue({\n code: 'custom',\n path: ['dobDay'],\n message: `You must be at least ${MEMBER_MIN_AGE_YEARS} to join`,\n });\n } else if (age > MEMBER_MAX_AGE_YEARS) {\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Check the year of birth' });\n }\n })\n /**\n * The API requires the hips measurement for female members (needed for the\n * body-fat estimate), so it fails inline on the measurements step instead of\n * 422-ing at Finish. Own refine — no early-return coupling with the DOB rule.\n */\n .superRefine((v, ctx) => {\n if (v.gender === 'female' && v.hipsCm == null) {\n ctx.addIssue({ code: 'custom', path: ['hipsCm'], message: 'Hips measurement is required' });\n }\n });\n\n/**\n * The form's working shape. Hand-written (not `z.infer`) so unset numeric /\n * choice fields can hold `null` while the schema's `requiredChoice` rules still\n * drive the field errors — the resolver is cast to this type at `useForm`.\n */\nexport type MemberOnboardingForm = {\n firstName: string;\n lastName: string;\n dobDay: string;\n dobMonth: string;\n dobYear: string;\n gender: 'male' | 'female' | 'other' | null;\n heightCm: number | null;\n weightKg: number | null;\n neckCm: number | null;\n waistCm: number | null;\n hipsCm: number | null;\n phone: string;\n emergencyName: string;\n emergencyPhone: string;\n bloodGroup: string | null;\n avatarUrl: string;\n};\n\nexport const EMPTY_MEMBER_ONBOARDING_FORM: MemberOnboardingForm = {\n firstName: '',\n lastName: '',\n dobDay: '',\n dobMonth: '',\n dobYear: '',\n gender: null,\n heightCm: null,\n weightKg: null,\n neckCm: null,\n waistCm: null,\n hipsCm: null,\n phone: '',\n emergencyName: '',\n emergencyPhone: '',\n bloodGroup: null,\n avatarUrl: '',\n};\n\n/** Fields validated before advancing each member step. */\nexport const MEMBER_STEP_FIELDS: (keyof MemberOnboardingForm)[][] = [\n ['firstName', 'dobDay', 'dobMonth', 'dobYear', 'gender'],\n ['heightCm', 'weightKg'],\n ['neckCm', 'waistCm', 'hipsCm'],\n ['phone', 'emergencyName', 'emergencyPhone', 'bloodGroup'],\n];\n\n// ─── Owner onboarding form ──────────────────────────────────────────────────\n\n/** Age bounds for a gym owner (the DOB itself stays optional). */\nexport const OWNER_MIN_AGE_YEARS = 16;\nexport const OWNER_MAX_AGE_YEARS = 100;\n\nexport const ownerOnboardingFormSchema = z\n .object({\n // 1 — basics\n firstName: z.string().trim().min(1, 'First name is required'),\n lastName: z.string().trim(),\n dobDay: z.string(),\n dobMonth: z.string(),\n dobYear: z.string(),\n gender: requiredChoice(z.enum(['male', 'female', 'other']), 'Select your gender'),\n // 2 — contact\n phone: formPhone,\n altEmail: optionalFormEmail,\n website: z.string(),\n noWebsite: z.boolean(),\n address: onboardingAddressSchema,\n socials: onboardingSocialLinksSchema,\n // 3 — organisation\n orgName: z.string().trim().min(1, 'Organisation name is required'),\n orgDescription: z.string(),\n hqAddress: onboardingAddressSchema,\n gstNumber: z.string(),\n cinNumber: z.string(),\n numberOfCenters: requiredChoice(z.number(), 'Add the number of centers'),\n orgSocials: onboardingSocialLinksSchema,\n // 4 — center\n centerName: z.string().trim().min(1, 'Center name is required'),\n centerDescription: z.string(),\n centerTypes: z.array(z.string()).min(1, 'Pick at least one type'),\n centerAddress: onboardingAddressSchema,\n floorArea: z.number().nullable(),\n centerPhone1: formPhone,\n centerPhone2: optionalFormPhone,\n centerEmail: optionalFormEmail,\n hours: z.object({\n mon: onboardingDayHoursSchema,\n tue: onboardingDayHoursSchema,\n wed: onboardingDayHoursSchema,\n thu: onboardingDayHoursSchema,\n fri: onboardingDayHoursSchema,\n sat: onboardingDayHoursSchema,\n sun: onboardingDayHoursSchema,\n }),\n photos: z.array(z.string()),\n /** GCS URL of the owner's uploaded photo. Optional. */\n avatarUrl: z.string(),\n // 5 — plan\n planId: requiredChoice(z.string(), 'Choose a plan'),\n tenureId: z.string().nullable(),\n })\n /**\n * The owner's DOB is optional, so this only engages once any cell is touched:\n * then all three are required and must form a real, plausible date.\n */\n .superRefine((v, ctx) => {\n const touched = Boolean(v.dobDay || v.dobMonth || v.dobYear);\n if (!touched) return;\n\n const fail = (message: string) => ctx.addIssue({ code: 'custom', path: ['dobDay'], message });\n\n if (!v.dobDay || !v.dobMonth || v.dobYear.length !== 4) {\n fail('Enter the full date of birth');\n return;\n }\n\n const day = Number(v.dobDay);\n const month = Number(v.dobMonth);\n const year = Number(v.dobYear);\n\n if (!isRealDate(year, month, day)) {\n fail('That date does not exist');\n return;\n }\n\n const age = ageInYears(year, month, day);\n if (age < OWNER_MIN_AGE_YEARS) fail(`You must be at least ${OWNER_MIN_AGE_YEARS}`);\n else if (age > OWNER_MAX_AGE_YEARS) fail('Check the year of birth');\n });\n\n/** Hand-written working shape (nullable \"unset\" fields); see the member form. */\nexport type OwnerOnboardingForm = {\n firstName: string;\n lastName: string;\n dobDay: string;\n dobMonth: string;\n dobYear: string;\n gender: 'male' | 'female' | 'other' | null;\n phone: string;\n altEmail: string;\n website: string;\n noWebsite: boolean;\n address: OnboardingAddress;\n socials: OnboardingSocialLinks;\n orgName: string;\n orgDescription: string;\n hqAddress: OnboardingAddress;\n gstNumber: string;\n cinNumber: string;\n numberOfCenters: number | null;\n orgSocials: OnboardingSocialLinks;\n centerName: string;\n centerDescription: string;\n centerTypes: string[];\n centerAddress: OnboardingAddress;\n floorArea: number | null;\n centerPhone1: string;\n centerPhone2: string;\n centerEmail: string;\n hours: OnboardingWeeklyHours;\n photos: string[];\n avatarUrl: string;\n planId: string | null;\n tenureId: string | null;\n};\n\nconst EMPTY_ONBOARDING_ADDRESS: OnboardingAddress = {\n street: '',\n city: '',\n state: null,\n pincode: '',\n};\nconst EMPTY_ONBOARDING_SOCIALS: OnboardingSocialLinks = {\n jansathi: '',\n youtube: '',\n instagram: '',\n facebook: '',\n};\n\n/** The opening hours a new center starts with. */\nexport const DEFAULT_ONBOARDING_HOURS: OnboardingWeeklyHours = {\n mon: { open: true, from: '06:00', to: '22:00' },\n tue: { open: true, from: '06:00', to: '22:00' },\n wed: { open: true, from: '06:00', to: '22:00' },\n thu: { open: true, from: '06:00', to: '22:00' },\n fri: { open: true, from: '06:00', to: '22:00' },\n sat: { open: true, from: '07:00', to: '21:00' },\n sun: { open: false, from: '07:00', to: '13:00' },\n};\n\nexport const EMPTY_OWNER_ONBOARDING_FORM: OwnerOnboardingForm = {\n firstName: '',\n lastName: '',\n dobDay: '',\n dobMonth: '',\n dobYear: '',\n gender: null,\n phone: '',\n altEmail: '',\n website: '',\n noWebsite: false,\n address: { ...EMPTY_ONBOARDING_ADDRESS },\n socials: { ...EMPTY_ONBOARDING_SOCIALS },\n orgName: '',\n orgDescription: '',\n hqAddress: { ...EMPTY_ONBOARDING_ADDRESS },\n gstNumber: '',\n cinNumber: '',\n numberOfCenters: null,\n orgSocials: { ...EMPTY_ONBOARDING_SOCIALS },\n centerName: '',\n centerDescription: '',\n centerTypes: [],\n centerAddress: { ...EMPTY_ONBOARDING_ADDRESS },\n floorArea: null,\n centerPhone1: '',\n centerPhone2: '',\n centerEmail: '',\n hours: structuredClone(DEFAULT_ONBOARDING_HOURS),\n photos: [],\n avatarUrl: '',\n planId: null,\n tenureId: null,\n};\n\n/**\n * Fields validated (via `form.trigger`) before advancing each owner step.\n * Triggering a parent path (e.g. \"address\") validates its nested fields too.\n *\n * The DOB cells belong here even though the date is optional: the refine above\n * still rejects a half-typed date, and a field missing from this map would be\n * unreachable twice over — Continue would not surface it, and the final submit\n * (which finds the offending STEP by searching these lists) would find no step\n * and leave the plan screen dead.\n */\nexport const OWNER_STEP_FIELDS: (keyof OwnerOnboardingForm)[][] = [\n ['firstName', 'gender', 'dobDay', 'dobMonth', 'dobYear'],\n ['phone', 'altEmail', 'address'],\n ['orgName', 'hqAddress', 'numberOfCenters'],\n ['centerName', 'centerTypes', 'centerAddress', 'centerPhone1', 'centerPhone2', 'centerEmail'],\n ['planId', 'tenureId'],\n];\n\n// ─── Gym selection (member step 0) ──────────────────────────────────────────\n\nexport const gymSelectionSchema = z.object({\n gymId: z.string().trim().min(1, 'Enter your gym id or pick a gym below'),\n});\nexport type GymSelection = z.infer<typeof gymSelectionSchema>;\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gymmonk-schema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Shared Zod schemas, enums and domain types for GymMonk (fitness SaaS) — single source of truth (SSOT) consumed by gymmonk-backend and gymmonk-web-client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|