diff --git a/package-lock.json b/package-lock.json index 3dc3ed2..0421d81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -2792,6 +2793,35 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.5.tgz", + "integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==", + "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-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-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", diff --git a/package.json b/package.json index 18a0da5..a359c9f 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/prisma/migrations/20250628155128_image_nsfw/migration.sql b/prisma/migrations/20250628155128_image_nsfw/migration.sql new file mode 100644 index 0000000..d9ffd3f --- /dev/null +++ b/prisma/migrations/20250628155128_image_nsfw/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Image" ADD COLUMN "nsfw" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 398340b..f3ff274 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -112,6 +112,7 @@ model Image { imageName String originalFile String uploadDate DateTime @default(now()) + nsfw Boolean @default(false) altText String? description String? diff --git a/src/actions/images/updateImage.ts b/src/actions/images/updateImage.ts index 21013b9..a3057b2 100644 --- a/src/actions/images/updateImage.ts +++ b/src/actions/images/updateImage.ts @@ -17,10 +17,11 @@ export async function updateImage( imageName, originalFile, uploadDate, + nsfw, altText, description, fileType, - imageData, + source, creationMonth, creationYear, fileSize, @@ -37,10 +38,11 @@ export async function updateImage( imageName, originalFile, uploadDate, + nsfw, altText, description, fileType, - imageData, + source, creationMonth, creationYear, fileSize, diff --git a/src/app/colors/[id]/page.tsx b/src/app/colors/[id]/page.tsx new file mode 100644 index 0000000..f5adf66 --- /dev/null +++ b/src/app/colors/[id]/page.tsx @@ -0,0 +1,30 @@ +import DisplayColor from "@/components/colors/single/DisplayColor"; +import prisma from "@/lib/prisma"; + +export default async function ColorsPage({ params }: { params: { id: string } }) { + const { id } = await params; + + const colorData = await prisma.color.findUnique({ + where: { + id, + }, + include: { + images: { + include: { + image: { + include: { + variants: { where: { type: "thumbnail" } } + } + } + } + } + } + }); + + return ( +
+

Show color

+ {colorData ? : 'Color not found...'} +
+ ); +} \ No newline at end of file diff --git a/src/app/colors/page.tsx b/src/app/colors/page.tsx new file mode 100644 index 0000000..177784b --- /dev/null +++ b/src/app/colors/page.tsx @@ -0,0 +1,22 @@ +import ListColors from "@/components/colors/list/ListColors"; +import prisma from "@/lib/prisma"; + +export default async function ColorsPage() { + const colors = await prisma.color.findMany( + { + orderBy: { name: "asc" }, + include: { + images: { select: { id: true } } + } + } + ); + + return ( +
+
+

Colors

+
+ {colors.length > 0 ? :

No colors found.

} +
+ ); +} \ No newline at end of file diff --git a/src/app/extract/[id]/page.tsx b/src/app/extract/[id]/page.tsx new file mode 100644 index 0000000..fa490a8 --- /dev/null +++ b/src/app/extract/[id]/page.tsx @@ -0,0 +1,30 @@ +import DisplayExtractColor from "@/components/extract/single/DisplayExtractColor"; +import prisma from "@/lib/prisma"; + +export default async function ColorsPage({ params }: { params: { id: string } }) { + const { id } = await params; + + const colorData = await prisma.extractColor.findUnique({ + where: { + id, + }, + include: { + images: { + include: { + image: { + include: { + variants: { where: { type: "thumbnail" } } + } + } + } + } + } + }); + + return ( +
+

Show color

+ {colorData ? : 'Color not found...'} +
+ ); +} \ No newline at end of file diff --git a/src/app/extract/page.tsx b/src/app/extract/page.tsx new file mode 100644 index 0000000..313b5bb --- /dev/null +++ b/src/app/extract/page.tsx @@ -0,0 +1,22 @@ +import ListExtractColors from "@/components/extract/list/ListExtractColors"; +import prisma from "@/lib/prisma"; + +export default async function ExtractColorsPage() { + const extractColors = await prisma.extractColor.findMany( + { + orderBy: { name: "asc" }, + include: { + images: { select: { id: true } } + } + } + ); + + return ( +
+
+

Extract colors

+
+ {extractColors.length > 0 ? :

No colors found.

} +
+ ); +} \ No newline at end of file diff --git a/src/app/images/page.tsx b/src/app/images/page.tsx index 49aead4..c081641 100644 --- a/src/app/images/page.tsx +++ b/src/app/images/page.tsx @@ -4,11 +4,11 @@ import { PlusCircleIcon } from "lucide-react"; import Link from "next/link"; export default async function ImagesPage() { - const images = await prisma.image.findMany({ orderBy: { createdAt: "asc" } }); + const images = await prisma.image.findMany({ orderBy: { imageName: "asc" } }); return (
-
+

Images

Upload new image diff --git a/src/components/colors/list/ListColors.tsx b/src/components/colors/list/ListColors.tsx new file mode 100644 index 0000000..bf5dd09 --- /dev/null +++ b/src/components/colors/list/ListColors.tsx @@ -0,0 +1,35 @@ +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Color } from "@/generated/prisma"; +import Link from "next/link"; + +type ColorsWithItems = Color & { + images: { id: string }[] +} + +export default function ListColors({ colors }: { colors: ColorsWithItems[] }) { + return ( +
+ {colors.map((col) => ( + + + + {col.name} + + +
+
+
+ + + Used by {col.images.length} image{col.images.length !== 1 ? "s" : ""} + + + + ))} +
+ ); +} \ No newline at end of file diff --git a/src/components/colors/single/DisplayColor.tsx b/src/components/colors/single/DisplayColor.tsx new file mode 100644 index 0000000..7fe5fae --- /dev/null +++ b/src/components/colors/single/DisplayColor.tsx @@ -0,0 +1,41 @@ + +import ImageColorCard from "@/components/images/ImageColorCard"; +import { Color, Image, ImageColor, ImageVariant } from "@/generated/prisma"; + +type ColorWithItems = Color & { + images: (ImageColor & { + image: Image & { + variants: ImageVariant[] + } + })[] +} + +export default function DisplayColor({ color }: { color: ColorWithItems }) { + + + return ( +
+
+

{color.name}

+
+ +
+
+
+ +
+

Used by {color.images.length} image(s)

+
+ {color.images.map((image) => ( + + ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/extract/list/ListExtractColors.tsx b/src/components/extract/list/ListExtractColors.tsx new file mode 100644 index 0000000..75218ae --- /dev/null +++ b/src/components/extract/list/ListExtractColors.tsx @@ -0,0 +1,35 @@ +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { ExtractColor } from "@/generated/prisma"; +import Link from "next/link"; + +type ColorsWithItems = ExtractColor & { + images: { id: string }[] +} + +export default function ListExtractColors({ colors }: { colors: ColorsWithItems[] }) { + return ( +
+ {colors.map((col) => ( + + + + {col.name} + + +
+
+
+ + + Used by {col.images.length} image{col.images.length !== 1 ? "s" : ""} + + + + ))} +
+ ); +} \ No newline at end of file diff --git a/src/components/extract/single/DisplayExtractColor.tsx b/src/components/extract/single/DisplayExtractColor.tsx new file mode 100644 index 0000000..b029d98 --- /dev/null +++ b/src/components/extract/single/DisplayExtractColor.tsx @@ -0,0 +1,41 @@ + +import ImageExtractColorCard from "@/components/images/ImageExtractColorCard"; +import { ExtractColor, Image, ImageExtractColor, ImageVariant } from "@/generated/prisma"; + +type ColorWithItems = ExtractColor & { + images: (ImageExtractColor & { + image: Image & { + variants: ImageVariant[] + } + })[] +} + +export default function DisplayExtractColor({ color }: { color: ColorWithItems }) { + + + return ( +
+
+

{color.name}

+
+ +
+
+
+ +
+

Used by {color.images.length} image(s)

+
+ {color.images.map((image) => ( + + ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/global/TopNav.tsx b/src/components/global/TopNav.tsx index f1f3363..78af5ef 100644 --- a/src/components/global/TopNav.tsx +++ b/src/components/global/TopNav.tsx @@ -47,6 +47,11 @@ export default function TopNav() { Colors + + + Extract-Colors + + Images diff --git a/src/components/images/ImageColorCard.tsx b/src/components/images/ImageColorCard.tsx new file mode 100644 index 0000000..dadfcfb --- /dev/null +++ b/src/components/images/ImageColorCard.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { Image, ImageColor, ImageVariant } from "@/generated/prisma"; +import ImageComponent from "next/image"; +import Link from "next/link"; + +type ImagePaletteWithItems = { + image: ImageColor & { + image: Image & { + variants: ImageVariant[]; + } + }; +}; + +export default function ImageColorCard({ image }: ImagePaletteWithItems) { + const thumbnail = image.image.variants.find((v) => v.type === "thumbnail"); + + return ( + + {thumbnail?.s3Key ? ( + + ) : ( +
+ No Thumbnail +
+ )} +
{image.image.imageName} ({image.type})
+ + ); +} diff --git a/src/components/images/ImageExtractColorCard.tsx b/src/components/images/ImageExtractColorCard.tsx new file mode 100644 index 0000000..3338579 --- /dev/null +++ b/src/components/images/ImageExtractColorCard.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { Image, ImageExtractColor, ImageVariant } from "@/generated/prisma"; +import ImageComponent from "next/image"; +import Link from "next/link"; + +type ImagePaletteWithItems = { + image: ImageExtractColor & { + image: Image & { + variants: ImageVariant[]; + } + }; +}; + +export default function ImageExtractColorCard({ image }: ImagePaletteWithItems) { + const thumbnail = image.image.variants.find((v) => v.type === "thumbnail"); + + return ( + + {thumbnail?.s3Key ? ( + + ) : ( +
+ No Thumbnail +
+ )} +
{image.image.imageName} ({image.type})
+ + ); +} diff --git a/src/components/images/edit/EditImageForm.tsx b/src/components/images/edit/EditImageForm.tsx index a160b41..7f1121c 100644 --- a/src/components/images/edit/EditImageForm.tsx +++ b/src/components/images/edit/EditImageForm.tsx @@ -3,11 +3,12 @@ import { updateImage } from "@/actions/images/updateImage"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; -import { Form, FormControl, 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 MultipleSelector from "@/components/ui/multiselect"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { Album, Artist, Category, Color, ColorPalette, ColorPaletteItem, ExtractColor, Image, ImageColor, ImageExtractColor, ImageMetadata, ImagePalette, ImageStats, ImageVariant, Tag } from "@/generated/prisma"; import { cn } from "@/lib/utils"; @@ -68,6 +69,7 @@ export default function EditImageForm({ image, albums, artists, categories, tags imageName: image.imageName, originalFile: image.originalFile, uploadDate: image.uploadDate, + nsfw: image.nsfw ?? false, altText: image.altText || "", description: image.description || "", @@ -82,7 +84,7 @@ export default function EditImageForm({ image, albums, artists, categories, tags albumId: image.album?.id || undefined, tagIds: image.tags?.map(tag => tag.id) ?? [], categoryIds: image.categories?.map(cat => cat.id) ?? [], - }, + } }) // const watchCreationDate = form.watch("creationDate"); @@ -167,6 +169,22 @@ export default function EditImageForm({ image, albums, artists, categories, tags )} /> + ( + +
+ NSFW + This image contains sensitive or adult content. +
+ + + +
+ )} + /> + +
{images.map((image) => ( - - - - {image.imageName} - - - - - - +
+ + + + {image.imageName} + + + + + + +
))}
); diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx new file mode 100644 index 0000000..6a2b524 --- /dev/null +++ b/src/components/ui/switch.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as SwitchPrimitive from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +function Switch({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { Switch } diff --git a/src/schemas/images/imageSchema.ts b/src/schemas/images/imageSchema.ts index 14d4646..07d62d0 100644 --- a/src/schemas/images/imageSchema.ts +++ b/src/schemas/images/imageSchema.ts @@ -14,6 +14,7 @@ export const imageSchema = z.object({ imageName: z.string().min(1, "Image name is required"), originalFile: z.string().min(1, "Original file is required"), uploadDate: z.date(), + nsfw: z.boolean(), altText: z.string().optional(), description: z.string().optional(),