From ac3b19a1f27f7df533a51ece5f1fbf6dc59706e2 Mon Sep 17 00:00:00 2001 From: Citali Date: Sun, 29 Jun 2025 01:56:19 +0200 Subject: [PATCH] Add better image styles --- prisma/schema.prisma | 2 + src/app/(galleries)/layout.tsx | 31 ----- .../[albumSlug]/[imageId]/page.tsx | 110 ++++++++++++++++++ .../[gallerySlug]/[albumSlug]/page.tsx | 0 src/app/{ => (normal)}/[gallerySlug]/page.tsx | 0 src/app/(normal)/categories/[id]/page.tsx | 57 +++++++++ src/app/{ => (normal)}/layout.tsx | 4 +- src/app/{ => (normal)}/page.tsx | 0 src/app/(raw)/layout.tsx | 42 +++++++ src/app/(raw)/raw/[imageId]/page.tsx | 51 ++++++++ .../[albumSlug]/[imageId]/page.tsx | 60 ---------- src/components/albums/ImageList.tsx | 6 +- .../categories/CategoryImageList.tsx | 48 ++++++++ src/components/images/GlowingImageBorder.tsx | 21 ++-- .../images/GlowingImageWithToggle.tsx | 16 ++- src/components/raw/RawCloseButton.tsx | 34 ++++++ 16 files changed, 378 insertions(+), 104 deletions(-) delete mode 100644 src/app/(galleries)/layout.tsx create mode 100644 src/app/(normal)/[gallerySlug]/[albumSlug]/[imageId]/page.tsx rename src/app/{ => (normal)}/[gallerySlug]/[albumSlug]/page.tsx (100%) rename src/app/{ => (normal)}/[gallerySlug]/page.tsx (100%) create mode 100644 src/app/(normal)/categories/[id]/page.tsx rename src/app/{ => (normal)}/layout.tsx (95%) rename src/app/{ => (normal)}/page.tsx (100%) create mode 100644 src/app/(raw)/layout.tsx create mode 100644 src/app/(raw)/raw/[imageId]/page.tsx delete mode 100644 src/app/[gallerySlug]/[albumSlug]/[imageId]/page.tsx create mode 100644 src/components/categories/CategoryImageList.tsx create mode 100644 src/components/raw/RawCloseButton.tsx diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 25c66d6..ab6a8a1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -201,6 +201,8 @@ model ImageVariant { sizeBytes Int? image Image @relation(fields: [imageId], references: [id]) + + @@unique([imageId, type]) } model ColorPalette { diff --git a/src/app/(galleries)/layout.tsx b/src/app/(galleries)/layout.tsx deleted file mode 100644 index 5312294..0000000 --- a/src/app/(galleries)/layout.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Breadcrumbs } from "@/components/global/Breadcrumbs"; -import { generateBreadcrumbsFromPath } from "@/utils/generateBreadcrumbs"; -import { notFound } from "next/navigation"; -import { ReactNode } from "react"; - -export default async function SubLayout({ - children -}: { - children: ReactNode -}) { - const segments = getSegmentsFromUrl(); - - const breadcrumbs = await generateBreadcrumbsFromPath(segments); - if (!breadcrumbs) return notFound(); - - return ( -
- - {children} -
- ); -} - -function getSegmentsFromUrl(): string[] { - const path = decodeURIComponent( - // remove leading slash - (typeof window !== "undefined" ? window.location.pathname : "") - .replace(/^\//, "") - ); - return path.split("/").filter(Boolean); -} \ No newline at end of file diff --git a/src/app/(normal)/[gallerySlug]/[albumSlug]/[imageId]/page.tsx b/src/app/(normal)/[gallerySlug]/[albumSlug]/[imageId]/page.tsx new file mode 100644 index 0000000..30900a9 --- /dev/null +++ b/src/app/(normal)/[gallerySlug]/[albumSlug]/[imageId]/page.tsx @@ -0,0 +1,110 @@ +import ArtistInfoBox from "@/components/images/ArtistInfoBox"; +import GlowingImageWithToggle from "@/components/images/GlowingImageWithToggle"; +import ImageMetadataBox from "@/components/images/ImageMetadataBox"; +import prisma from "@/lib/prisma"; +import Link from "next/link"; + +export default async function ImagePage({ params }: { params: { gallerySlug: string, albumSlug: string, imageId: string } }) { + const { imageId } = await params; + + const image = await prisma.image.findUnique({ + where: { + id: imageId + }, + include: { + artist: { include: { socials: true } }, + colors: { include: { color: true } }, + extractColors: { include: { extract: true } }, + palettes: { include: { palette: { include: { items: true } } } }, + album: true, + categories: true, + tags: true, + variants: true, + metadata: true, + stats: true + } + }) + + if (!image) return
Image not found
+ + const resizedVariant = image.variants.find(v => v.type === "resized"); + + return ( +
+ {image && ( +
+
+

{image.imageName}

+
+ + {resizedVariant && + + + + } +
+ {image.artist && } + +
+

Image Metadata

+ +

Name: {image.imageName}

+ {image.altText &&

Alt Text: {image.altText}

} + {image.description &&

Description: {image.description}

} + {/*

NSFW: {image.nsfw ? "Yes" : "No"}

+

Upload Date: {new Date(image.uploadDate).toLocaleDateString()}

+ {image.source &&

Source: {image.source}

} + {image.fileType &&

File Type: {image.fileType}

} + {image.fileSize &&

Size: {(image.fileSize / 1024).toFixed(1)} KB

} + {image.creationDate && ( +

Created: {new Date(image.creationDate).toLocaleDateString()}

+ )} + {image.creationYear && image.creationMonth && ( +

Creation: {image.creationMonth}/{image.creationYear}

+ )} */} + + {/* {image.metadata && ( + <> +

Metadata

+

Width: {image.metadata.width}px

+

Height: {image.metadata.height}px

+

Format: {image.metadata.format}

+

Color Space: {image.metadata.space}

+

Channels: {image.metadata.channels}

+ {image.metadata.bitsPerSample && ( +

Bits Per Sample: {image.metadata.bitsPerSample}

+ )} + {image.metadata.hasAlpha !== null && ( +

Has Alpha: {image.metadata.hasAlpha ? "Yes" : "No"}

+ )} + + )} */} + + {/* {image.stats && ( + <> +

Statistics

+

Entropy: {image.stats.entropy.toFixed(2)}

+

Sharpness: {image.stats.sharpness.toFixed(2)}

+

Dominant Color: rgb({image.stats.dominantR}, {image.stats.dominantG}, {image.stats.dominantB})

+

Is Opaque: {image.stats.isOpaque ? "Yes" : "No"}

+ + )} */} +
+ + +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/app/[gallerySlug]/[albumSlug]/page.tsx b/src/app/(normal)/[gallerySlug]/[albumSlug]/page.tsx similarity index 100% rename from src/app/[gallerySlug]/[albumSlug]/page.tsx rename to src/app/(normal)/[gallerySlug]/[albumSlug]/page.tsx diff --git a/src/app/[gallerySlug]/page.tsx b/src/app/(normal)/[gallerySlug]/page.tsx similarity index 100% rename from src/app/[gallerySlug]/page.tsx rename to src/app/(normal)/[gallerySlug]/page.tsx diff --git a/src/app/(normal)/categories/[id]/page.tsx b/src/app/(normal)/categories/[id]/page.tsx new file mode 100644 index 0000000..0285b6d --- /dev/null +++ b/src/app/(normal)/categories/[id]/page.tsx @@ -0,0 +1,57 @@ +import CategoryImageList from "@/components/categories/CategoryImageList"; +import prisma from "@/lib/prisma"; + +export default async function CategoriesSinglePage({ params }: { params: { id: string } }) { + const { id } = await params; + + const category = await prisma.category.findUnique({ + where: { + id, + }, + include: { + images: { + select: { + id: true, + imageName: true, + fileKey: true, + nsfw: true, + altText: true, + album: { + select: { + slug: true, + gallery: { + select: { + slug: true, + }, + }, + } + } + } + } + } + }); + + if (!category) { + return ( +
+

Category not found

+
+ ) + } + + return ( +
+ {category && ( + <> +
+

Category: {category.name}

+

+ {category.description} +

+
+ + + )} +
+ ); +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/(normal)/layout.tsx similarity index 95% rename from src/app/layout.tsx rename to src/app/(normal)/layout.tsx index 7650087..41dd554 100644 --- a/src/app/layout.tsx +++ b/src/app/(normal)/layout.tsx @@ -4,7 +4,7 @@ import { ThemeProvider } from "@/components/global/ThemeProvider"; import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import { Toaster } from "sonner"; -import "./globals.css"; +import "../globals.css"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -21,7 +21,7 @@ export const metadata: Metadata = { description: "Generated by create next app", }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; diff --git a/src/app/page.tsx b/src/app/(normal)/page.tsx similarity index 100% rename from src/app/page.tsx rename to src/app/(normal)/page.tsx diff --git a/src/app/(raw)/layout.tsx b/src/app/(raw)/layout.tsx new file mode 100644 index 0000000..ec01cc0 --- /dev/null +++ b/src/app/(raw)/layout.tsx @@ -0,0 +1,42 @@ +import { ThemeProvider } from "@/components/global/ThemeProvider"; +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "../globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default async function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + + ); +} diff --git a/src/app/(raw)/raw/[imageId]/page.tsx b/src/app/(raw)/raw/[imageId]/page.tsx new file mode 100644 index 0000000..d6f7735 --- /dev/null +++ b/src/app/(raw)/raw/[imageId]/page.tsx @@ -0,0 +1,51 @@ +// app/raw/[id]/page.tsx +import RawCloseButton from "@/components/raw/RawCloseButton"; +import prisma from "@/lib/prisma"; +import NextImage from "next/image"; +import { notFound } from "next/navigation"; + +export default async function RawImagePage({ params }: { params: { imageId: string } }) { + const { imageId } = params; + + const image = await prisma.image.findUnique({ + where: { id: imageId }, + include: { + album: { include: { gallery: true } }, + variants: true, + palettes: { + include: { palette: { include: { items: true } } }, + }, + }, + }); + + if (!image) return notFound(); + + const variant = image.variants.find(v => v.type === "watermarked"); + if (!variant) return notFound(); + + const palette = image.palettes.find(p => p.type === "primary")?.palette; + const hexColors = palette?.items.map(item => item.hex).filter(Boolean) as string[]; + + const backgroundStyle = + hexColors && hexColors.length > 1 + ? { backgroundImage: `linear-gradient(to bottom, ${hexColors.join(", ")})` } + : { backgroundColor: "#0f0f0f" }; + + const targetHref = `/${image.album?.gallery?.slug}/${image.album?.slug}/${image.id}`; + + return ( +
+ + +
+ ); +} diff --git a/src/app/[gallerySlug]/[albumSlug]/[imageId]/page.tsx b/src/app/[gallerySlug]/[albumSlug]/[imageId]/page.tsx deleted file mode 100644 index 21221b0..0000000 --- a/src/app/[gallerySlug]/[albumSlug]/[imageId]/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import ArtistInfoBox from "@/components/images/ArtistInfoBox"; -import GlowingImageWithToggle from "@/components/images/GlowingImageWithToggle"; -import ImageMetadataBox from "@/components/images/ImageMetadataBox"; -import prisma from "@/lib/prisma"; - -export default async function ImagePage({ params }: { params: { gallerySlug: string, albumSlug: string, imageId: string } }) { - const { imageId } = await params; - - const image = await prisma.image.findUnique({ - where: { - id: imageId - }, - include: { - artist: { include: { socials: true } }, - colors: { include: { color: true } }, - extractColors: { include: { extract: true } }, - palettes: { include: { palette: { include: { items: true } } } }, - album: true, - categories: true, - tags: true, - variants: true, - metadata: true, - stats: true - } - }) - - if (!image) return
Image not found
- - const resizedVariant = image.variants.find(v => v.type === "resized"); - - return ( -
- {image && ( -
-
-

{image.imageName}

-
- - {resizedVariant && - - } -
- {image.artist && } - - -
-
- )} -
- ); -} \ No newline at end of file diff --git a/src/components/albums/ImageList.tsx b/src/components/albums/ImageList.tsx index 0df8b8e..48769fc 100644 --- a/src/components/albums/ImageList.tsx +++ b/src/components/albums/ImageList.tsx @@ -1,4 +1,5 @@ import { Image } from "@/generated/prisma"; +import clsx from "clsx"; import NextImage from "next/image"; import Link from "next/link"; @@ -17,7 +18,10 @@ export default function ImageList({ images, gallerySlug, albumSlug }: { images: src={`/api/image/thumbnails/${img.fileKey}.webp`} alt={img.imageName} fill - className="object-cover" + className={clsx( + " object-cover", + img.nsfw && "blur-md scale-105" + )} /> ) : (
diff --git a/src/components/categories/CategoryImageList.tsx b/src/components/categories/CategoryImageList.tsx new file mode 100644 index 0000000..46f5e23 --- /dev/null +++ b/src/components/categories/CategoryImageList.tsx @@ -0,0 +1,48 @@ +import { Album, Gallery, Image } from "@/generated/prisma"; +import NextImage from "next/image"; +import Link from "next/link"; + +type ImagesWithItems = (Pick & { + album?: (Pick & { + gallery?: Pick | null + }) | null +})[] + +export default function CategoryImageList({ images }: { images: ImagesWithItems }) { + return ( +
+

Images

+
+ {images ? images.map((img) => { + const gallerySlug = img.album?.gallery?.slug; + const albumSlug = img.album?.slug; + if (!gallerySlug || !albumSlug || !img.id) return null; + + return ( + +
+
+ {img.fileKey ? ( + + ) : ( +
+ No cover image +
+ )} +
+
+

{img.imageName}

+
+
+ + ) + }) : "There are no images here!"} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/images/GlowingImageBorder.tsx b/src/components/images/GlowingImageBorder.tsx index bca0bc7..dddc173 100644 --- a/src/components/images/GlowingImageBorder.tsx +++ b/src/components/images/GlowingImageBorder.tsx @@ -14,8 +14,9 @@ type Props = { alt: string, variant: ImageVariant, colors: Colors[], - src: string - className?: string + src: string, + revealed?: boolean, + className?: string, animate?: boolean } @@ -25,6 +26,7 @@ export default function GlowingImageBorder({ colors, src, className, + revealed = true, animate = true, }: Props) { const { resolvedTheme } = useTheme() @@ -37,11 +39,11 @@ export default function GlowingImageBorder({ const getColor = (type: string) => colors.find((c) => c.type === type)?.color.hex - const vibrantLight = getColor("vibrant") || "#ff5ec4" - const mutedLight = getColor("muted") || "#5ecaff" + const vibrantLight = getColor("Vibrant") || "#ff5ec4" + const mutedLight = getColor("Muted") || "#5ecaff" - const darkVibrant = getColor("darkVibrant") || "#fc03a1" - const darkMuted = getColor("darkMuted") || "#035efc" + const darkVibrant = getColor("DarkVibrant") || "#fc03a1" + const darkMuted = getColor("DarkMuted") || "#035efc" const vibrant = resolvedTheme === "dark" ? darkVibrant : vibrantLight const muted = resolvedTheme === "dark" ? darkMuted : mutedLight @@ -62,13 +64,16 @@ export default function GlowingImageBorder({ } as React.CSSProperties } > -
+
diff --git a/src/components/images/GlowingImageWithToggle.tsx b/src/components/images/GlowingImageWithToggle.tsx index 094e5db..b2016d4 100644 --- a/src/components/images/GlowingImageWithToggle.tsx +++ b/src/components/images/GlowingImageWithToggle.tsx @@ -11,10 +11,12 @@ type Props = { colors: (ImageColor & { color: Color })[]; alt: string; src: string; -}; + nsfw: boolean; +} -export default function GlowingImageWithToggle({ variant, colors, alt, src }: Props) { +export default function GlowingImageWithToggle({ variant, colors, alt, src, nsfw }: Props) { const [animate, setAnimate] = useState(true); + const [revealed, setRevealed] = useState(!nsfw) return (
@@ -24,6 +26,7 @@ export default function GlowingImageWithToggle({ variant, colors, alt, src }: Pr colors={colors} src={src} animate={animate} + revealed={revealed} />
@@ -32,6 +35,15 @@ export default function GlowingImageWithToggle({ variant, colors, alt, src }: Pr
+ + {nsfw && ( +
+
+ + +
+
+ )}
); } diff --git a/src/components/raw/RawCloseButton.tsx b/src/components/raw/RawCloseButton.tsx new file mode 100644 index 0000000..d6c147d --- /dev/null +++ b/src/components/raw/RawCloseButton.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { X } from "lucide-react"; // react-lucide close icon +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; + +type RawCloseButtonProps = { + targetHref: string; +}; + +export default function RawCloseButton({ targetHref }: RawCloseButtonProps) { + const router = useRouter(); + + // ESC key listener + useEffect(() => { + const handleEsc = (e: KeyboardEvent) => { + if (e.key === "Escape") { + router.push(targetHref); + } + }; + window.addEventListener("keydown", handleEsc); + return () => window.removeEventListener("keydown", handleEsc); + }, [targetHref, router]); + + return ( + + ); +}