From a1fcd46bda3242ab73efec027986bf29baf5ce0f Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Sun, 6 Apr 2025 22:47:09 +0100 Subject: [PATCH] feat: image list in submit form --- package.json | 1 + pnpm-lock.yaml | 78 ++++++++++++++++++++++++ src/app/components/submit-form.tsx | 23 ++++--- src/app/components/submit/image-list.tsx | 71 +++++++++++++++++++++ src/app/components/submit/qr-upload.tsx | 4 +- 5 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 src/app/components/submit/image-list.tsx diff --git a/package.json b/package.json index 8bf07b5..9562279 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@auth/prisma-adapter": "2.7.2", + "@hello-pangea/dnd": "^18.0.1", "@prisma/client": "^6.5.0", "@trafficlunar/asmcrypto.js": "^1.0.2", "@yudiel/react-qr-scanner": "2.2.2-beta.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 058f506..bff814f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@auth/prisma-adapter': specifier: 2.7.2 version: 2.7.2(@prisma/client@6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2)) + '@hello-pangea/dnd': + specifier: ^18.0.1 + version: 18.0.1(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@prisma/client': specifier: ^6.5.0 version: 6.5.0(prisma@6.5.0(typescript@5.8.2))(typescript@5.8.2) @@ -408,6 +411,12 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@hello-pangea/dnd@18.0.1': + resolution: {integrity: sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -903,6 +912,9 @@ packages: '@types/react@19.1.0': resolution: {integrity: sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@typescript-eslint/eslint-plugin@8.29.0': resolution: {integrity: sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1195,6 +1207,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-box-model@1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -2062,6 +2077,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} peerDependencies: @@ -2076,6 +2094,18 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-select@5.10.1: resolution: {integrity: sha512-roPEZUL4aRZDx6DcsD+ZNreVl+fM8VsKn0Wtex1v4IazH60ILp5xhdlp464IsEAlJdXeD+BhDAFsBVMfvLQueA==} peerDependencies: @@ -2092,6 +2122,9 @@ packages: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -2277,6 +2310,9 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinyglobby@0.2.12: resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} engines: {node: '>=12.0.0'} @@ -2348,6 +2384,11 @@ packages: '@types/react': optional: true + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + webrtc-adapter@9.0.1: resolution: {integrity: sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==} engines: {node: '>=6.0.0', npm: '>=3.10.0'} @@ -2686,6 +2727,18 @@ snapshots: '@floating-ui/utils@0.2.9': {} + '@hello-pangea/dnd@18.0.1(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.0 + css-box-model: 1.2.1 + raf-schd: 4.0.3 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-redux: 9.2.0(@types/react@19.1.0)(react@19.1.0)(redux@5.0.1) + redux: 5.0.1 + transitivePeerDependencies: + - '@types/react' + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -3069,6 +3122,8 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/use-sync-external-store@0.0.6': {} + '@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -3399,6 +3454,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-box-model@1.2.1: + dependencies: + tiny-invariant: 1.3.3 + csstype@3.1.3: {} damerau-levenshtein@1.0.8: {} @@ -4411,6 +4470,8 @@ snapshots: queue-microtask@1.2.3: {} + raf-schd@4.0.3: {} + react-dom@19.1.0(react@19.1.0): dependencies: react: 19.1.0 @@ -4425,6 +4486,15 @@ snapshots: react-is@16.13.1: {} + react-redux@9.2.0(@types/react@19.1.0)(react@19.1.0)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.1.0 + use-sync-external-store: 1.5.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.0 + redux: 5.0.1 + react-select@5.10.1(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@babel/runtime': 7.27.0 @@ -4453,6 +4523,8 @@ snapshots: react@19.1.0: {} + redux@5.0.1: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -4717,6 +4789,8 @@ snapshots: tapable@2.2.1: {} + tiny-invariant@1.3.3: {} + tinyglobby@0.2.12: dependencies: fdir: 6.4.3(picomatch@4.0.2) @@ -4817,6 +4891,10 @@ snapshots: optionalDependencies: '@types/react': 19.1.0 + use-sync-external-store@1.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + webrtc-adapter@9.0.1: dependencies: sdp: 3.2.0 diff --git a/src/app/components/submit-form.tsx b/src/app/components/submit-form.tsx index e593ab4..671b280 100644 --- a/src/app/components/submit-form.tsx +++ b/src/app/components/submit-form.tsx @@ -2,8 +2,8 @@ import { redirect } from "next/navigation"; -import { useEffect, useState } from "react"; -import { useDropzone } from "react-dropzone"; +import { useCallback, useEffect, useState } from "react"; +import { FileWithPath, useDropzone } from "react-dropzone"; import { Icon } from "@iconify/react"; import { AES_CCM } from "@trafficlunar/asmcrypto.js"; @@ -16,11 +16,20 @@ import Mii from "@/utils/mii.js/mii"; import TomodachiLifeMii from "@/utils/tomodachi-life-mii"; import TagSelector from "./submit/tag-selector"; +import ImageList from "./submit/image-list"; import QrUpload from "./submit/qr-upload"; import QrScanner from "./submit/qr-scanner"; export default function SubmitForm() { - const { acceptedFiles, getRootProps, getInputProps } = useDropzone({ + const [files, setFiles] = useState([]); + + const handleDrop = useCallback((acceptedFiles: FileWithPath[]) => { + setFiles((prev) => [...prev, ...acceptedFiles]); + }, []); + + const { getRootProps, getInputProps } = useDropzone({ + onDrop: handleDrop, + maxFiles: 3, accept: { "image/*": [".png", ".jpg", ".jpeg", ".bmp", ".webp"], }, @@ -154,14 +163,14 @@ export default function SubmitForm() { /> -
+
- +

Drag and drop your images here @@ -171,7 +180,7 @@ export default function SubmitForm() {

- {/* todo: show file list here */} +
diff --git a/src/app/components/submit/image-list.tsx b/src/app/components/submit/image-list.tsx new file mode 100644 index 0000000..ebc7c4c --- /dev/null +++ b/src/app/components/submit/image-list.tsx @@ -0,0 +1,71 @@ +import { FileWithPath } from "react-dropzone"; +import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; +import { Icon } from "@iconify/react"; + +interface Props { + files: readonly FileWithPath[]; + setFiles: React.Dispatch>; +} + +export default function ImageList({ files, setFiles }: Props) { + const handleDelete = (index: number) => { + const newFiles = [...files]; + newFiles.splice(index, 1); + setFiles(newFiles); + }; + + const handleDragEnd = (result: DropResult) => { + if (!result.destination) return; + + const items = Array.from(files); + const [reorderedItem] = items.splice(result.source.index, 1); + items.splice(result.destination.index, 0, reorderedItem); + + setFiles(items); + }; + + return ( + + + {(provided) => ( +
+ {files.map((file, index) => ( + + {(provided) => ( +
+ {file.name} +
+ {file.name} + +
+ +
+ +
+
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+
+ ); +} diff --git a/src/app/components/submit/qr-upload.tsx b/src/app/components/submit/qr-upload.tsx index 429fb33..cbcac41 100644 --- a/src/app/components/submit/qr-upload.tsx +++ b/src/app/components/submit/qr-upload.tsx @@ -44,11 +44,11 @@ export default function QrUpload({ setQrBytesRaw }: Props) { }); return ( -
+