@invoice-sdk/widget 0.0.0 → 1.5.2
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 +5 -5
- package/src/components/button.tsx +22 -22
- package/src/components/form/custom-checkbox.tsx +20 -20
- package/src/components/form/file-upload.tsx +140 -140
- package/src/components/form/input-field.tsx +34 -34
- package/src/components/form/select-option.tsx +74 -74
- package/src/components/layout.tsx +13 -13
- package/src/components/process.tsx +20 -20
- package/src/index.ts +1 -1
- package/src/pages/register.tsx +222 -222
- package/src/pages/select-plan.tsx +39 -39
- package/src/pages/select-provider.tsx +73 -73
- package/src/pages/status.tsx +8 -8
- package/src/store/process.ts +17 -17
- package/src/store/register.ts +59 -59
- package/tsconfig.json +19 -19
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
|
-
"@invoice-sdk/api": "workspace:*",
|
|
4
3
|
"react-hook-form": "^7.56.4",
|
|
5
4
|
"react-router-dom": "6.30.1",
|
|
6
5
|
"zod": "^3.25.42",
|
|
7
|
-
"zustand": "^5.0.5"
|
|
6
|
+
"zustand": "^5.0.5",
|
|
7
|
+
"@invoice-sdk/api": "1.5.2"
|
|
8
8
|
},
|
|
9
9
|
"devDependencies": {
|
|
10
10
|
"@eslint/js": "^9.25.0",
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
|
+
"type": "module",
|
|
36
|
+
"version": "1.5.2",
|
|
35
37
|
"scripts": {
|
|
36
38
|
"build": "tsc -b && vite build",
|
|
37
39
|
"dev": "vite",
|
|
38
40
|
"lint": "eslint .",
|
|
39
41
|
"preview": "vite preview"
|
|
40
|
-
}
|
|
41
|
-
"type": "module",
|
|
42
|
-
"version": "0.0.0"
|
|
42
|
+
}
|
|
43
43
|
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
type Props = {
|
|
3
|
-
title: string
|
|
4
|
-
isDisabled?: boolean
|
|
5
|
-
handleClick?: () => void
|
|
6
|
-
type?: 'submit' | 'button'
|
|
7
|
-
className?: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const Button = (props: Props) => {
|
|
11
|
-
return (
|
|
12
|
-
<button
|
|
13
|
-
type={props.type || 'button'}
|
|
14
|
-
disabled={props.isDisabled}
|
|
15
|
-
className={`bg-green-500 hover:bg-green-600 text-white px-6 py-2 rounded disabled:opacity-50 *:disabled:cursor-not-allowed ${props.className}`}
|
|
16
|
-
onClick={props.handleClick}
|
|
17
|
-
>
|
|
18
|
-
{props.title}
|
|
19
|
-
</button>
|
|
20
|
-
)
|
|
21
|
-
}
|
|
22
|
-
|
|
1
|
+
|
|
2
|
+
type Props = {
|
|
3
|
+
title: string
|
|
4
|
+
isDisabled?: boolean
|
|
5
|
+
handleClick?: () => void
|
|
6
|
+
type?: 'submit' | 'button'
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const Button = (props: Props) => {
|
|
11
|
+
return (
|
|
12
|
+
<button
|
|
13
|
+
type={props.type || 'button'}
|
|
14
|
+
disabled={props.isDisabled}
|
|
15
|
+
className={`bg-green-500 hover:bg-green-600 text-white px-6 py-2 rounded disabled:opacity-50 *:disabled:cursor-not-allowed ${props.className}`}
|
|
16
|
+
onClick={props.handleClick}
|
|
17
|
+
>
|
|
18
|
+
{props.title}
|
|
19
|
+
</button>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
23
|
export default Button
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
type Props = {
|
|
2
|
-
checked: boolean
|
|
3
|
-
setChecked: (checked: boolean) => void
|
|
4
|
-
}
|
|
5
|
-
const CustomCheckbox = ({ checked, setChecked }: Props) => {
|
|
6
|
-
return (
|
|
7
|
-
<label className='relative inline-flex cursor-pointer items-center'>
|
|
8
|
-
<input
|
|
9
|
-
type='checkbox'
|
|
10
|
-
className='peer sr-only'
|
|
11
|
-
checked={checked}
|
|
12
|
-
onChange={(e) => setChecked(e.target.checked)}
|
|
13
|
-
/>
|
|
14
|
-
|
|
15
|
-
<div className='border-[#ACADAE] relative h-5 w-5 rounded-full border-2 before:absolute before:left-1/2 before:top-1/2 before:hidden before:h-2.5 before:w-2.5 before:-translate-x-1/2 before:-translate-y-1/2 before:rounded-full before:bg-green-500 peer-checked:border-green-500 peer-checked:before:block' />
|
|
16
|
-
</label>
|
|
17
|
-
)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export default CustomCheckbox
|
|
1
|
+
type Props = {
|
|
2
|
+
checked: boolean
|
|
3
|
+
setChecked: (checked: boolean) => void
|
|
4
|
+
}
|
|
5
|
+
const CustomCheckbox = ({ checked, setChecked }: Props) => {
|
|
6
|
+
return (
|
|
7
|
+
<label className='relative inline-flex cursor-pointer items-center'>
|
|
8
|
+
<input
|
|
9
|
+
type='checkbox'
|
|
10
|
+
className='peer sr-only'
|
|
11
|
+
checked={checked}
|
|
12
|
+
onChange={(e) => setChecked(e.target.checked)}
|
|
13
|
+
/>
|
|
14
|
+
|
|
15
|
+
<div className='border-[#ACADAE] relative h-5 w-5 rounded-full border-2 before:absolute before:left-1/2 before:top-1/2 before:hidden before:h-2.5 before:w-2.5 before:-translate-x-1/2 before:-translate-y-1/2 before:rounded-full before:bg-green-500 peer-checked:border-green-500 peer-checked:before:block' />
|
|
16
|
+
</label>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default CustomCheckbox
|
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState, type ChangeEvent } from "react";
|
|
2
|
-
|
|
3
|
-
type FileUploadProps = {
|
|
4
|
-
label: string;
|
|
5
|
-
required?: boolean;
|
|
6
|
-
file: File | null;
|
|
7
|
-
onFileChange: (file: File) => void;
|
|
8
|
-
error?: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const FileUpload: React.FC<FileUploadProps> = ({
|
|
12
|
-
label,
|
|
13
|
-
required = false,
|
|
14
|
-
file,
|
|
15
|
-
onFileChange,
|
|
16
|
-
error,
|
|
17
|
-
}) => {
|
|
18
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
19
|
-
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
20
|
-
const [isModalOpen, setModalOpen] = useState(false);
|
|
21
|
-
|
|
22
|
-
const handleFileInput = (e: ChangeEvent<HTMLInputElement>) => {
|
|
23
|
-
const chosen = e.target.files?.[0];
|
|
24
|
-
if (!chosen) return;
|
|
25
|
-
onFileChange(chosen);
|
|
26
|
-
setPreviewUrl(URL.createObjectURL(chosen));
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const openPicker = useCallback(() => {
|
|
30
|
-
inputRef.current?.click();
|
|
31
|
-
}, []);
|
|
32
|
-
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
if(file){
|
|
35
|
-
setPreviewUrl(URL.createObjectURL(file as File))
|
|
36
|
-
}
|
|
37
|
-
}, [file])
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<>
|
|
41
|
-
{/* Label + Upload button */}
|
|
42
|
-
<div className="flex flex-col">
|
|
43
|
-
<label className="font-medium mb-1 text-gray-700">
|
|
44
|
-
{label}{required && <span className="text-red-500 ml-1">*</span>}
|
|
45
|
-
</label>
|
|
46
|
-
<div className="flex items-center">
|
|
47
|
-
<button
|
|
48
|
-
type="button"
|
|
49
|
-
onClick={openPicker}
|
|
50
|
-
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
|
|
51
|
-
>
|
|
52
|
-
Upload file
|
|
53
|
-
</button>
|
|
54
|
-
<input
|
|
55
|
-
ref={inputRef}
|
|
56
|
-
type="file"
|
|
57
|
-
accept="image/*,.pdf"
|
|
58
|
-
className="hidden"
|
|
59
|
-
onChange={handleFileInput}
|
|
60
|
-
/>
|
|
61
|
-
</div>
|
|
62
|
-
|
|
63
|
-
{/* Preview box */}
|
|
64
|
-
<div
|
|
65
|
-
className="mt-2 h-40 w-full bg-gray-200 rounded overflow-hidden flex items-center justify-center cursor-pointer"
|
|
66
|
-
onClick={() => previewUrl && setModalOpen(true)}
|
|
67
|
-
>
|
|
68
|
-
{previewUrl ? (
|
|
69
|
-
file?.type === 'application/pdf' ? (
|
|
70
|
-
<object
|
|
71
|
-
data={previewUrl}
|
|
72
|
-
type="application/pdf"
|
|
73
|
-
width="100%"
|
|
74
|
-
height="100%"
|
|
75
|
-
>
|
|
76
|
-
<p className="text-gray-600 text-sm">
|
|
77
|
-
Cannot preview PDF.{' '}
|
|
78
|
-
<a
|
|
79
|
-
href={previewUrl}
|
|
80
|
-
target="_blank"
|
|
81
|
-
rel="noreferrer"
|
|
82
|
-
className="text-blue-600 underline"
|
|
83
|
-
>
|
|
84
|
-
Download PDF
|
|
85
|
-
</a>
|
|
86
|
-
</p>
|
|
87
|
-
</object>
|
|
88
|
-
) : (
|
|
89
|
-
<img
|
|
90
|
-
src={previewUrl}
|
|
91
|
-
alt="Preview"
|
|
92
|
-
className="h-full object-contain"
|
|
93
|
-
/>
|
|
94
|
-
)
|
|
95
|
-
) : (
|
|
96
|
-
<span className="text-gray-500">No file selected</span>
|
|
97
|
-
)}
|
|
98
|
-
</div>
|
|
99
|
-
|
|
100
|
-
{error && <span className="text-red-500 text-sm mt-1">{error}</span>}
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
{/* Lightbox Modal */}
|
|
104
|
-
{isModalOpen && previewUrl && (
|
|
105
|
-
<div
|
|
106
|
-
className="fixed inset-0 bg-black bg-opacity-75 z-50 flex items-center justify-center"
|
|
107
|
-
onClick={() => setModalOpen(false)}
|
|
108
|
-
>
|
|
109
|
-
<div
|
|
110
|
-
className="relative w-full h-full"
|
|
111
|
-
onClick={(e) => e.stopPropagation()}
|
|
112
|
-
>
|
|
113
|
-
<button
|
|
114
|
-
onClick={() => setModalOpen(false)}
|
|
115
|
-
className="absolute top-2 right-2 text-white text-2xl"
|
|
116
|
-
aria-label="Close preview"
|
|
117
|
-
>
|
|
118
|
-
×
|
|
119
|
-
</button>
|
|
120
|
-
|
|
121
|
-
{file?.type === 'application/pdf' ? (
|
|
122
|
-
<object
|
|
123
|
-
data={previewUrl}
|
|
124
|
-
type="application/pdf"
|
|
125
|
-
className="w-full h-full object-contain"
|
|
126
|
-
/>
|
|
127
|
-
) : (
|
|
128
|
-
<img
|
|
129
|
-
src={previewUrl}
|
|
130
|
-
alt="Full Preview"
|
|
131
|
-
className="w-full h-full object-contain"
|
|
132
|
-
/>
|
|
133
|
-
)}
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
)}
|
|
137
|
-
</>
|
|
138
|
-
);
|
|
139
|
-
};
|
|
140
|
-
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, type ChangeEvent } from "react";
|
|
2
|
+
|
|
3
|
+
type FileUploadProps = {
|
|
4
|
+
label: string;
|
|
5
|
+
required?: boolean;
|
|
6
|
+
file: File | null;
|
|
7
|
+
onFileChange: (file: File) => void;
|
|
8
|
+
error?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const FileUpload: React.FC<FileUploadProps> = ({
|
|
12
|
+
label,
|
|
13
|
+
required = false,
|
|
14
|
+
file,
|
|
15
|
+
onFileChange,
|
|
16
|
+
error,
|
|
17
|
+
}) => {
|
|
18
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
19
|
+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
20
|
+
const [isModalOpen, setModalOpen] = useState(false);
|
|
21
|
+
|
|
22
|
+
const handleFileInput = (e: ChangeEvent<HTMLInputElement>) => {
|
|
23
|
+
const chosen = e.target.files?.[0];
|
|
24
|
+
if (!chosen) return;
|
|
25
|
+
onFileChange(chosen);
|
|
26
|
+
setPreviewUrl(URL.createObjectURL(chosen));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const openPicker = useCallback(() => {
|
|
30
|
+
inputRef.current?.click();
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if(file){
|
|
35
|
+
setPreviewUrl(URL.createObjectURL(file as File))
|
|
36
|
+
}
|
|
37
|
+
}, [file])
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
{/* Label + Upload button */}
|
|
42
|
+
<div className="flex flex-col">
|
|
43
|
+
<label className="font-medium mb-1 text-gray-700">
|
|
44
|
+
{label}{required && <span className="text-red-500 ml-1">*</span>}
|
|
45
|
+
</label>
|
|
46
|
+
<div className="flex items-center">
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
onClick={openPicker}
|
|
50
|
+
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
|
|
51
|
+
>
|
|
52
|
+
Upload file
|
|
53
|
+
</button>
|
|
54
|
+
<input
|
|
55
|
+
ref={inputRef}
|
|
56
|
+
type="file"
|
|
57
|
+
accept="image/*,.pdf"
|
|
58
|
+
className="hidden"
|
|
59
|
+
onChange={handleFileInput}
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Preview box */}
|
|
64
|
+
<div
|
|
65
|
+
className="mt-2 h-40 w-full bg-gray-200 rounded overflow-hidden flex items-center justify-center cursor-pointer"
|
|
66
|
+
onClick={() => previewUrl && setModalOpen(true)}
|
|
67
|
+
>
|
|
68
|
+
{previewUrl ? (
|
|
69
|
+
file?.type === 'application/pdf' ? (
|
|
70
|
+
<object
|
|
71
|
+
data={previewUrl}
|
|
72
|
+
type="application/pdf"
|
|
73
|
+
width="100%"
|
|
74
|
+
height="100%"
|
|
75
|
+
>
|
|
76
|
+
<p className="text-gray-600 text-sm">
|
|
77
|
+
Cannot preview PDF.{' '}
|
|
78
|
+
<a
|
|
79
|
+
href={previewUrl}
|
|
80
|
+
target="_blank"
|
|
81
|
+
rel="noreferrer"
|
|
82
|
+
className="text-blue-600 underline"
|
|
83
|
+
>
|
|
84
|
+
Download PDF
|
|
85
|
+
</a>
|
|
86
|
+
</p>
|
|
87
|
+
</object>
|
|
88
|
+
) : (
|
|
89
|
+
<img
|
|
90
|
+
src={previewUrl}
|
|
91
|
+
alt="Preview"
|
|
92
|
+
className="h-full object-contain"
|
|
93
|
+
/>
|
|
94
|
+
)
|
|
95
|
+
) : (
|
|
96
|
+
<span className="text-gray-500">No file selected</span>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{error && <span className="text-red-500 text-sm mt-1">{error}</span>}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Lightbox Modal */}
|
|
104
|
+
{isModalOpen && previewUrl && (
|
|
105
|
+
<div
|
|
106
|
+
className="fixed inset-0 bg-black bg-opacity-75 z-50 flex items-center justify-center"
|
|
107
|
+
onClick={() => setModalOpen(false)}
|
|
108
|
+
>
|
|
109
|
+
<div
|
|
110
|
+
className="relative w-full h-full"
|
|
111
|
+
onClick={(e) => e.stopPropagation()}
|
|
112
|
+
>
|
|
113
|
+
<button
|
|
114
|
+
onClick={() => setModalOpen(false)}
|
|
115
|
+
className="absolute top-2 right-2 text-white text-2xl"
|
|
116
|
+
aria-label="Close preview"
|
|
117
|
+
>
|
|
118
|
+
×
|
|
119
|
+
</button>
|
|
120
|
+
|
|
121
|
+
{file?.type === 'application/pdf' ? (
|
|
122
|
+
<object
|
|
123
|
+
data={previewUrl}
|
|
124
|
+
type="application/pdf"
|
|
125
|
+
className="w-full h-full object-contain"
|
|
126
|
+
/>
|
|
127
|
+
) : (
|
|
128
|
+
<img
|
|
129
|
+
src={previewUrl}
|
|
130
|
+
alt="Full Preview"
|
|
131
|
+
className="w-full h-full object-contain"
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
141
|
export default FileUpload;
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
import { forwardRef } from "react";
|
|
2
|
-
|
|
3
|
-
type InputFieldProps = {
|
|
4
|
-
label: string;
|
|
5
|
-
required?: boolean;
|
|
6
|
-
error?: string;
|
|
7
|
-
} & React.InputHTMLAttributes<HTMLInputElement>;
|
|
8
|
-
|
|
9
|
-
const InputField = forwardRef<HTMLInputElement, InputFieldProps>(
|
|
10
|
-
({ label, required = false, error, ...props }, ref) => (
|
|
11
|
-
<div className="flex flex-col">
|
|
12
|
-
<label
|
|
13
|
-
htmlFor={props.name}
|
|
14
|
-
className="font-medium mb-1 text-gray-700"
|
|
15
|
-
>
|
|
16
|
-
{label}
|
|
17
|
-
{required && <span className="text-red-500 ml-1">*</span>}
|
|
18
|
-
</label>
|
|
19
|
-
<input
|
|
20
|
-
id={props.name}
|
|
21
|
-
ref={ref}
|
|
22
|
-
{...props}
|
|
23
|
-
className={`border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
|
24
|
-
error ? 'border-red-500' : 'border-gray-300'
|
|
25
|
-
}`}
|
|
26
|
-
/>
|
|
27
|
-
{error && (
|
|
28
|
-
<span className="text-red-500 text-sm mt-1">{error}</span>
|
|
29
|
-
)}
|
|
30
|
-
</div>
|
|
31
|
-
)
|
|
32
|
-
);
|
|
33
|
-
InputField.displayName = 'InputField';
|
|
34
|
-
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
|
|
3
|
+
type InputFieldProps = {
|
|
4
|
+
label: string;
|
|
5
|
+
required?: boolean;
|
|
6
|
+
error?: string;
|
|
7
|
+
} & React.InputHTMLAttributes<HTMLInputElement>;
|
|
8
|
+
|
|
9
|
+
const InputField = forwardRef<HTMLInputElement, InputFieldProps>(
|
|
10
|
+
({ label, required = false, error, ...props }, ref) => (
|
|
11
|
+
<div className="flex flex-col">
|
|
12
|
+
<label
|
|
13
|
+
htmlFor={props.name}
|
|
14
|
+
className="font-medium mb-1 text-gray-700"
|
|
15
|
+
>
|
|
16
|
+
{label}
|
|
17
|
+
{required && <span className="text-red-500 ml-1">*</span>}
|
|
18
|
+
</label>
|
|
19
|
+
<input
|
|
20
|
+
id={props.name}
|
|
21
|
+
ref={ref}
|
|
22
|
+
{...props}
|
|
23
|
+
className={`border rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
|
24
|
+
error ? 'border-red-500' : 'border-gray-300'
|
|
25
|
+
}`}
|
|
26
|
+
/>
|
|
27
|
+
{error && (
|
|
28
|
+
<span className="text-red-500 text-sm mt-1">{error}</span>
|
|
29
|
+
)}
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
InputField.displayName = 'InputField';
|
|
34
|
+
|
|
35
35
|
export default InputField;
|
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
type Option = {
|
|
4
|
-
value: string;
|
|
5
|
-
label: string;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
type SubscriptionSelectorProps = {
|
|
9
|
-
options: Option[];
|
|
10
|
-
value: string;
|
|
11
|
-
onChange: (value: string) => void;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const SubscriptionSelector: React.FC<SubscriptionSelectorProps> = ({
|
|
15
|
-
options,
|
|
16
|
-
value,
|
|
17
|
-
onChange,
|
|
18
|
-
}) => {
|
|
19
|
-
return (
|
|
20
|
-
<fieldset
|
|
21
|
-
role="radiogroup"
|
|
22
|
-
aria-label="Chọn gói đăng ký"
|
|
23
|
-
className="flex flex-col md:flex-row gap-4 md:gap-20"
|
|
24
|
-
>
|
|
25
|
-
{options.map((opt) => {
|
|
26
|
-
const selected = opt.value === value;
|
|
27
|
-
return (
|
|
28
|
-
<label
|
|
29
|
-
key={opt.value}
|
|
30
|
-
className={[
|
|
31
|
-
'flex items-center cursor-pointer rounded-lg border p-4 transition',
|
|
32
|
-
selected
|
|
33
|
-
? 'border-green-500 bg-green-50'
|
|
34
|
-
: 'border-gray-300 bg-white hover:bg-gray-50',
|
|
35
|
-
].join(' ')}
|
|
36
|
-
>
|
|
37
|
-
{/* Visually hidden native radio */}
|
|
38
|
-
<input
|
|
39
|
-
type="radio"
|
|
40
|
-
name="subscription"
|
|
41
|
-
value={opt.value}
|
|
42
|
-
checked={selected}
|
|
43
|
-
onChange={() => onChange(opt.value)}
|
|
44
|
-
className="sr-only"
|
|
45
|
-
/>
|
|
46
|
-
|
|
47
|
-
{/* Custom radio visual */}
|
|
48
|
-
<span
|
|
49
|
-
className={[
|
|
50
|
-
'flex-shrink-0 flex items-center justify-center rounded-full mr-3',
|
|
51
|
-
selected
|
|
52
|
-
? 'w-5 h-5 border-2 border-green-500 bg-green-500'
|
|
53
|
-
: 'w-4 h-4 border border-gray-400',
|
|
54
|
-
].join(' ')}
|
|
55
|
-
>
|
|
56
|
-
{selected && (
|
|
57
|
-
<span className="block w-2 h-2 bg-white rounded-full" />
|
|
58
|
-
)}
|
|
59
|
-
</span>
|
|
60
|
-
|
|
61
|
-
{/* Label text */}
|
|
62
|
-
<span
|
|
63
|
-
className={selected ? 'text-green-700 font-medium' : 'text-gray-700'}
|
|
64
|
-
>
|
|
65
|
-
{opt.label}
|
|
66
|
-
</span>
|
|
67
|
-
</label>
|
|
68
|
-
);
|
|
69
|
-
})}
|
|
70
|
-
</fieldset>
|
|
71
|
-
);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
export default SubscriptionSelector;
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
type Option = {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type SubscriptionSelectorProps = {
|
|
9
|
+
options: Option[];
|
|
10
|
+
value: string;
|
|
11
|
+
onChange: (value: string) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const SubscriptionSelector: React.FC<SubscriptionSelectorProps> = ({
|
|
15
|
+
options,
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
}) => {
|
|
19
|
+
return (
|
|
20
|
+
<fieldset
|
|
21
|
+
role="radiogroup"
|
|
22
|
+
aria-label="Chọn gói đăng ký"
|
|
23
|
+
className="flex flex-col md:flex-row gap-4 md:gap-20"
|
|
24
|
+
>
|
|
25
|
+
{options.map((opt) => {
|
|
26
|
+
const selected = opt.value === value;
|
|
27
|
+
return (
|
|
28
|
+
<label
|
|
29
|
+
key={opt.value}
|
|
30
|
+
className={[
|
|
31
|
+
'flex items-center cursor-pointer rounded-lg border p-4 transition',
|
|
32
|
+
selected
|
|
33
|
+
? 'border-green-500 bg-green-50'
|
|
34
|
+
: 'border-gray-300 bg-white hover:bg-gray-50',
|
|
35
|
+
].join(' ')}
|
|
36
|
+
>
|
|
37
|
+
{/* Visually hidden native radio */}
|
|
38
|
+
<input
|
|
39
|
+
type="radio"
|
|
40
|
+
name="subscription"
|
|
41
|
+
value={opt.value}
|
|
42
|
+
checked={selected}
|
|
43
|
+
onChange={() => onChange(opt.value)}
|
|
44
|
+
className="sr-only"
|
|
45
|
+
/>
|
|
46
|
+
|
|
47
|
+
{/* Custom radio visual */}
|
|
48
|
+
<span
|
|
49
|
+
className={[
|
|
50
|
+
'flex-shrink-0 flex items-center justify-center rounded-full mr-3',
|
|
51
|
+
selected
|
|
52
|
+
? 'w-5 h-5 border-2 border-green-500 bg-green-500'
|
|
53
|
+
: 'w-4 h-4 border border-gray-400',
|
|
54
|
+
].join(' ')}
|
|
55
|
+
>
|
|
56
|
+
{selected && (
|
|
57
|
+
<span className="block w-2 h-2 bg-white rounded-full" />
|
|
58
|
+
)}
|
|
59
|
+
</span>
|
|
60
|
+
|
|
61
|
+
{/* Label text */}
|
|
62
|
+
<span
|
|
63
|
+
className={selected ? 'text-green-700 font-medium' : 'text-gray-700'}
|
|
64
|
+
>
|
|
65
|
+
{opt.label}
|
|
66
|
+
</span>
|
|
67
|
+
</label>
|
|
68
|
+
);
|
|
69
|
+
})}
|
|
70
|
+
</fieldset>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default SubscriptionSelector;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { Outlet } from "react-router-dom"
|
|
2
|
-
import Process from "./process"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const MainLayout = () => {
|
|
6
|
-
return (
|
|
7
|
-
<div className="max-w-[800px] mx-auto py-10 flex flex-col gap-8">
|
|
8
|
-
<Process />
|
|
9
|
-
<Outlet />
|
|
10
|
-
</div>
|
|
11
|
-
)
|
|
12
|
-
}
|
|
13
|
-
|
|
1
|
+
import { Outlet } from "react-router-dom"
|
|
2
|
+
import Process from "./process"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const MainLayout = () => {
|
|
6
|
+
return (
|
|
7
|
+
<div className="max-w-[800px] mx-auto py-10 flex flex-col gap-8">
|
|
8
|
+
<Process />
|
|
9
|
+
<Outlet />
|
|
10
|
+
</div>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
14
|
export default MainLayout
|