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": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.837.0",
|
"@aws-sdk/client-s3": "^3.837.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^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",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@material/material-color-utilities": "^0.3.0",
|
"@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-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
@ -37,6 +42,7 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.58.1",
|
"react-hook-form": "^7.58.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
|
"replicate": "^1.0.1",
|
||||||
"sharp": "^0.34.2",
|
"sharp": "^0.34.2",
|
||||||
"sonner": "^2.0.5",
|
"sonner": "^2.0.5",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
@ -55,8 +61,9 @@
|
|||||||
"eslint-config-next": "15.3.4",
|
"eslint-config-next": "15.3.4",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-prettier": "^5.5.0",
|
"eslint-plugin-prettier": "^5.5.0",
|
||||||
"prisma": "^6.10.1",
|
"prisma": "^6.11.0",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
"tsx": "^4.20.3",
|
||||||
"tw-animate-css": "^1.3.4",
|
"tw-animate-css": "^1.3.4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
@ -998,6 +1005,73 @@
|
|||||||
"integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==",
|
"integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.4.3",
|
"version": "1.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
|
||||||
@ -1031,6 +1105,431 @@
|
|||||||
"tslib": "^2.4.0"
|
"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": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||||
@ -2125,9 +2624,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
"version": "6.10.1",
|
"version": "6.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.11.0.tgz",
|
||||||
"integrity": "sha512-Re4pMlcUsQsUTAYMK7EJ4Bw2kg3WfZAAlr8GjORJaK4VOP6LxRQUQ1TuLnxcF42XqGkWQ36q5CQF1yVadANQ6w==",
|
"integrity": "sha512-K9TkKepOYvCOg3qCuKz7ZHf6rf58BFKi08plKjU4qVv9y7/UxO6tLz7PlWcgODUZKURLPmRHjHERffIx/8az4w==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -2147,9 +2646,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/config": {
|
"node_modules/@prisma/config": {
|
||||||
"version": "6.10.1",
|
"version": "6.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.11.0.tgz",
|
||||||
"integrity": "sha512-kz4/bnqrOrzWo8KzYguN0cden4CzLJJ+2VSpKtF8utHS3l1JS0Lhv6BLwpOX6X9yNreTbZQZwewb+/BMPDCIYQ==",
|
"integrity": "sha512-icBfutMpdrwSf2ggo012zhQ4oianijXL/UPbv4PNVK3WUWbB3/F5Ltq8ZfElGrtwKC6XuFFPxU5qDC9x7vh8zQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -2157,53 +2656,53 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/debug": {
|
"node_modules/@prisma/debug": {
|
||||||
"version": "6.10.1",
|
"version": "6.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.11.0.tgz",
|
||||||
"integrity": "sha512-k2YT53cWxv9OLjW4zSYTZ6Z7j0gPfCzcr2Mj99qsuvlxr8WAKSZ2NcSR0zLf/mP4oxnYG842IMj3utTgcd7CaA==",
|
"integrity": "sha512-zo4oEZMWMt0BFWl+4NK9FUpaEOmjGR3y2/r0lkW/DK4BUBRgMj90s8QqK2K+vXG3xn0nAGg2kOSu+Swn60CFLg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "6.10.1",
|
"version": "6.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.11.0.tgz",
|
||||||
"integrity": "sha512-Q07P5rS2iPwk2IQr/rUQJ42tHjpPyFcbiH7PXZlV81Ryr9NYIgdxcUrwgVOWVm5T7ap02C0dNd1dpnNcSWig8A==",
|
"integrity": "sha512-uqnYxvPKZPvYZA7F0q4gTR+fVWUJSY5bif7JAKBIOD5SoRRy0qEIaPy4Nna5WDLQaFGshaY/Bh8dLOQMfxhJJw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.10.1",
|
"@prisma/debug": "6.11.0",
|
||||||
"@prisma/engines-version": "6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c",
|
"@prisma/engines-version": "6.11.0-18.9c30299f5a0ea26a96790e13f796dc6094db3173",
|
||||||
"@prisma/fetch-engine": "6.10.1",
|
"@prisma/fetch-engine": "6.11.0",
|
||||||
"@prisma/get-platform": "6.10.1"
|
"@prisma/get-platform": "6.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines-version": {
|
"node_modules/@prisma/engines-version": {
|
||||||
"version": "6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c",
|
"version": "6.11.0-18.9c30299f5a0ea26a96790e13f796dc6094db3173",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.11.0-18.9c30299f5a0ea26a96790e13f796dc6094db3173.tgz",
|
||||||
"integrity": "sha512-ZJFTsEqapiTYVzXya6TUKYDFnSWCNegfUiG5ik9fleQva5Sk3DNyyUi7X1+0ZxWFHwHDr6BZV5Vm+iwP+LlciA==",
|
"integrity": "sha512-M3vbyDICFIA1oJl0cFkM0omD4HsJZjFi0hu0f0UxyPABH8KEcZyUd5BToCrNl4B8lUeQn+L5+gfaQleOKp6Lrg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine": {
|
"node_modules/@prisma/fetch-engine": {
|
||||||
"version": "6.10.1",
|
"version": "6.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.11.0.tgz",
|
||||||
"integrity": "sha512-clmbG/Jgmrc/n6Y77QcBmAUlq9LrwI9Dbgy4pq5jeEARBpRCWJDJ7PWW1P8p0LfFU0i5fsyO7FqRzRB8mkdS4g==",
|
"integrity": "sha512-ZHHSP7vJFo5hePH+MNovxhqXabIg38ZpCwQfUBON29kwPX3f1pjYnzGpgJLCJy4k7mKGOzTgrXPqH8+nJvq2fw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.10.1",
|
"@prisma/debug": "6.11.0",
|
||||||
"@prisma/engines-version": "6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c",
|
"@prisma/engines-version": "6.11.0-18.9c30299f5a0ea26a96790e13f796dc6094db3173",
|
||||||
"@prisma/get-platform": "6.10.1"
|
"@prisma/get-platform": "6.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/get-platform": {
|
"node_modules/@prisma/get-platform": {
|
||||||
"version": "6.10.1",
|
"version": "6.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.11.0.tgz",
|
||||||
"integrity": "sha512-4CY5ndKylcsce9Mv+VWp5obbR2/86SHOLVV053pwIkhVtT9C9A83yqiqI/5kJM9T1v1u1qco/bYjDKycmei9HA==",
|
"integrity": "sha512-yspBGvOfJQwuoApk5B4aBlHDy6YDXAOe4Ml8U2eZ+M2b7fDd10YDomS3Q4qrYHUUVYF3TJyN86NcnRMOvCMUrA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.10.1"
|
"@prisma/debug": "6.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/number": {
|
"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": {
|
"node_modules/@radix-ui/react-collection": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
"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"
|
"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": {
|
"node_modules/escape-string-regexp": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
@ -6696,6 +7266,21 @@
|
|||||||
"node": ">= 0.12"
|
"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": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@ -7592,6 +8177,26 @@
|
|||||||
"whatwg-fetch": "^3.4.1"
|
"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": {
|
"node_modules/isstream": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||||
@ -8357,26 +8962,6 @@
|
|||||||
"node": ">=v0.6.5"
|
"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": {
|
"node_modules/node-vibrant": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/node-vibrant/-/node-vibrant-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/node-vibrant/-/node-vibrant-4.0.3.tgz",
|
||||||
@ -8810,15 +9395,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "6.10.1",
|
"version": "6.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.11.0.tgz",
|
||||||
"integrity": "sha512-khhlC/G49E4+uyA3T3H5PRBut486HD2bDqE2+rvkU0pwk9IAqGFacLFUyIx9Uw+W2eCtf6XGwsp+/strUwMNPw==",
|
"integrity": "sha512-gI69E7fusgk32XALpXzdgR10xUx2aFnHiu/JaUo4O07G4JvFT0xNtD0Iy81p37iBLTYFEhWa9VrHKXaiyZ5fLQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/config": "6.10.1",
|
"@prisma/config": "6.11.0",
|
||||||
"@prisma/engines": "6.10.1"
|
"@prisma/engines": "6.11.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"prisma": "build/index.js"
|
"prisma": "build/index.js"
|
||||||
@ -9156,6 +9741,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/request": {
|
||||||
"version": "2.88.2",
|
"version": "2.88.2",
|
||||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
|
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
|
||||||
@ -10072,6 +10672,26 @@
|
|||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"license": "0BSD"
|
"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": {
|
"node_modules/tunnel-agent": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
|
11
package.json
11
package.json
@ -11,9 +11,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.837.0",
|
"@aws-sdk/client-s3": "^3.837.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^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",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@material/material-color-utilities": "^0.3.0",
|
"@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-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
@ -38,6 +43,7 @@
|
|||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.58.1",
|
"react-hook-form": "^7.58.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
|
"replicate": "^1.0.1",
|
||||||
"sharp": "^0.34.2",
|
"sharp": "^0.34.2",
|
||||||
"sonner": "^2.0.5",
|
"sonner": "^2.0.5",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
@ -56,8 +62,9 @@
|
|||||||
"eslint-config-next": "15.3.4",
|
"eslint-config-next": "15.3.4",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-prettier": "^5.5.0",
|
"eslint-plugin-prettier": "^5.5.0",
|
||||||
"prisma": "^6.10.1",
|
"prisma": "^6.11.0",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
"tsx": "^4.20.3",
|
||||||
"tw-animate-css": "^1.3.4",
|
"tw-animate-css": "^1.3.4",
|
||||||
"typescript": "^5"
|
"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())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
slug String @unique
|
slug String @unique
|
||||||
name String
|
name String
|
||||||
@ -34,6 +35,7 @@ model Album {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
slug String
|
slug String
|
||||||
name String
|
name String
|
||||||
@ -54,11 +56,14 @@ model Artist {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
slug String @unique
|
slug String @unique
|
||||||
displayName String
|
displayName String
|
||||||
|
|
||||||
nickname String?
|
nickname String?
|
||||||
|
description String?
|
||||||
|
source String?
|
||||||
|
|
||||||
socials Social[]
|
socials Social[]
|
||||||
images Image[]
|
images Image[]
|
||||||
@ -68,10 +73,12 @@ model Social {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
handle String
|
handle String
|
||||||
platform String
|
platform String
|
||||||
isPrimary Boolean
|
isPrimary Boolean @default(false)
|
||||||
|
isVisible Boolean @default(true)
|
||||||
|
|
||||||
link String?
|
link String?
|
||||||
|
|
||||||
@ -83,6 +90,7 @@ model Category {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
name String @unique
|
name String @unique
|
||||||
|
|
||||||
@ -95,6 +103,7 @@ model Tag {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
name String @unique
|
name String @unique
|
||||||
|
|
||||||
@ -107,10 +116,11 @@ model Image {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
sortIndex Int @default(0)
|
||||||
|
|
||||||
fileKey String
|
fileKey String @unique
|
||||||
|
originalFile String @unique
|
||||||
imageName String
|
imageName String
|
||||||
originalFile String
|
|
||||||
uploadDate DateTime @default(now())
|
uploadDate DateTime @default(now())
|
||||||
nsfw Boolean @default(false)
|
nsfw Boolean @default(false)
|
||||||
|
|
||||||
@ -204,6 +214,8 @@ model ImageVariant {
|
|||||||
sizeBytes Int?
|
sizeBytes Int?
|
||||||
|
|
||||||
image Image @relation(fields: [imageId], references: [id])
|
image Image @relation(fields: [imageId], references: [id])
|
||||||
|
|
||||||
|
@@unique([imageId, type])
|
||||||
}
|
}
|
||||||
|
|
||||||
model ColorPalette {
|
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,
|
displayName: values.displayName,
|
||||||
slug: values.slug,
|
slug: values.slug,
|
||||||
nickname: values.nickname,
|
nickname: values.nickname,
|
||||||
|
source: values.source,
|
||||||
|
description: values.description,
|
||||||
socials: {
|
socials: {
|
||||||
deleteMany: {
|
deleteMany: {
|
||||||
id: { in: removedSocials.map(s => s.id) },
|
id: { in: removedSocials.map(s => s.id) },
|
||||||
@ -39,6 +41,7 @@ export async function updateArtist(
|
|||||||
handle: s.handle,
|
handle: s.handle,
|
||||||
link: s.link,
|
link: s.link,
|
||||||
isPrimary: s.isPrimary,
|
isPrimary: s.isPrimary,
|
||||||
|
isVisible: s.isVisible
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
create: (values.socials ?? [])
|
create: (values.socials ?? [])
|
||||||
@ -48,6 +51,7 @@ export async function updateArtist(
|
|||||||
handle: s.handle,
|
handle: s.handle,
|
||||||
link: s.link,
|
link: s.link,
|
||||||
isPrimary: s.isPrimary,
|
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 },
|
where: { id: imageId },
|
||||||
include: {
|
include: {
|
||||||
variants: true,
|
variants: true,
|
||||||
palettes: { include: { items: true } },
|
palettes: true,
|
||||||
colors: true,
|
colors: true,
|
||||||
extractColors: true,
|
extractColors: true,
|
||||||
theme: true,
|
|
||||||
metadata: true,
|
metadata: true,
|
||||||
pixels: true,
|
|
||||||
stats: true,
|
stats: true,
|
||||||
|
albumCover: true,
|
||||||
|
galleryCover: true,
|
||||||
|
tags: true,
|
||||||
|
categories: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -23,39 +25,80 @@ export async function deleteImage(imageId: string) {
|
|||||||
|
|
||||||
// Delete S3 objects
|
// Delete S3 objects
|
||||||
for (const variant of image.variants) {
|
for (const variant of image.variants) {
|
||||||
await s3.send(new DeleteObjectCommand({
|
try {
|
||||||
Bucket: "felliesartapp",
|
await s3.send(
|
||||||
Key: variant.s3Key,
|
new DeleteObjectCommand({
|
||||||
}));
|
Bucket: "felliesartapp",
|
||||||
}
|
Key: variant.s3Key,
|
||||||
|
})
|
||||||
// Delete image variants
|
);
|
||||||
await prisma.imageVariant.deleteMany({ where: { imageId } });
|
} catch (err) {
|
||||||
|
console.warn("Failed to delete S3 object: " + variant.s3Key + ". " + err);
|
||||||
// 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 } });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.imageMetadata.deleteMany({ where: { imageId } });
|
||||||
await prisma.imageStats.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
|
// Finally delete the image
|
||||||
await prisma.image.delete({ where: { id: imageId } });
|
await prisma.image.delete({ where: { id: imageId } });
|
||||||
|
@ -76,9 +76,21 @@ export async function uploadImage(values: z.infer<typeof imageUploadSchema>) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
//--- Resized file
|
//--- 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)
|
const resizedBuffer = await sharp(watermarkedBuffer)
|
||||||
.resize({ width: resizedWidth, withoutEnlargement: true })
|
.resize({ ...resizeOptions, withoutEnlargement: true })
|
||||||
.toFormat('webp')
|
.toFormat('webp')
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
const resizedMetadata = await sharp(resizedBuffer).metadata();
|
const resizedMetadata = await sharp(resizedBuffer).metadata();
|
||||||
@ -91,9 +103,20 @@ export async function uploadImage(values: z.infer<typeof imageUploadSchema>) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
//--- Thumbnail file
|
//--- 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)
|
const thumbnailBuffer = await sharp(watermarkedBuffer)
|
||||||
.resize({ width: thumbnailWidth, withoutEnlargement: true })
|
.resize({ ...thumbnailOptions, withoutEnlargement: true })
|
||||||
.toFormat('webp')
|
.toFormat('webp')
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
const thumbnailMetadata = await sharp(thumbnailBuffer).metadata();
|
const thumbnailMetadata = await sharp(thumbnailBuffer).metadata();
|
||||||
@ -112,7 +135,6 @@ export async function uploadImage(values: z.infer<typeof imageUploadSchema>) {
|
|||||||
fileKey,
|
fileKey,
|
||||||
originalFile: fileName,
|
originalFile: fileName,
|
||||||
uploadDate: new Date(),
|
uploadDate: new Date(),
|
||||||
|
|
||||||
creationDate: lastModified,
|
creationDate: lastModified,
|
||||||
creationMonth: month,
|
creationMonth: month,
|
||||||
creationYear: year,
|
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() {
|
export default async function AlbumsPage() {
|
||||||
const albums = await prisma.album.findMany(
|
const albums = await prisma.album.findMany(
|
||||||
{
|
{
|
||||||
include: { gallery: true, images: { select: { id: true } } },
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
orderBy: { name: "asc" }
|
include: { gallery: true, images: { select: { id: true } }, coverImage: true },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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>
|
<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">
|
<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
|
<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() {
|
export default async function ArtistsPage() {
|
||||||
const artists = await prisma.artist.findMany({
|
const artists = await prisma.artist.findMany({
|
||||||
orderBy: { createdAt: "asc" },
|
orderBy: [{ sortIndex: "asc" }, { displayName: "asc" }],
|
||||||
include: { images: { select: { id: true } } }
|
include: { images: { select: { id: true } } }
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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>
|
<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">
|
<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
|
<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() {
|
export default async function CategoriesPage() {
|
||||||
const categories = await prisma.category.findMany(
|
const categories = await prisma.category.findMany(
|
||||||
{
|
{
|
||||||
orderBy: { createdAt: "asc" },
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
include: { images: { select: { id: true } } }
|
include: { images: { select: { id: true } } }
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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>
|
<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">
|
<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
|
<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() {
|
export default async function GalleriesPage() {
|
||||||
const galleries = await prisma.gallery.findMany({
|
const galleries = await prisma.gallery.findMany({
|
||||||
orderBy: { createdAt: "asc" },
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
include: {
|
include: {
|
||||||
albums: { select: { id: true } }
|
albums: { select: { id: true } },
|
||||||
|
coverImage: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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>
|
<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">
|
<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
|
<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 categories = await prisma.category.findMany({ orderBy: { name: "asc" } });
|
||||||
const tags = await prisma.tag.findMany({ orderBy: { name: "asc" } });
|
const tags = await prisma.tag.findMany({ orderBy: { name: "asc" } });
|
||||||
|
|
||||||
console.log(image)
|
// console.log(image)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -17,8 +17,8 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Fellies Art Admin",
|
||||||
description: "Generated by create next app",
|
description: "Admin page for the fellies.art artworks",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
@ -6,13 +6,14 @@ import Link from "next/link";
|
|||||||
export default async function TagsPage() {
|
export default async function TagsPage() {
|
||||||
const tags = await prisma.tag.findMany(
|
const tags = await prisma.tag.findMany(
|
||||||
{
|
{
|
||||||
orderBy: { createdAt: "asc" }
|
orderBy: [{ sortIndex: "asc" }, { name: "asc" }],
|
||||||
|
include: { images: { select: { id: true } } }
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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>
|
<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">
|
<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
|
<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";
|
"use client";
|
||||||
import { Album, Gallery } from "@/generated/prisma";
|
|
||||||
import { TriangleAlert } from "lucide-react";
|
import { updateAlbumSortOrder } from "@/actions/albums/updateAlbumSortOrder";
|
||||||
import Link from "next/link";
|
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 & {
|
type AlbumWithGallery = Album & {
|
||||||
gallery: Gallery | null;
|
gallery: Gallery | null;
|
||||||
images: { id: string }[];
|
images: { id: string }[];
|
||||||
|
coverImage?: (Image) | null
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ListAlbums({ albums }: { albums: AlbumWithGallery[] }) {
|
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 (
|
return (
|
||||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
<SortableList
|
||||||
{albums.map((album) => (
|
items={sortableItems}
|
||||||
<Link href={`/albums/edit/${album.id}`} key={album.id}>
|
onReorder={handleReorder}
|
||||||
<Card className="overflow-hidden">
|
onSortDefault={handleSortDefault}
|
||||||
<CardHeader>
|
defaultSortLabel="Sort by name"
|
||||||
<CardTitle className="text-base truncate">{album.name}</CardTitle>
|
renderItem={(item) => {
|
||||||
</CardHeader>
|
const album = albums.find(g => g.id === item.id)!;
|
||||||
<CardContent>
|
return (
|
||||||
{album.description && <p className="text-sm text-muted-foreground">{album.description}</p>}
|
<SortableCardItem
|
||||||
</CardContent>
|
id={album.id}
|
||||||
<CardFooter>
|
item={{
|
||||||
<div className="flex flex-col gap-4">
|
id: album.id,
|
||||||
{album.gallery ? (
|
name: album.name,
|
||||||
<>Gallery: {album.gallery.name}</>
|
type: "album",
|
||||||
) : (
|
coverImage: album.coverImage,
|
||||||
<div className="flex items-center text-destructive">
|
count: album.images.length,
|
||||||
<TriangleAlert className="mr-2 h-4 w-4" />
|
textLabel: "Gallery: " + album.gallery?.name
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { updateArtist } from "@/actions/artists/updateArtist";
|
import { updateArtist } from "@/actions/artists/updateArtist";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Artist, Social } from "@/generated/prisma";
|
import { Artist, Social } from "@/generated/prisma";
|
||||||
@ -21,12 +22,15 @@ export default function EditAlbumForm({ artist }: { artist: Artist & { socials:
|
|||||||
displayName: artist.displayName,
|
displayName: artist.displayName,
|
||||||
slug: artist.slug,
|
slug: artist.slug,
|
||||||
nickname: artist.nickname || "",
|
nickname: artist.nickname || "",
|
||||||
|
source: artist.source || "",
|
||||||
|
description: artist.description || "",
|
||||||
socials: artist.socials.map((s) => ({
|
socials: artist.socials.map((s) => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
platform: s.platform,
|
platform: s.platform,
|
||||||
handle: s.handle,
|
handle: s.handle,
|
||||||
link: s.link ?? "", // Convert null to empty string
|
link: s.link ?? "", // Convert null to empty string
|
||||||
isPrimary: s.isPrimary,
|
isPrimary: s.isPrimary,
|
||||||
|
isVisible: s.isVisible
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -98,7 +102,38 @@ export default function EditAlbumForm({ artist }: { artist: Artist & { socials:
|
|||||||
</FormItem>
|
</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 */}
|
{/* Socials section */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<FormLabel>Social Links</FormLabel>
|
<FormLabel>Social Links</FormLabel>
|
||||||
@ -169,6 +204,21 @@ export default function EditAlbumForm({ artist }: { artist: Artist & { socials:
|
|||||||
</FormItem>
|
</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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -181,7 +231,7 @@ export default function EditAlbumForm({ artist }: { artist: Artist & { socials:
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => append({ platform: "", handle: "", link: "", isPrimary: false })}
|
onClick={() => append({ platform: "", handle: "", link: "", isPrimary: false, isVisible: true })}
|
||||||
>
|
>
|
||||||
+ Add Social
|
+ Add Social
|
||||||
</Button>
|
</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 { Artist } from "@/generated/prisma";
|
||||||
import Link from "next/link";
|
import { SortableItem } from "@/types/SortableItem";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
type ArtistsWithItems = Artist & {
|
type ArtistsWithItems = Artist & {
|
||||||
images: { id: string }[]
|
images: { id: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ListArtists({ artists }: { artists: ArtistsWithItems[] }) {
|
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 (
|
return (
|
||||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
<SortableList
|
||||||
{artists.map((artist) => (
|
items={sortableItems}
|
||||||
<Link href={`/artists/edit/${artist.id}`} key={artist.id}>
|
onReorder={handleReorder}
|
||||||
<Card className="overflow-hidden">
|
onSortDefault={handleSortDefault}
|
||||||
<CardHeader>
|
defaultSortLabel="Sort by displayName"
|
||||||
<CardTitle className="text-base truncate">{artist.displayName}</CardTitle>
|
renderItem={(item) => {
|
||||||
</CardHeader>
|
const artist = artists.find(g => g.id === item.id)!;
|
||||||
<CardContent>
|
return (
|
||||||
</CardContent>
|
<SortableCardItem
|
||||||
<CardFooter>
|
id={artist.id}
|
||||||
Connected to {artist.images.length} image{artist.images.length !== 1 ? "s" : ""}
|
item={{
|
||||||
</CardFooter>
|
id: artist.id,
|
||||||
</Card>
|
name: artist.displayName,
|
||||||
</Link>
|
type: "artist",
|
||||||
))}
|
count: artist.images.length
|
||||||
</div>
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -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 { Category } from "@/generated/prisma";
|
||||||
import Link from "next/link";
|
import { SortableItem } from "@/types/SortableItem";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
type CategoriesWithItems = Category & {
|
type CategoriesWithItems = Category & {
|
||||||
images: { id: string }[]
|
images: { id: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ListCategories({ categories }: { categories: CategoriesWithItems[] }) {
|
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 (
|
return (
|
||||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
<SortableList
|
||||||
{categories.map((cat) => (
|
items={sortableItems}
|
||||||
<Link href={`/categories/edit/${cat.id}`} key={cat.id}>
|
onReorder={handleReorder}
|
||||||
<Card className="overflow-hidden">
|
onSortDefault={handleSortDefault}
|
||||||
<CardHeader>
|
defaultSortLabel="Sort by name"
|
||||||
<CardTitle className="text-base truncate">{cat.name}</CardTitle>
|
renderItem={(item) => {
|
||||||
</CardHeader>
|
const cat = categories.find(g => g.id === item.id)!;
|
||||||
<CardContent>
|
return (
|
||||||
{cat.description && <p className="text-sm text-muted-foreground">{cat.description}</p>}
|
<SortableCardItem
|
||||||
</CardContent>
|
id={cat.id}
|
||||||
<CardFooter>
|
item={{
|
||||||
Connected to {cat.images.length} image{cat.images.length !== 1 ? "s" : ""}
|
id: cat.id,
|
||||||
</CardFooter>
|
name: cat.name,
|
||||||
</Card>
|
type: "category",
|
||||||
</Link>
|
count: cat.images.length
|
||||||
))}
|
}}
|
||||||
</div>
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,31 +1,64 @@
|
|||||||
// "use client"
|
"use client";
|
||||||
|
|
||||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
import { updateGallerySortOrder } from "@/actions/galleries/updateGallerySortOrder";
|
||||||
import { Gallery } from "@/generated/prisma";
|
import { SortableCardItem } from "@/components/sort/SortableItem";
|
||||||
import Link from "next/link";
|
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 & {
|
type GalleriesWithItems = Gallery & {
|
||||||
albums: { id: string }[]
|
albums: { id: string }[],
|
||||||
}
|
coverImage?: (Image) | null
|
||||||
|
};
|
||||||
|
|
||||||
export default function ListGalleries({ galleries }: { galleries: GalleriesWithItems[] }) {
|
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 (
|
return (
|
||||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
<SortableList
|
||||||
{galleries.map((gallery) => (
|
items={sortableItems}
|
||||||
<Link href={`/galleries/edit/${gallery.id}`} key={gallery.id}>
|
onReorder={handleReorder}
|
||||||
<Card className="overflow-hidden">
|
onSortDefault={handleSortDefault}
|
||||||
<CardHeader>
|
defaultSortLabel="Sort by name"
|
||||||
<CardTitle className="text-base truncate">{gallery.name}</CardTitle>
|
renderItem={(item) => {
|
||||||
</CardHeader>
|
const gallery = galleries.find(g => g.id === item.id)!;
|
||||||
<CardContent>
|
return (
|
||||||
{gallery.description && <p className="text-sm text-muted-foreground">{gallery.description}</p>}
|
<SortableCardItem
|
||||||
</CardContent>
|
id={gallery.id}
|
||||||
<CardFooter>
|
item={{
|
||||||
Connected to {gallery.albums.length} album{gallery.albums.length !== 1 ? "s" : ""}
|
id: gallery.id,
|
||||||
</CardFooter>
|
name: gallery.name,
|
||||||
</Card>
|
type: "gallery",
|
||||||
</Link>
|
coverImage: gallery.coverImage,
|
||||||
))}
|
count: gallery.albums.length,
|
||||||
</div>
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -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 { Album, Artist, Category, Color, ColorPalette, ColorPaletteItem, ExtractColor, Image, ImageColor, ImageExtractColor, ImageMetadata, ImagePalette, ImageStats, ImageVariant, Tag } from "@/generated/prisma";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { imageSchema } from "@/schemas/images/imageSchema";
|
import { imageSchema } from "@/schemas/images/imageSchema";
|
||||||
|
import { generateImageText } from "@/utils/generateImageText";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@ -207,6 +208,19 @@ export default function EditImageForm({ image, albums, artists, categories, tags
|
|||||||
</FormItem>
|
</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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="fileType"
|
name="fileType"
|
||||||
@ -224,12 +238,11 @@ export default function EditImageForm({ image, albums, artists, categories, tags
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>source</FormLabel>
|
<FormLabel>source</FormLabel>
|
||||||
<FormControl><Input {...field} disabled /></FormControl>
|
<FormControl><Input {...field} /></FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="creationDate"
|
name="creationDate"
|
||||||
@ -263,6 +276,9 @@ export default function EditImageForm({ image, albums, artists, categories, tags
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
initialFocus
|
initialFocus
|
||||||
|
fromYear={1900}
|
||||||
|
toYear={2100}
|
||||||
|
captionLayout="dropdown"
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</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">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@ -299,8 +316,12 @@ export default function EditImageForm({ image, albums, artists, categories, tags
|
|||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={12}
|
max={12}
|
||||||
disabled={!!form.watch("creationDate")}
|
|
||||||
{...field}
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = parseInt(e.target.value);
|
||||||
|
field.onChange(isNaN(val) ? undefined : val);
|
||||||
|
}}
|
||||||
|
disabled={!!form.watch("creationDate")}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@ -318,8 +339,12 @@ export default function EditImageForm({ image, albums, artists, categories, tags
|
|||||||
type="number"
|
type="number"
|
||||||
min={1900}
|
min={1900}
|
||||||
max={2100}
|
max={2100}
|
||||||
disabled={!!form.watch("creationDate")}
|
|
||||||
{...field}
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = parseInt(e.target.value);
|
||||||
|
field.onChange(isNaN(val) ? undefined : val);
|
||||||
|
}}
|
||||||
|
disabled={!!form.watch("creationDate")}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<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";
|
"use client";
|
||||||
import { Tag } from "@/generated/prisma";
|
|
||||||
import Link from "next/link";
|
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 (
|
return (
|
||||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
<SortableList
|
||||||
{tags.map((tag) => (
|
items={sortableItems}
|
||||||
<Link href={`/tags/edit/${tag.id}`} key={tag.id}>
|
onReorder={handleReorder}
|
||||||
<Card className="overflow-hidden">
|
onSortDefault={handleSortDefault}
|
||||||
<CardHeader>
|
defaultSortLabel="Sort by name"
|
||||||
<CardTitle className="text-base truncate">{tag.name}</CardTitle>
|
renderItem={(item) => {
|
||||||
</CardHeader>
|
const tag = tags.find(g => g.id === item.id)!;
|
||||||
<CardContent>
|
return (
|
||||||
{tag.description && <p className="text-sm text-muted-foreground">{tag.description}</p>}
|
<SortableCardItem
|
||||||
</CardContent>
|
id={tag.id}
|
||||||
<CardFooter>
|
item={{
|
||||||
</CardFooter>
|
id: tag.id,
|
||||||
</Card>
|
name: tag.name,
|
||||||
</Link>
|
type: "tag",
|
||||||
))}
|
count: tag.images.length
|
||||||
</div>
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
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"),
|
platform: z.string().min(1, "Platform is required"),
|
||||||
handle: z.string().min(1, "Handle is required"),
|
handle: z.string().min(1, "Handle is required"),
|
||||||
isPrimary: z.boolean(),
|
isPrimary: z.boolean(),
|
||||||
|
isVisible: z.boolean(),
|
||||||
link: z.string().optional(),
|
link: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -12,6 +13,8 @@ export const artistSchema = z.object({
|
|||||||
displayName: z.string().min(3, "Name is required. Min 3 characters."),
|
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)"),
|
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(),
|
nickname: z.string().optional(),
|
||||||
|
source: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
socials: z.array(socialSchema).optional(),
|
socials: z.array(socialSchema).optional(),
|
||||||
}).refine(
|
}).refine(
|
||||||
(data) => {
|
(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