diff --git a/apps/cs/src/pages/auth/login/ui/login-page.tsx b/apps/cs/src/pages/auth/login/ui/login-page.tsx index b5545f256..579b866f3 100644 --- a/apps/cs/src/pages/auth/login/ui/login-page.tsx +++ b/apps/cs/src/pages/auth/login/ui/login-page.tsx @@ -1,10 +1,10 @@ "use client"; import Image from "next/image"; +import { useRouter } from "next/navigation"; import { images } from "@common/assets"; import { LoginApi } from "@/entities/auth"; import "./login.css"; -import { useRouter } from "next/navigation"; function LoginPage() { const router = useRouter(); diff --git a/apps/cs/src/pages/image/lib/utils.ts b/apps/cs/src/pages/image/lib/utils.ts new file mode 100644 index 000000000..c6c8ca101 --- /dev/null +++ b/apps/cs/src/pages/image/lib/utils.ts @@ -0,0 +1,61 @@ +import { Upload, type HttpResponse } from "tus-js-client"; +import { getCookie, setCookie } from "@/shared/lib"; +import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config"; + +type UploadOptions = { + handleUploadStart?: (upload: Upload) => void; + handleProgress?: (percentage: string) => void; + handleSuccess?: (upload: Upload) => void; + handleResponse?: (res: HttpResponse) => void; +}; + +export const handleUpload = (files: FileList, options?: UploadOptions) => { + let metadata = {}; + + const file = files.item(0); + if (!file) return; + metadata = { + filename: file.name, + filetype: file.type, + }; + + const atflCd = getCookie("atfl-cd-key"); + if (atflCd) { + metadata = { + ...metadata, + atflCd, + }; + } + + const upload = new Upload(file, { + endpoint: `${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`, + chunkSize: 10 * 1024 * 1024, + headers: { + [CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "", + [AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "", + }, + metadata, + onError(error) { + console.error(`Failed because: ${error}`); + }, + onProgress(bytesUploaded, bytesTotal) { + const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2); + options?.handleProgress?.(percentage); + }, + onAfterResponse(req, res) { + const fileInfo = res.getHeader("X-File-Info"); + if (fileInfo === undefined || fileInfo === null) return; + + setCookie("atfl-cd-key", JSON.parse(fileInfo)?.atflCd ?? ""); + options?.handleResponse?.(res); + }, + onSuccess() { + options?.handleSuccess?.(upload); + }, + }); + + upload.start(); + options?.handleUploadStart?.(upload); + + console.log("upload start >>>>>", upload); +}; diff --git a/apps/cs/src/pages/image/ui/image-form.tsx b/apps/cs/src/pages/image/ui/image-form.tsx index 0229bc9a8..22fa52714 100644 --- a/apps/cs/src/pages/image/ui/image-form.tsx +++ b/apps/cs/src/pages/image/ui/image-form.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState } from "react"; import { useRouter } from "next/navigation"; import type { UseFormReturn } from "react-hook-form"; import { @@ -7,6 +8,7 @@ import { FileUploader, Input, Label, + Progress, RadioGroup, RadioGroupItem, Table, @@ -16,8 +18,10 @@ import { TableRow, } from "@common/components"; import { CodeSelect } from "@/entities/code"; -import type { ImageFormData } from "../model/image-interface"; import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form"; +import type { ImageFormData } from "../model/image-interface"; +import { handleUpload } from "../lib/utils"; +import type { HttpResponse, Upload } from "tus-js-client"; type ImageFormProps = { form: UseFormReturn; @@ -29,7 +33,10 @@ type ImageFormProps = { function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) { const isModifyMode = !!imgSn; const atflYn = form.watch("atflYn"); + const router = useRouter(); + const [upload, setUpload] = useState(null); + const [progress, setProgress] = useState(0); const onSubmit = async (data: ImageFormData) => { await handleSave?.(data); @@ -39,6 +46,17 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) { await handleDelete?.(); }; + const handleProgress = (percentage: string) => { + setProgress(Number(percentage)); + }; + + const handleUploadResponse = (res: HttpResponse) => { + const fileInfo = res.getHeader("X-File-Info"); + if (fileInfo === undefined || fileInfo === null) return; + + form.setValue("atflCd", JSON.parse(fileInfo)?.atflCd); + }; + return (
@@ -154,17 +172,36 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) { ( - + render={() => ( + { - const file = e.target.files?.[0]; - if (file) { - field.onChange(file.name); - } + const files = e.target.files; + if (!files) return; + handleUpload(files, { + handleUploadStart: setUpload, + handleProgress, + handleSuccess: setUpload, + handleResponse: handleUploadResponse, + }); }} + customPreview={ + + } /> diff --git a/packages/common/components/package.json b/packages/common/components/package.json index ca2986403..d9ba2a55e 100644 --- a/packages/common/components/package.json +++ b/packages/common/components/package.json @@ -35,6 +35,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-radio-group": "^1.2.2", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", diff --git a/packages/common/components/src/ui/file-uploader/file-uploader.css b/packages/common/components/src/ui/file-uploader/file-uploader.css index 16a92099c..4dd70bc7f 100644 --- a/packages/common/components/src/ui/file-uploader/file-uploader.css +++ b/packages/common/components/src/ui/file-uploader/file-uploader.css @@ -2,12 +2,12 @@ @layer components { .file-uploader-container { - @apply flex w-full max-w-sm items-center gap-2; + @apply flex h-[3.2rem] w-full max-w-sm items-center gap-2; } .file-uploader-preview { - @apply h-12 w-full min-w-56 overflow-x-auto whitespace-nowrap rounded-md border border-input bg-background px-3 py-2 text-[1.2rem]; + @apply h-full w-full min-w-56 overflow-x-auto whitespace-nowrap rounded-md border border-input bg-background px-3 py-2 text-[1.2rem]; } .file-uploader-label { - @apply flex-shrink-0 cursor-pointer rounded-sm border border-input bg-muted px-4 py-2 text-[1.2rem] font-medium text-muted-foreground; + @apply inline-flex h-full flex-shrink-0 cursor-pointer items-center rounded-sm border border-input bg-muted px-4 py-2 text-[1.2rem] font-medium text-muted-foreground; } } diff --git a/packages/common/components/src/ui/file-uploader/index.tsx b/packages/common/components/src/ui/file-uploader/index.tsx index d130cefa0..da3e04b67 100644 --- a/packages/common/components/src/ui/file-uploader/index.tsx +++ b/packages/common/components/src/ui/file-uploader/index.tsx @@ -8,13 +8,15 @@ import { combineFileName, validFileType } from "./utils"; type FileUploaderProps = React.InputHTMLAttributes & { id: string; files?: ReadonlyArray; + customPreview?: React.ReactNode; onUpload: (e: React.ChangeEvent) => void; }; export function FileUploader({ id, - className, files, + customPreview, + className, onUpload, ...props }: FileUploaderProps) { @@ -27,9 +29,11 @@ export function FileUploader({ {...props} onChange={onUpload} /> -
- {files?.filter(validFileType).map(combineFileName).join(", ")} -
+ {customPreview || ( +
+ {files?.filter(validFileType).map(combineFileName).join(", ")} +
+ )} diff --git a/packages/common/components/src/ui/index.ts b/packages/common/components/src/ui/index.ts index 007c8ab43..c3aad6220 100644 --- a/packages/common/components/src/ui/index.ts +++ b/packages/common/components/src/ui/index.ts @@ -15,6 +15,7 @@ export * from "./layout"; export * from "./loading"; export * from "./page-title"; export * from "./popover"; +export * from "./progress"; export * from "./radio-group"; export * from "./select"; export * from "./slider"; diff --git a/packages/common/components/src/ui/progress/index.tsx b/packages/common/components/src/ui/progress/index.tsx new file mode 100644 index 000000000..edb9b92af --- /dev/null +++ b/packages/common/components/src/ui/progress/index.tsx @@ -0,0 +1,28 @@ +"use client"; + +import * as React from "react"; +import * as ProgressPrimitive from "@radix-ui/react-progress"; + +import { cn } from "../../lib/utils"; + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)); +Progress.displayName = ProgressPrimitive.Root.displayName; + +export { Progress }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 553c3a9e2..a56b25dd8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -390,6 +390,9 @@ importers: '@radix-ui/react-popover': specifier: ^1.1.6 version: 1.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-progress': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-radio-group': specifier: ^1.2.2 version: 1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) @@ -1623,7 +1626,7 @@ packages: magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.6.3) typescript: 5.6.3 - vite: 6.2.0(@types/node@22.13.9) + vite: 6.2.0(@types/node@22.13.5) dev: true /@jridgewell/gen-mapping@0.3.8: @@ -2295,6 +2298,27 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /@radix-ui/react-progress@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==} + peerDependencies: + '@types/react': ^18.3.18 + '@types/react-dom': ^18.3.5 + react: ^18.3.1 + react-dom: ^18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) + '@types/react': 18.3.18 + '@types/react-dom': 18.3.5(@types/react@18.3.18) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-radio-group@1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==} peerDependencies: @@ -3059,7 +3083,7 @@ packages: browser-assert: 1.2.1 storybook: 8.6.0(prettier@3.5.2) ts-dedent: 2.2.0 - vite: 6.2.0(@types/node@22.13.9) + vite: 6.2.0(@types/node@22.13.5) dev: true /@storybook/components@8.6.0(storybook@8.6.0): @@ -3191,7 +3215,7 @@ packages: resolve: 1.22.10 storybook: 8.6.0(prettier@3.5.2) tsconfig-paths: 4.2.0 - vite: 6.2.0(@types/node@22.13.9) + vite: 6.2.0(@types/node@22.13.5) transitivePeerDependencies: - rollup - supports-color @@ -3637,7 +3661,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.9) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.2.0(@types/node@22.13.9) + vite: 6.2.0(@types/node@22.13.5) transitivePeerDependencies: - supports-color dev: true @@ -8414,7 +8438,7 @@ packages: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.5(typescript@5.6.3) - vite: 6.2.0(@types/node@22.13.9) + vite: 6.2.0(@types/node@22.13.5) transitivePeerDependencies: - supports-color - typescript