@bharat-ui/react 0.1.4 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bharat-ui/react",
3
- "version": "0.1.4",
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
- "./UPIButton": "./src/UPIButton.tsx"
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.4",
54
- "@bharat-ui/data": "0.1.3"
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
+ }
@@ -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 && result.meta && (
79
- <div
80
- style={{
81
- display: "flex",
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
- <div
128
- style={{
129
- display: "flex",
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
- {showCascade && result && !result.valid && value.length === 6 && (
143
- <span style={{ fontSize: "12px", color: "#dc2626" }}>
144
- {result.error}
145
- </span>
146
- )}
100
+ {error && <span style={{ fontSize: "12px", color: "#dc2626" }}>{error}</span>}
101
+ </div>
102
+ );
103
+ }
147
104
 
148
- {error && (
149
- <span style={{ fontSize: "12px", color: "#dc2626" }}>{error}</span>
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
  }
@@ -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>;