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 +36 -36
- package/dist/ikoncomponents/fileUpload/index.d.ts +11 -12
- package/dist/ikoncomponents/fileUpload/index.js +117 -59
- package/dist/ikoncomponents/fileUploadApi/index.d.ts +31 -0
- package/dist/ikoncomponents/fileUploadApi/index.js +159 -0
- package/dist/ikoncomponents/main-layout/nav-main.js +3 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +7 -1
- package/dist/styles.css +28 -0
- package/dist/utils/api/file-upload copy/index.d.ts +13 -0
- package/dist/utils/api/file-upload copy/index.js +120 -0
- package/package.json +2 -1
- package/dist/ikoncomponents/table/component/DataTable.d.ts +0 -14
- package/dist/ikoncomponents/table/component/DataTable.js +0 -10
- package/dist/ikoncomponents/table/component/DataTablePageSize.d.ts +0 -1
- package/dist/ikoncomponents/table/component/DataTablePageSize.js +0 -16
- package/dist/ikoncomponents/table/component/DataTablePagination.d.ts +0 -6
- package/dist/ikoncomponents/table/component/DataTablePagination.js +0 -14
- package/dist/ikoncomponents/table/component/DataTableSearch.d.ts +0 -1
- package/dist/ikoncomponents/table/component/DataTableSearch.js +0 -27
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
|
|
1
|
+
export declare const convertFileToObject: (file: File) => Promise<any>;
|
|
2
|
+
interface FileUploaderProps {
|
|
2
3
|
label?: string;
|
|
3
4
|
isDrag?: boolean;
|
|
4
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
117
|
+
/* ----------------------------------------------------
|
|
118
|
+
Convert Base64 back to Blob URL
|
|
119
|
+
---------------------------------------------------- */
|
|
120
|
+
export function getFileUrlFromObject(obj) {
|
|
62
121
|
const byteCharacters = atob(obj.base64);
|
|
63
|
-
const byteNumbers =
|
|
64
|
-
|
|
65
|
-
.
|
|
66
|
-
|
|
67
|
-
|
|
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.
|
|
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,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
|
-
}
|