@bharat-ui/react 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 +159 -0
- package/package.json +38 -0
- package/src/AmountInput.tsx +131 -0
- package/src/OTPInput.tsx +202 -0
- package/src/PANInput.tsx +128 -0
- package/src/PincodeInput.tsx +153 -0
- package/src/UPIButton.tsx +233 -0
- package/src/button.tsx +20 -0
- package/src/card.tsx +27 -0
- package/src/code.tsx +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# @bharat-ui/react
|
|
2
|
+
|
|
3
|
+
React components for Indian products — PAN input, Aadhaar OTP, UPI payments, pincode lookup and ₹ amount entry.
|
|
4
|
+
Built on top of [`@bharat-ui/validators`](https://npmjs.com/package/@bharat-ui/validators). Requires React 19.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @bharat-ui/react
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Components
|
|
13
|
+
|
|
14
|
+
Import each component from its own subpath:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { AmountInput } from '@bharat-ui/react/AmountInput';
|
|
18
|
+
import { OTPInput } from '@bharat-ui/react/OTPInput';
|
|
19
|
+
import { PANInput } from '@bharat-ui/react/PANInput';
|
|
20
|
+
import { PincodeInput } from '@bharat-ui/react/PincodeInput';
|
|
21
|
+
import { UPIButton } from '@bharat-ui/react/UPIButton';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
### `<AmountInput />`
|
|
27
|
+
|
|
28
|
+
Currency input with live INR formatting. Displays ₹ formatted output while storing the raw number internally.
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
<AmountInput
|
|
32
|
+
label="Loan amount"
|
|
33
|
+
value={amount}
|
|
34
|
+
onChange={(raw, formatted) => setAmount(raw)}
|
|
35
|
+
/>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Prop | Type | Default |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `value` | `number \| undefined` | — |
|
|
41
|
+
| `onChange` | `(raw: number, formatted: string) => void` | — |
|
|
42
|
+
| `label` | `string` | — |
|
|
43
|
+
| `placeholder` | `string` | `'0'` |
|
|
44
|
+
| `error` | `string` | — |
|
|
45
|
+
| `disabled` | `boolean` | `false` |
|
|
46
|
+
| `showCompact` | `boolean` | `true` |
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### `<OTPInput />`
|
|
51
|
+
|
|
52
|
+
Individual-box OTP entry with auto-advance, backspace handling, paste support and a built-in resend countdown timer.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
<OTPInput
|
|
56
|
+
label="Enter OTP sent to +91 98765 43210"
|
|
57
|
+
length={6}
|
|
58
|
+
resendAfterSeconds={30}
|
|
59
|
+
onComplete={(otp) => verifyOTP(otp)}
|
|
60
|
+
onResend={() => requestNewOTP()}
|
|
61
|
+
/>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Prop | Type | Default |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `length` | `4 \| 6` | `6` |
|
|
67
|
+
| `label` | `string` | — |
|
|
68
|
+
| `onComplete` | `(otp: string) => void` | — |
|
|
69
|
+
| `onChange` | `(otp: string) => void` | — |
|
|
70
|
+
| `resendAfterSeconds` | `number` | `30` |
|
|
71
|
+
| `onResend` | `() => void` | — |
|
|
72
|
+
| `error` | `string` | — |
|
|
73
|
+
| `disabled` | `boolean` | `false` |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### `<PANInput />`
|
|
78
|
+
|
|
79
|
+
Text input that auto-uppercases, validates PAN format on each keystroke, and shows the taxpayer type badge on valid input.
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
<PANInput
|
|
83
|
+
label="PAN number"
|
|
84
|
+
value={pan}
|
|
85
|
+
onChange={(value, valid) => setPan(value)}
|
|
86
|
+
/>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| Prop | Type | Default |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `value` | `string` | `''` |
|
|
92
|
+
| `onChange` | `(value: string, valid: boolean) => void` | — |
|
|
93
|
+
| `label` | `string` | — |
|
|
94
|
+
| `error` | `string` | — |
|
|
95
|
+
| `disabled` | `boolean` | `false` |
|
|
96
|
+
| `showTypeBadge` | `boolean` | `true` |
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### `<PincodeInput />`
|
|
101
|
+
|
|
102
|
+
Numeric input that looks up the 6-digit pincode in the India Post dataset and shows the resolved district and state on valid input — no network calls.
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<PincodeInput
|
|
106
|
+
label="Pincode"
|
|
107
|
+
value={pincode}
|
|
108
|
+
onChange={(value, valid) => setPincode(value)}
|
|
109
|
+
/>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| Prop | Type | Default |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `value` | `string` | `''` |
|
|
115
|
+
| `onChange` | `(value: string, valid: boolean) => void` | — |
|
|
116
|
+
| `label` | `string` | — |
|
|
117
|
+
| `error` | `string` | — |
|
|
118
|
+
| `disabled` | `boolean` | `false` |
|
|
119
|
+
| `showCascade` | `boolean` | `true` |
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### `<UPIButton />`
|
|
124
|
+
|
|
125
|
+
Renders a row of UPI app buttons (GPay, PhonePe, Paytm, BHIM) that each generate a deep-link payment URI. No SDK or backend required.
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
<UPIButton
|
|
129
|
+
vpa="merchant@okaxis"
|
|
130
|
+
amount={499}
|
|
131
|
+
merchantName="Acme Store"
|
|
132
|
+
transactionNote="Order #1234"
|
|
133
|
+
/>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
| Prop | Type | Default |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| `vpa` | `string` | — |
|
|
139
|
+
| `amount` | `number` | — |
|
|
140
|
+
| `merchantName` | `string` | — |
|
|
141
|
+
| `transactionNote` | `string` | — |
|
|
142
|
+
| `currency` | `string` | `'INR'` |
|
|
143
|
+
| `onSuccess` | `() => void` | — |
|
|
144
|
+
| `onError` | `(error: string) => void` | — |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Peer dependencies
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"react": "^19.0.0",
|
|
153
|
+
"react-dom": "^19.0.0"
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bharat-ui/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"exports": {
|
|
6
|
+
"./AmountInput": "./src/AmountInput.tsx",
|
|
7
|
+
"./OTPInput": "./src/OTPInput.tsx",
|
|
8
|
+
"./PANInput": "./src/PANInput.tsx",
|
|
9
|
+
"./PincodeInput": "./src/PincodeInput.tsx",
|
|
10
|
+
"./UPIButton": "./src/UPIButton.tsx"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"lint": "eslint . --max-warnings 0",
|
|
14
|
+
"check-types": "tsc --noEmit",
|
|
15
|
+
"build": "tsc"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@bharat-ui/validators": "0.1.0",
|
|
19
|
+
"@bharat-ui/data": "0.1.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@bharat-ui/eslint-config": "*",
|
|
23
|
+
"@bharat-ui/typescript-config": "*",
|
|
24
|
+
"@types/node": "^22.15.3",
|
|
25
|
+
"@types/react": "19.2.2",
|
|
26
|
+
"@types/react-dom": "19.2.2",
|
|
27
|
+
"eslint": "^9.39.1",
|
|
28
|
+
"typescript": "5.9.2"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": "^19.0.0",
|
|
36
|
+
"react-dom": "^19.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { formatINR, formatINRCompact, parseINR } from "@bharat-ui/validators";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
export interface AmountInputProps {
|
|
7
|
+
value?: number;
|
|
8
|
+
onChange?: (raw: number, formatted: string) => void;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
label?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
showCompact?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AmountInput({
|
|
17
|
+
value,
|
|
18
|
+
onChange,
|
|
19
|
+
placeholder = "0",
|
|
20
|
+
disabled = false,
|
|
21
|
+
label,
|
|
22
|
+
error,
|
|
23
|
+
showCompact = true,
|
|
24
|
+
}: AmountInputProps) {
|
|
25
|
+
const [display, setDisplay] = React.useState(value ? formatINR(value) : "");
|
|
26
|
+
const [focused, setFocused] = React.useState(false);
|
|
27
|
+
|
|
28
|
+
const handleFocus = () => {
|
|
29
|
+
setFocused(true);
|
|
30
|
+
if (display) {
|
|
31
|
+
setDisplay(String(parseINR(display)));
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handleBlur = () => {
|
|
36
|
+
setFocused(false);
|
|
37
|
+
const raw = parseINR(display);
|
|
38
|
+
if (!isNaN(raw) && raw > 0) {
|
|
39
|
+
setDisplay(formatINR(raw));
|
|
40
|
+
onChange?.(raw, formatINR(raw));
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
45
|
+
const val = e.target.value.replace(/[^\d]/g, "");
|
|
46
|
+
setDisplay(val);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const raw = parseINR(display);
|
|
50
|
+
const compact = !isNaN(raw) && raw > 0 ? formatINRCompact(raw) : null;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
|
54
|
+
{label && (
|
|
55
|
+
<label
|
|
56
|
+
style={{
|
|
57
|
+
fontSize: "13px",
|
|
58
|
+
fontWeight: 500,
|
|
59
|
+
color: error ? "#dc2626" : "#6b7280",
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{label}
|
|
63
|
+
</label>
|
|
64
|
+
)}
|
|
65
|
+
<div
|
|
66
|
+
style={{
|
|
67
|
+
display: "flex",
|
|
68
|
+
alignItems: "center",
|
|
69
|
+
border: `1.5px solid ${error ? "#dc2626" : focused ? "#B45309" : "#d1d5db"}`,
|
|
70
|
+
borderRadius: "8px",
|
|
71
|
+
padding: "0 12px",
|
|
72
|
+
gap: "6px",
|
|
73
|
+
background: disabled ? "#f9fafb" : "#ffffff",
|
|
74
|
+
boxShadow: focused ? "0 0 0 3px rgba(180,83,9,0.12)" : "none",
|
|
75
|
+
transition: "all 0.15s ease",
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<span
|
|
79
|
+
style={{
|
|
80
|
+
fontSize: "16px",
|
|
81
|
+
fontWeight: 500,
|
|
82
|
+
color: "#6b7280",
|
|
83
|
+
userSelect: "none",
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
₹
|
|
87
|
+
</span>
|
|
88
|
+
<input
|
|
89
|
+
type="text"
|
|
90
|
+
inputMode="numeric"
|
|
91
|
+
value={display}
|
|
92
|
+
placeholder={placeholder}
|
|
93
|
+
disabled={disabled}
|
|
94
|
+
onChange={handleChange}
|
|
95
|
+
onFocus={handleFocus}
|
|
96
|
+
onBlur={handleBlur}
|
|
97
|
+
style={{
|
|
98
|
+
border: "none",
|
|
99
|
+
outline: "none",
|
|
100
|
+
fontSize: "16px",
|
|
101
|
+
fontWeight: 500,
|
|
102
|
+
padding: "10px 0",
|
|
103
|
+
width: "100%",
|
|
104
|
+
background: "transparent",
|
|
105
|
+
color: "#111827",
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
{showCompact && compact && !focused && (
|
|
110
|
+
<span
|
|
111
|
+
style={{
|
|
112
|
+
fontSize: "12px",
|
|
113
|
+
color: "#6b7280",
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
{compact}
|
|
117
|
+
</span>
|
|
118
|
+
)}
|
|
119
|
+
{error && (
|
|
120
|
+
<span
|
|
121
|
+
style={{
|
|
122
|
+
fontSize: "12px",
|
|
123
|
+
color: "#dc2626",
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
{error}
|
|
127
|
+
</span>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
package/src/OTPInput.tsx
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
export interface OTPInputProps {
|
|
6
|
+
length?: 4 | 6;
|
|
7
|
+
onComplete?: (otp: string) => void;
|
|
8
|
+
onChange?: (otp: string) => void;
|
|
9
|
+
resendAfterSeconds?: number;
|
|
10
|
+
onResend?: () => void;
|
|
11
|
+
error?: string;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
label?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function OTPInput({
|
|
17
|
+
length = 6,
|
|
18
|
+
onComplete,
|
|
19
|
+
onChange,
|
|
20
|
+
resendAfterSeconds = 30,
|
|
21
|
+
onResend,
|
|
22
|
+
error,
|
|
23
|
+
disabled = false,
|
|
24
|
+
label,
|
|
25
|
+
}: OTPInputProps) {
|
|
26
|
+
const [values, setValues] = React.useState<string[]>(Array(length).fill(""));
|
|
27
|
+
const [seconds, setSeconds] = React.useState(resendAfterSeconds);
|
|
28
|
+
const [focused, setFocused] = React.useState<number | null>(null);
|
|
29
|
+
const refs = React.useRef<(HTMLInputElement | null)[]>([]);
|
|
30
|
+
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
if (seconds <= 0) return;
|
|
33
|
+
const timer = setTimeout(() => setSeconds((s) => s - 1), 1000);
|
|
34
|
+
return () => clearTimeout(timer);
|
|
35
|
+
}, [seconds]);
|
|
36
|
+
|
|
37
|
+
const handleChange = (index: number, val: string) => {
|
|
38
|
+
const digit = val.replace(/\D/g, "").slice(-1);
|
|
39
|
+
const next = [...values];
|
|
40
|
+
next[index] = digit;
|
|
41
|
+
setValues(next);
|
|
42
|
+
|
|
43
|
+
const otp = next.join("");
|
|
44
|
+
onChange?.(otp);
|
|
45
|
+
|
|
46
|
+
if (digit && index < length - 1) {
|
|
47
|
+
refs.current[index + 1]?.focus();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (next.every((v) => v !== "") && otp.length === length) {
|
|
51
|
+
onComplete?.(otp);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleKeyDown = (
|
|
56
|
+
index: number,
|
|
57
|
+
e: React.KeyboardEvent<HTMLInputElement>,
|
|
58
|
+
) => {
|
|
59
|
+
if (e.key === "Backspace") {
|
|
60
|
+
if (values[index]) {
|
|
61
|
+
const next = [...values];
|
|
62
|
+
next[index] = "";
|
|
63
|
+
setValues(next);
|
|
64
|
+
onChange?.(next.join(""));
|
|
65
|
+
} else if (index > 0) {
|
|
66
|
+
refs.current[index - 1]?.focus();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
const pasted = e.clipboardData
|
|
74
|
+
.getData("text")
|
|
75
|
+
.replace(/\D/g, "")
|
|
76
|
+
.slice(0, length);
|
|
77
|
+
|
|
78
|
+
if (!pasted) return;
|
|
79
|
+
|
|
80
|
+
const next = Array(length).fill("");
|
|
81
|
+
pasted.split("").forEach((char, i) => {
|
|
82
|
+
next[i] = char;
|
|
83
|
+
});
|
|
84
|
+
setValues(next);
|
|
85
|
+
onChange?.(next.join(""));
|
|
86
|
+
|
|
87
|
+
const lastFilled = Math.min(pasted.length, length - 1);
|
|
88
|
+
refs.current[lastFilled]?.focus();
|
|
89
|
+
|
|
90
|
+
if (pasted.length === length) {
|
|
91
|
+
onComplete?.(pasted);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handleResend = () => {
|
|
96
|
+
setSeconds(resendAfterSeconds);
|
|
97
|
+
setValues(Array(length).fill(""));
|
|
98
|
+
refs.current[0]?.focus();
|
|
99
|
+
onResend?.();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
|
104
|
+
{label && (
|
|
105
|
+
<label
|
|
106
|
+
style={{
|
|
107
|
+
fontSize: "13px",
|
|
108
|
+
fontWeight: 500,
|
|
109
|
+
color: error ? "#dc2626" : "#6b7280",
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
{label}
|
|
113
|
+
</label>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
<div
|
|
117
|
+
style={{ display: "flex", gap: "8px" }}
|
|
118
|
+
role="group"
|
|
119
|
+
aria-label={label ?? "OTP input"}
|
|
120
|
+
>
|
|
121
|
+
{values.map((val, i) => (
|
|
122
|
+
<input
|
|
123
|
+
key={i}
|
|
124
|
+
ref={(el) => {
|
|
125
|
+
refs.current[i] = el;
|
|
126
|
+
}}
|
|
127
|
+
type="text"
|
|
128
|
+
inputMode="numeric"
|
|
129
|
+
maxLength={1}
|
|
130
|
+
value={val}
|
|
131
|
+
disabled={disabled}
|
|
132
|
+
aria-label={`Digit ${i + 1} of ${length}`}
|
|
133
|
+
onChange={(e) => handleChange(i, e.target.value)}
|
|
134
|
+
onKeyDown={(e) => handleKeyDown(i, e)}
|
|
135
|
+
onPaste={handlePaste}
|
|
136
|
+
onFocus={() => setFocused(i)}
|
|
137
|
+
onBlur={() => setFocused(null)}
|
|
138
|
+
style={{
|
|
139
|
+
width: "44px",
|
|
140
|
+
height: "52px",
|
|
141
|
+
textAlign: "center",
|
|
142
|
+
fontSize: "20px",
|
|
143
|
+
fontWeight: 600,
|
|
144
|
+
border: `1.5px solid ${
|
|
145
|
+
error
|
|
146
|
+
? "#dc2626"
|
|
147
|
+
: focused === i
|
|
148
|
+
? "#B45309"
|
|
149
|
+
: val
|
|
150
|
+
? "#B45309"
|
|
151
|
+
: "#d1d5db"
|
|
152
|
+
}`,
|
|
153
|
+
borderRadius: "8px",
|
|
154
|
+
outline: "none",
|
|
155
|
+
background: disabled ? "#f9fafb" : "#ffffff",
|
|
156
|
+
color: "#111827",
|
|
157
|
+
boxShadow:
|
|
158
|
+
focused === i ? "0 0 0 3px rgba(180,83,9,0.12)" : "none",
|
|
159
|
+
transition: "all 0.15s ease",
|
|
160
|
+
}}
|
|
161
|
+
/>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{error && (
|
|
166
|
+
<span style={{ fontSize: "12px", color: "#dc2626" }}>{error}</span>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{onResend && (
|
|
170
|
+
<div style={{ fontSize: "13px", color: "#6b7280", marginTop: "4px" }}>
|
|
171
|
+
{seconds > 0 ? (
|
|
172
|
+
<>
|
|
173
|
+
Didn't get it?{" "}
|
|
174
|
+
<span style={{ color: "#B45309" }}>
|
|
175
|
+
Resend in 0:{String(seconds).padStart(2, "0")}
|
|
176
|
+
</span>
|
|
177
|
+
</>
|
|
178
|
+
) : (
|
|
179
|
+
<>
|
|
180
|
+
Didn't get it?{" "}
|
|
181
|
+
<button
|
|
182
|
+
onClick={handleResend}
|
|
183
|
+
style={{
|
|
184
|
+
background: "none",
|
|
185
|
+
border: "none",
|
|
186
|
+
color: "#B45309",
|
|
187
|
+
cursor: "pointer",
|
|
188
|
+
fontSize: "13px",
|
|
189
|
+
fontWeight: 500,
|
|
190
|
+
padding: 0,
|
|
191
|
+
textDecoration: "underline",
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
Resend OTP
|
|
195
|
+
</button>
|
|
196
|
+
</>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
package/src/PANInput.tsx
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { validatePAN } from "@bharat-ui/validators";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
export interface PANInputProps {
|
|
7
|
+
value?: string;
|
|
8
|
+
onChange?: (value: string, valid: boolean) => void;
|
|
9
|
+
label?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
showTypeBadge?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const TYPE_COLORS: Record<string, { bg: string; text: string }> = {
|
|
16
|
+
P: { bg: "#f0fdf4", text: "#15803d" },
|
|
17
|
+
C: { bg: "#eff6ff", text: "#1d4ed8" },
|
|
18
|
+
H: { bg: "#fef3c7", text: "#92400e" },
|
|
19
|
+
F: { bg: "#fdf4ff", text: "#7e22ce" },
|
|
20
|
+
T: { bg: "#fff1f2", text: "#be123c" },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function PANInput({
|
|
24
|
+
value = "",
|
|
25
|
+
onChange,
|
|
26
|
+
label,
|
|
27
|
+
error,
|
|
28
|
+
disabled = false,
|
|
29
|
+
showTypeBadge = true,
|
|
30
|
+
}: PANInputProps) {
|
|
31
|
+
const [focused, setFocused] = React.useState(false);
|
|
32
|
+
|
|
33
|
+
const result = value.length > 0 ? validatePAN(value) : null;
|
|
34
|
+
const typeCode = result?.meta?.typeCode ?? null;
|
|
35
|
+
const badgeColor = typeCode
|
|
36
|
+
? (TYPE_COLORS[typeCode] ?? { bg: "#f3f4f6", text: "#374151" })
|
|
37
|
+
: null;
|
|
38
|
+
|
|
39
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
40
|
+
const cleaned = e.target.value
|
|
41
|
+
.toUpperCase()
|
|
42
|
+
.replace(/[^A-Z0-9]/g, "")
|
|
43
|
+
.slice(0, 10);
|
|
44
|
+
onChange?.(cleaned, validatePAN(cleaned).valid);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
|
49
|
+
{label && (
|
|
50
|
+
<label
|
|
51
|
+
style={{
|
|
52
|
+
fontSize: "13px",
|
|
53
|
+
fontWeight: 500,
|
|
54
|
+
color: error ? "#dc2626" : "#6b7280",
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{label}
|
|
58
|
+
</label>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
<div
|
|
62
|
+
style={{
|
|
63
|
+
display: "flex",
|
|
64
|
+
alignItems: "center",
|
|
65
|
+
border: `1.5px solid ${
|
|
66
|
+
error ? "#dc2626" : focused ? "#B45309" : "#d1d5db"
|
|
67
|
+
}`,
|
|
68
|
+
borderRadius: "8px",
|
|
69
|
+
padding: "0 12px",
|
|
70
|
+
gap: "8px",
|
|
71
|
+
background: disabled ? "#f9fafb" : "#ffffff",
|
|
72
|
+
boxShadow: focused ? "0 0 0 3px rgba(180,83,9,0.12)" : "none",
|
|
73
|
+
transition: "all 0.15s ease",
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<input
|
|
77
|
+
type="text"
|
|
78
|
+
value={value}
|
|
79
|
+
onChange={handleChange}
|
|
80
|
+
onFocus={() => setFocused(true)}
|
|
81
|
+
onBlur={() => setFocused(false)}
|
|
82
|
+
disabled={disabled}
|
|
83
|
+
maxLength={10}
|
|
84
|
+
placeholder="ABCDE1234F"
|
|
85
|
+
style={{
|
|
86
|
+
border: "none",
|
|
87
|
+
outline: "none",
|
|
88
|
+
fontSize: "15px",
|
|
89
|
+
fontFamily: "var(--font-mono, monospace)",
|
|
90
|
+
letterSpacing: "0.12em",
|
|
91
|
+
padding: "10px 0",
|
|
92
|
+
width: "100%",
|
|
93
|
+
background: "transparent",
|
|
94
|
+
color: "#111827",
|
|
95
|
+
textTransform: "uppercase",
|
|
96
|
+
}}
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
{showTypeBadge && result?.valid && badgeColor && (
|
|
100
|
+
<span
|
|
101
|
+
style={{
|
|
102
|
+
fontSize: "11px",
|
|
103
|
+
fontWeight: 500,
|
|
104
|
+
padding: "2px 8px",
|
|
105
|
+
borderRadius: "99px",
|
|
106
|
+
background: badgeColor.bg,
|
|
107
|
+
color: badgeColor.text,
|
|
108
|
+
whiteSpace: "nowrap",
|
|
109
|
+
flexShrink: 0,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
{result.meta?.type}
|
|
113
|
+
</span>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{!error && result && !result.valid && value.length === 10 && (
|
|
118
|
+
<span style={{ fontSize: "12px", color: "#dc2626" }}>
|
|
119
|
+
{result.error}
|
|
120
|
+
</span>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
{error && (
|
|
124
|
+
<span style={{ fontSize: "12px", color: "#dc2626" }}>{error}</span>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { validatePincode } from "@bharat-ui/validators";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
export interface PincodeInputProps {
|
|
7
|
+
value?: string;
|
|
8
|
+
onChange?: (value: string, valid: boolean) => void;
|
|
9
|
+
label?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
showCascade?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function PincodeInput({
|
|
16
|
+
value = "",
|
|
17
|
+
onChange,
|
|
18
|
+
label,
|
|
19
|
+
error,
|
|
20
|
+
disabled = false,
|
|
21
|
+
showCascade = true,
|
|
22
|
+
}: PincodeInputProps) {
|
|
23
|
+
const [focused, setFocused] = React.useState(false);
|
|
24
|
+
|
|
25
|
+
const result = value.length === 6 ? validatePincode(value) : null;
|
|
26
|
+
|
|
27
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
28
|
+
const cleaned = e.target.value.replace(/\D/g, "").slice(0, 6);
|
|
29
|
+
onChange?.(
|
|
30
|
+
cleaned,
|
|
31
|
+
cleaned.length === 6 ? validatePincode(cleaned).valid : false,
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
|
37
|
+
{label && (
|
|
38
|
+
<label
|
|
39
|
+
style={{
|
|
40
|
+
fontSize: "13px",
|
|
41
|
+
fontWeight: 500,
|
|
42
|
+
color: error ? "#dc2626" : "#6b7280",
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
{label}
|
|
46
|
+
</label>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<input
|
|
50
|
+
type="text"
|
|
51
|
+
inputMode="numeric"
|
|
52
|
+
value={value}
|
|
53
|
+
onChange={handleChange}
|
|
54
|
+
onFocus={() => setFocused(true)}
|
|
55
|
+
onBlur={() => setFocused(false)}
|
|
56
|
+
disabled={disabled}
|
|
57
|
+
maxLength={6}
|
|
58
|
+
placeholder="390001"
|
|
59
|
+
style={{
|
|
60
|
+
border: `1.5px solid ${
|
|
61
|
+
error ? "#dc2626" : focused ? "#B45309" : "#d1d5db"
|
|
62
|
+
}`,
|
|
63
|
+
borderRadius: "8px",
|
|
64
|
+
padding: "10px 12px",
|
|
65
|
+
fontSize: "15px",
|
|
66
|
+
fontFamily: "var(--font-mono, monospace)",
|
|
67
|
+
letterSpacing: "0.08em",
|
|
68
|
+
outline: "none",
|
|
69
|
+
background: disabled ? "#f9fafb" : "#ffffff",
|
|
70
|
+
color: "#111827",
|
|
71
|
+
width: "100%",
|
|
72
|
+
boxSizing: "border-box",
|
|
73
|
+
boxShadow: focused ? "0 0 0 3px rgba(180,83,9,0.12)" : "none",
|
|
74
|
+
transition: "all 0.15s ease",
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
|
|
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>
|
|
126
|
+
</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>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
{showCascade && result && !result.valid && value.length === 6 && (
|
|
143
|
+
<span style={{ fontSize: "12px", color: "#dc2626" }}>
|
|
144
|
+
{result.error}
|
|
145
|
+
</span>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{error && (
|
|
149
|
+
<span style={{ fontSize: "12px", color: "#dc2626" }}>{error}</span>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { formatINR } from "@bharat-ui/validators";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
export interface UPIApp {
|
|
7
|
+
name: string;
|
|
8
|
+
packageName: string;
|
|
9
|
+
deepLinkScheme: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const UPI_APPS: UPIApp[] = [
|
|
13
|
+
{
|
|
14
|
+
name: "GPay",
|
|
15
|
+
packageName: "com.google.android.apps.nbu.paisa.user",
|
|
16
|
+
deepLinkScheme: "gpay",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "PhonePe",
|
|
20
|
+
packageName: "com.phonepe.app",
|
|
21
|
+
deepLinkScheme: "phonepe",
|
|
22
|
+
},
|
|
23
|
+
{ name: "Paytm", packageName: "net.one97.paytm", deepLinkScheme: "paytmmp" },
|
|
24
|
+
{ name: "BHIM", packageName: "in.org.npci.upiapp", deepLinkScheme: "upi" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export interface UPIButtonProps {
|
|
28
|
+
vpa: string;
|
|
29
|
+
amount: number;
|
|
30
|
+
merchantName: string;
|
|
31
|
+
transactionNote?: string;
|
|
32
|
+
currency?: string;
|
|
33
|
+
onSuccess?: () => void;
|
|
34
|
+
onError?: (error: string) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildUPIUrl(params: {
|
|
38
|
+
vpa: string;
|
|
39
|
+
amount: number;
|
|
40
|
+
merchantName: string;
|
|
41
|
+
transactionNote: string;
|
|
42
|
+
currency: string;
|
|
43
|
+
}): string {
|
|
44
|
+
const query = new URLSearchParams({
|
|
45
|
+
pa: params.vpa,
|
|
46
|
+
pn: params.merchantName,
|
|
47
|
+
am: params.amount.toFixed(2),
|
|
48
|
+
cu: params.currency,
|
|
49
|
+
tn: params.transactionNote,
|
|
50
|
+
});
|
|
51
|
+
return `upi://pay?${query.toString()}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function UPIButton({
|
|
55
|
+
vpa,
|
|
56
|
+
amount,
|
|
57
|
+
merchantName,
|
|
58
|
+
transactionNote = "Payment",
|
|
59
|
+
currency = "INR",
|
|
60
|
+
onSuccess,
|
|
61
|
+
onError,
|
|
62
|
+
}: UPIButtonProps) {
|
|
63
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
64
|
+
const [isDesktop, setIsDesktop] = React.useState(false);
|
|
65
|
+
|
|
66
|
+
React.useEffect(() => {
|
|
67
|
+
setIsDesktop(window.innerWidth > 768);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const upiUrl = buildUPIUrl({
|
|
71
|
+
vpa,
|
|
72
|
+
amount,
|
|
73
|
+
merchantName,
|
|
74
|
+
transactionNote,
|
|
75
|
+
currency,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const handlePayClick = () => {
|
|
79
|
+
if (isDesktop) {
|
|
80
|
+
setExpanded((e) => !e);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
window.location.href = upiUrl;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleAppClick = (app: UPIApp) => {
|
|
87
|
+
const appUrl = upiUrl.replace("upi://", `${app.deepLinkScheme}://`);
|
|
88
|
+
try {
|
|
89
|
+
window.location.href = appUrl;
|
|
90
|
+
onSuccess?.();
|
|
91
|
+
} catch {
|
|
92
|
+
onError?.(`Could not open ${app.name}`);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
|
98
|
+
<button
|
|
99
|
+
onClick={handlePayClick}
|
|
100
|
+
style={{
|
|
101
|
+
display: "flex",
|
|
102
|
+
alignItems: "center",
|
|
103
|
+
justifyContent: "center",
|
|
104
|
+
gap: "8px",
|
|
105
|
+
padding: "12px 20px",
|
|
106
|
+
borderRadius: "8px",
|
|
107
|
+
border: "1.5px solid #B45309",
|
|
108
|
+
background: "transparent",
|
|
109
|
+
color: "#B45309",
|
|
110
|
+
fontSize: "15px",
|
|
111
|
+
fontWeight: 500,
|
|
112
|
+
cursor: "pointer",
|
|
113
|
+
width: "100%",
|
|
114
|
+
transition: "all 0.15s ease",
|
|
115
|
+
}}
|
|
116
|
+
onMouseEnter={(e) => {
|
|
117
|
+
(e.target as HTMLButtonElement).style.background =
|
|
118
|
+
"rgba(180,83,9,0.06)";
|
|
119
|
+
}}
|
|
120
|
+
onMouseLeave={(e) => {
|
|
121
|
+
(e.target as HTMLButtonElement).style.background = "transparent";
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<svg
|
|
125
|
+
width="18"
|
|
126
|
+
height="18"
|
|
127
|
+
viewBox="0 0 24 24"
|
|
128
|
+
fill="none"
|
|
129
|
+
stroke="currentColor"
|
|
130
|
+
strokeWidth="2"
|
|
131
|
+
strokeLinecap="round"
|
|
132
|
+
strokeLinejoin="round"
|
|
133
|
+
>
|
|
134
|
+
<rect x="2" y="5" width="20" height="14" rx="2" />
|
|
135
|
+
<line x1="2" y1="10" x2="22" y2="10" />
|
|
136
|
+
</svg>
|
|
137
|
+
Pay {formatINR(amount)} via UPI
|
|
138
|
+
</button>
|
|
139
|
+
|
|
140
|
+
{expanded && isDesktop && (
|
|
141
|
+
<div
|
|
142
|
+
style={{
|
|
143
|
+
display: "flex",
|
|
144
|
+
flexDirection: "column",
|
|
145
|
+
gap: "4px",
|
|
146
|
+
border: "0.5px solid #e5e7eb",
|
|
147
|
+
borderRadius: "8px",
|
|
148
|
+
overflow: "hidden",
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
<div
|
|
152
|
+
style={{
|
|
153
|
+
padding: "8px 12px",
|
|
154
|
+
fontSize: "11px",
|
|
155
|
+
fontWeight: 500,
|
|
156
|
+
color: "#6b7280",
|
|
157
|
+
background: "#f9fafb",
|
|
158
|
+
letterSpacing: "0.06em",
|
|
159
|
+
textTransform: "uppercase",
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
Choose UPI app
|
|
163
|
+
</div>
|
|
164
|
+
{UPI_APPS.map((app) => (
|
|
165
|
+
<button
|
|
166
|
+
key={app.name}
|
|
167
|
+
onClick={() => handleAppClick(app)}
|
|
168
|
+
style={{
|
|
169
|
+
display: "flex",
|
|
170
|
+
alignItems: "center",
|
|
171
|
+
justifyContent: "space-between",
|
|
172
|
+
padding: "12px 16px",
|
|
173
|
+
border: "none",
|
|
174
|
+
borderTop: "0.5px solid #f3f4f6",
|
|
175
|
+
background: "#ffffff",
|
|
176
|
+
cursor: "pointer",
|
|
177
|
+
fontSize: "14px",
|
|
178
|
+
color: "#111827",
|
|
179
|
+
textAlign: "left",
|
|
180
|
+
transition: "background 0.1s ease",
|
|
181
|
+
}}
|
|
182
|
+
onMouseEnter={(e) => {
|
|
183
|
+
(e.currentTarget as HTMLButtonElement).style.background =
|
|
184
|
+
"#fef3c7";
|
|
185
|
+
}}
|
|
186
|
+
onMouseLeave={(e) => {
|
|
187
|
+
(e.currentTarget as HTMLButtonElement).style.background =
|
|
188
|
+
"#ffffff";
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
<span>{app.name}</span>
|
|
192
|
+
<svg
|
|
193
|
+
width="14"
|
|
194
|
+
height="14"
|
|
195
|
+
viewBox="0 0 24 24"
|
|
196
|
+
fill="none"
|
|
197
|
+
stroke="#9ca3af"
|
|
198
|
+
strokeWidth="2"
|
|
199
|
+
strokeLinecap="round"
|
|
200
|
+
strokeLinejoin="round"
|
|
201
|
+
>
|
|
202
|
+
<polyline points="9 18 15 12 9 6" />
|
|
203
|
+
</svg>
|
|
204
|
+
</button>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
|
|
209
|
+
<div
|
|
210
|
+
style={{
|
|
211
|
+
display: "flex",
|
|
212
|
+
gap: "6px",
|
|
213
|
+
flexWrap: "wrap",
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
{UPI_APPS.map((app) => (
|
|
217
|
+
<span
|
|
218
|
+
key={app.name}
|
|
219
|
+
style={{
|
|
220
|
+
fontSize: "11px",
|
|
221
|
+
padding: "3px 8px",
|
|
222
|
+
borderRadius: "99px",
|
|
223
|
+
border: "0.5px solid #e5e7eb",
|
|
224
|
+
color: "#6b7280",
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
{app.name}
|
|
228
|
+
</span>
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
package/src/button.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
interface ButtonProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
appName: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Button = ({ children, className, appName }: ButtonProps) => {
|
|
12
|
+
return (
|
|
13
|
+
<button
|
|
14
|
+
className={className}
|
|
15
|
+
onClick={() => alert(`Hello from your ${appName} app!`)}
|
|
16
|
+
>
|
|
17
|
+
{children}
|
|
18
|
+
</button>
|
|
19
|
+
);
|
|
20
|
+
};
|
package/src/card.tsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type JSX } from "react";
|
|
2
|
+
|
|
3
|
+
export function Card({
|
|
4
|
+
className,
|
|
5
|
+
title,
|
|
6
|
+
children,
|
|
7
|
+
href,
|
|
8
|
+
}: {
|
|
9
|
+
className?: string;
|
|
10
|
+
title: string;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
href: string;
|
|
13
|
+
}): JSX.Element {
|
|
14
|
+
return (
|
|
15
|
+
<a
|
|
16
|
+
className={className}
|
|
17
|
+
href={`${href}?utm_source=create-turbo&utm_medium=basic&utm_campaign=create-turbo"`}
|
|
18
|
+
rel="noopener noreferrer"
|
|
19
|
+
target="_blank"
|
|
20
|
+
>
|
|
21
|
+
<h2>
|
|
22
|
+
{title} <span>-></span>
|
|
23
|
+
</h2>
|
|
24
|
+
<p>{children}</p>
|
|
25
|
+
</a>
|
|
26
|
+
);
|
|
27
|
+
}
|