@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 +205 -0
- package/dist/types/components/OnlineLead.d.ts +2 -0
- package/dist/types/components/ui.d.ts +34 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/lib/api.d.ts +10 -0
- package/dist/types/lib/config.d.ts +19 -0
- package/dist/types/lib/defineElement.d.ts +18 -0
- package/dist/types/lib/lookups.d.ts +13 -0
- package/dist/types/lib/useMobileVerification.d.ts +32 -0
- package/dist/vmc-web-components.js +50 -0
- package/dist/vmc-web-components.js.map +1 -0
- package/package.json +46 -0
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,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,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 {};
|