@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
|
@@ -5,6 +5,8 @@ import { Button, Row, Select } from "../../../ui";
|
|
|
5
5
|
import { Form } from "../../../ui/components/Form";
|
|
6
6
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
7
7
|
import { FieldTextarea } from "../../../ui/forms/FieldTextarea";
|
|
8
|
+
import { bookConsultationSchema } from "../schemas";
|
|
9
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
8
10
|
const TIME_SLOTS = [
|
|
9
11
|
"9:00 AM",
|
|
10
12
|
"10:00 AM",
|
|
@@ -41,7 +43,7 @@ export function ConsultationForm({ onSubmit, isPending, concerns = [], }) {
|
|
|
41
43
|
e.preventDefault();
|
|
42
44
|
await onSubmit(form);
|
|
43
45
|
}
|
|
44
|
-
return (_jsxs(Form, { onSubmit: handleSubmit, spacing: "md", children: [_jsx(FieldInput, { name: "name", type: "text", placeholder: "Full name", value: form.name, onChange: (v) => handleValueChange("name", v), required: true }), _jsx(FieldInput, { name: "email", type: "email", placeholder: "Email address", value: form.email, onChange: (v) => handleValueChange("email", v), required: true }), _jsx(FieldInput, { name: "phone", type: "tel", placeholder: "Phone number", value: form.phone, onChange: (v) => handleValueChange("phone", v), required: true }), concerns.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", children: concerns.map((c) => (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => toggleConcern(c), className: `rounded-full border px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] transition ${form.concern.includes(c)
|
|
46
|
+
return (_jsxs(Form, { schema: bookConsultationSchema, onSubmit: handleSubmit, spacing: "md", children: [_jsx(FormErrorSummary, {}), _jsx(FieldInput, { name: "name", type: "text", placeholder: "Full name", value: form.name, onChange: (v) => handleValueChange("name", v), required: true }), _jsx(FieldInput, { name: "email", type: "email", placeholder: "Email address", value: form.email, onChange: (v) => handleValueChange("email", v), required: true }), _jsx(FieldInput, { name: "phone", type: "tel", placeholder: "Phone number", value: form.phone, onChange: (v) => handleValueChange("phone", v), required: true }), concerns.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", children: concerns.map((c) => (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => toggleConcern(c), className: `rounded-full border px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] transition ${form.concern.includes(c)
|
|
45
47
|
? "border-primary bg-primary text-white"
|
|
46
48
|
: "border-neutral-300 text-neutral-700 hover:border-primary"}`, children: c }, c))) })), _jsxs(Row, { gap: "3", children: [_jsx(FieldInput, { name: "preferredDate", type: "date", value: form.preferredDate, onChange: (v) => handleValueChange("preferredDate", v), required: true, className: "flex-1" }), _jsx(Select, { value: form.preferredTime, onValueChange: (value) => handleValueChange("preferredTime", value), options: TIME_SLOTS.map((t) => ({ value: t, label: t })), wrapperClassName: "flex-1" })] }), _jsx(Select, { value: form.mode, onValueChange: (value) => handleValueChange("mode", value), options: [
|
|
47
49
|
{ value: "remote", label: "Remote (Video Call)" },
|
|
@@ -60,8 +60,8 @@ export declare const consultationBookingSchema: z.ZodObject<{
|
|
|
60
60
|
export declare const bookConsultationSchema: z.ZodObject<{
|
|
61
61
|
name: z.ZodString;
|
|
62
62
|
email: z.ZodString;
|
|
63
|
-
phone: z.ZodOptional<z.ZodString>;
|
|
64
|
-
concern: z.ZodArray<z.ZodString, "many"
|
|
63
|
+
phone: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
64
|
+
concern: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
65
65
|
preferredDate: z.ZodOptional<z.ZodString>;
|
|
66
66
|
preferredTime: z.ZodOptional<z.ZodString>;
|
|
67
67
|
mode: z.ZodOptional<z.ZodEnum<["online", "in_person", "phone"]>>;
|
|
@@ -69,19 +69,19 @@ export declare const bookConsultationSchema: z.ZodObject<{
|
|
|
69
69
|
}, "strip", z.ZodTypeAny, {
|
|
70
70
|
email: string;
|
|
71
71
|
name: string;
|
|
72
|
-
concern: string[];
|
|
73
72
|
message?: string | undefined;
|
|
74
73
|
phone?: string | undefined;
|
|
75
74
|
mode?: "phone" | "online" | "in_person" | undefined;
|
|
75
|
+
concern?: string[] | undefined;
|
|
76
76
|
preferredDate?: string | undefined;
|
|
77
77
|
preferredTime?: string | undefined;
|
|
78
78
|
}, {
|
|
79
79
|
email: string;
|
|
80
80
|
name: string;
|
|
81
|
-
concern: string[];
|
|
82
81
|
message?: string | undefined;
|
|
83
82
|
phone?: string | undefined;
|
|
84
83
|
mode?: "phone" | "online" | "in_person" | undefined;
|
|
84
|
+
concern?: string[] | undefined;
|
|
85
85
|
preferredDate?: string | undefined;
|
|
86
86
|
preferredTime?: string | undefined;
|
|
87
87
|
}>;
|
|
@@ -36,12 +36,29 @@ export const consultationBookingSchema = z.object({
|
|
|
36
36
|
* Form schema for submitting a new consultation booking.
|
|
37
37
|
*/
|
|
38
38
|
export const bookConsultationSchema = z.object({
|
|
39
|
-
name: z.string().min(2),
|
|
40
|
-
email: z
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
name: z.string().trim().min(2, "Please tell us your name.").max(120),
|
|
40
|
+
email: z
|
|
41
|
+
.string()
|
|
42
|
+
.trim()
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
.min(1, "An email address is required.")
|
|
45
|
+
.email("That doesn't look like an email address."),
|
|
46
|
+
// Same shape as the corporate enquiry's — see the note there.
|
|
47
|
+
phone: z
|
|
48
|
+
.string()
|
|
49
|
+
.trim()
|
|
50
|
+
.regex(/^(\+?91[-\s]?)?[6-9]\d{9}$/, "Enter a 10-digit Indian mobile number.")
|
|
51
|
+
.optional()
|
|
52
|
+
.or(z.literal("")),
|
|
53
|
+
/*
|
|
54
|
+
* Optional here, though the document treats it as the subject of the
|
|
55
|
+
* booking: the public form does not render a concern picker, so requiring it
|
|
56
|
+
* would reject every submission the form can produce. The admin-side booking
|
|
57
|
+
* path is where it is set.
|
|
58
|
+
*/
|
|
59
|
+
concern: z.array(z.string()).optional(),
|
|
43
60
|
preferredDate: z.string().optional(),
|
|
44
61
|
preferredTime: z.string().optional(),
|
|
45
62
|
mode: consultationModeSchema.optional(),
|
|
46
|
-
message: z.string().optional(),
|
|
63
|
+
message: z.string().trim().max(4000).optional(),
|
|
47
64
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* A raffle entry is one optional message — the entry itself is the act of
|
|
4
|
+
* submitting. Bounded because the text is rendered back on a leaderboard.
|
|
5
|
+
*/
|
|
6
|
+
export declare const raffleEntrySchema: z.ZodObject<{
|
|
7
|
+
message: z.ZodOptional<z.ZodString>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
message?: string | undefined;
|
|
10
|
+
}, {
|
|
11
|
+
message?: string | undefined;
|
|
12
|
+
}>;
|
|
13
|
+
export type RaffleEntryValues = z.infer<typeof raffleEntrySchema>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: `EventRaffleEntryForm` rendered `<Form>` with no schema, so a raffle
|
|
3
|
+
* entry's message was unbounded — and it is rendered back on a public
|
|
4
|
+
* leaderboard.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this file holds ONE schema and not three
|
|
7
|
+
*
|
|
8
|
+
* It was drafted with three: consultation, corporate and raffle. Two of those
|
|
9
|
+
* were already implemented — `bookConsultationSchema` and
|
|
10
|
+
* `submitCorporateInquirySchema` have existed in their own features all along,
|
|
11
|
+
* and their forms simply never used them. Adding a second copy of each would
|
|
12
|
+
* have been precisely the duplication this rework exists to remove, so those
|
|
13
|
+
* two were improved in place instead and only the raffle schema remains here.
|
|
14
|
+
*
|
|
15
|
+
* EXPORTS: raffleEntrySchema, type RaffleEntryValues
|
|
16
|
+
*
|
|
17
|
+
* @tag domain:events
|
|
18
|
+
* @tag layer:schema
|
|
19
|
+
* @tag pattern:none
|
|
20
|
+
* @tag access:isomorphic
|
|
21
|
+
* @tag consumers:EventRaffleEntryForm
|
|
22
|
+
* @tag sideEffects:none
|
|
23
|
+
*/
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
26
|
+
/**
|
|
27
|
+
* A raffle entry is one optional message — the entry itself is the act of
|
|
28
|
+
* submitting. Bounded because the text is rendered back on a leaderboard.
|
|
29
|
+
*/
|
|
30
|
+
export const raffleEntrySchema = z.object({
|
|
31
|
+
message: annotate(z.string().trim().max(500, "Keep it under 500 characters.").optional(), {
|
|
32
|
+
section: "entry", sectionLabel: "Your entry", sectionRequired: true, quick: true, order: 1, row: "full", kind: "textarea",
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
@@ -6,6 +6,8 @@ import { Form } from "../../../ui/components/Form";
|
|
|
6
6
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
7
7
|
import { FieldTextarea } from "../../../ui/forms/FieldTextarea";
|
|
8
8
|
import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox";
|
|
9
|
+
import { submitCorporateInquirySchema } from "../schemas";
|
|
10
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
9
11
|
export function CorporateInquiryForm({ onSubmit, isPending, }) {
|
|
10
12
|
const [form, setForm] = React.useState({
|
|
11
13
|
companyName: "",
|
|
@@ -26,5 +28,5 @@ export function CorporateInquiryForm({ onSubmit, isPending, }) {
|
|
|
26
28
|
e.preventDefault();
|
|
27
29
|
await onSubmit(form);
|
|
28
30
|
}
|
|
29
|
-
return (_jsxs(Form, { onSubmit: handleSubmit, spacing: "md", children: [_jsxs(Div, { layout: "grid", gap: "4", className: "grid-cols-1 sm:grid-cols-2", children: [_jsx(FieldInput, { name: "companyName", type: "text", placeholder: "Company name", value: form.companyName, onChange: (v) => patch("companyName", v), required: true }), _jsx(FieldInput, { name: "contactPerson", type: "text", placeholder: "Contact person", value: form.contactPerson, onChange: (v) => patch("contactPerson", v), required: true }), _jsx(FieldInput, { name: "designation", type: "text", placeholder: "Designation (optional)", value: form.designation ?? "", onChange: (v) => patch("designation", v) }), _jsx(FieldInput, { name: "email", type: "email", placeholder: "Business email", value: form.email, onChange: (v) => patch("email", v), required: true }), _jsx(FieldInput, { name: "phone", type: "tel", placeholder: "Phone number", value: form.phone, onChange: (v) => patch("phone", v), required: true }), _jsx(FieldInput, { name: "units", type: "number", placeholder: "Number of units", value: form.units, onChange: (v) => patch("units", v === "" ? 0 : Number(v)), min: 1, required: true }), _jsx(FieldInput, { name: "budgetPerUnit", type: "number", placeholder: "Budget per unit (optional)", value: form.budgetPerUnit ?? "", onChange: (v) => patch("budgetPerUnit", v === "" ? undefined : Number(v)), min: 0 }), _jsx(FieldInput, { name: "deliveryDateRequired", type: "date", placeholder: "Required by (optional)", value: form.deliveryDateRequired ?? "", onChange: (v) => patch("deliveryDateRequired", v) })] }), _jsx(FieldCheckbox, { name: "customBranding", label: "Custom branding required", checked: form.customBranding, onChange: (c) => patch("customBranding", c) }), _jsx(FieldTextarea, { name: "message", placeholder: "Additional requirements (optional)", value: form.message ?? "", onChange: (v) => patch("message", v), rows: 4 }), _jsx(Button, { type: "submit", isLoading: isPending, disabled: isPending, variant: "primary", className: "w-full", children: isPending ? "Submitting..." : "Submit Inquiry" }), _jsx(Text, { size: "xs", variant: "secondary", children: "We'll respond within 1-2 business days." })] }));
|
|
31
|
+
return (_jsxs(Form, { schema: submitCorporateInquirySchema, onSubmit: handleSubmit, spacing: "md", children: [_jsx(FormErrorSummary, {}), _jsxs(Div, { layout: "grid", gap: "4", className: "grid-cols-1 sm:grid-cols-2", children: [_jsx(FieldInput, { name: "companyName", type: "text", placeholder: "Company name", value: form.companyName, onChange: (v) => patch("companyName", v), required: true }), _jsx(FieldInput, { name: "contactPerson", type: "text", placeholder: "Contact person", value: form.contactPerson, onChange: (v) => patch("contactPerson", v), required: true }), _jsx(FieldInput, { name: "designation", type: "text", placeholder: "Designation (optional)", value: form.designation ?? "", onChange: (v) => patch("designation", v) }), _jsx(FieldInput, { name: "email", type: "email", placeholder: "Business email", value: form.email, onChange: (v) => patch("email", v), required: true }), _jsx(FieldInput, { name: "phone", type: "tel", placeholder: "Phone number", value: form.phone, onChange: (v) => patch("phone", v), required: true }), _jsx(FieldInput, { name: "units", type: "number", placeholder: "Number of units", value: form.units, onChange: (v) => patch("units", v === "" ? 0 : Number(v)), min: 1, required: true }), _jsx(FieldInput, { name: "budgetPerUnit", type: "number", placeholder: "Budget per unit (optional)", value: form.budgetPerUnit ?? "", onChange: (v) => patch("budgetPerUnit", v === "" ? undefined : Number(v)), min: 0 }), _jsx(FieldInput, { name: "deliveryDateRequired", type: "date", placeholder: "Required by (optional)", value: form.deliveryDateRequired ?? "", onChange: (v) => patch("deliveryDateRequired", v) })] }), _jsx(FieldCheckbox, { name: "customBranding", label: "Custom branding required", checked: form.customBranding, onChange: (c) => patch("customBranding", c) }), _jsx(FieldTextarea, { name: "message", placeholder: "Additional requirements (optional)", value: form.message ?? "", onChange: (v) => patch("message", v), rows: 4 }), _jsx(Button, { type: "submit", isLoading: isPending, disabled: isPending, variant: "primary", className: "w-full", children: isPending ? "Submitting..." : "Submit Inquiry" }), _jsx(Text, { size: "xs", variant: "secondary", children: "We'll respond within 1-2 business days." })] }));
|
|
30
32
|
}
|
|
@@ -64,17 +64,19 @@ export declare const submitCorporateInquirySchema: z.ZodObject<{
|
|
|
64
64
|
contactPerson: z.ZodString;
|
|
65
65
|
designation: z.ZodOptional<z.ZodString>;
|
|
66
66
|
email: z.ZodString;
|
|
67
|
-
phone: z.ZodOptional<z.ZodString>;
|
|
67
|
+
phone: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
68
68
|
units: z.ZodOptional<z.ZodNumber>;
|
|
69
69
|
budgetPerUnit: z.ZodOptional<z.ZodNumber>;
|
|
70
70
|
totalBudget: z.ZodOptional<z.ZodNumber>;
|
|
71
71
|
deliveryDateRequired: z.ZodOptional<z.ZodString>;
|
|
72
72
|
customBranding: z.ZodDefault<z.ZodBoolean>;
|
|
73
|
+
message: z.ZodOptional<z.ZodString>;
|
|
73
74
|
}, "strip", z.ZodTypeAny, {
|
|
74
75
|
email: string;
|
|
75
76
|
companyName: string;
|
|
76
77
|
contactPerson: string;
|
|
77
78
|
customBranding: boolean;
|
|
79
|
+
message?: string | undefined;
|
|
78
80
|
phone?: string | undefined;
|
|
79
81
|
designation?: string | undefined;
|
|
80
82
|
units?: number | undefined;
|
|
@@ -85,6 +87,7 @@ export declare const submitCorporateInquirySchema: z.ZodObject<{
|
|
|
85
87
|
email: string;
|
|
86
88
|
companyName: string;
|
|
87
89
|
contactPerson: string;
|
|
90
|
+
message?: string | undefined;
|
|
88
91
|
phone?: string | undefined;
|
|
89
92
|
designation?: string | undefined;
|
|
90
93
|
units?: number | undefined;
|
|
@@ -37,14 +37,44 @@ export const corporateInquirySchema = z.object({
|
|
|
37
37
|
* Form schema for submitting a corporate inquiry.
|
|
38
38
|
*/
|
|
39
39
|
export const submitCorporateInquirySchema = z.object({
|
|
40
|
-
companyName: z.string().min(2),
|
|
41
|
-
contactPerson: z.string().min(2),
|
|
42
|
-
designation: z.string().optional(),
|
|
43
|
-
email: z
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
companyName: z.string().trim().min(2, "A company name is required.").max(160),
|
|
41
|
+
contactPerson: z.string().trim().min(2, "Who should we reply to?").max(120),
|
|
42
|
+
designation: z.string().trim().max(120).optional(),
|
|
43
|
+
email: z
|
|
44
|
+
.string()
|
|
45
|
+
.trim()
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.min(1, "An email address is required.")
|
|
48
|
+
.email("That doesn't look like an email address."),
|
|
49
|
+
// Indian mobile, tolerant of spacing and a country code. Optional — a
|
|
50
|
+
// reachable email is enough, and demanding a phone number loses enquiries.
|
|
51
|
+
phone: z
|
|
52
|
+
.string()
|
|
53
|
+
.trim()
|
|
54
|
+
.regex(/^(\+?91[-\s]?)?[6-9]\d{9}$/, "Enter a 10-digit Indian mobile number.")
|
|
55
|
+
.optional()
|
|
56
|
+
.or(z.literal("")),
|
|
57
|
+
/*
|
|
58
|
+
* 🛑 COERCED. These were `z.number()`, and this schema's own form sends
|
|
59
|
+
* strings — so every submission carrying a unit count failed to parse the
|
|
60
|
+
* moment anyone actually wired the form to the schema. Bounds are
|
|
61
|
+
* deliberately generous: they catch a mistyped figure, not a large order.
|
|
62
|
+
*
|
|
63
|
+
* A sales reply is priced from these two, and until now both were free text.
|
|
64
|
+
*/
|
|
65
|
+
units: z.coerce
|
|
66
|
+
.number({ invalid_type_error: "Enter the number of units as a number." })
|
|
67
|
+
.int("Units must be a whole number.")
|
|
68
|
+
.positive("At least one unit.")
|
|
69
|
+
.max(1000000, "That looks like a typo — contact us directly for orders this large.")
|
|
70
|
+
.optional(),
|
|
71
|
+
budgetPerUnit: z.coerce
|
|
72
|
+
.number({ invalid_type_error: "Enter a per-unit budget as a number." })
|
|
73
|
+
.min(0, "A budget cannot be negative.")
|
|
74
|
+
.max(10000000, "That looks like a typo.")
|
|
75
|
+
.optional(),
|
|
76
|
+
totalBudget: z.coerce.number().min(0).optional(),
|
|
48
77
|
deliveryDateRequired: z.string().optional(),
|
|
49
78
|
customBranding: z.boolean().default(false),
|
|
79
|
+
message: z.string().trim().max(4000).optional(),
|
|
50
80
|
});
|
|
@@ -6,6 +6,8 @@ import { Form } from "../../../ui/components/Form";
|
|
|
6
6
|
import { FieldTextarea } from "../../../ui/forms/FieldTextarea";
|
|
7
7
|
import { Div, Heading, Stack, Text } from "../../../ui";
|
|
8
8
|
import { Badge } from "../../../ui/components/Badge";
|
|
9
|
+
import { raffleEntrySchema } from "../../contact/schemas/inquiry-forms";
|
|
10
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
9
11
|
const __P = {
|
|
10
12
|
p6: "p-[var(--appkit-space-6)]",
|
|
11
13
|
};
|
|
@@ -38,5 +40,5 @@ export function EventRaffleEntryForm({ prizeLabel, raffleType = "open_raffle", t
|
|
|
38
40
|
: raffleType === "top_n_participants"
|
|
39
41
|
? `Earliest ${topN ?? "N"} entries qualify.`
|
|
40
42
|
: "Spin the wheel to claim your prize.";
|
|
41
|
-
return (_jsxs(Stack, { className: `${__P.p6}`, gap: "md", rounded: "2xl", border: "default", children: [prizeLabel ? (_jsxs(Stack, { gap: "xs", children: [_jsx(Badge, { variant: "warning", children: "Prize" }), _jsx(Heading, { level: 2, weight: "bold", size: "2xl", children: prizeLabel })] })) : null, _jsx(Text, { size: "sm", color: "muted", children: eligibilityCopy }), submitted ? (_jsx(Div, { textSize: "sm", color: "success", surface: "success-surface", padding: "inline", rounded: "lg", children: "You're in. Check the leaderboard for results." })) : overLimit ? (_jsxs(Div, { textSize: "sm", color: "warning", surface: "warning-surface", padding: "inline", rounded: "lg", children: ["You've used all ", maxEntriesPerUser, " entry slot", maxEntriesPerUser === 1 ? "" : "s", " for this raffle."] })) : (_jsxs(Form, { onSubmit: handleSubmit, className: "space-y-[0.75rem]", children: [_jsx(FieldTextarea, { name: "message", label: "Message", hint: "optional \u2014 share why you'd like to win", value: message, onChange: setMessage, rows: 3, maxLength: 500, placeholder: "Share why you'd like to win", disabled: busy }), _jsx(Button, { type: "submit", variant: "primary", disabled: busy, children: busy ? "Submitting…" : "Enter raffle" })] }))] }));
|
|
43
|
+
return (_jsxs(Stack, { className: `${__P.p6}`, gap: "md", rounded: "2xl", border: "default", children: [prizeLabel ? (_jsxs(Stack, { gap: "xs", children: [_jsx(Badge, { variant: "warning", children: "Prize" }), _jsx(Heading, { level: 2, weight: "bold", size: "2xl", children: prizeLabel })] })) : null, _jsx(Text, { size: "sm", color: "muted", children: eligibilityCopy }), submitted ? (_jsx(Div, { textSize: "sm", color: "success", surface: "success-surface", padding: "inline", rounded: "lg", children: "You're in. Check the leaderboard for results." })) : overLimit ? (_jsxs(Div, { textSize: "sm", color: "warning", surface: "warning-surface", padding: "inline", rounded: "lg", children: ["You've used all ", maxEntriesPerUser, " entry slot", maxEntriesPerUser === 1 ? "" : "s", " for this raffle."] })) : (_jsxs(Form, { schema: raffleEntrySchema, onSubmit: handleSubmit, className: "space-y-[0.75rem]", children: [_jsx(FormErrorSummary, {}), _jsx(FieldTextarea, { name: "message", label: "Message", hint: "optional \u2014 share why you'd like to win", value: message, onChange: setMessage, rows: 3, maxLength: 500, placeholder: "Share why you'd like to win", disabled: busy }), _jsx(Button, { type: "submit", variant: "primary", disabled: busy, children: busy ? "Submitting…" : "Enter raffle" })] }))] }));
|
|
42
44
|
}
|
|
@@ -6,6 +6,8 @@ import { Button, Div, Heading, Span, Text } from "../../../ui";
|
|
|
6
6
|
import { DynamicBgDiv } from "../../../ui/components/DynamicBgDiv";
|
|
7
7
|
import { Form } from "../../../ui/components/Form";
|
|
8
8
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
9
|
+
import { newsletterSubscribeSchema } from "../../admin/schemas/small-forms";
|
|
10
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
9
11
|
const CLS_ERROR_TEXT = "mt-3 text-[length:var(--appkit-text-sm)] text-error";
|
|
10
12
|
export function NewsletterBanner({ onSubscribe, eyebrow = "STAY CONNECTED", heading = "Join Our Newsletter", subheading = "Get exclusive offers, new arrivals and more.", placeholder = "Enter your email", ctaLabel = "Subscribe", successMessage = "You're in! Check your inbox.", errorMessage = "Something went wrong. Please try again.", }) {
|
|
11
13
|
const [email, setEmail] = useState("");
|
|
@@ -31,5 +33,5 @@ export function NewsletterBanner({ onSubscribe, eyebrow = "STAY CONNECTED", head
|
|
|
31
33
|
setLoading(false);
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
|
-
return (_jsxs(DynamicBgDiv, { background: "var(--newsletter-bg, linear-gradient(135deg, var(--appkit-color-primary) 0%, var(--appkit-color-secondary) 100%))", className: "py-[var(--appkit-space-24)] relative overflow-hidden", children: [_jsx(Div, { className: "pointer-events-none absolute -top-20 -right-20 h-60 w-60 border border-white/[0.06]", rounded: "full" }), _jsx(Div, { className: "pointer-events-none absolute -bottom-16 -left-16 h-48 w-48 border border-dashed border-white/[0.08]", rounded: "full" }), _jsxs(Div, { className: "relative mx-auto max-w-2xl text-left sm:px-[1.5rem]", padding: "x-md", children: [_jsx(Span, { color: "inverse", size: "xs", weight: "semibold", className: "mb-4 inline-flex border border-white/20 tracking-widest /70", rounded: "full", padding: "pill-md", transform: "uppercase", children: eyebrow }), _jsx(Heading, { color: "inverse", level: 2, smSize: "4xl", size: "3xl", weight: "bold", children: heading }), _jsx(Text, { color: "inverse", className: "mt-4 /60", children: subheading }), success ? (_jsx(Text, { paddingY: "md", color: "inverse", className: "mt-8 border border-white/20 bg-[rgba(255,255,255,0.1)]", paddingX: "x-lg", size: "sm", weight: "semibold", rounded: "xl", children: successMessage })) : (_jsxs(Form, { onSubmit: handleSubmit, className: "mt-8 flex gap-[0.75rem]", children: [_jsx(FieldInput, { name: "email", type: "email", value: email, onChange: setEmail, placeholder: placeholder, required: true, className: "flex-1" }), _jsx(Button, { rounded: "xl", type: "submit", disabled: loading, variant: "primary", paddingX: "lg", textSize: "sm", className: "shrink-0 py-[0.75rem] font-[700] transition-opacity disabled:opacity-60 bg-[var(--color-yellow)] text-black", children: loading ? "…" : ctaLabel })] })), error && (_jsx(Text, { className: CLS_ERROR_TEXT, children: errorMessage })), _jsx(Text, { color: "inverse", className: "mt-4 /40", size: "xs", children: "No spam, ever. Unsubscribe anytime." })] })] }));
|
|
36
|
+
return (_jsxs(DynamicBgDiv, { background: "var(--newsletter-bg, linear-gradient(135deg, var(--appkit-color-primary) 0%, var(--appkit-color-secondary) 100%))", className: "py-[var(--appkit-space-24)] relative overflow-hidden", children: [_jsx(Div, { className: "pointer-events-none absolute -top-20 -right-20 h-60 w-60 border border-white/[0.06]", rounded: "full" }), _jsx(Div, { className: "pointer-events-none absolute -bottom-16 -left-16 h-48 w-48 border border-dashed border-white/[0.08]", rounded: "full" }), _jsxs(Div, { className: "relative mx-auto max-w-2xl text-left sm:px-[1.5rem]", padding: "x-md", children: [_jsx(Span, { color: "inverse", size: "xs", weight: "semibold", className: "mb-4 inline-flex border border-white/20 tracking-widest /70", rounded: "full", padding: "pill-md", transform: "uppercase", children: eyebrow }), _jsx(Heading, { color: "inverse", level: 2, smSize: "4xl", size: "3xl", weight: "bold", children: heading }), _jsx(Text, { color: "inverse", className: "mt-4 /60", children: subheading }), success ? (_jsx(Text, { paddingY: "md", color: "inverse", className: "mt-8 border border-white/20 bg-[rgba(255,255,255,0.1)]", paddingX: "x-lg", size: "sm", weight: "semibold", rounded: "xl", children: successMessage })) : (_jsxs(Form, { schema: newsletterSubscribeSchema, onSubmit: handleSubmit, className: "mt-8 flex gap-[0.75rem]", children: [_jsx(FormErrorSummary, {}), _jsx(FieldInput, { name: "email", type: "email", value: email, onChange: setEmail, placeholder: placeholder, required: true, className: "flex-1" }), _jsx(Button, { rounded: "xl", type: "submit", disabled: loading, variant: "primary", paddingX: "lg", textSize: "sm", className: "shrink-0 py-[0.75rem] font-[700] transition-opacity disabled:opacity-60 bg-[var(--color-yellow)] text-black", children: loading ? "…" : ctaLabel })] })), error && (_jsx(Text, { className: CLS_ERROR_TEXT, children: errorMessage })), _jsx(Text, { color: "inverse", className: "mt-4 /40", size: "xs", children: "No spam, ever. Unsubscribe anytime." })] })] }));
|
|
35
37
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Cancelling an order.
|
|
4
|
+
*
|
|
5
|
+
* A reason floor of 5 characters, because this text is what the seller reads
|
|
6
|
+
* to decide whether to restock, refund early or dispute — and "x" tells them
|
|
7
|
+
* nothing. Deliberately low: the buyer is cancelling, not writing an essay,
|
|
8
|
+
* and a high floor would push them to type filler.
|
|
9
|
+
*/
|
|
10
|
+
export declare const orderCancelSchema: z.ZodObject<{
|
|
11
|
+
reason: z.ZodString;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
reason: string;
|
|
14
|
+
}, {
|
|
15
|
+
reason: string;
|
|
16
|
+
}>;
|
|
17
|
+
export type OrderCancelValues = z.infer<typeof orderCancelSchema>;
|
|
18
|
+
/**
|
|
19
|
+
* Uploading proof of a manual (cash / UPI / EMI) payment.
|
|
20
|
+
*
|
|
21
|
+
* ## Every field here is evidence
|
|
22
|
+
*
|
|
23
|
+
* An admin verifies or rejects on this, and a rejection carries a 7-day ban
|
|
24
|
+
* cascade — so the cost of a sloppy submission is real in both directions.
|
|
25
|
+
*
|
|
26
|
+
* `buyerMarkedPaid` and `buyerFraudAgreementAccepted` are `z.literal(true)`
|
|
27
|
+
* rather than `z.boolean()`: an unchecked declaration is not a submission with
|
|
28
|
+
* a `false` in it, it is a form that must not send.
|
|
29
|
+
*/
|
|
30
|
+
export declare const paymentProofSchema: z.ZodObject<{
|
|
31
|
+
transactionId: z.ZodString;
|
|
32
|
+
buyerReportedUpiId: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
|
33
|
+
buyerMarkedPaid: z.ZodLiteral<true>;
|
|
34
|
+
buyerFraudAgreementAccepted: z.ZodLiteral<true>;
|
|
35
|
+
}, "strip", z.ZodTypeAny, {
|
|
36
|
+
transactionId: string;
|
|
37
|
+
buyerMarkedPaid: true;
|
|
38
|
+
buyerFraudAgreementAccepted: true;
|
|
39
|
+
buyerReportedUpiId?: string | undefined;
|
|
40
|
+
}, {
|
|
41
|
+
transactionId: string;
|
|
42
|
+
buyerMarkedPaid: true;
|
|
43
|
+
buyerFraudAgreementAccepted: true;
|
|
44
|
+
buyerReportedUpiId?: string | undefined;
|
|
45
|
+
}>;
|
|
46
|
+
export type PaymentProofValues = z.infer<typeof paymentProofSchema>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Two buyer-facing order forms rendered `<Form>` with no schema.
|
|
3
|
+
*
|
|
4
|
+
* The manual-payment one is the consequential case. It carries the
|
|
5
|
+
* buyer's own claim that they have paid — a UTR, the UPI ID they sent to,
|
|
6
|
+
* and a fraud declaration — and an admin decides on that evidence whether
|
|
7
|
+
* to release goods. `transactionId` was free text with no floor, so a
|
|
8
|
+
* single character satisfied it and reached the review queue looking like
|
|
9
|
+
* a reference.
|
|
10
|
+
*
|
|
11
|
+
* WHAT: The two schemas, plus a shared `cancellationReason`.
|
|
12
|
+
*
|
|
13
|
+
* EXPORTS:
|
|
14
|
+
* orderCancelSchema, paymentProofSchema, and their inferred value types
|
|
15
|
+
*
|
|
16
|
+
* @tag domain:orders
|
|
17
|
+
* @tag layer:schema
|
|
18
|
+
* @tag pattern:none
|
|
19
|
+
* @tag access:isomorphic
|
|
20
|
+
* @tag consumers:user/orders/[id]/cancel,user/orders/[id]/payment
|
|
21
|
+
* @tag sideEffects:none
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
25
|
+
/**
|
|
26
|
+
* Cancelling an order.
|
|
27
|
+
*
|
|
28
|
+
* A reason floor of 5 characters, because this text is what the seller reads
|
|
29
|
+
* to decide whether to restock, refund early or dispute — and "x" tells them
|
|
30
|
+
* nothing. Deliberately low: the buyer is cancelling, not writing an essay,
|
|
31
|
+
* and a high floor would push them to type filler.
|
|
32
|
+
*/
|
|
33
|
+
export const orderCancelSchema = z.object({
|
|
34
|
+
reason: annotate(z
|
|
35
|
+
.string()
|
|
36
|
+
.trim()
|
|
37
|
+
.min(5, "Give a short reason — the seller sees this.")
|
|
38
|
+
.max(500, "Keep it under 500 characters."), {
|
|
39
|
+
section: "cancel",
|
|
40
|
+
sectionLabel: "Why are you cancelling?",
|
|
41
|
+
sectionRequired: true,
|
|
42
|
+
quick: true,
|
|
43
|
+
order: 1,
|
|
44
|
+
row: "full",
|
|
45
|
+
kind: "textarea",
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* Uploading proof of a manual (cash / UPI / EMI) payment.
|
|
50
|
+
*
|
|
51
|
+
* ## Every field here is evidence
|
|
52
|
+
*
|
|
53
|
+
* An admin verifies or rejects on this, and a rejection carries a 7-day ban
|
|
54
|
+
* cascade — so the cost of a sloppy submission is real in both directions.
|
|
55
|
+
*
|
|
56
|
+
* `buyerMarkedPaid` and `buyerFraudAgreementAccepted` are `z.literal(true)`
|
|
57
|
+
* rather than `z.boolean()`: an unchecked declaration is not a submission with
|
|
58
|
+
* a `false` in it, it is a form that must not send.
|
|
59
|
+
*/
|
|
60
|
+
export const paymentProofSchema = z.object({
|
|
61
|
+
transactionId: annotate(z
|
|
62
|
+
.string()
|
|
63
|
+
.trim()
|
|
64
|
+
.min(4, "Enter the UTR or reference number from your payment app.")
|
|
65
|
+
.max(120, "Keep the reference under 120 characters."), { section: "proof", sectionLabel: "Your payment", sectionRequired: true, quick: true, order: 1, row: "pair" }),
|
|
66
|
+
/*
|
|
67
|
+
* The UPI ID the buyer actually sent to. Compared against the store's
|
|
68
|
+
* displayed one so a mismatch is flagged to the admin — which is why the
|
|
69
|
+
* shape matters, not just its presence.
|
|
70
|
+
*/
|
|
71
|
+
buyerReportedUpiId: annotate(z
|
|
72
|
+
.string()
|
|
73
|
+
.trim()
|
|
74
|
+
.regex(/^[\w.\-_]{2,256}@[a-zA-Z]{2,64}$/, "Enter the UPI ID you paid to, e.g. name@bank.")
|
|
75
|
+
.optional()
|
|
76
|
+
.or(z.literal("")), { section: "proof", order: 2, row: "pair" }),
|
|
77
|
+
buyerMarkedPaid: annotate(z.literal(true, { errorMap: () => ({ message: "Confirm that you have sent the payment." }) }), { section: "declare", sectionLabel: "Declaration", sectionRequired: true, order: 1, row: "full" }),
|
|
78
|
+
buyerFraudAgreementAccepted: annotate(z.literal(true, {
|
|
79
|
+
errorMap: () => ({ message: "You must accept the fraud policy to submit proof." }),
|
|
80
|
+
}), { section: "declare", order: 2, row: "full" }),
|
|
81
|
+
});
|
|
@@ -75,7 +75,7 @@ export function MakeOfferButton({ productId, listedPrice, currency, minOfferPerc
|
|
|
75
75
|
if (state === "pending") {
|
|
76
76
|
return (_jsxs(Div, { className: `${CLS_PENDING_BOX} ${className}`, children: [_jsx(Span, { size: "lg", children: "\u23F3" }), _jsx(Text, { className: CLS_PENDING_TITLE, children: "Offer Pending" }), _jsx(Text, { className: CLS_PENDING_BODY, children: "You already have an offer on this item. Check My Offers for updates." })] }));
|
|
77
77
|
}
|
|
78
|
-
return (_jsxs(_Fragment, { children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to make an offer. Please log in or create an account to continue." }), _jsx(Button, { variant: "ghost", size: "md", border: "strong", className: `w-full ${className}`, onClick: () => setState("open"), children: "Make Offer" }), _jsx(Modal, { isOpen: state === "open", onClose: () => setState("idle"), size: "md", title: "Make an offer", children: _jsx(Form, { onSubmit: (e) => e.preventDefault(), children: ({ setFieldError, clearErrors }) => (_jsxs(Stack, { gap: "md", children: [_jsxs(Text, { size: "xs", color: "muted", children: ["Listed at ", fmt(listedPrice), " \u00B7 Minimum offer: ", fmt(minOffer)] }), _jsx(FieldInput, { name: "offerAmount", type: "number", label: "Your offer amount", required: true, value: offerAmount, onChange: (value) => {
|
|
78
|
+
return (_jsxs(_Fragment, { children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to make an offer. Please log in or create an account to continue." }), _jsx(Button, { variant: "ghost", size: "md", border: "strong", className: `w-full ${className}`, onClick: () => setState("open"), children: "Make Offer" }), _jsx(Modal, { isOpen: state === "open", onClose: () => setState("idle"), size: "md", title: "Make an offer", children: _jsx(Form, { schema: schema, onSubmit: (e) => e.preventDefault(), children: ({ setFieldError, clearErrors }) => (_jsxs(Stack, { gap: "md", children: [_jsxs(Text, { size: "xs", color: "muted", children: ["Listed at ", fmt(listedPrice), " \u00B7 Minimum offer: ", fmt(minOffer)] }), _jsx(FieldInput, { name: "offerAmount", type: "number", label: "Your offer amount", required: true, value: offerAmount, onChange: (value) => {
|
|
79
79
|
setOfferAmount(value);
|
|
80
80
|
clearErrors();
|
|
81
81
|
}, min: minOffer, max: listedPrice - 1, step: 1, "aria-label": "Offer amount" }), _jsxs(Text, { size: "xs", color: "faint", children: ["Must be between ", fmt(minOffer), " and ", fmt(listedPrice - 1)] }), _jsx(FieldInput, { name: "buyerNote", type: "text", label: "Note to seller (optional)", value: buyerNote, onChange: setBuyerNote, placeholder: "E.g. Bundle deal, long-time fan\u2026", maxLength: 300, "aria-label": "Note to seller" }), _jsx(Text, { size: "xs", color: "muted", children: "The seller will accept, decline, or suggest a counter price." }), _jsx(FormErrorSummary, {}), _jsxs(Stack, { gap: "xs", children: [_jsx(Button, { action: ACTIONS.PRODUCT["make-offer"], variant: "primary", size: "sm", type: "button", isLoading: isPending, disabled: isPending, onClick: () => submit(setFieldError, clearErrors), children: isPending ? "Sending…" : `Send offer of ${fmt(Number(offerAmount) || 0)}` }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => setState("idle"), disabled: isPending, children: "Cancel" })] })] })) }) })] }));
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const scamReportFormSchema: z.ZodEffects<z.ZodObject<{
|
|
3
|
+
displayName: z.ZodString;
|
|
4
|
+
phones: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
5
|
+
upiIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
6
|
+
emails: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
7
|
+
scamType: z.ZodString;
|
|
8
|
+
scamPlatform: z.ZodOptional<z.ZodString>;
|
|
9
|
+
amountLost: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
itemInvolved: z.ZodOptional<z.ZodString>;
|
|
11
|
+
description: z.ZodString;
|
|
12
|
+
reportedByAnon: z.ZodOptional<z.ZodBoolean>;
|
|
13
|
+
agreed: z.ZodLiteral<true>;
|
|
14
|
+
}, "strip", z.ZodTypeAny, {
|
|
15
|
+
displayName: string;
|
|
16
|
+
description: string;
|
|
17
|
+
scamType: string;
|
|
18
|
+
agreed: true;
|
|
19
|
+
reportedByAnon?: boolean | undefined;
|
|
20
|
+
scamPlatform?: string | undefined;
|
|
21
|
+
amountLost?: number | undefined;
|
|
22
|
+
itemInvolved?: string | undefined;
|
|
23
|
+
phones?: string[] | undefined;
|
|
24
|
+
upiIds?: string[] | undefined;
|
|
25
|
+
emails?: string[] | undefined;
|
|
26
|
+
}, {
|
|
27
|
+
displayName: string;
|
|
28
|
+
description: string;
|
|
29
|
+
scamType: string;
|
|
30
|
+
agreed: true;
|
|
31
|
+
reportedByAnon?: boolean | undefined;
|
|
32
|
+
scamPlatform?: string | undefined;
|
|
33
|
+
amountLost?: number | undefined;
|
|
34
|
+
itemInvolved?: string | undefined;
|
|
35
|
+
phones?: string[] | undefined;
|
|
36
|
+
upiIds?: string[] | undefined;
|
|
37
|
+
emails?: string[] | undefined;
|
|
38
|
+
}>, {
|
|
39
|
+
displayName: string;
|
|
40
|
+
description: string;
|
|
41
|
+
scamType: string;
|
|
42
|
+
agreed: true;
|
|
43
|
+
reportedByAnon?: boolean | undefined;
|
|
44
|
+
scamPlatform?: string | undefined;
|
|
45
|
+
amountLost?: number | undefined;
|
|
46
|
+
itemInvolved?: string | undefined;
|
|
47
|
+
phones?: string[] | undefined;
|
|
48
|
+
upiIds?: string[] | undefined;
|
|
49
|
+
emails?: string[] | undefined;
|
|
50
|
+
}, {
|
|
51
|
+
displayName: string;
|
|
52
|
+
description: string;
|
|
53
|
+
scamType: string;
|
|
54
|
+
agreed: true;
|
|
55
|
+
reportedByAnon?: boolean | undefined;
|
|
56
|
+
scamPlatform?: string | undefined;
|
|
57
|
+
amountLost?: number | undefined;
|
|
58
|
+
itemInvolved?: string | undefined;
|
|
59
|
+
phones?: string[] | undefined;
|
|
60
|
+
upiIds?: string[] | undefined;
|
|
61
|
+
emails?: string[] | undefined;
|
|
62
|
+
}>;
|
|
63
|
+
export type ScamReportFormValues = z.infer<typeof scamReportFormSchema>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: The public scam-report form (`/scams/report`) rendered `<Form>` with no
|
|
3
|
+
* schema. It is the highest-consequence unvalidated form in the app: a
|
|
4
|
+
* submission names a real person or business by phone number, UPI ID or
|
|
5
|
+
* email and asks the platform to publish an accusation against them.
|
|
6
|
+
*
|
|
7
|
+
* Everything was free text. `amountLost` in particular was a string with
|
|
8
|
+
* no bound, and it is displayed on the public scammer profile.
|
|
9
|
+
*
|
|
10
|
+
* WHAT: The submit contract.
|
|
11
|
+
*
|
|
12
|
+
* ## At least one identifier is required, and no single one is
|
|
13
|
+
*
|
|
14
|
+
* A report naming nobody is unactionable, but demanding a phone number would
|
|
15
|
+
* reject a report about an email-only scammer. So the requirement is on the
|
|
16
|
+
* SET, checked in a `superRefine` — the same shape the payout-method schema
|
|
17
|
+
* uses for its per-type bank rules.
|
|
18
|
+
*
|
|
19
|
+
* ## `agreed` is `z.literal(true)`
|
|
20
|
+
*
|
|
21
|
+
* An unticked declaration is not a submission carrying `false`; it is a form
|
|
22
|
+
* that must not send. The user is asserting the accusation is truthful, and
|
|
23
|
+
* that assertion is what makes publishing it defensible.
|
|
24
|
+
*
|
|
25
|
+
* EXPORTS: scamReportFormSchema, type ScamReportFormValues
|
|
26
|
+
*
|
|
27
|
+
* @tag domain:scams
|
|
28
|
+
* @tag layer:schema
|
|
29
|
+
* @tag pattern:none
|
|
30
|
+
* @tag access:isomorphic
|
|
31
|
+
* @tag consumers:scams/report page
|
|
32
|
+
* @tag sideEffects:none
|
|
33
|
+
*/
|
|
34
|
+
import { z } from "zod";
|
|
35
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
36
|
+
const UPI_VPA_RE = /^[\w.\-_]{2,256}@[a-zA-Z]{2,64}$/;
|
|
37
|
+
const PHONE_RE = /^(\+?91[-\s]?)?[6-9]\d{9}$/;
|
|
38
|
+
export const scamReportFormSchema = z
|
|
39
|
+
.object({
|
|
40
|
+
displayName: annotate(z.string().trim().min(2, "Give the name or handle they used.").max(160), { section: "who", sectionLabel: "Who", sectionRequired: true, quick: true, order: 1, row: "full" }),
|
|
41
|
+
// Each entry is shape-checked; the SET is checked in the superRefine.
|
|
42
|
+
phones: annotate(z.array(z.string().trim().regex(PHONE_RE, "Enter a 10-digit Indian mobile number.")).max(10).optional(), { section: "who", order: 2, row: "full", kind: "list" }),
|
|
43
|
+
upiIds: annotate(z.array(z.string().trim().regex(UPI_VPA_RE, "Enter a valid UPI ID, e.g. name@bank.")).max(10).optional(), { section: "who", order: 3, row: "full", kind: "list" }),
|
|
44
|
+
emails: annotate(z.array(z.string().trim().toLowerCase().email("Enter a valid email address.")).max(10).optional(), { section: "who", order: 4, row: "full", kind: "list" }),
|
|
45
|
+
scamType: annotate(z.string().trim().min(1, "Choose what kind of scam this was."), {
|
|
46
|
+
section: "what", sectionLabel: "What happened", sectionRequired: true, quick: true, order: 1, row: "pair",
|
|
47
|
+
}),
|
|
48
|
+
scamPlatform: annotate(z.string().trim().max(120).optional(), {
|
|
49
|
+
section: "what", order: 2, row: "pair",
|
|
50
|
+
}),
|
|
51
|
+
/*
|
|
52
|
+
* Coerced and bounded. It arrives from a text input and is rendered on the
|
|
53
|
+
* public profile, so an unbounded string could publish "one MILLION crore"
|
|
54
|
+
* as a loss figure. Empty is allowed — plenty of reports are about an
|
|
55
|
+
* attempt that cost nothing.
|
|
56
|
+
*/
|
|
57
|
+
amountLost: annotate(z.coerce
|
|
58
|
+
.number({ invalid_type_error: "Enter the amount as a number." })
|
|
59
|
+
.min(0, "An amount cannot be negative.")
|
|
60
|
+
.max(100000000, "That looks like a typo.")
|
|
61
|
+
.optional(), { section: "what", order: 3, row: "pair", kind: "number" }),
|
|
62
|
+
itemInvolved: annotate(z.string().trim().max(200).optional(), {
|
|
63
|
+
section: "what", order: 4, row: "pair",
|
|
64
|
+
}),
|
|
65
|
+
description: annotate(z
|
|
66
|
+
.string()
|
|
67
|
+
.trim()
|
|
68
|
+
.min(30, "Describe what happened in at least 30 characters — this is the evidence.")
|
|
69
|
+
.max(5000, "Keep the description under 5000 characters."), { section: "what", order: 5, row: "full", kind: "textarea" }),
|
|
70
|
+
reportedByAnon: annotate(z.boolean().optional(), {
|
|
71
|
+
section: "declare", sectionLabel: "Declaration", order: 1, row: "quarter",
|
|
72
|
+
}),
|
|
73
|
+
agreed: annotate(z.literal(true, {
|
|
74
|
+
errorMap: () => ({ message: "You must confirm this report is truthful before submitting." }),
|
|
75
|
+
}), { section: "declare", order: 2, row: "full" }),
|
|
76
|
+
})
|
|
77
|
+
.superRefine((v, ctx) => {
|
|
78
|
+
const count = (v.phones?.length ?? 0) + (v.upiIds?.length ?? 0) + (v.emails?.length ?? 0);
|
|
79
|
+
if (count === 0) {
|
|
80
|
+
// Reported on `phones` so the message has a field to land on; the text
|
|
81
|
+
// names all three, because any one of them satisfies the rule.
|
|
82
|
+
ctx.addIssue({
|
|
83
|
+
code: z.ZodIssueCode.custom,
|
|
84
|
+
path: ["phones"],
|
|
85
|
+
message: "Add at least one phone number, UPI ID or email — a report with no identifier cannot be acted on.",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const sublistingCategoryFormSchema: z.ZodObject<{
|
|
3
|
+
name: z.ZodString;
|
|
4
|
+
itemCode: z.ZodOptional<z.ZodString>;
|
|
5
|
+
description: z.ZodOptional<z.ZodString>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string | undefined;
|
|
9
|
+
itemCode?: string | undefined;
|
|
10
|
+
}, {
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string | undefined;
|
|
13
|
+
itemCode?: string | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
export type SublistingCategoryFormValues = z.infer<typeof sublistingCategoryFormSchema>;
|