@mbao01/common 0.0.33 → 0.0.35

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.
@@ -0,0 +1,14 @@
1
+ /// <reference types="react" />
2
+ import type { FileUploaderInputProps, FileUploaderProps } from "./types";
3
+ export declare const FileUploader: {
4
+ ({ className, dropzoneOptions, value, onValueChange, reSelect, orientation, children, dir, ...props }: FileUploaderProps): import("react/jsx-runtime").JSX.Element;
5
+ displayName: string;
6
+ Content: import("react").ForwardRefExoticComponent<import("react").HTMLAttributes<HTMLDivElement> & import("react").RefAttributes<HTMLDivElement>>;
7
+ Input: {
8
+ ({ className, children, ...props }: FileUploaderInputProps): import("react/jsx-runtime").JSX.Element;
9
+ displayName: string;
10
+ };
11
+ Item: import("react").ForwardRefExoticComponent<{
12
+ index: number;
13
+ } & import("react").HTMLAttributes<HTMLDivElement> & import("react").RefAttributes<HTMLDivElement>>;
14
+ };
@@ -0,0 +1,3 @@
1
+ /// <reference types="react" />
2
+ import { type FileUploaderContextType } from "./types";
3
+ export declare const FileUploaderContext: import("react").Context<FileUploaderContextType | null>;
@@ -0,0 +1 @@
1
+ export { FileUploader } from "./FileUploader";
@@ -0,0 +1,22 @@
1
+ import type { Dispatch, SetStateAction } from "react";
2
+ import type { DropzoneState, DropzoneOptions } from "react-dropzone";
3
+ export type FileUploaderProps = {
4
+ value: File[] | null;
5
+ reSelect?: boolean;
6
+ onValueChange: (value: File[] | null) => void;
7
+ dropzoneOptions: DropzoneOptions;
8
+ orientation?: "horizontal" | "vertical";
9
+ } & React.HTMLAttributes<HTMLDivElement>;
10
+ export type FileUploaderInputProps = React.InputHTMLAttributes<HTMLInputElement>;
11
+ export type DirectionOptions = "rtl" | "ltr" | undefined;
12
+ export type FileUploaderContextType = {
13
+ dropzoneState: DropzoneState;
14
+ isLOF: boolean;
15
+ isFileTooBig: boolean;
16
+ removeFileFromSet: (index: number) => void;
17
+ removeAll: () => void;
18
+ activeIndex: number;
19
+ setActiveIndex: Dispatch<SetStateAction<number>>;
20
+ orientation: "horizontal" | "vertical";
21
+ direction: DirectionOptions;
22
+ };
@@ -0,0 +1 @@
1
+ export declare const useFileUpload: () => import("./types").FileUploaderContextType;
@@ -0,0 +1,2 @@
1
+ import { type OtpInputProps } from "./types";
2
+ export declare const OtpInput: ({ className, inputProps, ...props }: OtpInputProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ export { OtpInput } from "./OtpInput";
@@ -0,0 +1,6 @@
1
+ import { type OTPInputProps } from "react-otp-input";
2
+ import { InputProps } from "../Input/types";
3
+ export type OtpInputProps = {
4
+ className?: string;
5
+ inputProps?: Omit<InputProps, "type">;
6
+ } & Omit<OTPInputProps, "renderInput" | "onChange">;
@@ -1,4 +1,5 @@
1
1
  export { Input } from "./Input";
2
+ export { OtpInput } from "./OtpInput";
2
3
  export { Phone } from "./Phone";
3
4
  export { Radio } from "./Radio";
4
5
  export { Range } from "./Range";
@@ -24,6 +24,7 @@ export * from "./components/Table";
24
24
  export * from "./components/Tabs";
25
25
  export * from "./components/Text";
26
26
  /** data input */
27
+ export * from "./components/FileUploader";
27
28
  export * from "./components/Form";
28
29
  export * from "./components/Combobox";
29
30
  export * from "./components/DatePicker";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mbao01/common",
3
3
  "private": false,
4
- "version": "0.0.33",
4
+ "version": "0.0.35",
5
5
  "type": "module",
6
6
  "author": "Ayomide Bakare",
7
7
  "license": "MIT",
@@ -91,7 +91,9 @@
91
91
  "date-fns": "^3.6.0",
92
92
  "embla-carousel-react": "^8.0.2",
93
93
  "react-day-picker": "^8.10.0",
94
+ "react-dropzone": "^14.2.3",
94
95
  "react-international-phone": "^4.2.6",
96
+ "react-otp-input": "^3.1.1",
95
97
  "sonner": "^1.4.41",
96
98
  "tailwind-merge": "^2.2.1",
97
99
  "tailwindcss-animate": "^1.0.7",
@@ -143,5 +145,5 @@
143
145
  "react-dom": "^18.2.0",
144
146
  "typescript": "^5.2.2"
145
147
  },
146
- "gitHead": "169aeecca51d4544bfed02de64e0d9806dd83c6c"
148
+ "gitHead": "4e024b19d151c05b6c1580f461e348f5376c33b9"
147
149
  }
@@ -0,0 +1,319 @@
1
+ "use client";
2
+
3
+ import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
4
+ import { useDropzone, FileRejection } from "react-dropzone";
5
+ import { toast } from "sonner";
6
+ import { TrashIcon } from "@radix-ui/react-icons";
7
+ import type {
8
+ DirectionOptions,
9
+ FileUploaderInputProps,
10
+ FileUploaderProps,
11
+ } from "./types";
12
+ import { cn } from "../../utilities";
13
+ import { FileUploaderContext } from "./FileUploaderContext";
14
+ import { useFileUpload } from "./useFileUpload";
15
+
16
+ export const FileUploader = ({
17
+ className,
18
+ dropzoneOptions,
19
+ value,
20
+ onValueChange,
21
+ reSelect,
22
+ orientation = "vertical",
23
+ children,
24
+ dir,
25
+ ...props
26
+ }: FileUploaderProps) => {
27
+ const [isFileTooBig, setIsFileTooBig] = useState(false);
28
+ const [isLOF, setIsLOF] = useState(false);
29
+ const [activeIndex, setActiveIndex] = useState(-1);
30
+ const {
31
+ accept = {
32
+ "image/*": [".jpg", ".jpeg", ".png", ".gif"],
33
+ },
34
+ maxFiles = 1,
35
+ maxSize = 4 * 1024 * 1024,
36
+ multiple = true,
37
+ } = dropzoneOptions;
38
+
39
+ const reSelectAll = maxFiles === 1 ? true : reSelect;
40
+ const direction: DirectionOptions = dir === "rtl" ? "rtl" : "ltr";
41
+
42
+ const removeFileFromSet = useCallback(
43
+ (i: number) => {
44
+ if (!value) return;
45
+ const newFiles = value.filter((_, index) => index !== i);
46
+ onValueChange(newFiles);
47
+ },
48
+ [value, onValueChange]
49
+ );
50
+
51
+ const removeAll = useCallback(() => {
52
+ onValueChange(null);
53
+ }, [onValueChange]);
54
+
55
+ const handleKeyDown = useCallback(
56
+ (e: React.KeyboardEvent<HTMLDivElement>) => {
57
+ e.preventDefault();
58
+ e.stopPropagation();
59
+
60
+ if (!value) return;
61
+
62
+ const moveNext = () => {
63
+ const nextIndex = activeIndex + 1;
64
+ setActiveIndex(nextIndex > value.length - 1 ? 0 : nextIndex);
65
+ };
66
+
67
+ const movePrev = () => {
68
+ const nextIndex = activeIndex - 1;
69
+ setActiveIndex(nextIndex < 0 ? value.length - 1 : nextIndex);
70
+ };
71
+
72
+ const prevKey =
73
+ orientation === "horizontal"
74
+ ? direction === "ltr"
75
+ ? "ArrowLeft"
76
+ : "ArrowRight"
77
+ : "ArrowUp";
78
+
79
+ const nextKey =
80
+ orientation === "horizontal"
81
+ ? direction === "ltr"
82
+ ? "ArrowRight"
83
+ : "ArrowLeft"
84
+ : "ArrowDown";
85
+
86
+ if (e.key === nextKey) {
87
+ moveNext();
88
+ } else if (e.key === prevKey) {
89
+ movePrev();
90
+ } else if (e.key === "Enter" || e.key === "Space") {
91
+ if (activeIndex === -1) {
92
+ dropzoneState.inputRef.current?.click();
93
+ }
94
+ } else if (e.key === "Delete" || e.key === "Backspace") {
95
+ if (activeIndex !== -1) {
96
+ removeFileFromSet(activeIndex);
97
+ if (value.length - 1 === 0) {
98
+ setActiveIndex(-1);
99
+ return;
100
+ }
101
+ movePrev();
102
+ }
103
+ } else if (e.key === "Escape") {
104
+ setActiveIndex(-1);
105
+ }
106
+ },
107
+ // eslint-disable-next-line react-hooks/exhaustive-deps
108
+ [value, activeIndex, removeFileFromSet]
109
+ );
110
+
111
+ const onDrop = useCallback(
112
+ (acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
113
+ const files = acceptedFiles;
114
+
115
+ if (!files) {
116
+ toast.error("file error , probably too big");
117
+ return;
118
+ }
119
+
120
+ const newValues: File[] = value ? [...value] : [];
121
+
122
+ if (reSelectAll) {
123
+ newValues.splice(0, newValues.length);
124
+ }
125
+
126
+ files.forEach((file) => {
127
+ if (newValues.length < maxFiles) {
128
+ newValues.push(file);
129
+ }
130
+ });
131
+
132
+ onValueChange(newValues);
133
+
134
+ if (rejectedFiles.length > 0) {
135
+ for (const rejectedFile of rejectedFiles) {
136
+ if (rejectedFile.errors[0]?.code === "file-too-large") {
137
+ toast.error(
138
+ `File is too large. Max size is ${maxSize / 1024 / 1024}MB`
139
+ );
140
+ break;
141
+ }
142
+ if (rejectedFile.errors[0]?.message) {
143
+ toast.error(rejectedFile.errors[0].message);
144
+ break;
145
+ }
146
+ }
147
+ }
148
+ },
149
+ // eslint-disable-next-line react-hooks/exhaustive-deps
150
+ [reSelectAll, value]
151
+ );
152
+
153
+ useEffect(() => {
154
+ if (!value) return;
155
+ if (value.length === maxFiles) {
156
+ setIsLOF(true);
157
+ return;
158
+ }
159
+ setIsLOF(false);
160
+ }, [value, maxFiles]);
161
+
162
+ const opts = dropzoneOptions
163
+ ? dropzoneOptions
164
+ : { accept, maxFiles, maxSize, multiple };
165
+
166
+ const dropzoneState = useDropzone({
167
+ ...opts,
168
+ onDrop,
169
+ onDropRejected: () => setIsFileTooBig(true),
170
+ onDropAccepted: () => setIsFileTooBig(false),
171
+ });
172
+
173
+ return (
174
+ <FileUploaderContext.Provider
175
+ value={{
176
+ dropzoneState,
177
+ isLOF,
178
+ isFileTooBig,
179
+ removeFileFromSet,
180
+ removeAll,
181
+ activeIndex,
182
+ setActiveIndex,
183
+ orientation,
184
+ direction,
185
+ }}
186
+ >
187
+ <div
188
+ tabIndex={0}
189
+ onKeyDownCapture={handleKeyDown}
190
+ className={cn(
191
+ "grid w-full focus:outline-none overflow-hidden ",
192
+ className,
193
+ {
194
+ "gap-2": value && value.length > 0,
195
+ }
196
+ )}
197
+ dir={dir}
198
+ {...props}
199
+ >
200
+ {children}
201
+ </div>
202
+ </FileUploaderContext.Provider>
203
+ );
204
+ };
205
+
206
+ FileUploader.displayName = "FileUploader";
207
+
208
+ const FileUploaderContent = forwardRef<
209
+ HTMLDivElement,
210
+ React.HTMLAttributes<HTMLDivElement>
211
+ >(({ children, className, ...props }, ref) => {
212
+ const { orientation } = useFileUpload();
213
+ const containerRef = useRef<HTMLDivElement>(null);
214
+
215
+ return (
216
+ <div
217
+ className={cn("w-full px-1")}
218
+ ref={containerRef}
219
+ aria-description="content file holder"
220
+ >
221
+ <div
222
+ {...props}
223
+ ref={ref}
224
+ className={cn(
225
+ "flex rounded-xl gap-1",
226
+ orientation === "horizontal" ? "flex-raw flex-wrap" : "flex-col",
227
+ className
228
+ )}
229
+ >
230
+ {children}
231
+ </div>
232
+ </div>
233
+ );
234
+ });
235
+
236
+ FileUploaderContent.displayName = "FileUploaderContent";
237
+
238
+ const FileUploaderItem = forwardRef<
239
+ HTMLDivElement,
240
+ { index: number } & React.HTMLAttributes<HTMLDivElement>
241
+ >(({ className, index, children, ...props }, ref) => {
242
+ const { removeFileFromSet, activeIndex, direction } = useFileUpload();
243
+ const isSelected = index === activeIndex;
244
+ return (
245
+ <div
246
+ ref={ref}
247
+ className={cn(
248
+ "h-6 p-1 justify-between cursor-pointer relative",
249
+ className,
250
+ isSelected ? "bg-muted" : ""
251
+ )}
252
+ {...props}
253
+ >
254
+ <div className="text-sm leading-none tracking-tight flex items-center gap-1.5 h-full w-full">
255
+ {children}
256
+ </div>
257
+ <button
258
+ type="button"
259
+ className={cn(
260
+ "absolute",
261
+ direction === "rtl" ? "top-1 left-1" : "top-1 right-1"
262
+ )}
263
+ onClick={() => removeFileFromSet(index)}
264
+ >
265
+ <span className="sr-only">remove item {index}</span>
266
+ <TrashIcon className="w-4 h-4 hover:stroke-destructive duration-200 ease-in-out" />
267
+ </button>
268
+ </div>
269
+ );
270
+ });
271
+
272
+ FileUploaderItem.displayName = "FileUploaderItem";
273
+
274
+ const FileUploaderInput = ({
275
+ className,
276
+ children,
277
+ ...props
278
+ }: FileUploaderInputProps) => {
279
+ const { dropzoneState, isFileTooBig, isLOF } = useFileUpload();
280
+ const rootProps = isLOF ? {} : dropzoneState.getRootProps();
281
+ return (
282
+ <div
283
+ {...props}
284
+ className={`relative w-full ${
285
+ isLOF ? "opacity-50 cursor-not-allowed " : "cursor-pointer "
286
+ }`}
287
+ >
288
+ <div
289
+ className={cn(
290
+ `w-full rounded-lg duration-300 ease-in-out
291
+ ${
292
+ dropzoneState.isDragAccept
293
+ ? "border-green-500"
294
+ : dropzoneState.isDragReject || isFileTooBig
295
+ ? "border-red-500"
296
+ : "border-gray-300"
297
+ }`,
298
+ className
299
+ )}
300
+ {...rootProps}
301
+ >
302
+ {children}
303
+ </div>
304
+ <input
305
+ {...props}
306
+ disabled={isLOF}
307
+ ref={dropzoneState.inputRef}
308
+ {...dropzoneState.getInputProps()}
309
+ className={`${isLOF ? "cursor-not-allowed" : ""}`}
310
+ />
311
+ </div>
312
+ );
313
+ };
314
+
315
+ FileUploaderInput.displayName = "FileInput";
316
+
317
+ FileUploader.Content = FileUploaderContent;
318
+ FileUploader.Input = FileUploaderInput;
319
+ FileUploader.Item = FileUploaderItem;
@@ -0,0 +1,5 @@
1
+ import { createContext } from "react";
2
+ import { type FileUploaderContextType } from "./types";
3
+
4
+ export const FileUploaderContext =
5
+ createContext<FileUploaderContextType | null>(null);
@@ -0,0 +1 @@
1
+ export { FileUploader } from "./FileUploader";
@@ -0,0 +1,27 @@
1
+ import type { Dispatch, SetStateAction } from "react";
2
+ import type { DropzoneState, DropzoneOptions } from "react-dropzone";
3
+
4
+ export type FileUploaderProps = {
5
+ value: File[] | null;
6
+ reSelect?: boolean;
7
+ onValueChange: (value: File[] | null) => void;
8
+ dropzoneOptions: DropzoneOptions;
9
+ orientation?: "horizontal" | "vertical";
10
+ } & React.HTMLAttributes<HTMLDivElement>;
11
+
12
+ export type FileUploaderInputProps =
13
+ React.InputHTMLAttributes<HTMLInputElement>;
14
+
15
+ export type DirectionOptions = "rtl" | "ltr" | undefined;
16
+
17
+ export type FileUploaderContextType = {
18
+ dropzoneState: DropzoneState;
19
+ isLOF: boolean;
20
+ isFileTooBig: boolean;
21
+ removeFileFromSet: (index: number) => void;
22
+ removeAll: () => void;
23
+ activeIndex: number;
24
+ setActiveIndex: Dispatch<SetStateAction<number>>;
25
+ orientation: "horizontal" | "vertical";
26
+ direction: DirectionOptions;
27
+ };
@@ -0,0 +1,10 @@
1
+ import { useContext } from "react";
2
+ import { FileUploaderContext } from "./FileUploaderContext";
3
+
4
+ export const useFileUpload = () => {
5
+ const context = useContext(FileUploaderContext);
6
+ if (!context) {
7
+ throw new Error("useFileUpload must be used within a FileUploaderProvider");
8
+ }
9
+ return context;
10
+ };
@@ -0,0 +1,34 @@
1
+ import OTPInput from "react-otp-input";
2
+ import { type OtpInputProps } from "./types";
3
+ import { Input } from "../Input";
4
+ import { cn } from "../../../utilities";
5
+ import { useState } from "react";
6
+
7
+ export const OtpInput = ({
8
+ className,
9
+ inputProps,
10
+ ...props
11
+ }: OtpInputProps) => {
12
+ const [otp, setOtp] = useState("");
13
+
14
+ return (
15
+ <OTPInput
16
+ {...props}
17
+ value={otp}
18
+ onChange={setOtp}
19
+ renderInput={(renderProps) => (
20
+ <Input
21
+ {...inputProps}
22
+ {...renderProps}
23
+ className={cn(
24
+ "!w-12 !appearance-none selection:bg-base text-base-content",
25
+ className
26
+ )}
27
+ />
28
+ )}
29
+ containerStyle={`flex justify-center items-center flex-wrap text-2xl font-bold ${
30
+ props.renderSeparator ? "gap-1" : "gap-x-3 gap-y-2"
31
+ }`}
32
+ />
33
+ );
34
+ };
@@ -0,0 +1 @@
1
+ export { OtpInput } from "./OtpInput";
@@ -0,0 +1,7 @@
1
+ import { type OTPInputProps } from "react-otp-input";
2
+ import { InputProps } from "../Input/types";
3
+
4
+ export type OtpInputProps = {
5
+ className?: string;
6
+ inputProps?: Omit<InputProps, "type">;
7
+ } & Omit<OTPInputProps, "renderInput" | "onChange">;
@@ -1,4 +1,5 @@
1
1
  export { Input } from "./Input";
2
+ export { OtpInput } from "./OtpInput";
2
3
  export { Phone } from "./Phone";
3
4
  export { Radio } from "./Radio";
4
5
  export { Range } from "./Range";
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ export * from "./components/Tabs";
26
26
  export * from "./components/Text";
27
27
 
28
28
  /** data input */
29
+ export * from "./components/FileUploader";
29
30
  export * from "./components/Form";
30
31
  export * from "./components/Combobox";
31
32
  export * from "./components/DatePicker";