@coding-blocks/vmc-web-components 0.1.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/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # @coding-blocks/vmc-web-components
2
+
3
+ Embeddable HTML web components for Vidyamandir Classes forms. React on the
4
+ inside, plain custom elements on the outside — a host page only needs one
5
+ `<script>` tag and the element.
6
+
7
+ Every component in this repo ships in **one bundle**: `dist/vmc-web-components.js`
8
+ (React and the CSS are inlined, so there are no peer dependencies and no extra
9
+ network requests).
10
+
11
+ ## Usage
12
+
13
+ ```html
14
+ <script src="https://unpkg.com/@coding-blocks/vmc-web-components"></script>
15
+
16
+ <vmc-online-lead></vmc-online-lead>
17
+ ```
18
+
19
+ The API base URL is **compiled into the bundle** — embedding pages never pass
20
+ it. See [Environments](#environments).
21
+
22
+ Pin a version in production:
23
+
24
+ ```html
25
+ <script src="https://unpkg.com/@coding-blocks/vmc-web-components@0.1.0/dist/vmc-web-components.js"></script>
26
+ ```
27
+
28
+ The script can go anywhere — elements already in the DOM upgrade as soon as it
29
+ loads. Each component renders inside a **shadow root**, so the host page's CSS
30
+ can't leak in and the component's CSS can't leak out.
31
+
32
+ ## Components
33
+
34
+ ### `<vmc-online-lead>`
35
+
36
+ The "Want a call back?" lead form. Fields posted to the backend:
37
+ `name`, `email`, `dial_code`, `mobile`, `class_id`, `course_id`, `state_id`,
38
+ `city_id`.
39
+
40
+ Mobile verification is mandatory: **Submit stays disabled until the number is
41
+ OTP-verified** and the consent box is ticked. Editing the number after
42
+ verification drops it back to unverified, so a submission can never carry an OTP
43
+ that belongs to a different number.
44
+
45
+ | Attribute | Default | Purpose |
46
+ | --- | --- | --- |
47
+ | `api-base` | build-time `VITE_API_URL` | Override the compiled-in base (testing only) |
48
+ | `heading` | `Want a call back ?` | Card heading |
49
+ | `dial-code` | `+91` | Pre-selected dial code |
50
+ | `consent-text` | VMC authorisation copy | Consent checkbox label |
51
+ | `submit-label` | `Submit` | Submit button label |
52
+ | `success-title` | `Thank you!` | Heading after a successful submit |
53
+ | `success-message` | counsellor call-back copy | Body after a successful submit |
54
+ | `source` | `website` | Sent through as `source` in the payload |
55
+ | `extra-payload` | — | JSON object merged into the submit payload |
56
+
57
+ Events (bubbling and composed, so they cross the shadow boundary):
58
+
59
+ ```js
60
+ const el = document.querySelector('vmc-online-lead')
61
+ el.addEventListener('vmc-lead-success', (e) => console.log(e.detail.response))
62
+ el.addEventListener('vmc-lead-error', (e) => console.log(e.detail.error))
63
+ ```
64
+
65
+ ## Environments
66
+
67
+ The base URL comes from `VITE_API_URL`, read at **build** time and inlined into
68
+ `dist/vmc-web-components.js`:
69
+
70
+ | File | Used by | Value |
71
+ | --- | --- | --- |
72
+ | `.env.development` | `npm run dev`, `npm run build:dev` | `http://localhost:3000/api` |
73
+ | `.env.production` | `npm run build` (what gets published) | `https://vmcrm-api.codingblocks.com/api` |
74
+
75
+ `npm run build` always builds in production mode, so `npm publish` can only ever
76
+ ship the production base. The build refuses to start if `VITE_API_URL` is
77
+ missing or isn't a URL, so a bundle pointing nowhere can't be produced.
78
+
79
+ Changing the base means a rebuild and a republish — that's the point: embedding
80
+ pages carry no configuration and can't drift.
81
+
82
+ ## Endpoints
83
+
84
+ Defaults are relative to the compiled-in base. Each is overridable per element with the
85
+ matching attribute, so a component can be repointed without a rebuild.
86
+
87
+ | Purpose | Default path | Attribute |
88
+ | --- | --- | --- |
89
+ | Classes | `GET /lookups/form-classes` | `classes-path` |
90
+ | Courses | `GET /lookups/form-courses` | `courses-path` |
91
+ | States | `GET /lookups/states` | `states-path` |
92
+ | Cities | `GET /lookups/cities` | `cities-path` |
93
+ | Send OTP | `POST /auth/login/send-otp` | `otp-send-path` |
94
+ | Verify OTP | `POST /auth/login/verify-otp` | `otp-verify-path` |
95
+ | Submit form | `POST /forms/vmc-online-lead` | `submit-path` |
96
+
97
+ **Dependent lookups** pass a Prisma-shaped `where` clause as the `filter` query
98
+ param, matching the CRM's lookup controller:
99
+
100
+ ```
101
+ GET /lookups/form-courses?filter={"form_class_id":6}
102
+ GET /lookups/cities?filter={"state_id":2}
103
+ ```
104
+
105
+ Lookup responses may be either a bare array or `{ data: [...] }`; both are
106
+ accepted. Items are `{ id, name }`.
107
+
108
+ **OTP flow** (same shape as the CRM login flow):
109
+
110
+ ```
111
+ POST {otp-send-path} { mobile_number, dial_code } -> { otp_id }
112
+ POST {otp-verify-path} { mobile_number, dial_code, otp_id, otp } -> 2xx
113
+ ```
114
+
115
+ **Submit payload**:
116
+
117
+ ```json
118
+ {
119
+ "name": "…",
120
+ "email": "…",
121
+ "dial_code": "+91",
122
+ "mobile": "9999999999",
123
+ "class_id": 6,
124
+ "course_id": 61,
125
+ "state_id": 2,
126
+ "city_id": 21,
127
+ "otp_id": "…",
128
+ "mobile_verified": true,
129
+ "consent": true,
130
+ "source": "website",
131
+ "page_url": "https://…"
132
+ }
133
+ ```
134
+
135
+ Errors are read from `{ error }` (falling back to `{ message }`) on any non-2xx
136
+ response, so backend validation messages surface directly in the form.
137
+
138
+ ## Theming
139
+
140
+ Override the CSS custom properties on the element — they pierce the shadow root:
141
+
142
+ ```html
143
+ <style>
144
+ vmc-online-lead {
145
+ --vmc-primary: #3b3663;
146
+ --vmc-heading: #322c78;
147
+ --vmc-font: 'Poppins', sans-serif;
148
+ --vmc-max-width: 560px;
149
+ --vmc-card-radius: 24px;
150
+ --vmc-field-height: 56px;
151
+ }
152
+ </style>
153
+ ```
154
+
155
+ The component does not load webfonts. If the page wants Poppins (the VMC look),
156
+ include it in the host page and it will be inherited.
157
+
158
+ ## Development
159
+
160
+ ```bash
161
+ npm install
162
+ npm run dev # http://localhost:5173/playground/index.html (.env.development)
163
+ npm run build # production bundle + dist/types (.env.production)
164
+ npm run build:dev # same bundle, pointed at localhost — for testing dist/ locally
165
+ npm run typecheck
166
+ ```
167
+
168
+ `playground/index.html` mounts every component and logs every request plus the
169
+ events it fires. Two controls at the top:
170
+
171
+ - **API base override** — leave empty to use the compiled-in
172
+ `.env.development` base; fill it in to point the component elsewhere.
173
+ - **Mode**:
174
+ - `hybrid` (default) — classes, courses and the real OTP flow hit the backend;
175
+ `/lookups/states`, `/lookups/cities` and `POST /forms/vmc-online-lead` are
176
+ stubbed, because the first two sit behind `requireAuth` and the third isn't
177
+ written yet.
178
+ - `live` — nothing is stubbed.
179
+ - `mock` — every endpoint is stubbed, no backend needed. OTP is `123456`.
180
+
181
+ Both can be driven from the URL: `?api=…&mode=mock`.
182
+
183
+ > The OTP defaults point at `/auth/login/send-otp` + `/auth/login/verify-otp`,
184
+ > which only accept mobiles that already belong to a registered CRM user —
185
+ > an unknown number gets `404 No account found for this mobile`. A public lead
186
+ > form will eventually need its own pair; point `otp-send-path` /
187
+ > `otp-verify-path` at them when they exist.
188
+
189
+ ## Adding a component
190
+
191
+ 1. Write the React component in `src/components/`, taking `{ props, ctx }`.
192
+ 2. Register it in `src/index.ts` via `defineElement('vmc-…', Component, attrs)`,
193
+ listing the attributes that should trigger a re-render.
194
+ 3. Mount it in `playground/index.html`.
195
+
196
+ It lands in the same single bundle automatically.
197
+
198
+ ## Publishing
199
+
200
+ ```bash
201
+ npm version patch
202
+ npm publish # runs the build via prepublishOnly
203
+ ```
204
+
205
+ Published under the `@coding-blocks` scope with public access.
@@ -0,0 +1,2 @@
1
+ import type { WebComponentProps } from '../lib/defineElement';
2
+ export declare const OnlineLead: ({ props, ctx }: WebComponentProps) => import("react").JSX.Element;
@@ -0,0 +1,34 @@
1
+ import type { LookupItem } from '../lib/lookups';
2
+ type InputProps = {
3
+ name: string;
4
+ placeholder: string;
5
+ value: string;
6
+ onChange: (value: string) => void;
7
+ type?: string;
8
+ inputMode?: 'text' | 'numeric' | 'tel' | 'email';
9
+ maxLength?: number;
10
+ autoComplete?: string;
11
+ disabled?: boolean;
12
+ invalid?: boolean;
13
+ };
14
+ export declare const Input: ({ name, placeholder, value, onChange, type, inputMode, maxLength, autoComplete, disabled, invalid, }: InputProps) => import("react").JSX.Element;
15
+ type SelectProps = {
16
+ name: string;
17
+ placeholder: string;
18
+ value: string;
19
+ onChange: (value: string) => void;
20
+ options: LookupItem[];
21
+ disabled?: boolean;
22
+ invalid?: boolean;
23
+ className?: string;
24
+ };
25
+ export declare const Select: ({ name, placeholder, value, onChange, options, disabled, invalid, className, }: SelectProps) => import("react").JSX.Element;
26
+ type OtpInputProps = {
27
+ length: number;
28
+ value: string;
29
+ onChange: (value: string) => void;
30
+ disabled?: boolean;
31
+ };
32
+ /** Six single-character boxes that behave like one field (paste, arrows, backspace). */
33
+ export declare const OtpInput: ({ length, value, onChange, disabled }: OtpInputProps) => import("react").JSX.Element;
34
+ export {};
@@ -0,0 +1,2 @@
1
+ export { defineElement } from './lib/defineElement';
2
+ export { API_BASE, DEFAULT_ENDPOINTS } from './lib/config';
@@ -0,0 +1,10 @@
1
+ export declare class ApiError extends Error {
2
+ status: number;
3
+ constructor(message: string, status: number);
4
+ }
5
+ export type Api = {
6
+ get: <T>(path: string, params?: Record<string, string | undefined>) => Promise<T>;
7
+ post: <T>(path: string, body?: unknown) => Promise<T>;
8
+ };
9
+ export declare const createApi: (base: string) => Api;
10
+ export declare const errorMessage: (err: unknown) => string;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Base URL of the CRM API, compiled in from VITE_API_URL at build time
3
+ * (.env.development / .env.production). Embedding pages don't pass it; the
4
+ * optional `api-base` attribute exists only as an escape hatch for testing.
5
+ */
6
+ export declare const API_BASE: string;
7
+ export type Endpoints = {
8
+ classesPath: string;
9
+ coursesPath: string;
10
+ statesPath: string;
11
+ citiesPath: string;
12
+ otpSendPath: string;
13
+ otpVerifyPath: string;
14
+ submitPath: string;
15
+ };
16
+ export declare const DEFAULT_ENDPOINTS: Endpoints;
17
+ export declare const OTP_LENGTH = 6;
18
+ export declare const DIAL_CODES: string[];
19
+ export declare const resolveEndpoints: (props: Record<string, string | undefined>) => Endpoints;
@@ -0,0 +1,18 @@
1
+ import type { ComponentType } from 'react';
2
+ export type ElementProps = Record<string, string | undefined>;
3
+ export type HostContext = {
4
+ /** Fire a bubbling, composed DOM event on the host element. */
5
+ emit: (name: string, detail?: unknown) => void;
6
+ host: HTMLElement;
7
+ };
8
+ export type WebComponentProps = {
9
+ props: ElementProps;
10
+ ctx: HostContext;
11
+ };
12
+ /**
13
+ * Wraps a React component as a custom element. The UI renders inside a shadow
14
+ * root with the stylesheet inlined, so a host page's CSS can never leak in and
15
+ * ours can never leak out. Attributes arrive as camelCased props and re-render
16
+ * on change.
17
+ */
18
+ export declare const defineElement: (tag: string, Component: ComponentType<WebComponentProps>, observedAttributes: string[]) => void;
@@ -0,0 +1,13 @@
1
+ import type { Api } from './api';
2
+ export type LookupItem = {
3
+ id: number | string;
4
+ name: string | null;
5
+ };
6
+ /**
7
+ * Fetches a dropdown list once per (path, filter) pair. `filter` is passed
8
+ * through as the CRM's Prisma-shaped `filter` query param.
9
+ */
10
+ export declare const useLookup: (api: Api, path: string, filter?: Record<string, unknown>, enabled?: boolean) => {
11
+ items: LookupItem[];
12
+ loading: boolean;
13
+ };
@@ -0,0 +1,32 @@
1
+ import { type Api } from './api';
2
+ export type VerificationStatus = 'idle' | 'sending' | 'sent' | 'verifying' | 'verified';
3
+ type Options = {
4
+ api: Api;
5
+ sendPath: string;
6
+ verifyPath: string;
7
+ dialCode: string;
8
+ mobile: string;
9
+ resendSeconds?: number;
10
+ };
11
+ /**
12
+ * OTP mobile verification, following the CRM login flow:
13
+ * POST sendPath { mobile_number, dial_code } -> { otp_id }
14
+ * POST verifyPath { mobile_number, otp_id, otp, … } -> 2xx
15
+ *
16
+ * Verification is bound to the exact number that was verified — editing the
17
+ * mobile field drops back to `idle` so a form can never be submitted with an
18
+ * OTP that belongs to a different number.
19
+ */
20
+ export declare const useMobileVerification: ({ api, sendPath, verifyPath, dialCode, mobile, resendSeconds, }: Options) => {
21
+ status: VerificationStatus;
22
+ otp: string;
23
+ setOtp: import("react").Dispatch<import("react").SetStateAction<string>>;
24
+ otpId: string;
25
+ error: string | null;
26
+ resendIn: number;
27
+ sendOtp: () => Promise<boolean>;
28
+ verifyOtp: () => Promise<boolean>;
29
+ isVerified: boolean;
30
+ isPending: boolean;
31
+ };
32
+ export {};