From 7cb9fa632030479ea895a9c9908b9c0b296db365 Mon Sep 17 00:00:00 2001 From: Citali Date: Thu, 3 Jul 2025 11:54:46 +0200 Subject: [PATCH] Refactor and add alt text generator --- package-lock.json | 732 ++++++++++++++++-- package.json | 11 +- .../migration.sql | 8 + .../migration.sql | 2 + .../migration.sql | 6 + .../20250701210825_sortindex/migration.sql | 20 + .../20250701224958_image_unique/migration.sql | 12 + prisma/schema.prisma | 20 +- scripts/resize-all-images.ts | 111 +++ src/actions/albums/updateAlbumSortOrder.ts | 15 + src/actions/artists/updateArtist.ts | 4 + src/actions/artists/updateArtistSortOrder.ts | 15 + .../categories/updateCategorySortOrder.ts | 15 + .../galleries/updateGallerySortOrder.ts | 15 + src/actions/images/deleteImage.ts | 105 ++- src/actions/images/uploadImage.ts | 32 +- src/actions/tags/updateTagSortOrder.ts | 15 + src/app/albums/page.tsx | 6 +- src/app/artists/page.tsx | 4 +- src/app/categories/page.tsx | 4 +- src/app/galleries/page.tsx | 7 +- src/app/images/edit/[id]/page.tsx | 2 +- src/app/layout.tsx | 4 +- src/app/tags/page.tsx | 5 +- src/components/albums/list/ListAlbums.tsx | 89 ++- .../artists/edit/EditArtistForm.tsx | 54 +- src/components/artists/list/ListArtists.tsx | 70 +- .../categories/list/ListCategories.tsx | 71 +- .../galleries/list/ListGalleries.tsx | 81 +- src/components/images/edit/EditImageForm.tsx | 33 +- src/components/sort/SortableItem.tsx | 103 +++ src/components/sort/SortableList.tsx | 85 ++ src/components/tags/list/ListTags.tsx | 77 +- src/components/ui/checkbox.tsx | 32 + src/schemas/artists/artistSchema.ts | 3 + src/types/SortableItem.ts | 6 + src/utils/generateImageText.ts | 79 ++ src/utils/setSortIndex.ts | 27 + 38 files changed, 1747 insertions(+), 233 deletions(-) create mode 100644 prisma/migrations/20250628230531_image_variant/migration.sql create mode 100644 prisma/migrations/20250629114921_artist_description/migration.sql create mode 100644 prisma/migrations/20250629115442_artist_source_social/migration.sql create mode 100644 prisma/migrations/20250701210825_sortindex/migration.sql create mode 100644 prisma/migrations/20250701224958_image_unique/migration.sql create mode 100644 scripts/resize-all-images.ts create mode 100644 src/actions/albums/updateAlbumSortOrder.ts create mode 100644 src/actions/artists/updateArtistSortOrder.ts create mode 100644 src/actions/categories/updateCategorySortOrder.ts create mode 100644 src/actions/galleries/updateGallerySortOrder.ts create mode 100644 src/actions/tags/updateTagSortOrder.ts create mode 100644 src/components/sort/SortableItem.tsx create mode 100644 src/components/sort/SortableList.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/types/SortableItem.ts create mode 100644 src/utils/generateImageText.ts create mode 100644 src/utils/setSortIndex.ts diff --git a/package-lock.json b/package-lock.json index 0421d81..440b518 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,14 @@ "dependencies": { "@aws-sdk/client-s3": "^3.837.0", "@aws-sdk/s3-request-presigner": "^3.837.0", + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", "@hookform/resolvers": "^5.1.1", "@material/material-color-utilities": "^0.3.0", - "@prisma/client": "^6.10.1", + "@prisma/client": "^6.11.0", + "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", @@ -37,6 +42,7 @@ "react-dom": "^19.0.0", "react-hook-form": "^7.58.1", "react-icons": "^5.5.0", + "replicate": "^1.0.1", "sharp": "^0.34.2", "sonner": "^2.0.5", "tailwind-merge": "^3.3.1", @@ -55,8 +61,9 @@ "eslint-config-next": "15.3.4", "eslint-config-prettier": "^10.1.5", "eslint-plugin-prettier": "^5.5.0", - "prisma": "^6.10.1", + "prisma": "^6.11.0", "tailwindcss": "^4", + "tsx": "^4.20.3", "tw-animate-css": "^1.3.4", "typescript": "^5" } @@ -998,6 +1005,73 @@ "integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==", "license": "MIT" }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/modifiers": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", + "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emnapi/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", @@ -1031,6 +1105,431 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -2125,9 +2624,9 @@ } }, "node_modules/@prisma/client": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.10.1.tgz", - "integrity": "sha512-Re4pMlcUsQsUTAYMK7EJ4Bw2kg3WfZAAlr8GjORJaK4VOP6LxRQUQ1TuLnxcF42XqGkWQ36q5CQF1yVadANQ6w==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.11.0.tgz", + "integrity": "sha512-K9TkKepOYvCOg3qCuKz7ZHf6rf58BFKi08plKjU4qVv9y7/UxO6tLz7PlWcgODUZKURLPmRHjHERffIx/8az4w==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { @@ -2147,9 +2646,9 @@ } }, "node_modules/@prisma/config": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.10.1.tgz", - "integrity": "sha512-kz4/bnqrOrzWo8KzYguN0cden4CzLJJ+2VSpKtF8utHS3l1JS0Lhv6BLwpOX6X9yNreTbZQZwewb+/BMPDCIYQ==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.11.0.tgz", + "integrity": "sha512-icBfutMpdrwSf2ggo012zhQ4oianijXL/UPbv4PNVK3WUWbB3/F5Ltq8ZfElGrtwKC6XuFFPxU5qDC9x7vh8zQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -2157,53 +2656,53 @@ } }, "node_modules/@prisma/debug": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.10.1.tgz", - "integrity": "sha512-k2YT53cWxv9OLjW4zSYTZ6Z7j0gPfCzcr2Mj99qsuvlxr8WAKSZ2NcSR0zLf/mP4oxnYG842IMj3utTgcd7CaA==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.11.0.tgz", + "integrity": "sha512-zo4oEZMWMt0BFWl+4NK9FUpaEOmjGR3y2/r0lkW/DK4BUBRgMj90s8QqK2K+vXG3xn0nAGg2kOSu+Swn60CFLg==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.10.1.tgz", - "integrity": "sha512-Q07P5rS2iPwk2IQr/rUQJ42tHjpPyFcbiH7PXZlV81Ryr9NYIgdxcUrwgVOWVm5T7ap02C0dNd1dpnNcSWig8A==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.11.0.tgz", + "integrity": "sha512-uqnYxvPKZPvYZA7F0q4gTR+fVWUJSY5bif7JAKBIOD5SoRRy0qEIaPy4Nna5WDLQaFGshaY/Bh8dLOQMfxhJJw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.10.1", - "@prisma/engines-version": "6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c", - "@prisma/fetch-engine": "6.10.1", - "@prisma/get-platform": "6.10.1" + "@prisma/debug": "6.11.0", + "@prisma/engines-version": "6.11.0-18.9c30299f5a0ea26a96790e13f796dc6094db3173", + "@prisma/fetch-engine": "6.11.0", + "@prisma/get-platform": "6.11.0" } }, "node_modules/@prisma/engines-version": { - "version": "6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c.tgz", - "integrity": "sha512-ZJFTsEqapiTYVzXya6TUKYDFnSWCNegfUiG5ik9fleQva5Sk3DNyyUi7X1+0ZxWFHwHDr6BZV5Vm+iwP+LlciA==", + "version": "6.11.0-18.9c30299f5a0ea26a96790e13f796dc6094db3173", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.11.0-18.9c30299f5a0ea26a96790e13f796dc6094db3173.tgz", + "integrity": "sha512-M3vbyDICFIA1oJl0cFkM0omD4HsJZjFi0hu0f0UxyPABH8KEcZyUd5BToCrNl4B8lUeQn+L5+gfaQleOKp6Lrg==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.10.1.tgz", - "integrity": "sha512-clmbG/Jgmrc/n6Y77QcBmAUlq9LrwI9Dbgy4pq5jeEARBpRCWJDJ7PWW1P8p0LfFU0i5fsyO7FqRzRB8mkdS4g==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.11.0.tgz", + "integrity": "sha512-ZHHSP7vJFo5hePH+MNovxhqXabIg38ZpCwQfUBON29kwPX3f1pjYnzGpgJLCJy4k7mKGOzTgrXPqH8+nJvq2fw==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.10.1", - "@prisma/engines-version": "6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c", - "@prisma/get-platform": "6.10.1" + "@prisma/debug": "6.11.0", + "@prisma/engines-version": "6.11.0-18.9c30299f5a0ea26a96790e13f796dc6094db3173", + "@prisma/get-platform": "6.11.0" } }, "node_modules/@prisma/get-platform": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.10.1.tgz", - "integrity": "sha512-4CY5ndKylcsce9Mv+VWp5obbR2/86SHOLVV053pwIkhVtT9C9A83yqiqI/5kJM9T1v1u1qco/bYjDKycmei9HA==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.11.0.tgz", + "integrity": "sha512-yspBGvOfJQwuoApk5B4aBlHDy6YDXAOe4Ml8U2eZ+M2b7fDd10YDomS3Q4qrYHUUVYF3TJyN86NcnRMOvCMUrA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.10.1" + "@prisma/debug": "6.11.0" } }, "node_modules/@radix-ui/number": { @@ -2241,6 +2740,36 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz", + "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -5958,6 +6487,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -6696,6 +7266,21 @@ "node": ">= 0.12" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7592,6 +8177,26 @@ "whatwg-fetch": "^3.4.1" } }, + "node_modules/isomorphic-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -8357,26 +8962,6 @@ "node": ">=v0.6.5" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-vibrant": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/node-vibrant/-/node-vibrant-4.0.3.tgz", @@ -8810,15 +9395,15 @@ } }, "node_modules/prisma": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.10.1.tgz", - "integrity": "sha512-khhlC/G49E4+uyA3T3H5PRBut486HD2bDqE2+rvkU0pwk9IAqGFacLFUyIx9Uw+W2eCtf6XGwsp+/strUwMNPw==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.11.0.tgz", + "integrity": "sha512-gI69E7fusgk32XALpXzdgR10xUx2aFnHiu/JaUo4O07G4JvFT0xNtD0Iy81p37iBLTYFEhWa9VrHKXaiyZ5fLQ==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "6.10.1", - "@prisma/engines": "6.10.1" + "@prisma/config": "6.11.0", + "@prisma/engines": "6.11.0" }, "bin": { "prisma": "build/index.js" @@ -9156,6 +9741,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/replicate": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replicate/-/replicate-1.0.1.tgz", + "integrity": "sha512-EY+rK1YR5bKHcM9pd6WyaIbv6m2aRIvHfHDh51j/LahlHTLKemTYXF6ptif2sLa+YospupAsIoxw8Ndt5nI3vg==", + "license": "Apache-2.0", + "engines": { + "git": ">=2.11.0", + "node": ">=18.0.0", + "npm": ">=7.19.0", + "yarn": ">=1.7.0" + }, + "optionalDependencies": { + "readable-stream": ">=4.0.0" + } + }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -10072,6 +10672,26 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", + "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index a359c9f..66982e7 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,14 @@ "dependencies": { "@aws-sdk/client-s3": "^3.837.0", "@aws-sdk/s3-request-presigner": "^3.837.0", + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", "@hookform/resolvers": "^5.1.1", "@material/material-color-utilities": "^0.3.0", - "@prisma/client": "^6.10.1", + "@prisma/client": "^6.11.0", + "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", @@ -38,6 +43,7 @@ "react-dom": "^19.0.0", "react-hook-form": "^7.58.1", "react-icons": "^5.5.0", + "replicate": "^1.0.1", "sharp": "^0.34.2", "sonner": "^2.0.5", "tailwind-merge": "^3.3.1", @@ -56,8 +62,9 @@ "eslint-config-next": "15.3.4", "eslint-config-prettier": "^10.1.5", "eslint-plugin-prettier": "^5.5.0", - "prisma": "^6.10.1", + "prisma": "^6.11.0", "tailwindcss": "^4", + "tsx": "^4.20.3", "tw-animate-css": "^1.3.4", "typescript": "^5" } diff --git a/prisma/migrations/20250628230531_image_variant/migration.sql b/prisma/migrations/20250628230531_image_variant/migration.sql new file mode 100644 index 0000000..9d3e020 --- /dev/null +++ b/prisma/migrations/20250628230531_image_variant/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[imageId,type]` on the table `ImageVariant` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "ImageVariant_imageId_type_key" ON "ImageVariant"("imageId", "type"); diff --git a/prisma/migrations/20250629114921_artist_description/migration.sql b/prisma/migrations/20250629114921_artist_description/migration.sql new file mode 100644 index 0000000..451ffac --- /dev/null +++ b/prisma/migrations/20250629114921_artist_description/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Artist" ADD COLUMN "description" TEXT; diff --git a/prisma/migrations/20250629115442_artist_source_social/migration.sql b/prisma/migrations/20250629115442_artist_source_social/migration.sql new file mode 100644 index 0000000..90dbba4 --- /dev/null +++ b/prisma/migrations/20250629115442_artist_source_social/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "Artist" ADD COLUMN "source" TEXT; + +-- AlterTable +ALTER TABLE "Social" ADD COLUMN "isVisible" BOOLEAN NOT NULL DEFAULT true, +ALTER COLUMN "isPrimary" SET DEFAULT false; diff --git a/prisma/migrations/20250701210825_sortindex/migration.sql b/prisma/migrations/20250701210825_sortindex/migration.sql new file mode 100644 index 0000000..4ed8c63 --- /dev/null +++ b/prisma/migrations/20250701210825_sortindex/migration.sql @@ -0,0 +1,20 @@ +-- AlterTable +ALTER TABLE "Album" ADD COLUMN "sortIndex" INTEGER NOT NULL DEFAULT 0; + +-- AlterTable +ALTER TABLE "Artist" ADD COLUMN "sortIndex" INTEGER NOT NULL DEFAULT 0; + +-- AlterTable +ALTER TABLE "Category" ADD COLUMN "sortIndex" INTEGER NOT NULL DEFAULT 0; + +-- AlterTable +ALTER TABLE "Gallery" ADD COLUMN "sortIndex" INTEGER NOT NULL DEFAULT 0; + +-- AlterTable +ALTER TABLE "Image" ADD COLUMN "sortIndex" INTEGER NOT NULL DEFAULT 0; + +-- AlterTable +ALTER TABLE "Social" ADD COLUMN "sortIndex" INTEGER NOT NULL DEFAULT 0; + +-- AlterTable +ALTER TABLE "Tag" ADD COLUMN "sortIndex" INTEGER NOT NULL DEFAULT 0; diff --git a/prisma/migrations/20250701224958_image_unique/migration.sql b/prisma/migrations/20250701224958_image_unique/migration.sql new file mode 100644 index 0000000..430fcfe --- /dev/null +++ b/prisma/migrations/20250701224958_image_unique/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - A unique constraint covering the columns `[fileKey]` on the table `Image` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[originalFile]` on the table `Image` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "Image_fileKey_key" ON "Image"("fileKey"); + +-- CreateIndex +CREATE UNIQUE INDEX "Image_originalFile_key" ON "Image"("originalFile"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f3ff274..7f4e654 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -18,6 +18,7 @@ model Gallery { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + sortIndex Int @default(0) slug String @unique name String @@ -34,6 +35,7 @@ model Album { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + sortIndex Int @default(0) slug String name String @@ -54,11 +56,14 @@ model Artist { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + sortIndex Int @default(0) slug String @unique displayName String - nickname String? + nickname String? + description String? + source String? socials Social[] images Image[] @@ -68,10 +73,12 @@ model Social { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + sortIndex Int @default(0) handle String platform String - isPrimary Boolean + isPrimary Boolean @default(false) + isVisible Boolean @default(true) link String? @@ -83,6 +90,7 @@ model Category { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + sortIndex Int @default(0) name String @unique @@ -95,6 +103,7 @@ model Tag { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + sortIndex Int @default(0) name String @unique @@ -107,10 +116,11 @@ model Image { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + sortIndex Int @default(0) - fileKey String + fileKey String @unique + originalFile String @unique imageName String - originalFile String uploadDate DateTime @default(now()) nsfw Boolean @default(false) @@ -204,6 +214,8 @@ model ImageVariant { sizeBytes Int? image Image @relation(fields: [imageId], references: [id]) + + @@unique([imageId, type]) } model ColorPalette { diff --git a/scripts/resize-all-images.ts b/scripts/resize-all-images.ts new file mode 100644 index 0000000..7bc94e2 --- /dev/null +++ b/scripts/resize-all-images.ts @@ -0,0 +1,111 @@ +// import prisma from "@/lib/prisma"; // adjust to correct path +// import { s3 } from "@/lib/s3"; +// import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; +// import sharp from "sharp"; +// import { Readable } from "stream"; + +// // const s3 = new S3Client({ region: "eu-central-1" }); // or use env config +// const BUCKET = "felliesartapp"; + +// async function streamToBuffer(stream: Readable): Promise { +// return new Promise((resolve, reject) => { +// const chunks: any[] = []; +// stream.on("data", (chunk) => chunks.push(chunk)); +// stream.on("end", () => resolve(Buffer.concat(chunks))); +// stream.on("error", reject); +// }); +// } + +// async function resizeShorterSideTo400(buffer: Buffer) { +// const meta = await sharp(buffer).metadata(); + +// let resizeOptions; +// if (meta.width && meta.height) { +// resizeOptions = meta.width < meta.height ? { width: 400 } : { height: 400 }; +// } else { +// resizeOptions = { width: 400 }; +// } + +// return sharp(buffer) +// .resize({ ...resizeOptions, withoutEnlargement: true }) +// .toFormat("webp") +// .toBuffer(); +// } + +// async function processAllImages() { +// const images = await prisma.image.findMany({ +// include: { +// variants: true, +// }, +// }); + +// for (const image of images) { +// const originalVariant = image.variants.find(v => v.type === "original"); +// const resizedVariant = image.variants.find(v => v.type === "resized"); + +// if (!originalVariant || !originalVariant.s3Key) { +// console.log(`❌ Skipping image ${image.id}: no original variant`); +// continue; +// } + +// try { +// // Fetch original image +// const originalData = await s3.send(new GetObjectCommand({ +// Bucket: BUCKET, +// Key: originalVariant.s3Key, +// })); + +// const originalBuffer = await streamToBuffer(originalData.Body as Readable); + +// // Resize +// const resizedBuffer = await resizeShorterSideTo400(originalBuffer); +// const resizedMeta = await sharp(resizedBuffer).metadata(); + +// // Upload (overwrite existing resized) +// const resizedKey = resizedVariant?.s3Key || `variants/${image.id}/resized.webp`; + +// await s3.send(new PutObjectCommand({ +// Bucket: BUCKET, +// Key: resizedKey, +// Body: resizedBuffer, +// ContentType: "image/webp", +// })); + +// // Update or create the variant +// await prisma.imageVariant.upsert({ +// where: resizedVariant ? { id: resizedVariant.id } : { +// imageId_type: { +// imageId: image.id, +// type: "resized", +// } +// }, +// update: { +// s3Key: resizedKey, +// width: resizedMeta.width || 0, +// height: resizedMeta.height || 0, +// mimeType: "image/webp", +// fileExtension: "webp", +// sizeBytes: resizedBuffer.length, +// }, +// create: { +// imageId: image.id, +// type: "resized", +// s3Key: resizedKey, +// width: resizedMeta.width || 0, +// height: resizedMeta.height || 0, +// mimeType: "image/webp", +// fileExtension: "webp", +// sizeBytes: resizedBuffer.length, +// }, +// }); + +// console.log(`✅ Resized image ${image.id}`); +// } catch (err) { +// console.error(`❌ Failed to process image ${image.id}:`, err); +// } +// } + +// console.log("🎉 Done."); +// } + +// processAllImages(); diff --git a/src/actions/albums/updateAlbumSortOrder.ts b/src/actions/albums/updateAlbumSortOrder.ts new file mode 100644 index 0000000..6a6491a --- /dev/null +++ b/src/actions/albums/updateAlbumSortOrder.ts @@ -0,0 +1,15 @@ +'use server'; + +import prisma from "@/lib/prisma"; +import { SortableItem } from "@/types/SortableItem"; + +export async function updateAlbumSortOrder(items: SortableItem[]) { + await Promise.all( + items.map(item => + prisma.album.update({ + where: { id: item.id }, + data: { sortIndex: item.sortIndex }, + }) + ) + ); +} diff --git a/src/actions/artists/updateArtist.ts b/src/actions/artists/updateArtist.ts index 494dcf7..6816293 100644 --- a/src/actions/artists/updateArtist.ts +++ b/src/actions/artists/updateArtist.ts @@ -26,6 +26,8 @@ export async function updateArtist( displayName: values.displayName, slug: values.slug, nickname: values.nickname, + source: values.source, + description: values.description, socials: { deleteMany: { id: { in: removedSocials.map(s => s.id) }, @@ -39,6 +41,7 @@ export async function updateArtist( handle: s.handle, link: s.link, isPrimary: s.isPrimary, + isVisible: s.isVisible }, })), create: (values.socials ?? []) @@ -48,6 +51,7 @@ export async function updateArtist( handle: s.handle, link: s.link, isPrimary: s.isPrimary, + isVisible: s.isVisible })), }, } diff --git a/src/actions/artists/updateArtistSortOrder.ts b/src/actions/artists/updateArtistSortOrder.ts new file mode 100644 index 0000000..b4ae035 --- /dev/null +++ b/src/actions/artists/updateArtistSortOrder.ts @@ -0,0 +1,15 @@ +'use server'; + +import prisma from "@/lib/prisma"; +import { SortableItem } from "@/types/SortableItem"; + +export async function updateArtistSortOrder(items: SortableItem[]) { + await Promise.all( + items.map(item => + prisma.artist.update({ + where: { id: item.id }, + data: { sortIndex: item.sortIndex }, + }) + ) + ); +} diff --git a/src/actions/categories/updateCategorySortOrder.ts b/src/actions/categories/updateCategorySortOrder.ts new file mode 100644 index 0000000..b36fdec --- /dev/null +++ b/src/actions/categories/updateCategorySortOrder.ts @@ -0,0 +1,15 @@ +'use server'; + +import prisma from "@/lib/prisma"; +import { SortableItem } from "@/types/SortableItem"; + +export async function updateCategorySortOrder(items: SortableItem[]) { + await Promise.all( + items.map(item => + prisma.category.update({ + where: { id: item.id }, + data: { sortIndex: item.sortIndex }, + }) + ) + ); +} diff --git a/src/actions/galleries/updateGallerySortOrder.ts b/src/actions/galleries/updateGallerySortOrder.ts new file mode 100644 index 0000000..f787683 --- /dev/null +++ b/src/actions/galleries/updateGallerySortOrder.ts @@ -0,0 +1,15 @@ +'use server'; + +import prisma from "@/lib/prisma"; +import { SortableItem } from "@/types/SortableItem"; + +export async function updateGallerySortOrder(items: SortableItem[]) { + await Promise.all( + items.map(item => + prisma.gallery.update({ + where: { id: item.id }, + data: { sortIndex: item.sortIndex }, + }) + ) + ); +} diff --git a/src/actions/images/deleteImage.ts b/src/actions/images/deleteImage.ts index f3919cd..bab8f5c 100644 --- a/src/actions/images/deleteImage.ts +++ b/src/actions/images/deleteImage.ts @@ -9,13 +9,15 @@ export async function deleteImage(imageId: string) { where: { id: imageId }, include: { variants: true, - palettes: { include: { items: true } }, + palettes: true, colors: true, extractColors: true, - theme: true, metadata: true, - pixels: true, stats: true, + albumCover: true, + galleryCover: true, + tags: true, + categories: true, }, }); @@ -23,39 +25,80 @@ export async function deleteImage(imageId: string) { // Delete S3 objects for (const variant of image.variants) { - await s3.send(new DeleteObjectCommand({ - Bucket: "felliesartapp", - Key: variant.s3Key, - })); - } - - // Delete image variants - await prisma.imageVariant.deleteMany({ where: { imageId } }); - - // Delete extract colors - await prisma.extractColor.deleteMany({ where: { imageId } }); - - // Delete image colors - await prisma.imageColor.deleteMany({ where: { imageId } }); - - // Delete palettes (and items only if no other image uses them) - const palettes = await prisma.colorPalette.findMany({ - where: { images: { some: { id: imageId } } }, - include: { images: { select: { id: true } }, items: true } - }); - - for (const palette of palettes) { - if (palette.images.length === 1 && palette.images[0].id === imageId) { - await prisma.colorPaletteItem.deleteMany({ where: { colorPaletteId: palette.id } }); - await prisma.colorPalette.delete({ where: { id: palette.id } }); + try { + await s3.send( + new DeleteObjectCommand({ + Bucket: "felliesartapp", + Key: variant.s3Key, + }) + ); + } catch (err) { + console.warn("Failed to delete S3 object: " + variant.s3Key + ". " + err); } } - // Delete metadata-related entries + // Step 1: Delete join entries + await prisma.imagePalette.deleteMany({ where: { imageId } }); + await prisma.imageColor.deleteMany({ where: { imageId } }); + await prisma.imageExtractColor.deleteMany({ where: { imageId } }); + + // ColorPalettes + const connectedPalettes = image.palettes; + for (const palette of connectedPalettes) { + const count = await prisma.imagePalette.count({ + where: { paletteId: palette.id }, + }); + if (count === 0) { + await prisma.colorPaletteItem.deleteMany({ where: { colorPaletteId: palette.id } }); + await prisma.colorPalette.deleteMany({ where: { id: palette.id } }); + } + } + + // ExtractColors + for (const extract of image.extractColors) { + const count = await prisma.imageExtractColor.count({ + where: { extractId: extract.extractId }, + }); + if (count === 0) { + await prisma.extractColor.delete({ where: { id: extract.extractId } }); + } + } + + // Colors + for (const color of image.colors) { + const count = await prisma.imageColor.count({ + where: { colorId: color.colorId }, + }); + if (count === 0) { + await prisma.color.delete({ where: { id: color.colorId } }); + } + } + + // Delete variants + await prisma.imageVariant.deleteMany({ where: { imageId } }); + + // Delete metadata await prisma.imageMetadata.deleteMany({ where: { imageId } }); await prisma.imageStats.deleteMany({ where: { imageId } }); - await prisma.pixelSummary.deleteMany({ where: { imageId } }); - await prisma.themeSeed.deleteMany({ where: { imageId } }); + + // Clean many-to-many tag/category joins + await prisma.image.update({ + where: { id: imageId }, + data: { + tags: { set: [] }, + categories: { set: [] }, + }, + }); + + // Delete possible coverImage relation + await prisma.album.updateMany({ + where: { coverImageId: imageId }, + data: { coverImageId: null }, + }); + await prisma.gallery.updateMany({ + where: { coverImageId: imageId }, + data: { coverImageId: null }, + }); // Finally delete the image await prisma.image.delete({ where: { id: imageId } }); diff --git a/src/actions/images/uploadImage.ts b/src/actions/images/uploadImage.ts index 3073a29..2b24606 100644 --- a/src/actions/images/uploadImage.ts +++ b/src/actions/images/uploadImage.ts @@ -76,9 +76,21 @@ export async function uploadImage(values: z.infer) { }) ); //--- Resized file - const resizedWidth = Math.min(watermarkedMetadata.width || 400, 400); + // const resizedWidth = Math.min(watermarkedMetadata.width || 400, 400); + const { width, height } = watermarkedMetadata; + const targetSize = 400; + let resizeOptions; + if (width && height) { + if (width < height) { + resizeOptions = { width: targetSize }; + } else { + resizeOptions = { height: targetSize }; + } + } else { + resizeOptions = { width: targetSize }; + } const resizedBuffer = await sharp(watermarkedBuffer) - .resize({ width: resizedWidth, withoutEnlargement: true }) + .resize({ ...resizeOptions, withoutEnlargement: true }) .toFormat('webp') .toBuffer(); const resizedMetadata = await sharp(resizedBuffer).metadata(); @@ -91,9 +103,20 @@ export async function uploadImage(values: z.infer) { }) ); //--- Thumbnail file - const thumbnailWidth = Math.min(watermarkedMetadata.width || 200, 200); + // const thumbnailWidth = Math.min(watermarkedMetadata.width || 200, 200); + const thumbnailTargetSize = 200; + let thumbnailOptions; + if (width && height) { + if (width < height) { + thumbnailOptions = { width: thumbnailTargetSize }; + } else { + thumbnailOptions = { height: thumbnailTargetSize }; + } + } else { + thumbnailOptions = { width: thumbnailTargetSize }; + } const thumbnailBuffer = await sharp(watermarkedBuffer) - .resize({ width: thumbnailWidth, withoutEnlargement: true }) + .resize({ ...thumbnailOptions, withoutEnlargement: true }) .toFormat('webp') .toBuffer(); const thumbnailMetadata = await sharp(thumbnailBuffer).metadata(); @@ -112,7 +135,6 @@ export async function uploadImage(values: z.infer) { fileKey, originalFile: fileName, uploadDate: new Date(), - creationDate: lastModified, creationMonth: month, creationYear: year, diff --git a/src/actions/tags/updateTagSortOrder.ts b/src/actions/tags/updateTagSortOrder.ts new file mode 100644 index 0000000..ae8fb9e --- /dev/null +++ b/src/actions/tags/updateTagSortOrder.ts @@ -0,0 +1,15 @@ +'use server'; + +import prisma from "@/lib/prisma"; +import { SortableItem } from "@/types/SortableItem"; + +export async function updateTagSortOrder(items: SortableItem[]) { + await Promise.all( + items.map(item => + prisma.tag.update({ + where: { id: item.id }, + data: { sortIndex: item.sortIndex }, + }) + ) + ); +} diff --git a/src/app/albums/page.tsx b/src/app/albums/page.tsx index 7497527..d61c984 100644 --- a/src/app/albums/page.tsx +++ b/src/app/albums/page.tsx @@ -6,14 +6,14 @@ import Link from "next/link"; export default async function AlbumsPage() { const albums = await prisma.album.findMany( { - include: { gallery: true, images: { select: { id: true } } }, - orderBy: { name: "asc" } + orderBy: [{ sortIndex: "asc" }, { name: "asc" }], + include: { gallery: true, images: { select: { id: true } }, coverImage: true }, } ); return (
-
+

Albums

Add new Album diff --git a/src/app/artists/page.tsx b/src/app/artists/page.tsx index 0188c57..f22cd60 100644 --- a/src/app/artists/page.tsx +++ b/src/app/artists/page.tsx @@ -5,13 +5,13 @@ import Link from "next/link"; export default async function ArtistsPage() { const artists = await prisma.artist.findMany({ - orderBy: { createdAt: "asc" }, + orderBy: [{ sortIndex: "asc" }, { displayName: "asc" }], include: { images: { select: { id: true } } } }); return (
-
+

Artists

Add new Artist diff --git a/src/app/categories/page.tsx b/src/app/categories/page.tsx index 71707bc..0919389 100644 --- a/src/app/categories/page.tsx +++ b/src/app/categories/page.tsx @@ -6,14 +6,14 @@ import Link from "next/link"; export default async function CategoriesPage() { const categories = await prisma.category.findMany( { - orderBy: { createdAt: "asc" }, + orderBy: [{ sortIndex: "asc" }, { name: "asc" }], include: { images: { select: { id: true } } } } ); return (
-
+

Categories

Add new Category diff --git a/src/app/galleries/page.tsx b/src/app/galleries/page.tsx index 5c6af7a..c92d206 100644 --- a/src/app/galleries/page.tsx +++ b/src/app/galleries/page.tsx @@ -5,15 +5,16 @@ import Link from "next/link"; export default async function GalleriesPage() { const galleries = await prisma.gallery.findMany({ - orderBy: { createdAt: "asc" }, + orderBy: [{ sortIndex: "asc" }, { name: "asc" }], include: { - albums: { select: { id: true } } + albums: { select: { id: true } }, + coverImage: true } }); return (
-
+

Galleries

Add new gallery diff --git a/src/app/images/edit/[id]/page.tsx b/src/app/images/edit/[id]/page.tsx index d1618e5..ffd9789 100644 --- a/src/app/images/edit/[id]/page.tsx +++ b/src/app/images/edit/[id]/page.tsx @@ -48,7 +48,7 @@ export default async function ImagesEditPage({ params }: { params: { id: string const categories = await prisma.category.findMany({ orderBy: { name: "asc" } }); const tags = await prisma.tag.findMany({ orderBy: { name: "asc" } }); - console.log(image) + // console.log(image) return (
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3ffa9eb..c1918a1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -17,8 +17,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Fellies Art Admin", + description: "Admin page for the fellies.art artworks", }; export default function RootLayout({ diff --git a/src/app/tags/page.tsx b/src/app/tags/page.tsx index 5afb374..4ee64b8 100644 --- a/src/app/tags/page.tsx +++ b/src/app/tags/page.tsx @@ -6,13 +6,14 @@ import Link from "next/link"; export default async function TagsPage() { const tags = await prisma.tag.findMany( { - orderBy: { createdAt: "asc" } + orderBy: [{ sortIndex: "asc" }, { name: "asc" }], + include: { images: { select: { id: true } } } } ); return (
-
+

Tags

Add new Tag diff --git a/src/components/albums/list/ListAlbums.tsx b/src/components/albums/list/ListAlbums.tsx index 6776247..5a6fbe9 100644 --- a/src/components/albums/list/ListAlbums.tsx +++ b/src/components/albums/list/ListAlbums.tsx @@ -1,43 +1,66 @@ -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { Album, Gallery } from "@/generated/prisma"; -import { TriangleAlert } from "lucide-react"; -import Link from "next/link"; +"use client"; + +import { updateAlbumSortOrder } from "@/actions/albums/updateAlbumSortOrder"; +import { SortableCardItem } from "@/components/sort/SortableItem"; +import { SortableList } from "@/components/sort/SortableList"; +import { Album, Gallery, Image } from "@/generated/prisma"; +import { SortableItem } from "@/types/SortableItem"; +import { useEffect, useState } from "react"; type AlbumWithGallery = Album & { gallery: Gallery | null; images: { id: string }[]; + coverImage?: (Image) | null }; export default function ListAlbums({ albums }: { albums: AlbumWithGallery[] }) { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + const sortableItems: SortableItem[] = albums.map(album => ({ + id: album.id, + sortIndex: album.sortIndex, + label: album.name, + })); + + const handleSortDefault = async () => { + const sorted = [...sortableItems] + .sort((a, b) => a.label.localeCompare(b.label)) + .map((item, index) => ({ ...item, sortIndex: index * 10 })); + await updateAlbumSortOrder(sorted); + }; + + const handleReorder = async (items: SortableItem[]) => { + await updateAlbumSortOrder(items); + }; + + if (!isMounted) return null; + return ( -
- {albums.map((album) => ( - - - - {album.name} - - - {album.description &&

{album.description}

} -
- -
- {album.gallery ? ( - <>Gallery: {album.gallery.name} - ) : ( -
- - No gallery -
- )} -

- Total images in this album: {album.images.length} -

-
-
-
- - ))} -
+ { + const album = albums.find(g => g.id === item.id)!; + return ( + + ); + }} + /> ); } \ No newline at end of file diff --git a/src/components/artists/edit/EditArtistForm.tsx b/src/components/artists/edit/EditArtistForm.tsx index 1e46ca0..9333e52 100644 --- a/src/components/artists/edit/EditArtistForm.tsx +++ b/src/components/artists/edit/EditArtistForm.tsx @@ -2,6 +2,7 @@ import { updateArtist } from "@/actions/artists/updateArtist"; import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Artist, Social } from "@/generated/prisma"; @@ -21,12 +22,15 @@ export default function EditAlbumForm({ artist }: { artist: Artist & { socials: displayName: artist.displayName, slug: artist.slug, nickname: artist.nickname || "", + source: artist.source || "", + description: artist.description || "", socials: artist.socials.map((s) => ({ id: s.id, platform: s.platform, handle: s.handle, link: s.link ?? "", // Convert null to empty string isPrimary: s.isPrimary, + isVisible: s.isVisible })), }, }) @@ -98,7 +102,38 @@ export default function EditAlbumForm({ artist }: { artist: Artist & { socials: )} /> - + ( + + Artist source (optional) + + + + + Where did the artist come from? + + + + )} + /> + ( + + Artist description (optional) + + + + + Description of the Artist. + + + + )} + /> {/* Socials section */}
Social Links @@ -169,6 +204,21 @@ export default function EditAlbumForm({ artist }: { artist: Artist & { socials: )} /> + ( + + + field.onChange(!!checked)} + /> + + Visible + + )} + /> diff --git a/src/components/artists/list/ListArtists.tsx b/src/components/artists/list/ListArtists.tsx index 9fdf2a1..2a8624b 100644 --- a/src/components/artists/list/ListArtists.tsx +++ b/src/components/artists/list/ListArtists.tsx @@ -1,28 +1,62 @@ -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +"use client"; + +import { updateArtistSortOrder } from "@/actions/artists/updateArtistSortOrder"; +import { SortableCardItem } from "@/components/sort/SortableItem"; +import { SortableList } from "@/components/sort/SortableList"; import { Artist } from "@/generated/prisma"; -import Link from "next/link"; +import { SortableItem } from "@/types/SortableItem"; +import { useEffect, useState } from "react"; type ArtistsWithItems = Artist & { images: { id: string }[] } export default function ListArtists({ artists }: { artists: ArtistsWithItems[] }) { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + const sortableItems: SortableItem[] = artists.map(artist => ({ + id: artist.id, + sortIndex: artist.sortIndex, + label: artist.displayName, + })); + + const handleSortDefault = async () => { + const sorted = [...sortableItems] + .sort((a, b) => a.label.localeCompare(b.label)) + .map((item, index) => ({ ...item, sortIndex: index * 10 })); + await updateArtistSortOrder(sorted); + }; + + const handleReorder = async (items: SortableItem[]) => { + await updateArtistSortOrder(items); + }; + + if (!isMounted) return null; + return ( -
- {artists.map((artist) => ( - - - - {artist.displayName} - - - - - Connected to {artist.images.length} image{artist.images.length !== 1 ? "s" : ""} - - - - ))} -
+ { + const artist = artists.find(g => g.id === item.id)!; + return ( + + ); + }} + /> ); } \ No newline at end of file diff --git a/src/components/categories/list/ListCategories.tsx b/src/components/categories/list/ListCategories.tsx index 6a77f3f..5036030 100644 --- a/src/components/categories/list/ListCategories.tsx +++ b/src/components/categories/list/ListCategories.tsx @@ -1,29 +1,62 @@ -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +"use client"; + +import { updateCategorySortOrder } from "@/actions/categories/updateCategorySortOrder"; +import { SortableCardItem } from "@/components/sort/SortableItem"; +import { SortableList } from "@/components/sort/SortableList"; import { Category } from "@/generated/prisma"; -import Link from "next/link"; +import { SortableItem } from "@/types/SortableItem"; +import { useEffect, useState } from "react"; type CategoriesWithItems = Category & { images: { id: string }[] } export default function ListCategories({ categories }: { categories: CategoriesWithItems[] }) { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + const sortableItems: SortableItem[] = categories.map(cat => ({ + id: cat.id, + sortIndex: cat.sortIndex, + label: cat.name, + })); + + const handleSortDefault = async () => { + const sorted = [...sortableItems] + .sort((a, b) => a.label.localeCompare(b.label)) + .map((item, index) => ({ ...item, sortIndex: index * 10 })); + await updateCategorySortOrder(sorted); + }; + + const handleReorder = async (items: SortableItem[]) => { + await updateCategorySortOrder(items); + }; + + if (!isMounted) return null; + return ( -
- {categories.map((cat) => ( - - - - {cat.name} - - - {cat.description &&

{cat.description}

} -
- - Connected to {cat.images.length} image{cat.images.length !== 1 ? "s" : ""} - -
- - ))} -
+ { + const cat = categories.find(g => g.id === item.id)!; + return ( + + ); + }} + /> ); } \ No newline at end of file diff --git a/src/components/galleries/list/ListGalleries.tsx b/src/components/galleries/list/ListGalleries.tsx index 7cafe6a..56ba487 100644 --- a/src/components/galleries/list/ListGalleries.tsx +++ b/src/components/galleries/list/ListGalleries.tsx @@ -1,31 +1,64 @@ -// "use client" +"use client"; -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { Gallery } from "@/generated/prisma"; -import Link from "next/link"; +import { updateGallerySortOrder } from "@/actions/galleries/updateGallerySortOrder"; +import { SortableCardItem } from "@/components/sort/SortableItem"; +import { SortableList } from "@/components/sort/SortableList"; +import { Gallery, Image } from "@/generated/prisma"; +import { SortableItem } from "@/types/SortableItem"; +import { useEffect, useState } from "react"; type GalleriesWithItems = Gallery & { - albums: { id: string }[] -} + albums: { id: string }[], + coverImage?: (Image) | null +}; export default function ListGalleries({ galleries }: { galleries: GalleriesWithItems[] }) { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + const sortableItems: SortableItem[] = galleries.map(gallery => ({ + id: gallery.id, + sortIndex: gallery.sortIndex, + label: gallery.name, + })); + + const handleSortDefault = async () => { + const sorted = [...sortableItems] + .sort((a, b) => a.label.localeCompare(b.label)) + .map((item, index) => ({ ...item, sortIndex: index * 10 })); + await updateGallerySortOrder(sorted); + }; + + const handleReorder = async (items: SortableItem[]) => { + await updateGallerySortOrder(items); + }; + + if (!isMounted) return null; + return ( -
- {galleries.map((gallery) => ( - - - - {gallery.name} - - - {gallery.description &&

{gallery.description}

} -
- - Connected to {gallery.albums.length} album{gallery.albums.length !== 1 ? "s" : ""} - -
- - ))} -
+ { + const gallery = galleries.find(g => g.id === item.id)!; + return ( + + ); + }} + /> ); -} \ No newline at end of file +} diff --git a/src/components/images/edit/EditImageForm.tsx b/src/components/images/edit/EditImageForm.tsx index 7f1121c..d6bcf00 100644 --- a/src/components/images/edit/EditImageForm.tsx +++ b/src/components/images/edit/EditImageForm.tsx @@ -13,6 +13,7 @@ import { Textarea } from "@/components/ui/textarea"; import { Album, Artist, Category, Color, ColorPalette, ColorPaletteItem, ExtractColor, Image, ImageColor, ImageExtractColor, ImageMetadata, ImagePalette, ImageStats, ImageVariant, Tag } from "@/generated/prisma"; import { cn } from "@/lib/utils"; import { imageSchema } from "@/schemas/images/imageSchema"; +import { generateImageText } from "@/utils/generateImageText"; import { zodResolver } from "@hookform/resolvers/zod"; import { format } from "date-fns"; import { useRouter } from "next/navigation"; @@ -207,6 +208,19 @@ export default function EditImageForm({ image, albums, artists, categories, tags )} /> + ( source - + )} /> - @@ -287,6 +303,7 @@ export default function EditImageForm({ image, albums, artists, categories, tags )} /> + {/* Year/Month fallback – number inputs */}
{ + const val = parseInt(e.target.value); + field.onChange(isNaN(val) ? undefined : val); + }} + disabled={!!form.watch("creationDate")} /> @@ -318,8 +339,12 @@ export default function EditImageForm({ image, albums, artists, categories, tags type="number" min={1900} max={2100} - disabled={!!form.watch("creationDate")} {...field} + onChange={(e) => { + const val = parseInt(e.target.value); + field.onChange(isNaN(val) ? undefined : val); + }} + disabled={!!form.watch("creationDate")} /> diff --git a/src/components/sort/SortableItem.tsx b/src/components/sort/SortableItem.tsx new file mode 100644 index 0000000..3cf0f23 --- /dev/null +++ b/src/components/sort/SortableItem.tsx @@ -0,0 +1,103 @@ +'use client'; + +import type { Color, Image, ImageColor } from '@/generated/prisma'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import clsx from 'clsx'; +import { GripVertical } from 'lucide-react'; +import NextImage from 'next/image'; +import Link from 'next/link'; + +type SupportedTypes = 'gallery' | 'album' | 'artist' | 'category' | 'tag'; + +const pluralMap: Record = { + gallery: 'galleries', + album: 'albums', + artist: 'artists', + category: 'categories', + tag: 'tags', +}; + +type SortableCardItemProps = { + id: string; + item: { + id: string; + name: string; + type: 'gallery' | 'album' | 'artist' | 'category' | 'tag'; + coverImage?: (Image & { colors?: (ImageColor & { color: Color })[] }) | null; + count?: number; + textLabel?: string; + }; +}; + +export function SortableCardItem({ id, item }: SortableCardItemProps) { + const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + const href = `/${pluralMap[item.type]}/edit/${item.id}`; + + const isVisualType = item.type === 'gallery' || item.type === 'album'; + + let countLabel = ''; + if (item.count !== undefined) { + if (item.type === 'gallery') { + countLabel = `${item.count} album${item.count !== 1 ? 's' : ''}`; + } else { + countLabel = `${item.count} image${item.count !== 1 ? 's' : ''}`; + } + } + + return ( +
+
+ +
+ + +
+ {isVisualType ? ( +
+ {item.coverImage?.fileKey ? ( + + ) : ( +
+ No cover image +
+ )} +
+ ) : null} + +
+

+ {item.name} +

+ {countLabel && ( +

{countLabel}

+ )} + {item.textLabel && ( +

{item.textLabel}

+ )} +
+
+ +
+ ); +} diff --git a/src/components/sort/SortableList.tsx b/src/components/sort/SortableList.tsx new file mode 100644 index 0000000..d90bc4c --- /dev/null +++ b/src/components/sort/SortableList.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { + closestCenter, + DndContext, + DragEndEvent, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { useState } from 'react'; + +import type { SortableItem as ItemType } from '@/types/SortableItem'; + +interface Props { + items: ItemType[]; + onReorder: (items: ItemType[]) => void; + onSortDefault?: () => void; + defaultSortLabel?: string; + renderItem: (item: ItemType) => React.ReactNode; +} + +export function SortableList({ + items, + onReorder, + onSortDefault, + defaultSortLabel = 'Sort by Name', + renderItem, +}: Props) { + const [localItems, setLocalItems] = useState(items); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + + const oldIndex = localItems.findIndex(item => item.id === active.id); + const newIndex = localItems.findIndex(item => item.id === over.id); + if (oldIndex === -1 || newIndex === -1) return; + + const reordered = arrayMove(localItems, oldIndex, newIndex).map((item, index) => ({ + ...item, + sortIndex: index * 10, + })); + + setLocalItems(reordered); + onReorder(reordered); + }; + + return ( +
+ {onSortDefault && ( + + )} + + + i.id)} strategy={verticalListSortingStrategy}> +
+ {localItems.map(item => ( +
{renderItem(item)}
+ ))} +
+
+
+
+ ); +} diff --git a/src/components/tags/list/ListTags.tsx b/src/components/tags/list/ListTags.tsx index 949672a..b7ed879 100644 --- a/src/components/tags/list/ListTags.tsx +++ b/src/components/tags/list/ListTags.tsx @@ -1,24 +1,61 @@ -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { Tag } from "@/generated/prisma"; -import Link from "next/link"; +"use client"; + +import { updateTagSortOrder } from "@/actions/tags/updateTagSortOrder"; +import { SortableCardItem } from "@/components/sort/SortableItem"; +import { SortableList } from "@/components/sort/SortableList"; +import { Tag } from "@/generated/prisma"; +import { SortableItem } from "@/types/SortableItem"; +import { useEffect, useState } from "react"; + +type TagsWithItems = Tag & { + images: { id: string }[] +} +export default function ListTags({ tags }: { tags: TagsWithItems[] }) { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + const sortableItems: SortableItem[] = tags.map(tag => ({ + id: tag.id, + sortIndex: tag.sortIndex, + label: tag.name, + })); + + const handleSortDefault = async () => { + const sorted = [...sortableItems] + .sort((a, b) => a.label.localeCompare(b.label)) + .map((item, index) => ({ ...item, sortIndex: index * 10 })); + await updateTagSortOrder(sorted); + }; + + const handleReorder = async (items: SortableItem[]) => { + await updateTagSortOrder(items); + }; + + if (!isMounted) return null; -export default function ListTags({ tags }: { tags: Tag[] }) { return ( -
- {tags.map((tag) => ( - - - - {tag.name} - - - {tag.description &&

{tag.description}

} -
- - -
- - ))} -
+ { + const tag = tags.find(g => g.id === item.id)!; + return ( + + ); + }} + /> ); } \ No newline at end of file diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..fa0e4b5 --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/src/schemas/artists/artistSchema.ts b/src/schemas/artists/artistSchema.ts index 0cc3ef0..bea3291 100644 --- a/src/schemas/artists/artistSchema.ts +++ b/src/schemas/artists/artistSchema.ts @@ -5,6 +5,7 @@ export const socialSchema = z.object({ platform: z.string().min(1, "Platform is required"), handle: z.string().min(1, "Handle is required"), isPrimary: z.boolean(), + isVisible: z.boolean(), link: z.string().optional(), }); @@ -12,6 +13,8 @@ export const artistSchema = z.object({ displayName: z.string().min(3, "Name is required. Min 3 characters."), slug: z.string().min(3, "Slug is required. Min 3 characters.").regex(/^[a-z]+$/, "Only lowercase letters are allowed (no numbers, spaces, or uppercase)"), nickname: z.string().optional(), + source: z.string().optional(), + description: z.string().optional(), socials: z.array(socialSchema).optional(), }).refine( (data) => { diff --git a/src/types/SortableItem.ts b/src/types/SortableItem.ts new file mode 100644 index 0000000..6c18dec --- /dev/null +++ b/src/types/SortableItem.ts @@ -0,0 +1,6 @@ +export interface SortableItem { + id: string; + sortIndex: number; + label: string; // e.g., name, displayName, or handle + secondary?: string | boolean; // optional (e.g. isPrimary) +} \ No newline at end of file diff --git a/src/utils/generateImageText.ts b/src/utils/generateImageText.ts new file mode 100644 index 0000000..16c3f6c --- /dev/null +++ b/src/utils/generateImageText.ts @@ -0,0 +1,79 @@ +"use server" + +import { getSignedImageUrl } from "@/lib/s3"; + +// const OpenAIImageSchema = z.object({ +// description: z.string(), +// alt: z.string(), +// }) + +export async function generateImageText(fileKey: string) { + const imageUrl = await getSignedImageUrl(`original/${fileKey}.webp`); + console.log("Generating image text...") + + try { + const imageRes = await fetch(imageUrl); + if (!imageRes.ok) throw new Error("Failed to fetch image"); + + const arrayBuffer = await imageRes.arrayBuffer(); + const base64 = arrayBufferToBase64(arrayBuffer); + + const resDescription = await fetch("http://10.0.20.11:11434/api/generate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: "moondream", + prompt: "Describe this image in detail", + // format: "json", + stream: false, + images: [base64], + }), + }) + + const resAlt = await fetch("http://10.0.20.11:11434/api/generate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: "moondream", + prompt: "Write a short alt text describing this image.", + // format: "json", + stream: false, + images: [base64], + }), + }) + + const descJson = await resDescription.json(); + const altJson = await resAlt.json(); + console.log("Response:", descJson.response) + console.log("Response:", altJson.response) + + // const json = await res.json(); + // const responseText = json.response?.trim(); + + // const data = await res.json(); + // const text = data.res?.trim(); + + // // const text = await res.text() + // console.log("Response:", data) + // console.log("Response:", text) + // const parsed = JSON.parse(text) + // return parsed.response || "(no response)" + return { + description: descJson.response, + alt: altJson.response, + } + } catch (err) { + console.error("error:", err) + return null + } +} + +function arrayBufferToBase64(buffer: ArrayBuffer): string { + let binary = ""; + const bytes = new Uint8Array(buffer); + const len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); +} diff --git a/src/utils/setSortIndex.ts b/src/utils/setSortIndex.ts new file mode 100644 index 0000000..c5e3b33 --- /dev/null +++ b/src/utils/setSortIndex.ts @@ -0,0 +1,27 @@ +import prisma from "@/lib/prisma" + +async function updateSortIndices() { + const galleries = await prisma.gallery.findMany({ orderBy: { createdAt: "asc" } }) + const albums = await prisma.album.findMany({ orderBy: { createdAt: "asc" } }) + + for (let i = 0; i < galleries.length; i++) { + await prisma.gallery.update({ + where: { id: galleries[i].id }, + data: { sortIndex: i }, + }) + } + + for (let i = 0; i < albums.length; i++) { + await prisma.album.update({ + where: { id: albums[i].id }, + data: { sortIndex: i }, + }) + } + + console.log("✅ sortIndex updated for all galleries and albums") +} + +updateSortIndices().catch((e) => { + console.error(e) + process.exit(1) +})