@bharat-ui/react 0.1.3 → 0.1.5
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/package.json +14 -4
- package/src/IFSCInput.tsx +128 -0
- package/src/PincodeInput.tsx +53 -83
- package/src/resolvers.ts +21 -0
- package/src/types.ts +19 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bharat-ui/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"./OTPInput": "./src/OTPInput.tsx",
|
|
15
15
|
"./PANInput": "./src/PANInput.tsx",
|
|
16
16
|
"./PincodeInput": "./src/PincodeInput.tsx",
|
|
17
|
-
"./
|
|
17
|
+
"./IFSCInput": "./src/IFSCInput.tsx",
|
|
18
|
+
"./UPIButton": "./src/UPIButton.tsx",
|
|
19
|
+
"./resolvers": "./src/resolvers.ts"
|
|
18
20
|
},
|
|
19
21
|
"publishConfig": {
|
|
20
22
|
"exports": {
|
|
@@ -34,9 +36,17 @@
|
|
|
34
36
|
"import": "./dist/PincodeInput.js",
|
|
35
37
|
"types": "./dist/PincodeInput.d.ts"
|
|
36
38
|
},
|
|
39
|
+
"./IFSCInput": {
|
|
40
|
+
"import": "./dist/IFSCInput.js",
|
|
41
|
+
"types": "./dist/IFSCInput.d.ts"
|
|
42
|
+
},
|
|
37
43
|
"./UPIButton": {
|
|
38
44
|
"import": "./dist/UPIButton.js",
|
|
39
45
|
"types": "./dist/UPIButton.d.ts"
|
|
46
|
+
},
|
|
47
|
+
"./resolvers": {
|
|
48
|
+
"import": "./dist/resolvers.js",
|
|
49
|
+
"types": "./dist/resolvers.d.ts"
|
|
40
50
|
}
|
|
41
51
|
},
|
|
42
52
|
"files": [
|
|
@@ -50,8 +60,8 @@
|
|
|
50
60
|
"build": "tsc"
|
|
51
61
|
},
|
|
52
62
|
"dependencies": {
|
|
53
|
-
"@bharat-ui/validators": "0.1.
|
|
54
|
-
"@bharat-ui/data": "0.1.
|
|
63
|
+
"@bharat-ui/validators": "0.1.5",
|
|
64
|
+
"@bharat-ui/data": "0.1.4"
|
|
55
65
|
},
|
|
56
66
|
"devDependencies": {
|
|
57
67
|
"@bharat-ui/eslint-config": "*",
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { validateIFSC } from "@bharat-ui/validators";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import type { IFSCData, IFSCResolver } from "./types";
|
|
6
|
+
|
|
7
|
+
export type { IFSCData, IFSCResolver };
|
|
8
|
+
|
|
9
|
+
export interface IFSCInputProps {
|
|
10
|
+
value?: string;
|
|
11
|
+
onChange?: (value: string, valid: boolean) => void;
|
|
12
|
+
label?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
showDetails?: boolean;
|
|
16
|
+
resolver?: IFSCResolver;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function IFSCInput({
|
|
20
|
+
value = "",
|
|
21
|
+
onChange,
|
|
22
|
+
label,
|
|
23
|
+
error,
|
|
24
|
+
disabled = false,
|
|
25
|
+
showDetails = true,
|
|
26
|
+
resolver,
|
|
27
|
+
}: IFSCInputProps) {
|
|
28
|
+
const [focused, setFocused] = React.useState(false);
|
|
29
|
+
const [resolvedData, setResolvedData] = React.useState<IFSCData | null>(null);
|
|
30
|
+
const [resolving, setResolving] = React.useState(false);
|
|
31
|
+
|
|
32
|
+
const result = value.length === 11 ? validateIFSC(value) : null;
|
|
33
|
+
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
if (!resolver || !result?.valid) {
|
|
36
|
+
setResolvedData(null);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let cancelled = false;
|
|
40
|
+
setResolving(true);
|
|
41
|
+
resolver(value).then((data) => {
|
|
42
|
+
if (!cancelled) {
|
|
43
|
+
setResolvedData(data);
|
|
44
|
+
setResolving(false);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return () => { cancelled = true; };
|
|
48
|
+
}, [value, resolver, result?.valid]);
|
|
49
|
+
|
|
50
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
51
|
+
const cleaned = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 11);
|
|
52
|
+
onChange?.(cleaned, cleaned.length === 11 ? validateIFSC(cleaned).valid : false);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
|
57
|
+
{label && (
|
|
58
|
+
<label style={{ fontSize: "13px", fontWeight: 500, color: error ? "#dc2626" : "#6b7280" }}>
|
|
59
|
+
{label}
|
|
60
|
+
</label>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
<input
|
|
64
|
+
type="text"
|
|
65
|
+
value={value}
|
|
66
|
+
onChange={handleChange}
|
|
67
|
+
onFocus={() => setFocused(true)}
|
|
68
|
+
onBlur={() => setFocused(false)}
|
|
69
|
+
disabled={disabled}
|
|
70
|
+
maxLength={11}
|
|
71
|
+
placeholder="SBIN0001234"
|
|
72
|
+
style={{
|
|
73
|
+
border: `1.5px solid ${error ? "#dc2626" : focused ? "#B45309" : "#d1d5db"}`,
|
|
74
|
+
borderRadius: "8px",
|
|
75
|
+
padding: "10px 12px",
|
|
76
|
+
fontSize: "15px",
|
|
77
|
+
fontFamily: "var(--font-mono, monospace)",
|
|
78
|
+
letterSpacing: "0.1em",
|
|
79
|
+
outline: "none",
|
|
80
|
+
background: disabled ? "#f9fafb" : "#ffffff",
|
|
81
|
+
color: "#111827",
|
|
82
|
+
width: "100%",
|
|
83
|
+
boxSizing: "border-box",
|
|
84
|
+
boxShadow: focused ? "0 0 0 3px rgba(180,83,9,0.12)" : "none",
|
|
85
|
+
transition: "all 0.15s ease",
|
|
86
|
+
textTransform: "uppercase",
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
{showDetails && result?.valid && resolver && (
|
|
91
|
+
resolving ? (
|
|
92
|
+
<div style={{ padding: "10px 12px", background: "#f9fafb", borderRadius: "8px", border: "0.5px solid #e5e7eb", fontSize: "12px", color: "#9ca3af" }}>
|
|
93
|
+
Looking up...
|
|
94
|
+
</div>
|
|
95
|
+
) : resolvedData ? (
|
|
96
|
+
<BankCard data={resolvedData} />
|
|
97
|
+
) : null
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{result && !result.valid && value.length === 11 && !error && (
|
|
101
|
+
<span style={{ fontSize: "12px", color: "#dc2626" }}>{result.error}</span>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{error && <span style={{ fontSize: "12px", color: "#dc2626" }}>{error}</span>}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function BankCard({ data }: { data: IFSCData }) {
|
|
110
|
+
const rows = [
|
|
111
|
+
{ label: "Bank", value: data.bank },
|
|
112
|
+
data.branch ? { label: "Branch", value: data.branch } : null,
|
|
113
|
+
data.city ? { label: "City", value: data.city } : null,
|
|
114
|
+
data.state ? { label: "State", value: data.state } : null,
|
|
115
|
+
data.address ? { label: "Address", value: data.address } : null,
|
|
116
|
+
].filter(Boolean) as { label: string; value: string }[];
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "6px", padding: "10px 12px", background: "#f9fafb", borderRadius: "8px", border: "0.5px solid #e5e7eb", marginTop: "2px" }}>
|
|
120
|
+
{rows.map((row) => (
|
|
121
|
+
<div key={row.label} style={{ display: "flex", justifyContent: "space-between", fontSize: "13px", gap: "12px" }}>
|
|
122
|
+
<span style={{ color: "#6b7280", flexShrink: 0 }}>{row.label}</span>
|
|
123
|
+
<span style={{ fontWeight: 500, color: "#111827", textAlign: "right" }}>{row.value}</span>
|
|
124
|
+
</div>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
package/src/PincodeInput.tsx
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { validatePincode } from "@bharat-ui/validators";
|
|
4
4
|
import * as React from "react";
|
|
5
|
+
import type { PincodeData, PincodeResolver } from "./types";
|
|
6
|
+
|
|
7
|
+
export type { PincodeData, PincodeResolver };
|
|
5
8
|
|
|
6
9
|
export interface PincodeInputProps {
|
|
7
10
|
value?: string;
|
|
@@ -10,6 +13,7 @@ export interface PincodeInputProps {
|
|
|
10
13
|
error?: string;
|
|
11
14
|
disabled?: boolean;
|
|
12
15
|
showCascade?: boolean;
|
|
16
|
+
resolver?: PincodeResolver;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export function PincodeInput({
|
|
@@ -19,29 +23,39 @@ export function PincodeInput({
|
|
|
19
23
|
error,
|
|
20
24
|
disabled = false,
|
|
21
25
|
showCascade = true,
|
|
26
|
+
resolver,
|
|
22
27
|
}: PincodeInputProps) {
|
|
23
28
|
const [focused, setFocused] = React.useState(false);
|
|
29
|
+
const [resolvedData, setResolvedData] = React.useState<PincodeData | null>(null);
|
|
30
|
+
const [resolving, setResolving] = React.useState(false);
|
|
24
31
|
|
|
25
32
|
const result = value.length === 6 ? validatePincode(value) : null;
|
|
26
33
|
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
if (!resolver || !result?.valid) {
|
|
36
|
+
setResolvedData(null);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let cancelled = false;
|
|
40
|
+
setResolving(true);
|
|
41
|
+
resolver(value).then((data) => {
|
|
42
|
+
if (!cancelled) {
|
|
43
|
+
setResolvedData(data);
|
|
44
|
+
setResolving(false);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return () => { cancelled = true; };
|
|
48
|
+
}, [value, resolver, result?.valid]);
|
|
49
|
+
|
|
27
50
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
28
51
|
const cleaned = e.target.value.replace(/\D/g, "").slice(0, 6);
|
|
29
|
-
onChange?.(
|
|
30
|
-
cleaned,
|
|
31
|
-
cleaned.length === 6 ? validatePincode(cleaned).valid : false,
|
|
32
|
-
);
|
|
52
|
+
onChange?.(cleaned, cleaned.length === 6 ? validatePincode(cleaned).valid : false);
|
|
33
53
|
};
|
|
34
54
|
|
|
35
55
|
return (
|
|
36
56
|
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
|
37
57
|
{label && (
|
|
38
|
-
<label
|
|
39
|
-
style={{
|
|
40
|
-
fontSize: "13px",
|
|
41
|
-
fontWeight: 500,
|
|
42
|
-
color: error ? "#dc2626" : "#6b7280",
|
|
43
|
-
}}
|
|
44
|
-
>
|
|
58
|
+
<label style={{ fontSize: "13px", fontWeight: 500, color: error ? "#dc2626" : "#6b7280" }}>
|
|
45
59
|
{label}
|
|
46
60
|
</label>
|
|
47
61
|
)}
|
|
@@ -57,9 +71,7 @@ export function PincodeInput({
|
|
|
57
71
|
maxLength={6}
|
|
58
72
|
placeholder="390001"
|
|
59
73
|
style={{
|
|
60
|
-
border: `1.5px solid ${
|
|
61
|
-
error ? "#dc2626" : focused ? "#B45309" : "#d1d5db"
|
|
62
|
-
}`,
|
|
74
|
+
border: `1.5px solid ${error ? "#dc2626" : focused ? "#B45309" : "#d1d5db"}`,
|
|
63
75
|
borderRadius: "8px",
|
|
64
76
|
padding: "10px 12px",
|
|
65
77
|
fontSize: "15px",
|
|
@@ -75,79 +87,37 @@ export function PincodeInput({
|
|
|
75
87
|
}}
|
|
76
88
|
/>
|
|
77
89
|
|
|
78
|
-
{showCascade && result?.valid &&
|
|
79
|
-
|
|
80
|
-
style={{
|
|
81
|
-
|
|
82
|
-
flexDirection: "column",
|
|
83
|
-
gap: "6px",
|
|
84
|
-
padding: "10px 12px",
|
|
85
|
-
background: "#f9fafb",
|
|
86
|
-
borderRadius: "8px",
|
|
87
|
-
border: "0.5px solid #e5e7eb",
|
|
88
|
-
marginTop: "2px",
|
|
89
|
-
}}
|
|
90
|
-
>
|
|
91
|
-
<div
|
|
92
|
-
style={{
|
|
93
|
-
display: "flex",
|
|
94
|
-
justifyContent: "space-between",
|
|
95
|
-
fontSize: "13px",
|
|
96
|
-
}}
|
|
97
|
-
>
|
|
98
|
-
<span style={{ color: "#6b7280" }}>District</span>
|
|
99
|
-
<span style={{ fontWeight: 500, color: "#111827" }}>
|
|
100
|
-
{result.meta.district}
|
|
101
|
-
</span>
|
|
102
|
-
</div>
|
|
103
|
-
<div
|
|
104
|
-
style={{
|
|
105
|
-
display: "flex",
|
|
106
|
-
justifyContent: "space-between",
|
|
107
|
-
fontSize: "13px",
|
|
108
|
-
}}
|
|
109
|
-
>
|
|
110
|
-
<span style={{ color: "#6b7280" }}>State</span>
|
|
111
|
-
<span style={{ fontWeight: 500, color: "#111827" }}>
|
|
112
|
-
{result.meta.state}
|
|
113
|
-
</span>
|
|
114
|
-
</div>
|
|
115
|
-
<div
|
|
116
|
-
style={{
|
|
117
|
-
display: "flex",
|
|
118
|
-
justifyContent: "space-between",
|
|
119
|
-
fontSize: "13px",
|
|
120
|
-
}}
|
|
121
|
-
>
|
|
122
|
-
<span style={{ color: "#6b7280" }}>Zone</span>
|
|
123
|
-
<span style={{ fontWeight: 500, color: "#111827" }}>
|
|
124
|
-
{result.meta.zone}
|
|
125
|
-
</span>
|
|
90
|
+
{showCascade && result?.valid && resolver && (
|
|
91
|
+
resolving ? (
|
|
92
|
+
<div style={{ padding: "10px 12px", background: "#f9fafb", borderRadius: "8px", border: "0.5px solid #e5e7eb", fontSize: "12px", color: "#9ca3af" }}>
|
|
93
|
+
Looking up...
|
|
126
94
|
</div>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
justifyContent: "space-between",
|
|
131
|
-
fontSize: "13px",
|
|
132
|
-
}}
|
|
133
|
-
>
|
|
134
|
-
<span style={{ color: "#6b7280" }}>Head PO</span>
|
|
135
|
-
<span style={{ fontWeight: 500, color: "#111827" }}>
|
|
136
|
-
{result.meta.headPO}
|
|
137
|
-
</span>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
95
|
+
) : resolvedData ? (
|
|
96
|
+
<CascadeCard data={resolvedData} />
|
|
97
|
+
) : null
|
|
140
98
|
)}
|
|
141
99
|
|
|
142
|
-
{
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)}
|
|
100
|
+
{error && <span style={{ fontSize: "12px", color: "#dc2626" }}>{error}</span>}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
147
104
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
105
|
+
function CascadeCard({ data }: { data: PincodeData }) {
|
|
106
|
+
const rows = [
|
|
107
|
+
data.district ? { label: "District", value: data.district } : null,
|
|
108
|
+
{ label: "State", value: data.state },
|
|
109
|
+
data.zone ? { label: "Zone", value: data.zone } : null,
|
|
110
|
+
data.headPO ? { label: "Head PO", value: data.headPO } : null,
|
|
111
|
+
].filter(Boolean) as { label: string; value: string }[];
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "6px", padding: "10px 12px", background: "#f9fafb", borderRadius: "8px", border: "0.5px solid #e5e7eb", marginTop: "2px" }}>
|
|
115
|
+
{rows.map((row) => (
|
|
116
|
+
<div key={row.label} style={{ display: "flex", justifyContent: "space-between", fontSize: "13px" }}>
|
|
117
|
+
<span style={{ color: "#6b7280" }}>{row.label}</span>
|
|
118
|
+
<span style={{ fontWeight: 500, color: "#111827" }}>{row.value}</span>
|
|
119
|
+
</div>
|
|
120
|
+
))}
|
|
151
121
|
</div>
|
|
152
122
|
);
|
|
153
123
|
}
|
package/src/resolvers.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PINCODE_DATA, PINCODE_PREFIX } from "@bharat-ui/data/pincode";
|
|
4
|
+
import { IFSC_DATA } from "@bharat-ui/data/ifsc";
|
|
5
|
+
import type { PincodeData, IFSCData } from "./types";
|
|
6
|
+
|
|
7
|
+
// Bundled resolvers — backed by the small popular dataset in @bharat-ui/data.
|
|
8
|
+
// Bundle cost: ~8KB. Import only if you want offline/zero-config lookup.
|
|
9
|
+
// For full coverage swap with an API-backed resolver instead.
|
|
10
|
+
|
|
11
|
+
export const bundledPincodeResolver = async (pincode: string): Promise<PincodeData | null> => {
|
|
12
|
+
const entry = PINCODE_DATA[pincode];
|
|
13
|
+
if (entry) return entry;
|
|
14
|
+
const prefix = pincode.slice(0, 2);
|
|
15
|
+
const prefixEntry = PINCODE_PREFIX[prefix];
|
|
16
|
+
return prefixEntry ?? null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const bundledIFSCResolver = async (ifsc: string): Promise<IFSCData | null> => {
|
|
20
|
+
return IFSC_DATA[ifsc.toUpperCase()] ?? null;
|
|
21
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface PincodeData {
|
|
2
|
+
district?: string;
|
|
3
|
+
state: string;
|
|
4
|
+
zone?: string;
|
|
5
|
+
headPO?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type PincodeResolver = (pincode: string) => Promise<PincodeData | null>;
|
|
9
|
+
|
|
10
|
+
export interface IFSCData {
|
|
11
|
+
bank: string;
|
|
12
|
+
branch?: string;
|
|
13
|
+
address?: string;
|
|
14
|
+
city?: string;
|
|
15
|
+
state?: string;
|
|
16
|
+
contact?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type IFSCResolver = (ifsc: string) => Promise<IFSCData | null>;
|