@mohasinac/appkit 4.16.1 → 4.18.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/client.d.ts +7 -0
- package/dist/client.js +7 -0
- package/dist/features/admin/components/AdminAdEditorView.js +4 -2
- package/dist/features/admin/components/AdminAddressEditorView.js +4 -2
- package/dist/features/admin/components/AdminEmployeeEditorView.js +3 -1
- package/dist/features/admin/components/AdminFeatureFlagsView.js +4 -2
- package/dist/features/admin/components/AdminOrderEditorView.js +4 -2
- package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +18 -6
- package/dist/features/admin/components/AdminSiteView.js +4 -2
- package/dist/features/admin/components/AdminStoreEditorView.js +11 -9
- package/dist/features/admin/components/AdminUsersView.js +4 -2
- package/dist/features/admin/components/BrandQuickCreateForm.js +13 -5
- package/dist/features/admin/components/CategoryQuickCreateForm.js +13 -5
- package/dist/features/admin/schemas/admin-editor-forms.d.ts +119 -0
- package/dist/features/admin/schemas/admin-editor-forms.js +141 -0
- package/dist/features/admin/schemas/admin-ops-forms.d.ts +123 -0
- package/dist/features/admin/schemas/admin-ops-forms.js +147 -0
- package/dist/features/admin/schemas/small-forms.d.ts +54 -0
- package/dist/features/admin/schemas/small-forms.js +85 -0
- package/dist/features/auth/components/ForgotPasswordView.js +2 -1
- package/dist/features/auth/components/LoginForm.js +2 -1
- package/dist/features/auth/components/RegisterForm.js +2 -1
- package/dist/features/auth/components/ResetPasswordView.js +2 -1
- package/dist/features/consultation/components/ConsultationForm.js +3 -1
- package/dist/features/consultation/schemas/index.d.ts +4 -4
- package/dist/features/consultation/schemas/index.js +22 -5
- package/dist/features/contact/schemas/inquiry-forms.d.ts +13 -0
- package/dist/features/contact/schemas/inquiry-forms.js +34 -0
- package/dist/features/corporate/components/CorporateInquiryForm.js +3 -1
- package/dist/features/corporate/schemas/index.d.ts +4 -1
- package/dist/features/corporate/schemas/index.js +38 -8
- package/dist/features/events/components/EventRaffleEntryForm.js +3 -1
- package/dist/features/homepage/components/NewsletterBanner.js +3 -1
- package/dist/features/orders/schemas/buyer-forms.d.ts +46 -0
- package/dist/features/orders/schemas/buyer-forms.js +81 -0
- package/dist/features/products/components/MakeOfferButton.js +1 -1
- package/dist/features/scams/schemas/report-form.d.ts +63 -0
- package/dist/features/scams/schemas/report-form.js +88 -0
- package/dist/features/store-extensions/schemas/sublisting-category-form.d.ts +15 -0
- package/dist/features/store-extensions/schemas/sublisting-category-form.js +41 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/package.json +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: A cluster of admin editors rendered `<Form>` with no schema. Two of
|
|
3
|
+
* them write PII, and one changes a store's standing on the platform.
|
|
4
|
+
*
|
|
5
|
+
* `AdminAddressEditorView` is the PII one: `fullName`, `phone` and the
|
|
6
|
+
* address lines are all encrypted at rest, and none was checked — a
|
|
7
|
+
* postal code could be any string, and `ownerId` (which decides WHOSE
|
|
8
|
+
* address this is) was free text.
|
|
9
|
+
*
|
|
10
|
+
* `AdminStoreEditorView` sets `storeStatus`, `isVerified`, `isFeatured`
|
|
11
|
+
* and `capabilities`. Recurrent Root Cause #38 lived here: saving any
|
|
12
|
+
* field re-sent every other one, so an unrelated edit could silently
|
|
13
|
+
* un-verify a store.
|
|
14
|
+
*
|
|
15
|
+
* WHAT: One schema per editor.
|
|
16
|
+
*
|
|
17
|
+
* EXPORTS:
|
|
18
|
+
* adminAddressFormSchema, adminStoreUpdateSchema, announcementBarSchema,
|
|
19
|
+
* and their inferred value types
|
|
20
|
+
*
|
|
21
|
+
* @tag domain:admin
|
|
22
|
+
* @tag layer:schema
|
|
23
|
+
* @tag pattern:none
|
|
24
|
+
* @tag access:isomorphic
|
|
25
|
+
* @tag consumers:AdminAddressEditorView,AdminStoreEditorView,AdminSiteView
|
|
26
|
+
* @tag sideEffects:none
|
|
27
|
+
*/
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
30
|
+
const PHONE_RE = /^(\+?91[-\s]?)?[6-9]\d{9}$/;
|
|
31
|
+
/** Indian PIN: six digits, never starting at zero. */
|
|
32
|
+
const PIN_RE = /^[1-9]\d{5}$/;
|
|
33
|
+
/**
|
|
34
|
+
* An address written by an admin on someone's behalf.
|
|
35
|
+
*
|
|
36
|
+
* Shapes match `AddressDocument` (`addressLine1`, not `line1`) — deliberately
|
|
37
|
+
* NOT `userAddressSchema`, which is the account-side view of the same data
|
|
38
|
+
* under different field names. Reusing that one here would have meant
|
|
39
|
+
* translating on every write.
|
|
40
|
+
*/
|
|
41
|
+
export const adminAddressFormSchema = z.object({
|
|
42
|
+
ownerType: annotate(z.enum(["user", "store"]), {
|
|
43
|
+
section: "owner", sectionLabel: "Belongs to", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
44
|
+
}),
|
|
45
|
+
ownerId: annotate(z.string().trim().min(1, "Choose who this address belongs to."), {
|
|
46
|
+
section: "owner", quick: true, order: 2, row: "pair",
|
|
47
|
+
}),
|
|
48
|
+
label: annotate(z.string().trim().max(60, "Keep the label short — e.g. Home, Warehouse.").optional(), {
|
|
49
|
+
section: "address", sectionLabel: "Address", order: 1, row: "pair",
|
|
50
|
+
}),
|
|
51
|
+
fullName: annotate(z.string().trim().min(2, "A recipient name is required.").max(120), {
|
|
52
|
+
section: "address", sectionRequired: true, quick: true, order: 2, row: "pair",
|
|
53
|
+
}),
|
|
54
|
+
phone: annotate(z.string().trim().regex(PHONE_RE, "Enter a 10-digit Indian mobile number."), {
|
|
55
|
+
section: "address", quick: true, order: 3, row: "pair",
|
|
56
|
+
}),
|
|
57
|
+
addressLine1: annotate(z.string().trim().min(3, "Street address is required.").max(200), {
|
|
58
|
+
section: "address", quick: true, order: 4, row: "full",
|
|
59
|
+
}),
|
|
60
|
+
addressLine2: annotate(z.string().trim().max(200).optional(), {
|
|
61
|
+
section: "address", order: 5, row: "full",
|
|
62
|
+
}),
|
|
63
|
+
landmark: annotate(z.string().trim().max(120).optional(), {
|
|
64
|
+
section: "address", order: 6, row: "pair",
|
|
65
|
+
}),
|
|
66
|
+
city: annotate(z.string().trim().min(2, "City is required.").max(80), {
|
|
67
|
+
section: "address", order: 7, row: "pair",
|
|
68
|
+
}),
|
|
69
|
+
state: annotate(z.string().trim().min(2, "State is required.").max(80), {
|
|
70
|
+
section: "address", order: 8, row: "pair",
|
|
71
|
+
}),
|
|
72
|
+
// A wrong PIN is the single most common reason a parcel is undeliverable,
|
|
73
|
+
// and it was previously any string at all.
|
|
74
|
+
postalCode: annotate(z.string().trim().regex(PIN_RE, "Enter a 6-digit PIN code."), {
|
|
75
|
+
section: "address", order: 9, row: "pair",
|
|
76
|
+
}),
|
|
77
|
+
country: annotate(z.string().trim().min(2).max(60), {
|
|
78
|
+
section: "address", order: 10, row: "pair",
|
|
79
|
+
}),
|
|
80
|
+
isDefault: annotate(z.boolean().optional(), {
|
|
81
|
+
section: "address", order: 11, row: "quarter",
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
/**
|
|
85
|
+
* An admin changing a store's standing.
|
|
86
|
+
*
|
|
87
|
+
* `suspensionReason` is required when — and only when — the status is
|
|
88
|
+
* `suspended`: suspending a seller without a recorded reason leaves them, and
|
|
89
|
+
* the next admin, with nothing to appeal or review.
|
|
90
|
+
*/
|
|
91
|
+
export const adminStoreUpdateSchema = z
|
|
92
|
+
.object({
|
|
93
|
+
storeStatus: annotate(z.enum(["pending", "active", "suspended", "closed"]), {
|
|
94
|
+
section: "standing", sectionLabel: "Store standing", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
95
|
+
}),
|
|
96
|
+
isVerified: annotate(z.boolean().optional(), {
|
|
97
|
+
section: "standing", order: 2, row: "quarter",
|
|
98
|
+
}),
|
|
99
|
+
isFeatured: annotate(z.boolean().optional(), {
|
|
100
|
+
section: "standing", order: 3, row: "quarter",
|
|
101
|
+
}),
|
|
102
|
+
suspensionReason: annotate(z.string().trim().max(500).optional(), {
|
|
103
|
+
section: "standing", order: 4, row: "full", kind: "textarea",
|
|
104
|
+
}),
|
|
105
|
+
capabilities: annotate(z.array(z.string().min(1)).max(50).optional(), {
|
|
106
|
+
section: "capabilities", sectionLabel: "Capabilities", order: 1, row: "full", kind: "list",
|
|
107
|
+
}),
|
|
108
|
+
adminNotes: annotate(z.string().trim().max(2000).optional(), {
|
|
109
|
+
section: "capabilities", order: 2, row: "full", kind: "textarea",
|
|
110
|
+
help: "Internal only — never shown to the store owner or the public.",
|
|
111
|
+
}),
|
|
112
|
+
})
|
|
113
|
+
.superRefine((v, ctx) => {
|
|
114
|
+
if (v.storeStatus === "suspended" && !v.suspensionReason?.trim()) {
|
|
115
|
+
ctx.addIssue({
|
|
116
|
+
code: z.ZodIssueCode.custom,
|
|
117
|
+
path: ["suspensionReason"],
|
|
118
|
+
message: "Record why this store is being suspended.",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
/**
|
|
123
|
+
* The homepage announcement bar.
|
|
124
|
+
*
|
|
125
|
+
* 🛑 `message` is the canonical key. The main admin editor saved this copy
|
|
126
|
+
* under `text` until 2026-08-24 — a key no renderer has ever read — so
|
|
127
|
+
* admin-authored announcements saved successfully and never appeared. Naming
|
|
128
|
+
* it here is what stops that recurring.
|
|
129
|
+
*/
|
|
130
|
+
export const announcementBarSchema = z.object({
|
|
131
|
+
message: annotate(z.string().trim().max(300, "Keep the announcement under 300 characters.").optional(), {
|
|
132
|
+
section: "announcement", sectionLabel: "Announcement bar", sectionRequired: true,
|
|
133
|
+
quick: true, order: 1, row: "full",
|
|
134
|
+
}),
|
|
135
|
+
enabled: annotate(z.boolean().optional(), {
|
|
136
|
+
section: "announcement", order: 2, row: "quarter",
|
|
137
|
+
}),
|
|
138
|
+
link: annotate(z.string().trim().max(300).optional(), {
|
|
139
|
+
section: "announcement", order: 3, row: "full",
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const adminOrderUpdateSchema: z.ZodObject<{
|
|
3
|
+
status: z.ZodEnum<[string, ...string[]]>;
|
|
4
|
+
trackingNumber: z.ZodOptional<z.ZodString>;
|
|
5
|
+
carrier: z.ZodOptional<z.ZodString>;
|
|
6
|
+
refundAmount: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
status: string;
|
|
10
|
+
notes?: string | undefined;
|
|
11
|
+
trackingNumber?: string | undefined;
|
|
12
|
+
refundAmount?: number | undefined;
|
|
13
|
+
carrier?: string | undefined;
|
|
14
|
+
}, {
|
|
15
|
+
status: string;
|
|
16
|
+
notes?: string | undefined;
|
|
17
|
+
trackingNumber?: string | undefined;
|
|
18
|
+
refundAmount?: number | undefined;
|
|
19
|
+
carrier?: string | undefined;
|
|
20
|
+
}>;
|
|
21
|
+
export type AdminOrderUpdateValues = z.infer<typeof adminOrderUpdateSchema>;
|
|
22
|
+
/** Inviting a team member — an email plus the permissions it grants. */
|
|
23
|
+
export declare const employeeInviteSchema: z.ZodObject<{
|
|
24
|
+
email: z.ZodString;
|
|
25
|
+
permissionGroup: z.ZodString;
|
|
26
|
+
permissions: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
27
|
+
}, "strip", z.ZodTypeAny, {
|
|
28
|
+
email: string;
|
|
29
|
+
permissionGroup: string;
|
|
30
|
+
permissions?: string[] | undefined;
|
|
31
|
+
}, {
|
|
32
|
+
email: string;
|
|
33
|
+
permissionGroup: string;
|
|
34
|
+
permissions?: string[] | undefined;
|
|
35
|
+
}>;
|
|
36
|
+
export type EmployeeInviteValues = z.infer<typeof employeeInviteSchema>;
|
|
37
|
+
/**
|
|
38
|
+
* The reason recorded against a hard ban.
|
|
39
|
+
*
|
|
40
|
+
* A hard ban runs an eight-stage destructive cascade; this string is the only
|
|
41
|
+
* durable record of why. A 10-character floor, matching the admin offer-cancel
|
|
42
|
+
* reason, because "spam" is not an audit trail.
|
|
43
|
+
*/
|
|
44
|
+
export declare const hardBanReasonSchema: z.ZodObject<{
|
|
45
|
+
reason: z.ZodString;
|
|
46
|
+
}, "strip", z.ZodTypeAny, {
|
|
47
|
+
reason: string;
|
|
48
|
+
}, {
|
|
49
|
+
reason: string;
|
|
50
|
+
}>;
|
|
51
|
+
export type HardBanReasonValues = z.infer<typeof hardBanReasonSchema>;
|
|
52
|
+
/**
|
|
53
|
+
* Feature flags.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately open records: flags are added and removed constantly, and a
|
|
56
|
+
* closed schema here would silently STRIP a newly-added flag on the next save
|
|
57
|
+
* — the `productBaseSchema` failure applied to configuration. The values are
|
|
58
|
+
* still constrained, which is what stops a flag being set to an object.
|
|
59
|
+
*/
|
|
60
|
+
export declare const featureFlagsUpdateSchema: z.ZodObject<{
|
|
61
|
+
flags: z.ZodRecord<z.ZodString, z.ZodBoolean>;
|
|
62
|
+
rollouts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
63
|
+
}, "strip", z.ZodTypeAny, {
|
|
64
|
+
flags: Record<string, boolean>;
|
|
65
|
+
rollouts?: Record<string, number> | undefined;
|
|
66
|
+
}, {
|
|
67
|
+
flags: Record<string, boolean>;
|
|
68
|
+
rollouts?: Record<string, number> | undefined;
|
|
69
|
+
}>;
|
|
70
|
+
export type FeatureFlagsUpdateValues = z.infer<typeof featureFlagsUpdateSchema>;
|
|
71
|
+
/** An ad placement. `priority` orders competing ads in the same slot. */
|
|
72
|
+
export declare const adminAdFormSchema: z.ZodEffects<z.ZodObject<{
|
|
73
|
+
name: z.ZodString;
|
|
74
|
+
provider: z.ZodEnum<["manual", "adsense", "thirdParty"]>;
|
|
75
|
+
status: z.ZodEnum<["draft", "active", "scheduled", "paused"]>;
|
|
76
|
+
priority: z.ZodNumber;
|
|
77
|
+
placementIds: z.ZodArray<z.ZodString, "many">;
|
|
78
|
+
requiresConsent: z.ZodBoolean;
|
|
79
|
+
startAt: z.ZodOptional<z.ZodString>;
|
|
80
|
+
endAt: z.ZodOptional<z.ZodString>;
|
|
81
|
+
creative: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<import("../../..").FirestoreValue, z.ZodTypeDef, import("../../..").FirestoreValue>>>;
|
|
82
|
+
}, "strip", z.ZodTypeAny, {
|
|
83
|
+
status: "draft" | "active" | "scheduled" | "paused";
|
|
84
|
+
name: string;
|
|
85
|
+
priority: number;
|
|
86
|
+
provider: "manual" | "adsense" | "thirdParty";
|
|
87
|
+
placementIds: string[];
|
|
88
|
+
requiresConsent: boolean;
|
|
89
|
+
startAt?: string | undefined;
|
|
90
|
+
endAt?: string | undefined;
|
|
91
|
+
creative?: Record<string, import("../../..").FirestoreValue> | undefined;
|
|
92
|
+
}, {
|
|
93
|
+
status: "draft" | "active" | "scheduled" | "paused";
|
|
94
|
+
name: string;
|
|
95
|
+
priority: number;
|
|
96
|
+
provider: "manual" | "adsense" | "thirdParty";
|
|
97
|
+
placementIds: string[];
|
|
98
|
+
requiresConsent: boolean;
|
|
99
|
+
startAt?: string | undefined;
|
|
100
|
+
endAt?: string | undefined;
|
|
101
|
+
creative?: Record<string, import("../../..").FirestoreValue> | undefined;
|
|
102
|
+
}>, {
|
|
103
|
+
status: "draft" | "active" | "scheduled" | "paused";
|
|
104
|
+
name: string;
|
|
105
|
+
priority: number;
|
|
106
|
+
provider: "manual" | "adsense" | "thirdParty";
|
|
107
|
+
placementIds: string[];
|
|
108
|
+
requiresConsent: boolean;
|
|
109
|
+
startAt?: string | undefined;
|
|
110
|
+
endAt?: string | undefined;
|
|
111
|
+
creative?: Record<string, import("../../..").FirestoreValue> | undefined;
|
|
112
|
+
}, {
|
|
113
|
+
status: "draft" | "active" | "scheduled" | "paused";
|
|
114
|
+
name: string;
|
|
115
|
+
priority: number;
|
|
116
|
+
provider: "manual" | "adsense" | "thirdParty";
|
|
117
|
+
placementIds: string[];
|
|
118
|
+
requiresConsent: boolean;
|
|
119
|
+
startAt?: string | undefined;
|
|
120
|
+
endAt?: string | undefined;
|
|
121
|
+
creative?: Record<string, import("../../..").FirestoreValue> | undefined;
|
|
122
|
+
}>;
|
|
123
|
+
export type AdminAdFormValues = z.infer<typeof adminAdFormSchema>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Four more admin editors rendered `<Form>` with no schema. Each writes
|
|
3
|
+
* something an admin cannot easily undo.
|
|
4
|
+
*
|
|
5
|
+
* `AdminOrderEditorView` is the one that touches money: `refundAmount`
|
|
6
|
+
* was parsed with a bare `parseFloat` and guarded only by
|
|
7
|
+
* `!isNaN(amount) && amount > 0`, with no upper bound and no check
|
|
8
|
+
* against the order — so a mistyped figure went straight through as a
|
|
9
|
+
* refund instruction.
|
|
10
|
+
*
|
|
11
|
+
* `AdminEmployeeEditorView` grants permissions, and `AdminUsersView`'s
|
|
12
|
+
* hard-ban takes a reason that is the entire record of why an account
|
|
13
|
+
* was destroyed.
|
|
14
|
+
*
|
|
15
|
+
* WHAT: One schema per editor.
|
|
16
|
+
*
|
|
17
|
+
* EXPORTS:
|
|
18
|
+
* adminOrderUpdateSchema, employeeInviteSchema, hardBanReasonSchema,
|
|
19
|
+
* featureFlagsUpdateSchema, adminAdFormSchema, and their value types
|
|
20
|
+
*
|
|
21
|
+
* @tag domain:admin
|
|
22
|
+
* @tag layer:schema
|
|
23
|
+
* @tag pattern:none
|
|
24
|
+
* @tag access:isomorphic
|
|
25
|
+
* @tag consumers:AdminOrderEditorView,AdminEmployeeEditorView,AdminUsersView,AdminFeatureFlagsView,AdminAdEditorView
|
|
26
|
+
* @tag sideEffects:none
|
|
27
|
+
*/
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
30
|
+
import { firestoreValueSchema } from "../../../schemas/firestore-value";
|
|
31
|
+
import { OrderStatusValues } from "../../orders/schemas/firestore";
|
|
32
|
+
/** Derived from the real 9-value union, never restated (Root Cause #36). */
|
|
33
|
+
const orderStatusSchema = z.enum(Object.values(OrderStatusValues));
|
|
34
|
+
export const adminOrderUpdateSchema = z.object({
|
|
35
|
+
status: annotate(orderStatusSchema, {
|
|
36
|
+
section: "status", sectionLabel: "Order", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
37
|
+
}),
|
|
38
|
+
trackingNumber: annotate(z.string().trim().max(120).optional(), {
|
|
39
|
+
section: "shipping", sectionLabel: "Shipping", order: 1, row: "pair",
|
|
40
|
+
}),
|
|
41
|
+
carrier: annotate(z.string().trim().max(80).optional(), {
|
|
42
|
+
section: "shipping", order: 2, row: "pair",
|
|
43
|
+
}),
|
|
44
|
+
/*
|
|
45
|
+
* 🛑 Bounded. This was `parseFloat` + `> 0` with no ceiling, so a slipped
|
|
46
|
+
* decimal point issued a refund an order of magnitude too large. The cap is
|
|
47
|
+
* a sanity bound, not a business rule — the route still checks the amount
|
|
48
|
+
* against the order itself.
|
|
49
|
+
*/
|
|
50
|
+
refundAmount: annotate(z.coerce
|
|
51
|
+
.number({ invalid_type_error: "Enter the refund amount as a number." })
|
|
52
|
+
.positive("A refund must be greater than zero.")
|
|
53
|
+
.max(10000000, "That looks like a typo — check the decimal point.")
|
|
54
|
+
.optional(), { section: "shipping", order: 3, row: "pair", kind: "number" }),
|
|
55
|
+
notes: annotate(z.string().trim().max(2000).optional(), {
|
|
56
|
+
section: "shipping", order: 4, row: "full", kind: "textarea",
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
/** Inviting a team member — an email plus the permissions it grants. */
|
|
60
|
+
export const employeeInviteSchema = z.object({
|
|
61
|
+
email: annotate(z.string().trim().toLowerCase().min(1, "An email address is required.").email("That doesn't look like an email address."), { section: "member", sectionLabel: "Team member", sectionRequired: true, quick: true, order: 1, row: "pair" }),
|
|
62
|
+
permissionGroup: annotate(z.string().trim().min(1, "Choose a permission group."), {
|
|
63
|
+
section: "member", quick: true, order: 2, row: "pair",
|
|
64
|
+
}),
|
|
65
|
+
permissions: annotate(z.array(z.string().min(1)).max(200).optional(), {
|
|
66
|
+
section: "member", order: 3, row: "full", kind: "list",
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* The reason recorded against a hard ban.
|
|
71
|
+
*
|
|
72
|
+
* A hard ban runs an eight-stage destructive cascade; this string is the only
|
|
73
|
+
* durable record of why. A 10-character floor, matching the admin offer-cancel
|
|
74
|
+
* reason, because "spam" is not an audit trail.
|
|
75
|
+
*/
|
|
76
|
+
export const hardBanReasonSchema = z.object({
|
|
77
|
+
reason: annotate(z.string().trim().min(10, "Record why this account is being banned — at least 10 characters.").max(500), { section: "ban", sectionLabel: "Reason", sectionRequired: true, quick: true, order: 1, row: "full", kind: "textarea" }),
|
|
78
|
+
});
|
|
79
|
+
/**
|
|
80
|
+
* Feature flags.
|
|
81
|
+
*
|
|
82
|
+
* Deliberately open records: flags are added and removed constantly, and a
|
|
83
|
+
* closed schema here would silently STRIP a newly-added flag on the next save
|
|
84
|
+
* — the `productBaseSchema` failure applied to configuration. The values are
|
|
85
|
+
* still constrained, which is what stops a flag being set to an object.
|
|
86
|
+
*/
|
|
87
|
+
export const featureFlagsUpdateSchema = z.object({
|
|
88
|
+
flags: annotate(z.record(z.string(), z.boolean()), {
|
|
89
|
+
section: "flags", sectionLabel: "Feature flags", sectionRequired: true, order: 1, row: "full", kind: "list",
|
|
90
|
+
}),
|
|
91
|
+
rollouts: annotate(z.record(z.string(), z.coerce.number().min(0).max(100)).optional(), {
|
|
92
|
+
section: "flags", order: 2, row: "full", kind: "list",
|
|
93
|
+
help: "Percentage rollout per flag, 0–100.",
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
/** An ad placement. `priority` orders competing ads in the same slot. */
|
|
97
|
+
export const adminAdFormSchema = z
|
|
98
|
+
.object({
|
|
99
|
+
name: annotate(z.string().trim().min(2, "Give the ad a name.").max(120), {
|
|
100
|
+
section: "basics", sectionLabel: "Ad", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
101
|
+
}),
|
|
102
|
+
provider: annotate(z.enum(["manual", "adsense", "thirdParty"]), {
|
|
103
|
+
section: "basics", quick: true, order: 2, row: "pair",
|
|
104
|
+
}),
|
|
105
|
+
status: annotate(z.enum(["draft", "active", "scheduled", "paused"]), {
|
|
106
|
+
section: "basics", quick: true, order: 3, row: "pair",
|
|
107
|
+
}),
|
|
108
|
+
priority: annotate(z.coerce.number().int().min(0).max(1000), {
|
|
109
|
+
section: "basics", order: 4, row: "pair", kind: "number",
|
|
110
|
+
}),
|
|
111
|
+
placementIds: annotate(z.array(z.string().min(1)).min(1, "Choose at least one placement."), {
|
|
112
|
+
section: "placement", sectionLabel: "Placement", sectionRequired: true, order: 1, row: "full", kind: "list",
|
|
113
|
+
}),
|
|
114
|
+
requiresConsent: annotate(z.boolean(), {
|
|
115
|
+
section: "placement", order: 2, row: "quarter",
|
|
116
|
+
}),
|
|
117
|
+
startAt: annotate(z.string().optional(), {
|
|
118
|
+
section: "schedule", sectionLabel: "Schedule", order: 1, row: "pair", kind: "date",
|
|
119
|
+
}),
|
|
120
|
+
endAt: annotate(z.string().optional(), {
|
|
121
|
+
section: "schedule", order: 2, row: "pair", kind: "date",
|
|
122
|
+
}),
|
|
123
|
+
// Creative shape varies by provider, so it stays open — an AdSense slot and
|
|
124
|
+
// a manual banner share almost no fields. Open to a Firestore VALUE tree,
|
|
125
|
+
// not to anything: `z.unknown()` would pass a function straight to the
|
|
126
|
+
// driver, which then throws at write time rather than at parse time.
|
|
127
|
+
creative: annotate(z.record(firestoreValueSchema).optional(), {
|
|
128
|
+
section: "creative", sectionLabel: "Creative", order: 1, row: "full", kind: "list",
|
|
129
|
+
}),
|
|
130
|
+
})
|
|
131
|
+
.superRefine((v, ctx) => {
|
|
132
|
+
// A scheduled ad with no start date never runs, and nothing would say so.
|
|
133
|
+
if (v.status === "scheduled" && !v.startAt) {
|
|
134
|
+
ctx.addIssue({
|
|
135
|
+
code: z.ZodIssueCode.custom,
|
|
136
|
+
path: ["startAt"],
|
|
137
|
+
message: "A scheduled ad needs a start date.",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (v.startAt && v.endAt && v.endAt <= v.startAt) {
|
|
141
|
+
ctx.addIssue({
|
|
142
|
+
code: z.ZodIssueCode.custom,
|
|
143
|
+
path: ["endAt"],
|
|
144
|
+
message: "The end date must be after the start date.",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Newsletter subscription — one field, shared by all three boxes (the homepage
|
|
4
|
+
* banner, the homepage inline form, and the footer slot).
|
|
5
|
+
*
|
|
6
|
+
* Trimmed and lowercased before validation so ` Foo@Bar.com ` and
|
|
7
|
+
* `foo@bar.com` cannot become two subscriptions for one person.
|
|
8
|
+
*/
|
|
9
|
+
export declare const newsletterSubscribeSchema: z.ZodObject<{
|
|
10
|
+
email: z.ZodString;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
email: string;
|
|
13
|
+
}, {
|
|
14
|
+
email: string;
|
|
15
|
+
}>;
|
|
16
|
+
export type NewsletterSubscribeValues = z.infer<typeof newsletterSubscribeSchema>;
|
|
17
|
+
/**
|
|
18
|
+
* The quick-create drawer shared in shape by brands and categories.
|
|
19
|
+
*
|
|
20
|
+
* Both drawers post `{ name, description, isActive }` to their own endpoint
|
|
21
|
+
* and differ in nothing else, so one schema serves both. `slug` is absent
|
|
22
|
+
* deliberately — it is derived from the name on the server, and this codebase
|
|
23
|
+
* treats a slug as create-only and non-editable everywhere else.
|
|
24
|
+
*/
|
|
25
|
+
export declare const quickCreateTaxonomySchema: z.ZodObject<{
|
|
26
|
+
name: z.ZodString;
|
|
27
|
+
description: z.ZodOptional<z.ZodString>;
|
|
28
|
+
isActive: z.ZodOptional<z.ZodBoolean>;
|
|
29
|
+
}, "strip", z.ZodTypeAny, {
|
|
30
|
+
name: string;
|
|
31
|
+
description?: string | undefined;
|
|
32
|
+
isActive?: boolean | undefined;
|
|
33
|
+
}, {
|
|
34
|
+
name: string;
|
|
35
|
+
description?: string | undefined;
|
|
36
|
+
isActive?: boolean | undefined;
|
|
37
|
+
}>;
|
|
38
|
+
export type QuickCreateTaxonomyValues = z.infer<typeof quickCreateTaxonomySchema>;
|
|
39
|
+
/**
|
|
40
|
+
* Marking a payout paid.
|
|
41
|
+
*
|
|
42
|
+
* `transactionId` is REQUIRED, which it was not before. This is the record
|
|
43
|
+
* that money left the building — an empty one leaves a payout marked paid with
|
|
44
|
+
* nothing to reconcile it against, and no way to answer a seller asking where
|
|
45
|
+
* their money went.
|
|
46
|
+
*/
|
|
47
|
+
export declare const payoutMarkPaidSchema: z.ZodObject<{
|
|
48
|
+
transactionId: z.ZodString;
|
|
49
|
+
}, "strip", z.ZodTypeAny, {
|
|
50
|
+
transactionId: string;
|
|
51
|
+
}, {
|
|
52
|
+
transactionId: string;
|
|
53
|
+
}>;
|
|
54
|
+
export type PayoutMarkPaidValues = z.infer<typeof payoutMarkPaidSchema>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: A cluster of small forms — three newsletter boxes, two quick-create
|
|
3
|
+
* drawers, and the payout mark-paid modal — each rendered `<Form>` with
|
|
4
|
+
* no schema. Individually each is two or three fields; together they are
|
|
5
|
+
* six surfaces where the only validation was whatever the server said
|
|
6
|
+
* after the round-trip, reported as a toast.
|
|
7
|
+
*
|
|
8
|
+
* The newsletter trio matters most despite being one field: an email box
|
|
9
|
+
* that accepts anything sends a subscription request for `"asdf"` and
|
|
10
|
+
* shows a success message.
|
|
11
|
+
*
|
|
12
|
+
* WHAT: One module for the small shared shapes, rather than six schema files
|
|
13
|
+
* of three lines each.
|
|
14
|
+
*
|
|
15
|
+
* ## Why these live together and the bigger ones do not
|
|
16
|
+
*
|
|
17
|
+
* These have no per-entity semantics worth a file of their own — an email is
|
|
18
|
+
* an email. An entity with real rules (a payout method's per-type bank
|
|
19
|
+
* requirements, a listing template's defaults tree) keeps its own module,
|
|
20
|
+
* because that is where the reasoning belongs. Grouping by *size* rather than
|
|
21
|
+
* by domain is the exception here, and it stops at the point where a schema
|
|
22
|
+
* needs a paragraph to explain itself.
|
|
23
|
+
*
|
|
24
|
+
* EXPORTS:
|
|
25
|
+
* newsletterSubscribeSchema, quickCreateTaxonomySchema,
|
|
26
|
+
* payoutMarkPaidSchema, and their inferred value types
|
|
27
|
+
*
|
|
28
|
+
* @tag domain:admin,homepage,payments
|
|
29
|
+
* @tag layer:schema
|
|
30
|
+
* @tag pattern:none
|
|
31
|
+
* @tag access:isomorphic
|
|
32
|
+
* @tag consumers:newsletter forms,quick-create drawers,AdminPayoutMarkPaidModal
|
|
33
|
+
* @tag sideEffects:none
|
|
34
|
+
*/
|
|
35
|
+
import { z } from "zod";
|
|
36
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
37
|
+
/**
|
|
38
|
+
* Newsletter subscription — one field, shared by all three boxes (the homepage
|
|
39
|
+
* banner, the homepage inline form, and the footer slot).
|
|
40
|
+
*
|
|
41
|
+
* Trimmed and lowercased before validation so ` Foo@Bar.com ` and
|
|
42
|
+
* `foo@bar.com` cannot become two subscriptions for one person.
|
|
43
|
+
*/
|
|
44
|
+
export const newsletterSubscribeSchema = z.object({
|
|
45
|
+
email: annotate(z
|
|
46
|
+
.string()
|
|
47
|
+
.trim()
|
|
48
|
+
.toLowerCase()
|
|
49
|
+
.min(1, "Enter your email address.")
|
|
50
|
+
.email("That doesn't look like an email address."), { section: "subscribe", sectionLabel: "Newsletter", sectionRequired: true, quick: true, order: 1, row: "full" }),
|
|
51
|
+
});
|
|
52
|
+
/**
|
|
53
|
+
* The quick-create drawer shared in shape by brands and categories.
|
|
54
|
+
*
|
|
55
|
+
* Both drawers post `{ name, description, isActive }` to their own endpoint
|
|
56
|
+
* and differ in nothing else, so one schema serves both. `slug` is absent
|
|
57
|
+
* deliberately — it is derived from the name on the server, and this codebase
|
|
58
|
+
* treats a slug as create-only and non-editable everywhere else.
|
|
59
|
+
*/
|
|
60
|
+
export const quickCreateTaxonomySchema = z.object({
|
|
61
|
+
name: annotate(z.string().trim().min(1, "A name is required.").max(80, "Keep the name under 80 characters."), {
|
|
62
|
+
section: "basics", sectionLabel: "Details", sectionRequired: true, quick: true, order: 1, row: "full",
|
|
63
|
+
}),
|
|
64
|
+
description: annotate(z.string().max(500, "Keep the description under 500 characters.").optional(), {
|
|
65
|
+
section: "basics", order: 2, row: "full", kind: "textarea",
|
|
66
|
+
}),
|
|
67
|
+
isActive: annotate(z.boolean().optional(), {
|
|
68
|
+
section: "basics", order: 3, row: "quarter",
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
/**
|
|
72
|
+
* Marking a payout paid.
|
|
73
|
+
*
|
|
74
|
+
* `transactionId` is REQUIRED, which it was not before. This is the record
|
|
75
|
+
* that money left the building — an empty one leaves a payout marked paid with
|
|
76
|
+
* nothing to reconcile it against, and no way to answer a seller asking where
|
|
77
|
+
* their money went.
|
|
78
|
+
*/
|
|
79
|
+
export const payoutMarkPaidSchema = z.object({
|
|
80
|
+
transactionId: annotate(z
|
|
81
|
+
.string()
|
|
82
|
+
.trim()
|
|
83
|
+
.min(3, "Enter the bank or UPI reference for this transfer.")
|
|
84
|
+
.max(120, "Keep the reference under 120 characters."), { section: "payment", sectionLabel: "Payment", sectionRequired: true, quick: true, order: 1, row: "full" }),
|
|
85
|
+
});
|
|
@@ -7,6 +7,7 @@ import { Form } from "../../../ui/components/Form";
|
|
|
7
7
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
8
8
|
import { applyZodIssues } from "../../../ui/forms/FormShell";
|
|
9
9
|
import { forgotPasswordSchema } from "../schemas";
|
|
10
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
10
11
|
export function ForgotPasswordView({ onSubmit, isLoading = false, error, success, labels = {}, renderBackLink, className = "", }) {
|
|
11
12
|
const [email, setEmail] = useState("");
|
|
12
13
|
const { showToast } = useToast();
|
|
@@ -26,7 +27,7 @@ export function ForgotPasswordView({ onSubmit, isLoading = false, error, success
|
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
return (_jsx(Row, { className: `min-h-[60vh] ${className}`, align: "center", justify: "center", padding: "x-md", children: _jsxs(Stack, { className: "max-w-md w-full", gap: "lg", children: [_jsxs(Div, { className: "text-center", children: [_jsx(Heading, { level: 1, className: "font-extrabold", size: "3xl", children: labels.title ?? "Forgot Password" }), _jsx(Text, { className: "mt-2 text-[var(--appkit-color-text-muted)]", size: "sm", children: labels.description ??
|
|
29
|
-
"Enter your email and we'll send you a reset link." })] }), error && (_jsx(Alert, { variant: "error", compact: true, children: error })), success && (_jsx(Alert, { variant: "success", compact: true, children: success })), !success && (_jsx(Form, { noValidate: true, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FieldInput, { name: "email", label: labels.emailLabel ?? "Email address", type: "email", autoComplete: "username", required: true, placeholder: labels.emailPlaceholder ?? "you@example.com", value: email, onChange: setEmail }), _jsx(Button, { type: "submit", isLoading: isLoading, disabled: isLoading, className: "w-full", onClick: buildClickHandler(setFieldError, clearErrors), children: isLoading
|
|
30
|
+
"Enter your email and we'll send you a reset link." })] }), error && (_jsx(Alert, { variant: "error", compact: true, children: error })), success && (_jsx(Alert, { variant: "success", compact: true, children: success })), !success && (_jsx(Form, { schema: forgotPasswordSchema, noValidate: true, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FormErrorSummary, {}), _jsx(FieldInput, { name: "email", label: labels.emailLabel ?? "Email address", type: "email", autoComplete: "username", required: true, placeholder: labels.emailPlaceholder ?? "you@example.com", value: email, onChange: setEmail }), _jsx(Button, { type: "submit", isLoading: isLoading, disabled: isLoading, className: "w-full", onClick: buildClickHandler(setFieldError, clearErrors), children: isLoading
|
|
30
31
|
? (labels.submittingLabel ?? "Sending…")
|
|
31
32
|
: (labels.submitLabel ?? "Send reset link") })] })) })), renderBackLink && (_jsx(Div, { textSize: "sm", className: "text-center", children: renderBackLink() }))] }) }));
|
|
32
33
|
}
|
|
@@ -9,6 +9,7 @@ import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox";
|
|
|
9
9
|
import { applyZodIssues } from "../../../ui/forms/FormShell";
|
|
10
10
|
import { loginSchema } from "../schemas";
|
|
11
11
|
import { SocialAuthButtons } from "./SocialAuthButtons";
|
|
12
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
12
13
|
export function LoginForm({ onSubmit, onGoogleLogin, isLoading = false, error, labels = {}, renderSocialButtons, renderCreateAccountLink, renderForgotPasswordLink, className = "", }) {
|
|
13
14
|
const { showToast } = useToast();
|
|
14
15
|
const [values, setValues] = useState({
|
|
@@ -16,7 +17,7 @@ export function LoginForm({ onSubmit, onGoogleLogin, isLoading = false, error, l
|
|
|
16
17
|
password: "",
|
|
17
18
|
rememberMe: false,
|
|
18
19
|
});
|
|
19
|
-
return (_jsx(Row, { className: `min-h-[60vh] ${className}`, align: "center", justify: "center", padding: "x-md", children: _jsxs(Stack, { className: "max-w-md w-full", gap: "lg", children: [_jsxs(Div, { className: "text-center", children: [_jsx(Row, { justify: "center", className: "mb-3", children: _jsx(SiteMark, { size: "lg" }) }), _jsx(Heading, { level: 1, className: "font-extrabold", size: "3xl", children: labels.title ?? "Sign In" }), (labels.subtitle || renderCreateAccountLink) && (_jsxs(Text, { className: "mt-2", color: "muted", size: "sm", children: [labels.subtitle ?? "Or", " ", renderCreateAccountLink?.()] }))] }), error && (_jsx(Alert, { variant: "error", compact: true, children: error })), _jsx(Form, { noValidate: true, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FieldInput, { name: "email", label: labels.emailLabel ?? "Email address", type: "email", autoComplete: "username", required: true, placeholder: labels.emailPlaceholder ?? "you@example.com", value: values.email, onChange: (v) => setValues({ ...values, email: v }) }), _jsx(FieldInput, { name: "password", label: labels.passwordLabel ?? "Password", type: "password", autoComplete: "current-password", required: true, placeholder: labels.passwordPlaceholder ?? "••••••••", value: values.password, onChange: (v) => setValues({ ...values, password: v }) }), _jsxs(Row, { justify: "between", children: [_jsx(FieldCheckbox, { name: "rememberMe", label: labels.rememberMe ?? "Remember me", checked: values.rememberMe ?? false, onChange: (c) => setValues({ ...values, rememberMe: c }) }), renderForgotPasswordLink?.()] }), _jsx(Button, { type: "submit", isLoading: isLoading, disabled: isLoading, className: "w-full", onClick: async () => {
|
|
20
|
+
return (_jsx(Row, { className: `min-h-[60vh] ${className}`, align: "center", justify: "center", padding: "x-md", children: _jsxs(Stack, { className: "max-w-md w-full", gap: "lg", children: [_jsxs(Div, { className: "text-center", children: [_jsx(Row, { justify: "center", className: "mb-3", children: _jsx(SiteMark, { size: "lg" }) }), _jsx(Heading, { level: 1, className: "font-extrabold", size: "3xl", children: labels.title ?? "Sign In" }), (labels.subtitle || renderCreateAccountLink) && (_jsxs(Text, { className: "mt-2", color: "muted", size: "sm", children: [labels.subtitle ?? "Or", " ", renderCreateAccountLink?.()] }))] }), error && (_jsx(Alert, { variant: "error", compact: true, children: error })), _jsx(Form, { schema: loginSchema, noValidate: true, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FormErrorSummary, {}), _jsx(FieldInput, { name: "email", label: labels.emailLabel ?? "Email address", type: "email", autoComplete: "username", required: true, placeholder: labels.emailPlaceholder ?? "you@example.com", value: values.email, onChange: (v) => setValues({ ...values, email: v }) }), _jsx(FieldInput, { name: "password", label: labels.passwordLabel ?? "Password", type: "password", autoComplete: "current-password", required: true, placeholder: labels.passwordPlaceholder ?? "••••••••", value: values.password, onChange: (v) => setValues({ ...values, password: v }) }), _jsxs(Row, { justify: "between", children: [_jsx(FieldCheckbox, { name: "rememberMe", label: labels.rememberMe ?? "Remember me", checked: values.rememberMe ?? false, onChange: (c) => setValues({ ...values, rememberMe: c }) }), renderForgotPasswordLink?.()] }), _jsx(Button, { type: "submit", isLoading: isLoading, disabled: isLoading, className: "w-full", onClick: async () => {
|
|
20
21
|
clearErrors();
|
|
21
22
|
const parsed = loginSchema.safeParse(values);
|
|
22
23
|
if (!parsed.success)
|
|
@@ -9,6 +9,7 @@ import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
|
9
9
|
import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox";
|
|
10
10
|
import { applyZodIssues } from "../../../ui/forms/FormShell";
|
|
11
11
|
import { registerSchema, registerPasswordSchema } from "../schemas";
|
|
12
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
12
13
|
const registerClientSchema = registerSchema.extend({
|
|
13
14
|
displayName: z.string().min(1, "Enter your name"),
|
|
14
15
|
confirmPassword: registerPasswordSchema,
|
|
@@ -28,7 +29,7 @@ export function RegisterForm({ onSubmit, isLoading = false, error, success, labe
|
|
|
28
29
|
displayName: "",
|
|
29
30
|
acceptTerms: false,
|
|
30
31
|
});
|
|
31
|
-
return (_jsx(Row, { className: `min-h-[60vh] ${className}`, align: "center", justify: "center", padding: "x-md", children: _jsxs(Stack, { className: "max-w-md w-full", gap: "lg", children: [_jsxs(Div, { className: "text-center", children: [_jsx(Row, { justify: "center", className: "mb-3", children: _jsx(SiteMark, { size: "lg" }) }), _jsx(Heading, { level: 1, className: "font-extrabold", size: "3xl", children: labels.title ?? "Create Account" }), (labels.subtitle || renderLoginLink) && (_jsxs(Text, { className: "mt-2 text-[var(--appkit-color-text-muted)]", size: "sm", children: [labels.subtitle ?? "Already have an account?", " ", renderLoginLink?.()] }))] }), success && (_jsx(Alert, { variant: "success", compact: true, children: success })), error && (_jsx(Alert, { variant: "error", compact: true, children: error })), _jsx(Form, { noValidate: true, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FieldInput, { name: "displayName", label: labels.displayNameLabel ?? "Full name", type: "text", autoComplete: "name", required: true, placeholder: labels.displayNamePlaceholder ?? "Your name", value: values.displayName, onChange: (v) => setValues({ ...values, displayName: v }) }), _jsx(FieldInput, { name: "email", label: labels.emailLabel ?? "Email address", type: "email", autoComplete: "username", required: true, placeholder: labels.emailPlaceholder ?? "you@example.com", value: values.email, onChange: (v) => setValues({ ...values, email: v }) }), _jsxs(Div, { children: [_jsx(FieldInput, { name: "password", label: labels.passwordLabel ?? "Password", type: "password", autoComplete: "new-password", required: true, placeholder: labels.passwordPlaceholder ?? "••••••••", value: values.password, onChange: (v) => setValues({ ...values, password: v }) }), renderPasswordStrength?.(values.password)] }), _jsx(FieldInput, { name: "confirmPassword", label: labels.confirmPasswordLabel ?? "Confirm password", type: "password", autoComplete: "new-password", required: true, placeholder: labels.confirmPasswordPlaceholder ?? "••••••••", value: values.confirmPassword, onChange: (v) => setValues({ ...values, confirmPassword: v }) }), _jsx(FieldCheckbox, { name: "acceptTerms", label: `${labels.acceptTermsLabel ?? "I accept the terms"}`, checked: values.acceptTerms, onChange: (c) => setValues({ ...values, acceptTerms: c }) }), renderTermsLink && (_jsx(Text, { size: "xs", variant: "secondary", children: renderTermsLink() })), _jsx(Button, { type: "submit", isLoading: isLoading, disabled: isLoading, className: "w-full", onClick: async () => {
|
|
32
|
+
return (_jsx(Row, { className: `min-h-[60vh] ${className}`, align: "center", justify: "center", padding: "x-md", children: _jsxs(Stack, { className: "max-w-md w-full", gap: "lg", children: [_jsxs(Div, { className: "text-center", children: [_jsx(Row, { justify: "center", className: "mb-3", children: _jsx(SiteMark, { size: "lg" }) }), _jsx(Heading, { level: 1, className: "font-extrabold", size: "3xl", children: labels.title ?? "Create Account" }), (labels.subtitle || renderLoginLink) && (_jsxs(Text, { className: "mt-2 text-[var(--appkit-color-text-muted)]", size: "sm", children: [labels.subtitle ?? "Already have an account?", " ", renderLoginLink?.()] }))] }), success && (_jsx(Alert, { variant: "success", compact: true, children: success })), error && (_jsx(Alert, { variant: "error", compact: true, children: error })), _jsx(Form, { schema: registerClientSchema, noValidate: true, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FormErrorSummary, {}), _jsx(FieldInput, { name: "displayName", label: labels.displayNameLabel ?? "Full name", type: "text", autoComplete: "name", required: true, placeholder: labels.displayNamePlaceholder ?? "Your name", value: values.displayName, onChange: (v) => setValues({ ...values, displayName: v }) }), _jsx(FieldInput, { name: "email", label: labels.emailLabel ?? "Email address", type: "email", autoComplete: "username", required: true, placeholder: labels.emailPlaceholder ?? "you@example.com", value: values.email, onChange: (v) => setValues({ ...values, email: v }) }), _jsxs(Div, { children: [_jsx(FieldInput, { name: "password", label: labels.passwordLabel ?? "Password", type: "password", autoComplete: "new-password", required: true, placeholder: labels.passwordPlaceholder ?? "••••••••", value: values.password, onChange: (v) => setValues({ ...values, password: v }) }), renderPasswordStrength?.(values.password)] }), _jsx(FieldInput, { name: "confirmPassword", label: labels.confirmPasswordLabel ?? "Confirm password", type: "password", autoComplete: "new-password", required: true, placeholder: labels.confirmPasswordPlaceholder ?? "••••••••", value: values.confirmPassword, onChange: (v) => setValues({ ...values, confirmPassword: v }) }), _jsx(FieldCheckbox, { name: "acceptTerms", label: `${labels.acceptTermsLabel ?? "I accept the terms"}`, checked: values.acceptTerms, onChange: (c) => setValues({ ...values, acceptTerms: c }) }), renderTermsLink && (_jsx(Text, { size: "xs", variant: "secondary", children: renderTermsLink() })), _jsx(Button, { type: "submit", isLoading: isLoading, disabled: isLoading, className: "w-full", onClick: async () => {
|
|
32
33
|
clearErrors();
|
|
33
34
|
const parsed = registerClientSchema.safeParse(values);
|
|
34
35
|
if (!parsed.success)
|
|
@@ -7,6 +7,7 @@ import { Alert, Button, Div, Heading, Row, Stack, Text, useToast } from "../../.
|
|
|
7
7
|
import { Form } from "../../../ui/components/Form";
|
|
8
8
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
9
9
|
import { applyZodIssues } from "../../../ui/forms/FormShell";
|
|
10
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
10
11
|
const resetClientSchema = z.object({
|
|
11
12
|
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
12
13
|
confirmPassword: z.string().min(6),
|
|
@@ -33,7 +34,7 @@ export function ResetPasswordView({ oobCode, onSubmit, isLoading = false, error,
|
|
|
33
34
|
}
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
|
-
return (_jsx(Row, { className: `min-h-[60vh] ${className}`, align: "center", justify: "center", padding: "x-md", children: _jsxs(Stack, { className: "max-w-md w-full", gap: "lg", children: [_jsxs(Div, { className: "text-center", children: [_jsx(Heading, { level: 1, className: "font-extrabold", size: "3xl", children: labels.title ?? "Reset Password" }), labels.description && (_jsx(Text, { className: "mt-2 text-[var(--appkit-color-text-muted)]", size: "sm", children: labels.description }))] }), error && (_jsx(Alert, { variant: "error", compact: true, children: error })), success ? (_jsxs(Alert, { variant: "success", compact: true, children: [success, renderLoginLink && _jsx(Div, { className: "mt-2", children: renderLoginLink() })] })) : (_jsx(Form, { noValidate: true, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FieldInput, { name: "password", label: labels.passwordLabel ?? "New password", type: "password", autoComplete: "new-password", required: true, placeholder: labels.passwordPlaceholder ?? "••••••••", value: password, onChange: setPassword }), _jsx(FieldInput, { name: "confirmPassword", label: labels.confirmPasswordLabel ?? "Confirm new password", type: "password", autoComplete: "new-password", required: true, placeholder: labels.confirmPasswordPlaceholder ?? "••••••••", value: confirmPassword, onChange: setConfirmPassword }), _jsx(Button, { type: "submit", variant: "primary", isLoading: isLoading, disabled: isLoading, className: "w-full", onClick: buildClickHandler(setFieldError, clearErrors), children: isLoading
|
|
37
|
+
return (_jsx(Row, { className: `min-h-[60vh] ${className}`, align: "center", justify: "center", padding: "x-md", children: _jsxs(Stack, { className: "max-w-md w-full", gap: "lg", children: [_jsxs(Div, { className: "text-center", children: [_jsx(Heading, { level: 1, className: "font-extrabold", size: "3xl", children: labels.title ?? "Reset Password" }), labels.description && (_jsx(Text, { className: "mt-2 text-[var(--appkit-color-text-muted)]", size: "sm", children: labels.description }))] }), error && (_jsx(Alert, { variant: "error", compact: true, children: error })), success ? (_jsxs(Alert, { variant: "success", compact: true, children: [success, renderLoginLink && _jsx(Div, { className: "mt-2", children: renderLoginLink() })] })) : (_jsx(Form, { schema: resetClientSchema, noValidate: true, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsx(FormErrorSummary, {}), _jsx(FieldInput, { name: "password", label: labels.passwordLabel ?? "New password", type: "password", autoComplete: "new-password", required: true, placeholder: labels.passwordPlaceholder ?? "••••••••", value: password, onChange: setPassword }), _jsx(FieldInput, { name: "confirmPassword", label: labels.confirmPasswordLabel ?? "Confirm new password", type: "password", autoComplete: "new-password", required: true, placeholder: labels.confirmPasswordPlaceholder ?? "••••••••", value: confirmPassword, onChange: setConfirmPassword }), _jsx(Button, { type: "submit", variant: "primary", isLoading: isLoading, disabled: isLoading, className: "w-full", onClick: buildClickHandler(setFieldError, clearErrors), children: isLoading
|
|
37
38
|
? (labels.submittingLabel ?? "Resetting…")
|
|
38
39
|
: (labels.submitLabel ?? "Reset password") })] })) }))] }) }));
|
|
39
40
|
}
|