Refactor and add alt text generator
This commit is contained in:
732
package-lock.json
generated
732
package-lock.json
generated
@ -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",
|
||||
|
11
package.json
11
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"
|
||||
}
|
||||
|
@ -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");
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Artist" ADD COLUMN "description" TEXT;
|
@ -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;
|
20
prisma/migrations/20250701210825_sortindex/migration.sql
Normal file
20
prisma/migrations/20250701210825_sortindex/migration.sql
Normal file
@ -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;
|
12
prisma/migrations/20250701224958_image_unique/migration.sql
Normal file
12
prisma/migrations/20250701224958_image_unique/migration.sql
Normal file
@ -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");
|
@ -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 {
|
||||
|
111
scripts/resize-all-images.ts
Normal file
111
scripts/resize-all-images.ts
Normal file
@ -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<Buffer> {
|
||||
// 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();
|
15
src/actions/albums/updateAlbumSortOrder.ts
Normal file
15
src/actions/albums/updateAlbumSortOrder.ts
Normal file
@ -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 },
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
@ -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
|
||||
})),
|
||||
},
|
||||
}
|
||||
|
15
src/actions/artists/updateArtistSortOrder.ts
Normal file
15
src/actions/artists/updateArtistSortOrder.ts
Normal file
@ -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 },
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
15
src/actions/categories/updateCategorySortOrder.ts
Normal file
15
src/actions/categories/updateCategorySortOrder.ts
Normal file
@ -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 },
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
15
src/actions/galleries/updateGallerySortOrder.ts
Normal file
15
src/actions/galleries/updateGallerySortOrder.ts
Normal file
@ -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 },
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
@ -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 } });
|
||||
|
@ -76,9 +76,21 @@ export async function uploadImage(values: z.infer<typeof imageUploadSchema>) {
|
||||
})
|
||||
);
|
||||
//--- 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<typeof imageUploadSchema>) {
|
||||
})
|
||||
);
|
||||
//--- 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<typeof imageUploadSchema>) {
|
||||
fileKey,
|
||||
originalFile: fileName,
|
||||
uploadDate: new Date(),
|
||||
|
||||
creationDate: lastModified,
|
||||
creationMonth: month,
|
||||
creationYear: year,
|
||||
|
15
src/actions/tags/updateTagSortOrder.ts
Normal file
15
src/actions/tags/updateTagSortOrder.ts
Normal file
@ -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 },
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between">
|
||||
<div className="flex gap-4 justify-between pb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Albums</h1>
|
||||
<Link href="/albums/new" className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded">
|
||||
<PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Add new Album
|
||||
|
@ -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 (
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between">
|
||||
<div className="flex gap-4 justify-between pb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Artists</h1>
|
||||
<Link href="/artists/new" className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded">
|
||||
<PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Add new Artist
|
||||
|
@ -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 (
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between">
|
||||
<div className="flex gap-4 justify-between pb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Categories</h1>
|
||||
<Link href="/categories/new" className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded">
|
||||
<PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Add new Category
|
||||
|
@ -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 (
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between">
|
||||
<div className="flex gap-4 justify-between pb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Galleries</h1>
|
||||
<Link href="/galleries/new" className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded">
|
||||
<PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Add new gallery
|
||||
|
@ -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 (
|
||||
<div>
|
||||
|
@ -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({
|
||||
|
@ -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 (
|
||||
<div>
|
||||
<div className="flex gap-4 justify-between">
|
||||
<div className="flex gap-4 justify-between pb-8">
|
||||
<h1 className="text-2xl font-bold mb-4">Tags</h1>
|
||||
<Link href="/tags/new" className="flex gap-2 items-center cursor-pointer bg-primary hover:bg-primary/90 text-primary-foreground px-4 py-2 rounded">
|
||||
<PlusCircleIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all text-primary-foreground" /> Add new Tag
|
||||
|
@ -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 (
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
{albums.map((album) => (
|
||||
<Link href={`/albums/edit/${album.id}`} key={album.id}>
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base truncate">{album.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{album.description && <p className="text-sm text-muted-foreground">{album.description}</p>}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex flex-col gap-4">
|
||||
{album.gallery ? (
|
||||
<>Gallery: {album.gallery.name}</>
|
||||
) : (
|
||||
<div className="flex items-center text-destructive">
|
||||
<TriangleAlert className="mr-2 h-4 w-4" />
|
||||
No gallery
|
||||
</div>
|
||||
)}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Total images in this album: <span className="font-semibold">{album.images.length}</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<SortableList
|
||||
items={sortableItems}
|
||||
onReorder={handleReorder}
|
||||
onSortDefault={handleSortDefault}
|
||||
defaultSortLabel="Sort by name"
|
||||
renderItem={(item) => {
|
||||
const album = albums.find(g => g.id === item.id)!;
|
||||
return (
|
||||
<SortableCardItem
|
||||
id={album.id}
|
||||
item={{
|
||||
id: album.id,
|
||||
name: album.name,
|
||||
type: "album",
|
||||
coverImage: album.coverImage,
|
||||
count: album.images.length,
|
||||
textLabel: "Gallery: " + album.gallery?.name
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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:
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="source"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Artist source (optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Artist source" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Where did the artist come from?
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Artist description (optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Artist description" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Description of the Artist.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Socials section */}
|
||||
<div className="space-y-4">
|
||||
<FormLabel>Social Links</FormLabel>
|
||||
@ -169,6 +204,21 @@ export default function EditAlbumForm({ artist }: { artist: Artist & { socials:
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`socials.${index}.isVisible`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center gap-2 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) => field.onChange(!!checked)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="m-0 text-sm">Visible</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@ -181,7 +231,7 @@ export default function EditAlbumForm({ artist }: { artist: Artist & { socials:
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => append({ platform: "", handle: "", link: "", isPrimary: false })}
|
||||
onClick={() => append({ platform: "", handle: "", link: "", isPrimary: false, isVisible: true })}
|
||||
>
|
||||
+ Add Social
|
||||
</Button>
|
||||
|
@ -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 (
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
{artists.map((artist) => (
|
||||
<Link href={`/artists/edit/${artist.id}`} key={artist.id}>
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base truncate">{artist.displayName}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
Connected to {artist.images.length} image{artist.images.length !== 1 ? "s" : ""}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<SortableList
|
||||
items={sortableItems}
|
||||
onReorder={handleReorder}
|
||||
onSortDefault={handleSortDefault}
|
||||
defaultSortLabel="Sort by displayName"
|
||||
renderItem={(item) => {
|
||||
const artist = artists.find(g => g.id === item.id)!;
|
||||
return (
|
||||
<SortableCardItem
|
||||
id={artist.id}
|
||||
item={{
|
||||
id: artist.id,
|
||||
name: artist.displayName,
|
||||
type: "artist",
|
||||
count: artist.images.length
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
{categories.map((cat) => (
|
||||
<Link href={`/categories/edit/${cat.id}`} key={cat.id}>
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base truncate">{cat.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{cat.description && <p className="text-sm text-muted-foreground">{cat.description}</p>}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
Connected to {cat.images.length} image{cat.images.length !== 1 ? "s" : ""}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<SortableList
|
||||
items={sortableItems}
|
||||
onReorder={handleReorder}
|
||||
onSortDefault={handleSortDefault}
|
||||
defaultSortLabel="Sort by name"
|
||||
renderItem={(item) => {
|
||||
const cat = categories.find(g => g.id === item.id)!;
|
||||
return (
|
||||
<SortableCardItem
|
||||
id={cat.id}
|
||||
item={{
|
||||
id: cat.id,
|
||||
name: cat.name,
|
||||
type: "category",
|
||||
count: cat.images.length
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
{galleries.map((gallery) => (
|
||||
<Link href={`/galleries/edit/${gallery.id}`} key={gallery.id}>
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base truncate">{gallery.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{gallery.description && <p className="text-sm text-muted-foreground">{gallery.description}</p>}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
Connected to {gallery.albums.length} album{gallery.albums.length !== 1 ? "s" : ""}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<SortableList
|
||||
items={sortableItems}
|
||||
onReorder={handleReorder}
|
||||
onSortDefault={handleSortDefault}
|
||||
defaultSortLabel="Sort by name"
|
||||
renderItem={(item) => {
|
||||
const gallery = galleries.find(g => g.id === item.id)!;
|
||||
return (
|
||||
<SortableCardItem
|
||||
id={gallery.id}
|
||||
item={{
|
||||
id: gallery.id,
|
||||
name: gallery.name,
|
||||
type: "gallery",
|
||||
coverImage: gallery.coverImage,
|
||||
count: gallery.albums.length,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
const result = await generateImageText(image.fileKey);
|
||||
if (result) {
|
||||
form.setValue("altText", result.alt);
|
||||
form.setValue("description", result.description);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Autofill Alt + Description
|
||||
</Button>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fileType"
|
||||
@ -224,12 +238,11 @@ export default function EditImageForm({ image, albums, artists, categories, tags
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>source</FormLabel>
|
||||
<FormControl><Input {...field} disabled /></FormControl>
|
||||
<FormControl><Input {...field} /></FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="creationDate"
|
||||
@ -263,6 +276,9 @@ export default function EditImageForm({ image, albums, artists, categories, tags
|
||||
}
|
||||
}}
|
||||
initialFocus
|
||||
fromYear={1900}
|
||||
toYear={2100}
|
||||
captionLayout="dropdown"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
@ -287,6 +303,7 @@ export default function EditImageForm({ image, albums, artists, categories, tags
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Year/Month fallback – number inputs */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@ -299,8 +316,12 @@ export default function EditImageForm({ image, albums, artists, categories, tags
|
||||
type="number"
|
||||
min={1}
|
||||
max={12}
|
||||
disabled={!!form.watch("creationDate")}
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const val = parseInt(e.target.value);
|
||||
field.onChange(isNaN(val) ? undefined : val);
|
||||
}}
|
||||
disabled={!!form.watch("creationDate")}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@ -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")}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
103
src/components/sort/SortableItem.tsx
Normal file
103
src/components/sort/SortableItem.tsx
Normal file
@ -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<SupportedTypes, string> = {
|
||||
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 (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
className="relative cursor-grab active:cursor-grabbing"
|
||||
>
|
||||
<div
|
||||
{...listeners}
|
||||
className="absolute top-2 left-2 z-20 text-muted-foreground bg-white/70 rounded-full p-1"
|
||||
title="Drag to reorder"
|
||||
>
|
||||
<GripVertical className="w-4 h-4" />
|
||||
</div>
|
||||
|
||||
<Link href={href}>
|
||||
<div className="group rounded-lg border overflow-hidden hover:shadow-md transition-shadow bg-background relative">
|
||||
{isVisualType ? (
|
||||
<div className="relative aspect-[4/3] w-full bg-muted items-center justify-center">
|
||||
{item.coverImage?.fileKey ? (
|
||||
<NextImage
|
||||
src={`/api/image/thumbnails/${item.coverImage.fileKey}.webp`}
|
||||
alt={item.coverImage.imageName}
|
||||
fill
|
||||
className={clsx("object-cover transition duration-300")}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
||||
No cover image
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className={clsx("p-4", !isVisualType && "text-center")}>
|
||||
<h2 className={clsx("text-lg font-semibold truncate", isVisualType && "text-center")}>
|
||||
{item.name}
|
||||
</h2>
|
||||
{countLabel && (
|
||||
<p className="text-sm text-muted-foreground mt-1">{countLabel}</p>
|
||||
)}
|
||||
{item.textLabel && (
|
||||
<p className="text-sm text-muted-foreground mt-1">{item.textLabel}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
85
src/components/sort/SortableList.tsx
Normal file
85
src/components/sort/SortableList.tsx
Normal file
@ -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 (
|
||||
<div className="space-y-4">
|
||||
{onSortDefault && (
|
||||
<button
|
||||
onClick={onSortDefault}
|
||||
className="px-4 py-2 text-sm font-medium bg-gray-200 rounded hover:bg-gray-300 text-primary-foreground"
|
||||
>
|
||||
{defaultSortLabel}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={localItems.map(i => i.id)} strategy={verticalListSortingStrategy}>
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
{localItems.map(item => (
|
||||
<div key={item.id}>{renderItem(item)}</div>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||
{tags.map((tag) => (
|
||||
<Link href={`/tags/edit/${tag.id}`} key={tag.id}>
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base truncate">{tag.name}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{tag.description && <p className="text-sm text-muted-foreground">{tag.description}</p>}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<SortableList
|
||||
items={sortableItems}
|
||||
onReorder={handleReorder}
|
||||
onSortDefault={handleSortDefault}
|
||||
defaultSortLabel="Sort by name"
|
||||
renderItem={(item) => {
|
||||
const tag = tags.find(g => g.id === item.id)!;
|
||||
return (
|
||||
<SortableCardItem
|
||||
id={tag.id}
|
||||
item={{
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
type: "tag",
|
||||
count: tag.images.length
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
32
src/components/ui/checkbox.tsx
Normal file
32
src/components/ui/checkbox.tsx
Normal file
@ -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<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
@ -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) => {
|
||||
|
6
src/types/SortableItem.ts
Normal file
6
src/types/SortableItem.ts
Normal file
@ -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)
|
||||
}
|
79
src/utils/generateImageText.ts
Normal file
79
src/utils/generateImageText.ts
Normal file
@ -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);
|
||||
}
|
27
src/utils/setSortIndex.ts
Normal file
27
src/utils/setSortIndex.ts
Normal file
@ -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)
|
||||
})
|
Reference in New Issue
Block a user