ikoncomponents 1.6.9 → 1.7.1

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 CHANGED
@@ -1,36 +1,36 @@
1
- This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
-
3
- ## Getting Started
4
-
5
- First, run the development server:
6
-
7
- ```bash
8
- npm run dev
9
- # or
10
- yarn dev
11
- # or
12
- pnpm dev
13
- # or
14
- bun dev
15
- ```
16
-
17
- Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
-
19
- You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
-
21
- This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
-
23
- ## Learn More
24
-
25
- To learn more about Next.js, take a look at the following resources:
26
-
27
- - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
- - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
-
30
- You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
-
32
- ## Deploy on Vercel
33
-
34
- The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
-
36
- Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
@@ -1,15 +1,14 @@
1
- export interface FileUploaderProps {
1
+ export declare const convertFileToObject: (file: File) => Promise<any>;
2
+ interface FileUploaderProps {
2
3
  label?: string;
3
4
  isDrag?: boolean;
4
- onFileSelect: (fileObj: any) => Promise<any> | void;
5
+ fileInput?: boolean;
6
+ isMultiple?: boolean;
7
+ tooltipContent?: string;
8
+ fileNamePlaceholder?: string;
9
+ showPreview?: boolean;
10
+ onFileSelect: (files: any[]) => Promise<any> | void;
5
11
  }
6
- export declare const convertFileToObject: (file: File) => Promise<{
7
- message: string;
8
- fileName: string;
9
- size: number;
10
- type: string;
11
- lastModified: number;
12
- base64: string;
13
- }>;
14
- export declare function FileUploader({ label, isDrag, onFileSelect, }: FileUploaderProps): import("react/jsx-runtime").JSX.Element;
15
- export declare function getImageFromObject(obj: any): string;
12
+ export default function FileUploader({ label, isDrag, fileInput, isMultiple, tooltipContent, fileNamePlaceholder, showPreview, onFileSelect, }: FileUploaderProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function getFileUrlFromObject(obj: any): string;
14
+ export {};
@@ -1,69 +1,127 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState } from "react";
4
- import { UploadCloud, FileUp } from "lucide-react";
5
- // --- Helper: Convert File to Object with Base64 ---
6
- export const convertFileToObject = async (file) => {
7
- const arrayBuffer = await file.arrayBuffer();
8
- const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
9
- return {
10
- message: "File processed successfully",
11
- fileName: file.name,
12
- size: file.size,
13
- type: file.type,
14
- lastModified: file.lastModified,
15
- base64: base64,
16
- };
17
- };
18
- export function FileUploader({ label = "Upload File", isDrag = false, onFileSelect, }) {
19
- const [isDragging, setIsDragging] = useState(false);
20
- const handleFile = async (file) => {
21
- if (!file)
22
- return;
23
- const fileObj = await convertFileToObject(file); // convert to object
24
- await onFileSelect(fileObj); // pass object to parent
25
- };
26
- // ---- DRAG HANDLERS ----
27
- const handleDrop = (e) => {
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState, useRef } from "react";
4
+ import { UploadCloud, FileUp, Upload, X } from "lucide-react";
5
+ import { v4 as uuidv4 } from "uuid";
6
+ import { Input } from "@/shadcn/input";
7
+ import { IconButtonWithTooltip } from "../buttons";
8
+ /* ----------------------------------------------------
9
+ SAFE Base64 Converter
10
+ ---------------------------------------------------- */
11
+ export const convertFileToObject = (file) => new Promise((resolve, reject) => {
12
+ const reader = new FileReader();
13
+ reader.onload = () => {
28
14
  var _a;
29
- if (!isDrag)
30
- return;
31
- e.preventDefault();
32
- setIsDragging(false);
33
- const file = (_a = e.dataTransfer.files) === null || _a === void 0 ? void 0 : _a[0];
34
- if (file)
35
- handleFile(file);
15
+ const result = reader.result;
16
+ const base64 = result.split(",")[1];
17
+ const nameParts = file.name.split(".");
18
+ const ext = nameParts.length >= 2 ? (_a = nameParts.pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase() : "";
19
+ const fileType = file.type || (ext ? `${ext}` : "application/octet-stream");
20
+ resolve({
21
+ resourceId: uuidv4(),
22
+ message: "File processed successfully",
23
+ fileName: file.name,
24
+ fileSize: file.size,
25
+ fileType, // ✅ FIXED
26
+ base64,
27
+ });
36
28
  };
37
- const handleDragOver = (e) => {
38
- if (!isDrag)
29
+ reader.onerror = reject;
30
+ reader.readAsDataURL(file);
31
+ });
32
+ /* ----------------------------------------------------
33
+ FileInput UI (Text + Button Input)
34
+ ---------------------------------------------------- */
35
+ function FileInputUI({ tooltipContent, fileNames, fileNamePlaceholder, onFileNamesChange, onFileSelect, isMultiple, }) {
36
+ const inputRef = useRef(null);
37
+ const handleChange = async (e) => {
38
+ const files = e.target.files;
39
+ if (!(files === null || files === void 0 ? void 0 : files.length))
39
40
  return;
40
- e.preventDefault();
41
- setIsDragging(true);
41
+ const fileObjects = await Promise.all(Array.from(files).map((f) => convertFileToObject(f)));
42
+ const names = fileObjects.map((f) => f.fileName).join(", ");
43
+ onFileNamesChange === null || onFileNamesChange === void 0 ? void 0 : onFileNamesChange(names);
44
+ onFileSelect === null || onFileSelect === void 0 ? void 0 : onFileSelect(fileObjects);
42
45
  };
43
- const handleDragLeave = () => {
44
- if (!isDrag)
45
- return;
46
- setIsDragging(false);
46
+ const filesCount = fileNames ? fileNames.split(", ").length : 0;
47
+ return (_jsxs("div", { className: "flex w-full", children: [_jsx(Input, { ref: inputRef, type: "file", multiple: isMultiple, className: "hidden", onChange: handleChange }), _jsx(Input, { className: "rounded-e-none", placeholder: fileNamePlaceholder, value: filesCount === 0
48
+ ? ""
49
+ : filesCount === 1
50
+ ? fileNames
51
+ : `${filesCount} files selected`, readOnly: true }), _jsx(IconButtonWithTooltip, { tooltipContent: tooltipContent || "Browse File", onClick: () => { var _a; return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, className: "border-s-0 rounded-s-none", children: _jsx(Upload, {}) })] }));
52
+ }
53
+ /* ----------------------------------------------------
54
+ Image Preview Component
55
+ ---------------------------------------------------- */
56
+ function ImagePreview({ previews, onRemove, }) {
57
+ if (!previews.length)
58
+ return null;
59
+ return (_jsx("div", { className: "flex flex-wrap gap-2 mt-2 justify-center", children: previews.map((preview, i) => (_jsxs("div", { className: "relative group", children: [_jsx("img", { src: preview.url, alt: preview.name, className: "h-16 w-16 object-cover rounded-md border p-1" }), onRemove && (_jsx("button", { onClick: (e) => {
60
+ e.stopPropagation();
61
+ onRemove(i);
62
+ }, className: "absolute -top-1.5 -right-1.5 bg-red-500 text-white rounded-full w-4 h-4 \r\n flex items-center justify-center opacity-0 group-hover:opacity-100 \r\n transition-opacity shadow-sm", children: _jsx(X, { className: "w-2.5 h-2.5" }) })), _jsx("p", { className: "text-xs text-gray-500 text-center mt-0.5 max-w-[64px] truncate", children: preview.name })] }, i))) }));
63
+ }
64
+ export default function FileUploader({ label = "Upload File", isDrag = false, fileInput = false, isMultiple = false, tooltipContent, fileNamePlaceholder = "Choose files...", showPreview = true, onFileSelect, }) {
65
+ const [isDragging, setIsDragging] = useState(false);
66
+ const [fileNames, setFileNames] = useState("");
67
+ const [previews, setPreviews] = useState([]);
68
+ const filesCount = fileNames ? fileNames.split(", ").length : 0;
69
+ /* ------------ GENERATE PREVIEWS ------------ */
70
+ const generatePreviews = (files) => {
71
+ previews.forEach((p) => URL.revokeObjectURL(p.url));
72
+ const newPreviews = files
73
+ .filter((f) => f.type.startsWith("image/"))
74
+ .map((f) => ({ url: URL.createObjectURL(f), name: f.name }));
75
+ setPreviews(newPreviews);
47
76
  };
48
- const handleInputChange = (e) => {
49
- var _a;
50
- const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
51
- if (file)
52
- handleFile(file);
77
+ /* ------------ REMOVE A SINGLE PREVIEW ------------ */
78
+ const handleRemovePreview = (index) => {
79
+ URL.revokeObjectURL(previews[index].url);
80
+ setPreviews((prev) => prev.filter((_, i) => i !== index));
81
+ };
82
+ /* ------------ COMMON FILE HANDLER ------------ */
83
+ const handleFiles = async (files) => {
84
+ const fileArray = Array.from(files);
85
+ const fileList = isMultiple ? fileArray : [fileArray[0]];
86
+ const fileObjects = await Promise.all(fileList.map((file) => convertFileToObject(file)));
87
+ setFileNames(fileObjects.map((f) => f.fileName).join(", "));
88
+ if (showPreview)
89
+ generatePreviews(fileList);
90
+ onFileSelect(fileObjects);
53
91
  };
54
- return (_jsxs("div", { className: "flex flex-col gap-2 w-full", children: [_jsx("label", { className: "text-sm font-medium", children: label }), _jsx("input", { type: "file", id: "fileInput", className: "hidden", onChange: handleInputChange }), isDrag ? (_jsx("div", { className: `border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition
55
- ${isDragging ? "border-blue-600 bg-blue-50" : "border-gray-300"}
56
- `, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onClick: () => { var _a; return (_a = document.getElementById("fileInput")) === null || _a === void 0 ? void 0 : _a.click(); }, children: _jsxs("div", { className: "flex flex-col items-center gap-3", children: [_jsx(UploadCloud, { className: "w-10 h-10 text-blue-600" }), _jsxs("p", { className: "text-gray-600", children: ["Drag & drop your file here or", " ", _jsx("span", { className: "text-blue-600 underline", children: "browse" })] })] }) })) : (
57
- // ----- SIMPLE UPLOAD BOX -----
58
- _jsxs("div", { className: "border rounded-lg p-4 flex flex-col items-center gap-2 cursor-pointer text-center", onClick: () => { var _a; return (_a = document.getElementById("fileInput")) === null || _a === void 0 ? void 0 : _a.click(); }, children: [_jsx(FileUp, { className: "w-8 h-8 text-blue-600" }), _jsx("span", { className: "text-blue-600 underline", children: "Browse File" })] }))] }));
92
+ /* ------------ DRAG HANDLERS ------------ */
93
+ const dragHandlers = isDrag
94
+ ? {
95
+ onDragOver: (e) => {
96
+ e.preventDefault();
97
+ setIsDragging(true);
98
+ },
99
+ onDragLeave: () => setIsDragging(false),
100
+ onDrop: (e) => {
101
+ e.preventDefault();
102
+ setIsDragging(false);
103
+ if (e.dataTransfer.files.length) {
104
+ handleFiles(e.dataTransfer.files);
105
+ }
106
+ },
107
+ }
108
+ : {};
109
+ return (_jsxs("div", Object.assign({}, dragHandlers, { className: `flex flex-col ${fileInput ? "items-start text-left" : "items-center text-center"} justify-center gap-2 cursor-pointer px-4 pb-4 rounded-lg transition ${fileInput
110
+ ? ""
111
+ : `border-2 border-dashed ${isDragging ? "border-blue-600 bg-blue-50" : "border-gray-300"}`}`, children: [_jsx("label", { className: "text-md mt-4 font-bold", children: label }), fileInput && (_jsxs(_Fragment, { children: [_jsx(FileInputUI, { fileNames: fileNames, fileNamePlaceholder: fileNamePlaceholder, tooltipContent: tooltipContent, onFileNamesChange: setFileNames, onFileSelect: onFileSelect, isMultiple: isMultiple }), isDrag && (_jsx("p", { className: "text-sm text-gray-500", children: isDragging ? "Drop files here..." : "or drag & drop files here" })), filesCount > 1 && (_jsxs("p", { className: "text-sm font-medium", children: [filesCount, " files selected"] }))] })), !fileInput && (_jsxs(_Fragment, { children: [_jsx("input", { type: "file", id: "fileInput", className: "hidden", multiple: isMultiple, onChange: (e) => e.target.files && handleFiles(e.target.files) }), isDrag ? (_jsxs("div", { className: "mt-3 p-6 w-full text-center cursor-pointer", onClick: () => { var _a; return (_a = document.getElementById("fileInput")) === null || _a === void 0 ? void 0 : _a.click(); }, children: [filesCount === 0 && (_jsxs("div", { className: "flex flex-col items-center gap-3", children: [_jsx(UploadCloud, { className: "w-10 h-10 text-blue-600" }), _jsxs("p", { className: "text-gray-600", children: ["Drag & drop ", isMultiple ? "files" : "a file", " or", " ", _jsx("span", { className: "text-blue-600 underline", children: "browse" })] })] })), filesCount > 0 && (_jsx("p", { className: "mt-3 text-sm font-medium", children: filesCount === 1
112
+ ? fileNames
113
+ : `${filesCount} files selected` }))] })) : (_jsxs("div", { className: "border rounded-lg p-4 flex flex-col items-center gap-2 cursor-pointer text-center", onClick: () => { var _a; return (_a = document.getElementById("fileInput")) === null || _a === void 0 ? void 0 : _a.click(); }, children: [filesCount === 0 && (_jsxs(_Fragment, { children: [_jsx(FileUp, { className: "w-8 h-8 text-blue-600" }), _jsx("span", { className: "text-blue-600 underline", children: isMultiple ? "Browse Files" : "Browse File" })] })), filesCount > 0 && (_jsx("p", { className: "text-sm font-medium", children: filesCount === 1
114
+ ? fileNames
115
+ : `${filesCount} files selected` }))] })), showPreview && (_jsx(ImagePreview, { previews: previews, onRemove: handleRemovePreview }))] }))] })));
59
116
  }
60
- // --- Helper Function: Recreate Image from File Object ---
61
- export function getImageFromObject(obj) {
117
+ /* ----------------------------------------------------
118
+ Convert Base64 back to Blob URL
119
+ ---------------------------------------------------- */
120
+ export function getFileUrlFromObject(obj) {
62
121
  const byteCharacters = atob(obj.base64);
63
- const byteNumbers = new Array(byteCharacters.length)
64
- .fill(0)
65
- .map((_, i) => byteCharacters.charCodeAt(i));
66
- const byteArray = new Uint8Array(byteNumbers);
67
- const blob = new Blob([byteArray], { type: obj.type });
68
- return URL.createObjectURL(blob); // usable in <img src="..." />
122
+ const byteNumbers = Array.from(byteCharacters).map((c) => c.charCodeAt(0));
123
+ const blob = new Blob([new Uint8Array(byteNumbers)], {
124
+ type: obj.resourceType,
125
+ });
126
+ return URL.createObjectURL(blob);
69
127
  }
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ export declare const convertFileToObject: (file: File) => Promise<any>;
3
+ export interface FileUploader2Ref {
4
+ /** Triggers the upload for already-selected files. Resolves when done. */
5
+ upload: () => Promise<void>;
6
+ }
7
+ export interface FileUploader2Props {
8
+ label?: string;
9
+ isDrag?: boolean;
10
+ onUploadComplete?: (response: any | any[]) => void;
11
+ imageUrl?: string;
12
+ fileInput?: boolean;
13
+ isMultiple?: boolean;
14
+ tooltipContent?: string;
15
+ fileNamePlaceholder?: string;
16
+ /** Called with the raw File objects before upload */
17
+ onFileSelect?: (files: File[]) => void;
18
+ /**
19
+ * Custom upload endpoint. When provided, files are POSTed as multipart/form-data
20
+ * and the full JSON response is passed to onUploadComplete.
21
+ * Falls back to uploadFilePublic() when omitted.
22
+ */
23
+ apiUrl?: string;
24
+ /**
25
+ * When true, the upload does NOT start automatically on file selection.
26
+ * Instead, call ref.upload() to trigger it manually.
27
+ */
28
+ manual?: boolean;
29
+ }
30
+ export declare const FileUploaderApi: React.ForwardRefExoticComponent<FileUploader2Props & React.RefAttributes<FileUploader2Ref>>;
31
+ export declare function getFileUrlFromObject(obj: any): string;
@@ -0,0 +1,159 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState, useRef, useImperativeHandle, forwardRef } from "react";
4
+ import { UploadCloud, FileUp, Upload } from "lucide-react";
5
+ import { v4 as uuidv4 } from "uuid";
6
+ import { Input } from "@/shadcn/input";
7
+ import { IconButtonWithTooltip } from "../buttons";
8
+ import { uploadFilePublic } from "../../utils/api/file-upload copy";
9
+ /* ----------------------------------------------------
10
+ SAFE Base64 Converter (kept for local preview use)
11
+ ---------------------------------------------------- */
12
+ export const convertFileToObject = (file) => new Promise((resolve, reject) => {
13
+ const reader = new FileReader();
14
+ reader.onload = () => {
15
+ const result = reader.result;
16
+ const base64 = result.split(",")[1];
17
+ resolve({
18
+ resourceId: uuidv4(),
19
+ message: "File processed successfully",
20
+ resourceName: file.name,
21
+ resourceSize: file.size,
22
+ resourceType: file.type,
23
+ base64,
24
+ });
25
+ };
26
+ reader.onerror = reject;
27
+ reader.readAsDataURL(file);
28
+ });
29
+ /* ----------------------------------------------------
30
+ FileInput UI (Text + Button Input)
31
+ ---------------------------------------------------- */
32
+ function FileInputUI({ tooltipContent, fileNames, fileNamePlaceholder, onFileNamesChange, onFileSelect, isMultiple, inputId, }) {
33
+ const inputRef = useRef(null);
34
+ const handleChange = (e) => {
35
+ const files = e.target.files;
36
+ if (!(files === null || files === void 0 ? void 0 : files.length))
37
+ return;
38
+ const fileList = isMultiple ? Array.from(files) : [files[0]];
39
+ const names = fileList.map((f) => f.name).join(", ");
40
+ onFileNamesChange === null || onFileNamesChange === void 0 ? void 0 : onFileNamesChange(names);
41
+ onFileSelect === null || onFileSelect === void 0 ? void 0 : onFileSelect(fileList);
42
+ };
43
+ const filesCount = fileNames ? fileNames.split(", ").length : 0;
44
+ return (_jsxs("div", { className: "flex w-full", children: [_jsx(Input, { ref: inputRef, type: "file", multiple: isMultiple, className: "hidden", onChange: handleChange, id: inputId }), _jsx(Input, { className: "rounded-e-none", placeholder: fileNamePlaceholder, value: filesCount === 0
45
+ ? ""
46
+ : filesCount === 1
47
+ ? fileNames
48
+ : `${filesCount} files selected`, readOnly: true }), _jsx(IconButtonWithTooltip, { tooltipContent: tooltipContent || "Browse File", onClick: () => { var _a; return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, className: "border-s-0 rounded-s-none", children: _jsx(Upload, {}) })] }));
49
+ }
50
+ /* ----------------------------------------------------
51
+ Main FileUploader2 Component
52
+ ---------------------------------------------------- */
53
+ export const FileUploaderApi = forwardRef(function FileUploaderApi({ label = "Upload File", isDrag = false, onUploadComplete, imageUrl, fileInput = false, isMultiple = false, tooltipContent, fileNamePlaceholder = "Choose files...", onFileSelect, apiUrl, manual = false, }, ref) {
54
+ const [isDragging, setIsDragging] = useState(false);
55
+ const [previews, setPreviews] = useState([]);
56
+ const [fileNames, setFileNames] = useState("");
57
+ const [loading, setLoading] = useState(false);
58
+ // Holds files pending upload when manual=true
59
+ const pendingFilesRef = useRef([]);
60
+ const inputId = `fileInput-${label.replace(/\s+/g, "-").toLowerCase()}`;
61
+ const filesCount = fileNames ? fileNames.split(", ").length : 0;
62
+ /* ------------ UPLOAD EXECUTOR ------------ */
63
+ const executeUpload = async (fileList) => {
64
+ try {
65
+ setLoading(true);
66
+ const responses = await Promise.all(fileList.map((f) => {
67
+ if (apiUrl) {
68
+ const formData = new FormData();
69
+ formData.append("file", f);
70
+ return fetch(apiUrl, { method: "POST", body: formData })
71
+ .then((res) => {
72
+ if (!res.ok)
73
+ throw new Error(`Upload failed: ${res.statusText}`);
74
+ return res.json();
75
+ })
76
+ .then((data) => data);
77
+ }
78
+ return uploadFilePublic(f);
79
+ }));
80
+ onUploadComplete === null || onUploadComplete === void 0 ? void 0 : onUploadComplete(isMultiple ? responses : responses[0]);
81
+ }
82
+ catch (err) {
83
+ console.error("Upload failed:", err);
84
+ }
85
+ finally {
86
+ setLoading(false);
87
+ }
88
+ };
89
+ /* ------------ EXPOSE upload() TO PARENT ------------ */
90
+ useImperativeHandle(ref, () => ({
91
+ upload: async () => {
92
+ if (!pendingFilesRef.current.length) {
93
+ console.warn("FileUploader2: no files selected to upload.");
94
+ return;
95
+ }
96
+ await executeUpload(pendingFilesRef.current);
97
+ },
98
+ }));
99
+ /* ------------ CORE FILE HANDLER ------------ */
100
+ const handleFiles = async (files) => {
101
+ const fileList = isMultiple ? files : [files[0]];
102
+ // Local previews for images
103
+ const newPreviews = fileList.map((f) => f.type.startsWith("image/") ? URL.createObjectURL(f) : "");
104
+ setPreviews(newPreviews.filter(Boolean));
105
+ setFileNames(fileList.map((f) => f.name).join(", "));
106
+ // Notify parent of raw files
107
+ onFileSelect === null || onFileSelect === void 0 ? void 0 : onFileSelect(fileList);
108
+ if (manual) {
109
+ // Store files for later — upload() will consume them
110
+ pendingFilesRef.current = fileList;
111
+ }
112
+ else {
113
+ await executeUpload(fileList);
114
+ }
115
+ };
116
+ /* ------------ INPUT CHANGE ------------ */
117
+ const handleInputChange = (e) => {
118
+ const files = e.target.files;
119
+ if (files === null || files === void 0 ? void 0 : files.length)
120
+ handleFiles(Array.from(files));
121
+ };
122
+ /* ------------ DRAG HANDLERS ------------ */
123
+ const dragHandlers = isDrag
124
+ ? {
125
+ onDragOver: (e) => {
126
+ e.preventDefault();
127
+ setIsDragging(true);
128
+ },
129
+ onDragLeave: () => setIsDragging(false),
130
+ onDrop: (e) => {
131
+ e.preventDefault();
132
+ setIsDragging(false);
133
+ if (e.dataTransfer.files.length) {
134
+ handleFiles(Array.from(e.dataTransfer.files));
135
+ }
136
+ },
137
+ }
138
+ : {};
139
+ /* ------------ DISPLAY PREVIEWS ------------ */
140
+ const displayPreviews = previews.length > 0 ? previews : imageUrl ? [imageUrl] : [];
141
+ return (_jsxs("div", Object.assign({}, dragHandlers, { className: `flex flex-col ${fileInput ? "items-start text-left" : "items-center text-center"} justify-center gap-2 cursor-pointer px-4 pb-4 rounded-lg transition ${fileInput
142
+ ? ""
143
+ : `border-2 border-dashed ${isDragging ? "border-blue-600 bg-blue-50" : "border-gray-300"}`}`, children: [_jsx("label", { className: "text-md mt-4 font-bold", children: label }), _jsx("input", { type: "file", id: inputId, className: "hidden", multiple: isMultiple, onChange: handleInputChange }), fileInput && (_jsxs(_Fragment, { children: [_jsx(FileInputUI, { fileNames: fileNames, fileNamePlaceholder: fileNamePlaceholder, tooltipContent: tooltipContent, onFileNamesChange: setFileNames, onFileSelect: (files) => handleFiles(files), isMultiple: isMultiple, inputId: `${inputId}-fileinput` }), isDrag && (_jsx("p", { className: "text-sm text-gray-500", children: isDragging ? "Drop files here..." : "or drag & drop files here" })), loading && (_jsx("p", { className: "text-sm text-blue-500 animate-pulse", children: "Uploading..." })), filesCount > 1 && !loading && (_jsxs("p", { className: "text-sm font-medium", children: [filesCount, " files selected"] }))] })), !fileInput && (_jsx(_Fragment, { children: isDrag ? (_jsxs("div", { className: "mt-3 p-6 w-full text-center cursor-pointer", onClick: () => { var _a; return (_a = document.getElementById(inputId)) === null || _a === void 0 ? void 0 : _a.click(); }, children: [filesCount === 0 && !loading && (_jsxs("div", { className: "flex flex-col items-center gap-3", children: [_jsx(UploadCloud, { className: "w-10 h-10 text-blue-600" }), _jsxs("p", { className: "text-gray-600", children: ["Drag & drop ", isMultiple ? "files" : "a file", " or", " ", _jsx("span", { className: "text-blue-600 underline", children: "browse" })] })] })), loading && (_jsx("p", { className: "text-sm text-blue-500 animate-pulse", children: "Uploading..." })), filesCount > 0 && !loading && (_jsx("p", { className: "mt-3 text-sm font-medium", children: filesCount === 1
144
+ ? fileNames
145
+ : `${filesCount} files selected` }))] })) : (_jsxs("div", { className: "border rounded-lg p-4 flex flex-col items-center gap-2 cursor-pointer text-center", onClick: () => { var _a; return (_a = document.getElementById(inputId)) === null || _a === void 0 ? void 0 : _a.click(); }, children: [filesCount === 0 && !loading && (_jsxs(_Fragment, { children: [_jsx(FileUp, { className: "w-8 h-8 text-blue-600" }), _jsx("span", { className: "text-blue-600 underline", children: isMultiple ? "Browse Files" : "Browse File" })] })), loading && (_jsx("p", { className: "text-sm text-blue-500 animate-pulse", children: "Uploading..." })), filesCount > 0 && !loading && (_jsx("p", { className: "text-sm font-medium", children: filesCount === 1
146
+ ? fileNames
147
+ : `${filesCount} files selected` }))] })) })), !fileInput && displayPreviews.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 mt-1 justify-center", children: displayPreviews.map((src, i) => (_jsx("img", { src: src, alt: `preview-${i}`, className: "h-10 object-cover rounded-md border p-1" }, i))) }))] })));
148
+ });
149
+ /* ----------------------------------------------------
150
+ Convert Base64 back to Blob URL (utility, kept for compat)
151
+ ---------------------------------------------------- */
152
+ export function getFileUrlFromObject(obj) {
153
+ const byteCharacters = atob(obj.base64);
154
+ const byteNumbers = Array.from(byteCharacters).map((c) => c.charCodeAt(0));
155
+ const blob = new Blob([new Uint8Array(byteNumbers)], {
156
+ type: obj.resourceType,
157
+ });
158
+ return URL.createObjectURL(blob);
159
+ }
@@ -5,13 +5,15 @@ import { ChevronRight } from "lucide-react";
5
5
  import { SidebarGroup, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem } from "../../shadcn/sidebar";
6
6
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../shadcn/collapsible";
7
7
  import Link from "next/link";
8
+ import { usePathname } from "next/navigation";
8
9
  import { useSidebarNav } from "./SidebarNavContext";
9
10
  export function NavMain() {
10
11
  const { navItems } = useSidebarNav();
12
+ const pathname = usePathname();
11
13
  // if (!navItems || navItems.length === 0) {
12
14
  // return null;
13
15
  // }
14
- return (_jsx(SidebarGroup, { children: _jsx(SidebarMenu, { children: navItems.map((item) => item.items && item.items.length > 0 ? (_jsx(Collapsible, { asChild: true, defaultOpen: item.isActive, className: "group/collapsible", children: _jsxs(SidebarMenuItem, { children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { tooltip: item.title, children: [item.icon && _jsx(item.icon, {}), _jsx("span", { children: item.title }), _jsx(ChevronRight, { className: "ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" })] }) }), _jsx(CollapsibleContent, { children: _jsx(SidebarMenuSub, { children: item.items.map((subItem) => (_jsx(SidebarMenuSubItem, { children: _jsx(SidebarMenuSubButton, { asChild: true, children: _jsx(Link, { href: subItem.url, children: _jsx("span", { children: subItem.title }) }) }) }, subItem.title))) }) })] }) }, item.title)) : (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.title, children: _jsxs(Link, { href: item.url, className: "flex items-center gap-2 w-full", children: [item.icon && _jsx(item.icon, {}), _jsx("span", { children: item.title })] }) }) }, item.title))) }) }));
16
+ return (_jsx(SidebarGroup, { children: _jsx(SidebarMenu, { children: navItems.map((item) => item.items && item.items.length > 0 ? (_jsx(Collapsible, { asChild: true, defaultOpen: item.isActive || item.items.some(sub => pathname.startsWith(sub.url)), className: "group/collapsible", children: _jsxs(SidebarMenuItem, { children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { tooltip: item.title, isActive: pathname.startsWith(item.url) || item.items.some(sub => pathname.startsWith(sub.url)), children: [item.icon && _jsx(item.icon, {}), _jsx("span", { children: item.title }), _jsx(ChevronRight, { className: "ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" })] }) }), _jsx(CollapsibleContent, { children: _jsx(SidebarMenuSub, { children: item.items.map((subItem) => (_jsx(SidebarMenuSubItem, { children: _jsx(SidebarMenuSubButton, { asChild: true, isActive: pathname === subItem.url || pathname.startsWith(`${subItem.url}/`), children: _jsx(Link, { href: subItem.url, children: _jsx("span", { children: subItem.title }) }) }) }, subItem.title))) }) })] }) }, item.title)) : (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.title, isActive: pathname === item.url || pathname.startsWith(`${item.url}/`), children: _jsxs(Link, { href: item.url, className: "flex items-center gap-2 w-full", children: [item.icon && _jsx(item.icon, {}), _jsx("span", { children: item.title })] }) }) }, item.title))) }) }));
15
17
  }
16
18
  // Helper component to set nav items from pages
17
19
  export function RenderSidebarNav({ items, sidebarHeader, sidebarFooter }) {
package/dist/index.d.ts CHANGED
@@ -53,8 +53,6 @@ export type { ComboBoxInputProps, ComboboxItemProps, } from "./ikoncomponents/co
53
53
  export { DataTableColumnFilter } from "./ikoncomponents/data-table/datatable-column-filter";
54
54
  export { DataTableFacetedFilter } from "./ikoncomponents/data-table/datatable-faceted-filter";
55
55
  export { DataTableFilterMenu } from "./ikoncomponents/data-table/datatable-filter-menu";
56
- export { convertFileToObject, FileUploader, getImageFromObject, } from "./ikoncomponents/fileUpload";
57
- export type { FileUploaderProps } from "./ikoncomponents/fileUpload";
58
56
  export { DataTablePagination } from "./ikoncomponents/data-table/datatable-pagination";
59
57
  export { DataTableToolbar } from "./ikoncomponents/data-table/datatable-toolbar";
60
58
  export { getDataTableColumnTitle } from "./ikoncomponents/data-table/function";
@@ -140,3 +138,11 @@ export { getValidAccessToken, refreshAccessToken, decodeAccessToken, logOut } fr
140
138
  export type { TokenResponse } from "./utils/token-management/types";
141
139
  export { useIsMobile } from "./hooks/use-mobile";
142
140
  export { useRefresh } from "./ikoncomponents/main-layout/RefreshContext";
141
+ export { FileUploaderApi } from "./ikoncomponents/fileUploadApi";
142
+ export type { FileUploader2Ref, FileUploader2Props } from "./ikoncomponents/fileUploadApi";
143
+ export { fetchFileAsBase64 } from "./utils/api/file-upload copy/index";
144
+ export { downloadFileByResourceId } from "./utils/api/file-upload copy/index";
145
+ export { fetchImagePreview } from "./utils/api/file-upload copy/index";
146
+ export { convertFileToObject } from "./ikoncomponents/fileUpload";
147
+ export { getFileUrlFromObject } from "./ikoncomponents/fileUpload";
148
+ export { default as FileUploader } from "./ikoncomponents/fileUpload";
package/dist/index.js CHANGED
@@ -50,7 +50,6 @@ export { ComboboxInput } from "./ikoncomponents/combobox-input";
50
50
  export { DataTableColumnFilter } from "./ikoncomponents/data-table/datatable-column-filter";
51
51
  export { DataTableFacetedFilter } from "./ikoncomponents/data-table/datatable-faceted-filter";
52
52
  export { DataTableFilterMenu } from "./ikoncomponents/data-table/datatable-filter-menu";
53
- export { convertFileToObject, FileUploader, getImageFromObject, } from "./ikoncomponents/fileUpload";
54
53
  export { DataTablePagination } from "./ikoncomponents/data-table/datatable-pagination";
55
54
  export { DataTableToolbar } from "./ikoncomponents/data-table/datatable-toolbar";
56
55
  export { getDataTableColumnTitle } from "./ikoncomponents/data-table/function";
@@ -117,3 +116,10 @@ export { setCookieSession, getCookieSession, clearCookieSession, clearAllCookieS
117
116
  export { getValidAccessToken, refreshAccessToken, decodeAccessToken, logOut } from "./utils/token-management";
118
117
  export { useIsMobile } from "./hooks/use-mobile";
119
118
  export { useRefresh } from "./ikoncomponents/main-layout/RefreshContext";
119
+ export { FileUploaderApi } from "./ikoncomponents/fileUploadApi";
120
+ export { fetchFileAsBase64 } from "./utils/api/file-upload copy/index";
121
+ export { downloadFileByResourceId } from "./utils/api/file-upload copy/index";
122
+ export { fetchImagePreview } from "./utils/api/file-upload copy/index";
123
+ export { convertFileToObject } from "./ikoncomponents/fileUpload";
124
+ export { getFileUrlFromObject } from "./ikoncomponents/fileUpload";
125
+ export { default as FileUploader } from "./ikoncomponents/fileUpload";
package/dist/styles.css CHANGED
@@ -273,6 +273,9 @@
273
273
  .inset-y-0 {
274
274
  inset-block: calc(var(--spacing) * 0);
275
275
  }
276
+ .-top-1\.5 {
277
+ top: calc(var(--spacing) * -1.5);
278
+ }
276
279
  .-top-12 {
277
280
  top: calc(var(--spacing) * -12);
278
281
  }
@@ -303,6 +306,9 @@
303
306
  .top-full {
304
307
  top: 100%;
305
308
  }
309
+ .-right-1\.5 {
310
+ right: calc(var(--spacing) * -1.5);
311
+ }
306
312
  .right-0 {
307
313
  right: calc(var(--spacing) * 0);
308
314
  }
@@ -444,6 +450,9 @@
444
450
  .my-auto {
445
451
  margin-block: auto;
446
452
  }
453
+ .mt-0\.5 {
454
+ margin-top: calc(var(--spacing) * 0.5);
455
+ }
447
456
  .mt-1 {
448
457
  margin-top: calc(var(--spacing) * 1);
449
458
  }
@@ -686,6 +695,9 @@
686
695
  .h-12 {
687
696
  height: calc(var(--spacing) * 12);
688
697
  }
698
+ .h-16 {
699
+ height: calc(var(--spacing) * 16);
700
+ }
689
701
  .h-20 {
690
702
  height: calc(var(--spacing) * 20);
691
703
  }
@@ -845,6 +857,9 @@
845
857
  .w-12 {
846
858
  width: calc(var(--spacing) * 12);
847
859
  }
860
+ .w-16 {
861
+ width: calc(var(--spacing) * 16);
862
+ }
848
863
  .w-32 {
849
864
  width: calc(var(--spacing) * 32);
850
865
  }
@@ -908,6 +923,9 @@
908
923
  .max-w-20 {
909
924
  max-width: calc(var(--spacing) * 20);
910
925
  }
926
+ .max-w-\[64px\] {
927
+ max-width: 64px;
928
+ }
911
929
  .max-w-\[120px\] {
912
930
  max-width: 120px;
913
931
  }
@@ -1661,6 +1679,9 @@
1661
1679
  background-color: color-mix(in oklab, var(--primary) 10%, transparent);
1662
1680
  }
1663
1681
  }
1682
+ .bg-red-500 {
1683
+ background-color: var(--color-red-500);
1684
+ }
1664
1685
  .bg-secondary {
1665
1686
  background-color: var(--secondary);
1666
1687
  }
@@ -2363,6 +2384,13 @@
2363
2384
  opacity: 100%;
2364
2385
  }
2365
2386
  }
2387
+ .group-hover\:opacity-100 {
2388
+ &:is(:where(.group):hover *) {
2389
+ @media (hover: hover) {
2390
+ opacity: 100%;
2391
+ }
2392
+ }
2393
+ }
2366
2394
  .group-hover\/menu-item\:opacity-100 {
2367
2395
  &:is(:where(.group\/menu-item):hover *) {
2368
2396
  @media (hover: hover) {
@@ -0,0 +1,13 @@
1
+ export declare const uploadFilePublic: (file: File) => Promise<any>;
2
+ export declare const fetchImagePreview: (resourceId: string) => Promise<string>;
3
+ export declare const fileUpload: (fileObjArray: Array<{
4
+ fileName: string;
5
+ type: string;
6
+ base64: string;
7
+ }>) => Promise<any>;
8
+ export declare const downloadFileByResourceId: (resourceId: string) => Promise<void>;
9
+ export declare const fetchFileAsBase64: (resourceId: string) => Promise<{
10
+ base64: string;
11
+ fileSize: number;
12
+ fileType: string;
13
+ }>;
@@ -0,0 +1,120 @@
1
+ import { getValidAccessToken } from "@/utils/token-management";
2
+ const IKON_BASE_API_URL = "https://ikoncloud-dev.keross.com/ikon-api";
3
+ export const uploadFilePublic = async (file) => {
4
+ const accessToken = await getValidAccessToken(IKON_BASE_API_URL || "");
5
+ const formData = new FormData();
6
+ formData.append("file", file);
7
+ const response = await fetch(`${IKON_BASE_API_URL}/platform/file-resource/upload/public`, {
8
+ method: "POST",
9
+ headers: Object.assign({}, (accessToken ? { Authorization: `Bearer ${accessToken}` } : {})),
10
+ body: formData,
11
+ });
12
+ if (!response.ok) {
13
+ const errorText = await response.text();
14
+ throw new Error(`Upload failed: ${errorText}`);
15
+ }
16
+ const data = await response.json();
17
+ if (data.files && data.files.length > 0) {
18
+ //just return the 1st resourceId
19
+ return data;
20
+ }
21
+ throw new Error("No files returned from upload");
22
+ };
23
+ export const fetchImagePreview = async (resourceId) => {
24
+ const accessToken = await getValidAccessToken(IKON_BASE_API_URL || "");
25
+ const response = await fetch(`${IKON_BASE_API_URL}/platform/file-resource/download/public/${resourceId}`, {
26
+ method: "GET",
27
+ headers: {
28
+ "Authorization": `Bearer ${accessToken}`
29
+ }
30
+ });
31
+ if (!response.ok)
32
+ throw new Error("Failed to load image");
33
+ // Convert binary response to a temporary URL
34
+ const imageBlob = await response.blob();
35
+ return URL.createObjectURL(imageBlob);
36
+ };
37
+ export const fileUpload = async (fileObjArray) => {
38
+ try {
39
+ const accessToken = await getValidAccessToken(process.env.IKON_API_URL || "");
40
+ console.log("Uploading files:", fileObjArray);
41
+ const response = await fetch(`${process.env.SALES_CRM_API_URL}/api/upload-base64`, {
42
+ method: "POST",
43
+ headers: Object.assign({ "Content-Type": "application/json" }, (accessToken ? { Authorization: `Bearer ${accessToken}` } : {})),
44
+ body: JSON.stringify({ files: fileObjArray }), // send array
45
+ });
46
+ if (!response.ok) {
47
+ const errorText = await response.text();
48
+ throw new Error(`Upload failed: ${errorText}`);
49
+ }
50
+ const data = await response.json();
51
+ console.log("Server response:", data);
52
+ return data;
53
+ }
54
+ catch (err) {
55
+ console.error("File upload error:", err);
56
+ throw err;
57
+ }
58
+ };
59
+ export const downloadFileByResourceId = async (resourceId) => {
60
+ const accessToken = await getValidAccessToken(IKON_BASE_API_URL || "");
61
+ const response = await fetch(`${IKON_BASE_API_URL}/platform/file-resource/download/public/${resourceId}`, {
62
+ method: "GET",
63
+ headers: Object.assign({}, (accessToken ? { Authorization: `Bearer ${accessToken}` } : {})),
64
+ });
65
+ if (!response.ok) {
66
+ throw new Error("Failed to download file");
67
+ }
68
+ const blob = await response.blob();
69
+ const contentDisposition = response.headers.get("Content-Disposition");
70
+ let fileName = "downloaded-file";
71
+ if (contentDisposition) {
72
+ const match = contentDisposition.match(/filename="(.+)"/);
73
+ if (match)
74
+ fileName = match[1];
75
+ }
76
+ const url = window.URL.createObjectURL(blob);
77
+ const a = document.createElement("a");
78
+ a.href = url;
79
+ a.download = fileName;
80
+ document.body.appendChild(a);
81
+ a.click();
82
+ // cleanup
83
+ a.remove();
84
+ window.URL.revokeObjectURL(url);
85
+ };
86
+ export const fetchFileAsBase64 = async (resourceId) => {
87
+ const accessToken = await getValidAccessToken(IKON_BASE_API_URL || "");
88
+ const response = await fetch(`${IKON_BASE_API_URL}/platform/file-resource/download/public/${resourceId}`, {
89
+ method: "GET",
90
+ headers: Object.assign({}, (accessToken ? { Authorization: `Bearer ${accessToken}` } : {})),
91
+ });
92
+ if (!response.ok) {
93
+ throw new Error("Failed to fetch file");
94
+ }
95
+ const blob = await response.blob();
96
+ // ✅ Convert blob → base64
97
+ const base64 = await new Promise((resolve, reject) => {
98
+ const reader = new FileReader();
99
+ reader.onloadend = () => {
100
+ const result = reader.result;
101
+ const base64String = result.split(",")[1]; // remove data:*/*;base64,
102
+ resolve(base64String);
103
+ };
104
+ reader.onerror = reject;
105
+ reader.readAsDataURL(blob);
106
+ });
107
+ // ✅ Extract filename (if available)
108
+ const contentDisposition = response.headers.get("Content-Disposition");
109
+ let fileName = "unknown";
110
+ if (contentDisposition) {
111
+ const match = contentDisposition.match(/filename="(.+)"/);
112
+ if (match)
113
+ fileName = match[1];
114
+ }
115
+ return {
116
+ base64,
117
+ fileSize: blob.size,
118
+ fileType: blob.type,
119
+ };
120
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikoncomponents",
3
- "version": "1.6.9",
3
+ "version": "1.7.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "css": "dist/styles.css",
@@ -74,6 +74,7 @@
74
74
  "tailwind-merge": "^3.3.1",
75
75
  "tailwindcss-animate": "^1.0.7",
76
76
  "tap": "^21.5.0",
77
+ "use-debounce": "^10.1.1",
77
78
  "uuid": "^13.0.0",
78
79
  "vaul": "^1.1.2",
79
80
  "zod": "^4.3.6",
@@ -1,14 +0,0 @@
1
- import React from "react";
2
- export interface ColumnDef<T> {
3
- header: string | (() => React.ReactNode);
4
- accessorKey?: keyof T;
5
- cell?: (row: T) => React.ReactNode;
6
- }
7
- interface DataTableProps<T> {
8
- data: T[];
9
- columns: ColumnDef<T>[];
10
- keyExtractor: (row: T) => string | number;
11
- onRowClick?: (row: T) => void;
12
- }
13
- export declare function DataTable<T>({ data, columns, keyExtractor, onRowClick }: DataTableProps<T>): import("react/jsx-runtime").JSX.Element;
14
- export {};
@@ -1,10 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/shadcn/table";
3
- import { Table } from "lucide-react";
4
- export function DataTable({ data, columns, keyExtractor, onRowClick }) {
5
- return (_jsx("div", { className: "rounded-md border bg-background shadow-sm overflow-hidden", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsx(TableRow, { className: "bg-muted border-b border-border", children: columns.map((col, index) => (_jsx(TableHead, { className: "font-semibold text-muted-foreground", children: typeof col.header === "function" ? col.header() : col.header }, index))) }) }), _jsx(TableBody, { children: data && data.length > 0 ? (data.map((row) => (_jsx(TableRow, { onClick: () => onRowClick && onRowClick(row), className: `hover:bg-muted/50 transition-colors border-b border-border ${onRowClick ? "cursor-pointer" : ""}`, children: columns.map((col, colIndex) => (_jsx(TableCell, { className: colIndex === 0 ? "font-medium" : "", children: col.cell
6
- ? col.cell(row)
7
- : col.accessorKey
8
- ? String(row[col.accessorKey] || "N/A")
9
- : null }, colIndex))) }, keyExtractor(onRowClick ? row : row))))) : (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columns.length, className: "h-24 text-center text-muted-foreground", children: "No records found." }) })) })] }) }));
10
- }
@@ -1 +0,0 @@
1
- export declare function DataTablePageSize(): import("react/jsx-runtime").JSX.Element;
@@ -1,16 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useRouter, usePathname, useSearchParams } from "next/navigation";
4
- export function DataTablePageSize() {
5
- const router = useRouter();
6
- const pathname = usePathname();
7
- const searchParams = useSearchParams();
8
- const currentSize = searchParams.get("size") || "10";
9
- const handleSizeChange = (e) => {
10
- const params = new URLSearchParams(searchParams.toString());
11
- params.set("size", e.target.value);
12
- params.set("page", "1");
13
- router.push(`${pathname}?${params.toString()}`);
14
- };
15
- return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: "Rows per page:" }), _jsxs("select", { className: "h-9 rounded-md border border-border bg-background text-foreground px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-primary", value: currentSize, onChange: handleSizeChange, children: [_jsx("option", { value: "10", children: "10" }), _jsx("option", { value: "20", children: "20" }), _jsx("option", { value: "50", children: "50" }), _jsx("option", { value: "100", children: "100" })] })] }));
16
- }
@@ -1,6 +0,0 @@
1
- interface PaginationProps {
2
- totalPages: number;
3
- currentPage: number;
4
- }
5
- export declare function DataTablePagination({ totalPages, currentPage }: PaginationProps): import("react/jsx-runtime").JSX.Element;
6
- export {};
@@ -1,14 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useRouter, usePathname, useSearchParams } from "next/navigation";
4
- export function DataTablePagination({ totalPages, currentPage }) {
5
- const router = useRouter();
6
- const pathname = usePathname();
7
- const searchParams = useSearchParams();
8
- const handlePageChange = (newPage) => {
9
- const params = new URLSearchParams(searchParams.toString());
10
- params.set("page", newPage.toString());
11
- router.push(`${pathname}?${params.toString()}`);
12
- };
13
- return (_jsxs("div", { className: "flex items-center space-x-2", children: [_jsx("button", { onClick: () => handlePageChange(currentPage - 1), disabled: currentPage <= 1, className: "h-9 px-3 py-1 text-sm font-medium border border-border rounded-md bg-background text-foreground hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors", children: "Previous" }), _jsxs("span", { className: "text-sm text-muted-foreground px-2", children: ["Page ", _jsx("span", { className: "font-semibold", children: currentPage }), " of", " ", _jsx("span", { className: "font-semibold", children: Math.max(totalPages, 1) })] }), _jsx("button", { onClick: () => handlePageChange(currentPage + 1), disabled: currentPage >= totalPages, className: "h-9 px-3 py-1 text-sm font-medium border border-border rounded-md bg-background text-foreground hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors", children: "Next" })] }));
14
- }
@@ -1 +0,0 @@
1
- export declare function DataTableSearch(): import("react/jsx-runtime").JSX.Element;
@@ -1,27 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useRouter, usePathname, useSearchParams } from "next/navigation";
4
- import { useState, useEffect } from "react";
5
- import { useDebounce } from "use-debounce";
6
- import { Search } from "lucide-react";
7
- export function DataTableSearch() {
8
- const router = useRouter();
9
- const pathname = usePathname();
10
- const searchParams = useSearchParams();
11
- const [searchTerm, setSearchTerm] = useState(searchParams.get("search") || "");
12
- const [debouncedSearch] = useDebounce(searchTerm, 500);
13
- useEffect(() => {
14
- const params = new URLSearchParams(searchParams.toString());
15
- if (debouncedSearch) {
16
- params.set("search", debouncedSearch);
17
- }
18
- else {
19
- params.delete("search");
20
- }
21
- if (params.get("search") !== searchParams.get("search")) {
22
- params.set("page", "1");
23
- router.push(`${pathname}?${params.toString()}`);
24
- }
25
- }, [debouncedSearch, pathname, router, searchParams]);
26
- return (_jsxs("div", { className: "relative w-full max-w-sm", children: [_jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx("input", { type: "text", placeholder: "Search ...", value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), className: "flex h-9 w-full rounded-md border border-border bg-transparent pl-9 pr-3 py-1 text-sm shadow-sm transition-colors text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary" })] }));
27
- }