Refactor and add NSFW boolean
This commit is contained in:
		
							
								
								
									
										30
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										30
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -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",
 | 
			
		||||
 | 
			
		||||
@ -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",
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,2 @@
 | 
			
		||||
-- AlterTable
 | 
			
		||||
ALTER TABLE "Image" ADD COLUMN     "nsfw" BOOLEAN NOT NULL DEFAULT false;
 | 
			
		||||
@ -112,6 +112,7 @@ model Image {
 | 
			
		||||
  imageName    String
 | 
			
		||||
  originalFile String
 | 
			
		||||
  uploadDate   DateTime @default(now())
 | 
			
		||||
  nsfw         Boolean  @default(false)
 | 
			
		||||
 | 
			
		||||
  altText       String?
 | 
			
		||||
  description   String?
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										30
									
								
								src/app/colors/[id]/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/app/colors/[id]/page.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <div>
 | 
			
		||||
      <h1 className="text-2xl font-bold mb-4">Show color</h1>
 | 
			
		||||
      {colorData ? <DisplayColor color={colorData} /> : 'Color not found...'}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								src/app/colors/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/app/colors/page.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className="flex gap-4 justify-between">
 | 
			
		||||
        <h1 className="text-2xl font-bold mb-4">Colors</h1>
 | 
			
		||||
      </div>
 | 
			
		||||
      {colors.length > 0 ? <ListColors colors={colors} /> : <p className="text-muted-foreground italic">No colors found.</p>}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								src/app/extract/[id]/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/app/extract/[id]/page.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <div>
 | 
			
		||||
      <h1 className="text-2xl font-bold mb-4">Show color</h1>
 | 
			
		||||
      {colorData ? <DisplayExtractColor color={colorData} /> : 'Color not found...'}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								src/app/extract/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/app/extract/page.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className="flex gap-4 justify-between">
 | 
			
		||||
        <h1 className="text-2xl font-bold mb-4">Extract colors</h1>
 | 
			
		||||
      </div>
 | 
			
		||||
      {extractColors.length > 0 ? <ListExtractColors colors={extractColors} /> : <p className="text-muted-foreground italic">No colors found.</p>}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -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 (
 | 
			
		||||
    <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">Images</h1>
 | 
			
		||||
        <Link href="/images/upload" 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" /> Upload new image
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										35
									
								
								src/components/colors/list/ListColors.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/components/colors/list/ListColors.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
 | 
			
		||||
      {colors.map((col) => (
 | 
			
		||||
        <Link href={`/colors/${col.id}`} key={col.id}>
 | 
			
		||||
          <Card className="overflow-hidden">
 | 
			
		||||
            <CardHeader>
 | 
			
		||||
              <CardTitle className="text-base truncate">{col.name}</CardTitle>
 | 
			
		||||
            </CardHeader>
 | 
			
		||||
            <CardContent>
 | 
			
		||||
              <div className="flex flex-wrap gap-2">
 | 
			
		||||
                <div
 | 
			
		||||
                  className="w-10 h-10 rounded-full border"
 | 
			
		||||
                  style={{ backgroundColor: col.hex ?? "#ccc" }}
 | 
			
		||||
                  title={col.hex ?? "n/a"}
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
            </CardContent>
 | 
			
		||||
            <CardFooter>
 | 
			
		||||
              Used by {col.images.length} image{col.images.length !== 1 ? "s" : ""}
 | 
			
		||||
            </CardFooter>
 | 
			
		||||
          </Card>
 | 
			
		||||
        </Link>
 | 
			
		||||
      ))}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								src/components/colors/single/DisplayColor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/components/colors/single/DisplayColor.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <div className="space-y-6">
 | 
			
		||||
      <div>
 | 
			
		||||
        <h1 className="text-2xl font-bold">{color.name}</h1>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className="flex flex-wrap gap-2">
 | 
			
		||||
        <div
 | 
			
		||||
          key={color.id}
 | 
			
		||||
          className="w-10 h-10 rounded-full border"
 | 
			
		||||
          style={{ backgroundColor: color.hex ?? "#ccc" }}
 | 
			
		||||
          title={color.hex ?? "n/a"}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div>
 | 
			
		||||
        <h2 className="text-xl font-semibold">Used by {color.images.length} image(s)</h2>
 | 
			
		||||
        <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-2">
 | 
			
		||||
          {color.images.map((image) => (
 | 
			
		||||
            <ImageColorCard key={image.id} image={image} />
 | 
			
		||||
          ))}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								src/components/extract/list/ListExtractColors.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/components/extract/list/ListExtractColors.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
 | 
			
		||||
      {colors.map((col) => (
 | 
			
		||||
        <Link href={`/extract/${col.id}`} key={col.id}>
 | 
			
		||||
          <Card className="overflow-hidden">
 | 
			
		||||
            <CardHeader>
 | 
			
		||||
              <CardTitle className="text-base truncate">{col.name}</CardTitle>
 | 
			
		||||
            </CardHeader>
 | 
			
		||||
            <CardContent>
 | 
			
		||||
              <div className="flex flex-wrap gap-2">
 | 
			
		||||
                <div
 | 
			
		||||
                  className="w-10 h-10 rounded-full border"
 | 
			
		||||
                  style={{ backgroundColor: col.hex ?? "#ccc" }}
 | 
			
		||||
                  title={col.hex ?? "n/a"}
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
            </CardContent>
 | 
			
		||||
            <CardFooter>
 | 
			
		||||
              Used by {col.images.length} image{col.images.length !== 1 ? "s" : ""}
 | 
			
		||||
            </CardFooter>
 | 
			
		||||
          </Card>
 | 
			
		||||
        </Link>
 | 
			
		||||
      ))}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								src/components/extract/single/DisplayExtractColor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/components/extract/single/DisplayExtractColor.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <div className="space-y-6">
 | 
			
		||||
      <div>
 | 
			
		||||
        <h1 className="text-2xl font-bold">{color.name}</h1>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className="flex flex-wrap gap-2">
 | 
			
		||||
        <div
 | 
			
		||||
          key={color.id}
 | 
			
		||||
          className="w-10 h-10 rounded-full border"
 | 
			
		||||
          style={{ backgroundColor: color.hex ?? "#ccc" }}
 | 
			
		||||
          title={color.hex ?? "n/a"}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div>
 | 
			
		||||
        <h2 className="text-xl font-semibold">Used by {color.images.length} image(s)</h2>
 | 
			
		||||
        <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-2">
 | 
			
		||||
          {color.images.map((image) => (
 | 
			
		||||
            <ImageExtractColorCard key={image.id} image={image} />
 | 
			
		||||
          ))}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -47,6 +47,11 @@ export default function TopNav() {
 | 
			
		||||
            <Link href="/colors">Colors</Link>
 | 
			
		||||
          </NavigationMenuLink>
 | 
			
		||||
        </NavigationMenuItem>
 | 
			
		||||
        <NavigationMenuItem>
 | 
			
		||||
          <NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
 | 
			
		||||
            <Link href="/extract">Extract-Colors</Link>
 | 
			
		||||
          </NavigationMenuLink>
 | 
			
		||||
        </NavigationMenuItem>
 | 
			
		||||
        <NavigationMenuItem>
 | 
			
		||||
          <NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
 | 
			
		||||
            <Link href="/images">Images</Link>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										39
									
								
								src/components/images/ImageColorCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/components/images/ImageColorCard.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <Link
 | 
			
		||||
      href={`/images/${image.image.id}`}
 | 
			
		||||
      className="block overflow-hidden rounded-md border shadow-sm hover:shadow-md transition-shadow"
 | 
			
		||||
    >
 | 
			
		||||
      {thumbnail?.s3Key ? (
 | 
			
		||||
        <ImageComponent
 | 
			
		||||
          src={`/api/image/${thumbnail.s3Key}`}
 | 
			
		||||
          alt={image.image.altText || image.image.imageName}
 | 
			
		||||
          width={thumbnail.width}
 | 
			
		||||
          height={thumbnail.height}
 | 
			
		||||
          className="w-full h-auto object-cover"
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <div className="w-full h-48 bg-gray-100 flex items-center justify-center text-muted-foreground text-sm">
 | 
			
		||||
          No Thumbnail
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      <div className="p-2 text-sm truncate">{image.image.imageName} ({image.type})</div>
 | 
			
		||||
    </Link>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								src/components/images/ImageExtractColorCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/components/images/ImageExtractColorCard.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
			
		||||
    <Link
 | 
			
		||||
      href={`/images/${image.image.id}`}
 | 
			
		||||
      className="block overflow-hidden rounded-md border shadow-sm hover:shadow-md transition-shadow"
 | 
			
		||||
    >
 | 
			
		||||
      {thumbnail?.s3Key ? (
 | 
			
		||||
        <ImageComponent
 | 
			
		||||
          src={`/api/image/${thumbnail.s3Key}`}
 | 
			
		||||
          alt={image.image.altText || image.image.imageName}
 | 
			
		||||
          width={thumbnail.width}
 | 
			
		||||
          height={thumbnail.height}
 | 
			
		||||
          className="w-full h-auto object-cover"
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <div className="w-full h-48 bg-gray-100 flex items-center justify-center text-muted-foreground text-sm">
 | 
			
		||||
          No Thumbnail
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      <div className="p-2 text-sm truncate">{image.image.imageName} ({image.type})</div>
 | 
			
		||||
    </Link>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
            )}
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <FormField
 | 
			
		||||
            control={form.control}
 | 
			
		||||
            name="nsfw"
 | 
			
		||||
            render={({ field }) => (
 | 
			
		||||
              <FormItem className="flex items-center justify-between rounded-lg border p-4">
 | 
			
		||||
                <div className="space-y-0.5">
 | 
			
		||||
                  <FormLabel>NSFW</FormLabel>
 | 
			
		||||
                  <FormDescription>This image contains sensitive or adult content.</FormDescription>
 | 
			
		||||
                </div>
 | 
			
		||||
                <FormControl>
 | 
			
		||||
                  <Switch checked={field.value} onCheckedChange={field.onChange} />
 | 
			
		||||
                </FormControl>
 | 
			
		||||
              </FormItem>
 | 
			
		||||
            )}
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <FormField
 | 
			
		||||
            control={form.control}
 | 
			
		||||
            name="altText"
 | 
			
		||||
 | 
			
		||||
@ -7,18 +7,20 @@ import Link from "next/link";
 | 
			
		||||
 | 
			
		||||
export default function ListImages({ images }: { images: Image[] }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
 | 
			
		||||
    <div className="columns-1 sm:columns-2 md:columns-3 xl:columns-4 gap-4 space-y-4">
 | 
			
		||||
      {images.map((image) => (
 | 
			
		||||
        <Link href={`/images/edit/${image.id}`} key={image.id}>
 | 
			
		||||
          <Card className="overflow-hidden">
 | 
			
		||||
            <CardHeader>
 | 
			
		||||
              <CardTitle className="text-base truncate">{image.imageName}</CardTitle>
 | 
			
		||||
            </CardHeader>
 | 
			
		||||
            <CardContent>
 | 
			
		||||
              <NetImage src={`/api/image/thumbnails/${image.fileKey}.webp`} alt={image.altText ? image.altText : "Image"} width={200} height={200} />
 | 
			
		||||
            </CardContent>
 | 
			
		||||
          </Card>
 | 
			
		||||
        </Link>
 | 
			
		||||
        <div key={image.id} className="break-inside-avoid">
 | 
			
		||||
          <Link href={`/images/edit/${image.id}`} key={image.id}>
 | 
			
		||||
            <Card className="overflow-hidden">
 | 
			
		||||
              <CardHeader>
 | 
			
		||||
                <CardTitle className="text-base truncate">{image.imageName}</CardTitle>
 | 
			
		||||
              </CardHeader>
 | 
			
		||||
              <CardContent className="flex justify-center items-center">
 | 
			
		||||
                <NetImage src={`/api/image/thumbnails/${image.fileKey}.webp`} alt={image.altText ? image.altText : "Image"} width={200} height={200} className="rounded max-w-full h-auto object-contain" />
 | 
			
		||||
              </CardContent>
 | 
			
		||||
            </Card>
 | 
			
		||||
          </Link>
 | 
			
		||||
        </div>
 | 
			
		||||
      ))}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										31
									
								
								src/components/ui/switch.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/components/ui/switch.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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<typeof SwitchPrimitive.Root>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <SwitchPrimitive.Root
 | 
			
		||||
      data-slot="switch"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <SwitchPrimitive.Thumb
 | 
			
		||||
        data-slot="switch-thumb"
 | 
			
		||||
        className={cn(
 | 
			
		||||
          "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
 | 
			
		||||
        )}
 | 
			
		||||
      />
 | 
			
		||||
    </SwitchPrimitive.Root>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Switch }
 | 
			
		||||
@ -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(),
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user