@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 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
- &times;
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
+ &times;
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