From 51c85b93de39efea1f701e01bc17fd1ff1ffdd5f Mon Sep 17 00:00:00 2001 From: Citali Date: Tue, 1 Jul 2025 22:19:48 +0200 Subject: [PATCH] Refactr a lot of things --- package-lock.json | 32 ++++++- package.json | 3 +- .../[gallerySlug]/[albumSlug]/page.tsx | 6 +- .../(normal)/galleries/[gallerySlug]/page.tsx | 6 +- src/app/(normal)/page.tsx | 2 +- src/components/cards/AlbumCard.tsx | 47 ++++++++++ src/components/cards/CoverCard.tsx | 93 +++++++++++++++++++ src/components/cards/GlowingBorderWrapper.tsx | 42 +++++++++ src/components/cards/ImageCard.tsx | 67 +++++++++++++ src/components/cards/TagBadge.tsx | 32 +++++++ src/components/global/AnimateToggle.tsx | 20 ++++ src/components/global/Header.tsx | 8 +- src/components/global/NSFWToggle.tsx | 20 ++++ src/components/home/GalleryList.tsx | 42 --------- src/components/lists/AlbumList.tsx | 38 ++++++++ src/components/lists/GalleryList.tsx | 37 ++++++++ src/components/lists/ImageList.tsx | 16 ++++ src/components/ui/badge.tsx | 46 +++++++++ src/hooks/useGlobalSettings.ts | 15 +++ 19 files changed, 520 insertions(+), 52 deletions(-) create mode 100644 src/components/cards/AlbumCard.tsx create mode 100644 src/components/cards/CoverCard.tsx create mode 100644 src/components/cards/GlowingBorderWrapper.tsx create mode 100644 src/components/cards/ImageCard.tsx create mode 100644 src/components/cards/TagBadge.tsx create mode 100644 src/components/global/AnimateToggle.tsx create mode 100644 src/components/global/NSFWToggle.tsx delete mode 100644 src/components/home/GalleryList.tsx create mode 100644 src/components/lists/AlbumList.tsx create mode 100644 src/components/lists/GalleryList.tsx create mode 100644 src/components/lists/ImageList.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/hooks/useGlobalSettings.ts diff --git a/package-lock.json b/package-lock.json index d3351f8..f4bae82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,8 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "sonner": "^2.0.5", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.3.1", + "zustand": "^5.0.6" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -8988,6 +8989,35 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.6.tgz", + "integrity": "sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 7df721b..65f81eb 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "sonner": "^2.0.5", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.3.1", + "zustand": "^5.0.6" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/src/app/(normal)/galleries/[gallerySlug]/[albumSlug]/page.tsx b/src/app/(normal)/galleries/[gallerySlug]/[albumSlug]/page.tsx index 2398601..49c7eff 100644 --- a/src/app/(normal)/galleries/[gallerySlug]/[albumSlug]/page.tsx +++ b/src/app/(normal)/galleries/[gallerySlug]/[albumSlug]/page.tsx @@ -1,4 +1,4 @@ -import ImageList from "@/components/albums/ImageList"; +import ImageList from "@/components/lists/ImageList"; import prisma from "@/lib/prisma"; export default async function GalleryPage({ params }: { params: { gallerySlug: string, albumSlug: string } }) { @@ -21,7 +21,7 @@ export default async function GalleryPage({ params }: { params: { gallerySlug: s }, }, include: { - images: true + images: { include: { colors: { include: { color: true } } } }, } }) @@ -30,7 +30,7 @@ export default async function GalleryPage({ params }: { params: { gallerySlug: s {album && ( <>
-

{album.name}

+

Album: {album.name}

{album.description}

diff --git a/src/app/(normal)/galleries/[gallerySlug]/page.tsx b/src/app/(normal)/galleries/[gallerySlug]/page.tsx index 53f87ac..374ad1d 100644 --- a/src/app/(normal)/galleries/[gallerySlug]/page.tsx +++ b/src/app/(normal)/galleries/[gallerySlug]/page.tsx @@ -1,4 +1,4 @@ -import AlbumList from "@/components/galleries/AlbumList"; +import AlbumList from "@/components/lists/AlbumList"; import prisma from "@/lib/prisma"; export default async function GalleryPage({ params }: { params: { gallerySlug: string } }) { @@ -9,7 +9,7 @@ export default async function GalleryPage({ params }: { params: { gallerySlug: s slug: gallerySlug }, include: { - albums: { where: { images: { some: {} } }, include: { coverImage: true } } + albums: { where: { images: { some: {} } }, include: { coverImage: { include: { colors: { include: { color: true } } } } } } } }) @@ -18,7 +18,7 @@ export default async function GalleryPage({ params }: { params: { gallerySlug: s {gallery && ( <>
-

{gallery.name}

+

Gallery: {gallery.name}

{gallery.description}

diff --git a/src/app/(normal)/page.tsx b/src/app/(normal)/page.tsx index 143bb24..b1eebca 100644 --- a/src/app/(normal)/page.tsx +++ b/src/app/(normal)/page.tsx @@ -1,4 +1,4 @@ -import GalleryList from "@/components/home/GalleryList"; +import GalleryList from "@/components/lists/GalleryList"; export default function HomePage() { return ( diff --git a/src/components/cards/AlbumCard.tsx b/src/components/cards/AlbumCard.tsx new file mode 100644 index 0000000..2adc6b1 --- /dev/null +++ b/src/components/cards/AlbumCard.tsx @@ -0,0 +1,47 @@ +import { Album, Image } from "@/generated/prisma"; +import NextImage from "next/image"; +import Link from "next/link"; + +type Props = { + album: Album & { + coverImage: Image | null; + }; + gallerySlug: string; +}; + +export default function AlbumCard({ album, gallerySlug }: Props) { + const href = `/galleries/${gallerySlug}/${album.slug}`; + const cover = album.coverImage; + + return ( + +
+ {cover ? ( + + ) : ( +
+ No cover image +
+ )} + + {/* Overlay: Album label */} +
+ Album +
+ + {/* Bottom: Album name */} +
+ {album.name} +
+
+ + ); +} diff --git a/src/components/cards/CoverCard.tsx b/src/components/cards/CoverCard.tsx new file mode 100644 index 0000000..3c1722b --- /dev/null +++ b/src/components/cards/CoverCard.tsx @@ -0,0 +1,93 @@ +"use client" + +import { type Color, type Image, type ImageColor } from "@/generated/prisma" +import { useGlobalSettings } from "@/hooks/useGlobalSettings" +import clsx from "clsx" +import { BookOpenIcon, EyeOffIcon, ImagePlusIcon } from "lucide-react" +import NextImage from "next/image" +import Link from "next/link" +import { GlowingBorderWrapper } from "./GlowingBorderWrapper" +import { TagBadge } from "./TagBadge" + +type CoverCardProps = { + item: { + id: string + name: string + slug: string + coverImage?: (Image & { + colors?: (ImageColor & { color: Color })[] + }) | null + type: "album" | "gallery" + gallerySlug?: string + } +} + +export default function CoverCard({ item }: CoverCardProps) { + const { showNSFW, animateGlow } = useGlobalSettings() + const { coverImage } = item + + const href = + item.type === "album" + ? `/galleries/${item.gallerySlug}/${item.slug}` + : `/galleries/${item.slug}` + + const badgeText = item.type === "album" ? "Album" : "Gallery" + const badgeIcon = + item.type === "album" ? : + + const borderColors = + coverImage?.colors?.map((c) => c.color?.hex).filter((hex): hex is string => Boolean(hex)) ?? [] + + const shouldBlur = coverImage?.nsfw && !showNSFW + + const content = ( +
+
+ {coverImage?.fileKey ? ( + + ) : ( +
+ No cover image +
+ )} +
+ + {coverImage?.nsfw && ( + } + className="text-xs px-2 py-0.5 inline-flex items-center" + /> + )} +
+
+
+

{item.name}

+
+
+ ) + + return ( + + {coverImage && animateGlow && borderColors.length > 0 ? ( + {content} + ) : ( + content + )} + + ) +} diff --git a/src/components/cards/GlowingBorderWrapper.tsx b/src/components/cards/GlowingBorderWrapper.tsx new file mode 100644 index 0000000..b28c081 --- /dev/null +++ b/src/components/cards/GlowingBorderWrapper.tsx @@ -0,0 +1,42 @@ +"use client" + +import { cn } from "@/lib/utils" +import { useTheme } from "next-themes" +import { ReactNode } from "react" + +interface GlowingBorderWrapperProps { + children: ReactNode + className?: string + animate?: boolean + colors?: string[] +} + +export function GlowingBorderWrapper({ + children, + className, + animate = true, + colors = [], +}: GlowingBorderWrapperProps) { + const { theme } = useTheme() + + const gradientColors = colors.length + ? colors.join(", ") + : theme === "dark" + ? "rgba(255,255,255,0.2), rgba(255,255,255,0.05)" + : "rgba(0,0,0,0.1), rgba(0,0,0,0.03)" + + return ( +
+
{children}
+
+ ) +} diff --git a/src/components/cards/ImageCard.tsx b/src/components/cards/ImageCard.tsx new file mode 100644 index 0000000..1eb595b --- /dev/null +++ b/src/components/cards/ImageCard.tsx @@ -0,0 +1,67 @@ +"use client" + +import { type Color, type Image, type ImageColor } from "@/generated/prisma" +import { useGlobalSettings } from "@/hooks/useGlobalSettings" +import clsx from "clsx" +import { EyeOffIcon } from "lucide-react" +import NextImage from "next/image" +import Link from "next/link" +import { GlowingBorderWrapper } from "./GlowingBorderWrapper" +import { TagBadge } from "./TagBadge" + +type ImageWithColors = Image & { + colors?: (ImageColor & { color: Color })[] +} + +type ImageCardProps = { + image: ImageWithColors, + gallerySlug?: string + albumSlug?: string +} + +export default function ImageCard({ image, gallerySlug, albumSlug }: ImageCardProps) { + const { showNSFW, animateGlow } = useGlobalSettings() + + const href = `/galleries/${gallerySlug}/${albumSlug}/${image.id}` + + const borderColors = + image.colors?.map((c) => c.color?.hex).filter((hex): hex is string => Boolean(hex)) ?? [] + + const shouldBlur = image.nsfw && !showNSFW + + const content = ( +
+
+ + {image.nsfw && ( +
+ } + className="text-xs px-2 py-0.5 inline-flex items-center" + /> +
+ )} +
+
+

{image.imageName}

+
+
+ ) + + return ( + + {animateGlow && borderColors.length > 0 ? ( + {content} + ) : ( + content + )} + + ) +} diff --git a/src/components/cards/TagBadge.tsx b/src/components/cards/TagBadge.tsx new file mode 100644 index 0000000..ee92658 --- /dev/null +++ b/src/components/cards/TagBadge.tsx @@ -0,0 +1,32 @@ +import { cn } from "@/lib/utils" +import { HTMLAttributes, ReactNode } from "react" +import { Badge } from "../ui/badge" + +interface TagBadgeProps extends HTMLAttributes { + label: string + icon?: ReactNode + variant?: "default" | "secondary" | "outline" | "destructive" | null + hidden?: boolean +} + +export function TagBadge({ + label, + icon, + variant = "default", + hidden = false, + className, + ...props +}: TagBadgeProps) { + if (hidden) return null + + return ( + + {icon} + {label} + + ) +} diff --git a/src/components/global/AnimateToggle.tsx b/src/components/global/AnimateToggle.tsx new file mode 100644 index 0000000..280f05e --- /dev/null +++ b/src/components/global/AnimateToggle.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { useGlobalSettings } from "@/hooks/useGlobalSettings"; +import { Sparkle, Sparkles } from "lucide-react"; + +export default function AnimateToggle() { + const { animateGlow, setAnimateGlow } = useGlobalSettings(); + + return ( + + ); +} diff --git a/src/components/global/Header.tsx b/src/components/global/Header.tsx index 4866398..74ffb1d 100644 --- a/src/components/global/Header.tsx +++ b/src/components/global/Header.tsx @@ -1,11 +1,17 @@ +import AnimateToggle from "./AnimateToggle"; import ModeToggle from "./ModeToggle"; +import NSFWToggle from "./NSFWToggle"; import TopNav from "./TopNav"; export default function Header() { return (
- +
+ + + +
); } \ No newline at end of file diff --git a/src/components/global/NSFWToggle.tsx b/src/components/global/NSFWToggle.tsx new file mode 100644 index 0000000..90b56f1 --- /dev/null +++ b/src/components/global/NSFWToggle.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { useGlobalSettings } from "@/hooks/useGlobalSettings"; +import { Eye, EyeOff } from "lucide-react"; + +export default function NSFWToggle() { + const { showNSFW, setShowNSFW } = useGlobalSettings(); + + return ( + + ); +} diff --git a/src/components/home/GalleryList.tsx b/src/components/home/GalleryList.tsx deleted file mode 100644 index d30c8cc..0000000 --- a/src/components/home/GalleryList.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import prisma from "@/lib/prisma"; -import Image from "next/image"; -import Link from "next/link"; - -export default async function GalleryList() { - const galleries = await prisma.gallery.findMany({ - include: { - coverImage: true - } - }); - - return ( -
-

Galleries

-
- {galleries ? galleries.map((gallery) => ( - -
-
- {gallery.coverImage?.fileKey ? ( - {gallery.coverImage.imageName} - ) : ( -
- No cover image -
- )} -
-
-

{gallery.name}

-
-
- - )) : "There are no galleries here!"} -
-
- ); -} \ No newline at end of file diff --git a/src/components/lists/AlbumList.tsx b/src/components/lists/AlbumList.tsx new file mode 100644 index 0000000..6613825 --- /dev/null +++ b/src/components/lists/AlbumList.tsx @@ -0,0 +1,38 @@ +import { Album, Color, Image, ImageColor } from "@/generated/prisma"; +import CoverCard from "../cards/CoverCard"; + +type AlbumsWithCover = (Album & { + coverImage: (Image & { + colors?: (ImageColor & { color: Color })[]; + }) | null; +})[]; + +export default function AlbumList({ + albums, + gallerySlug, +}: { + albums: AlbumsWithCover; + gallerySlug: string; +}) { + if (!albums.length) return

There are no albums here!

; + + return ( +
+
+ {albums.map((album) => ( + + ))} +
+
+ ); +} diff --git a/src/components/lists/GalleryList.tsx b/src/components/lists/GalleryList.tsx new file mode 100644 index 0000000..61c1716 --- /dev/null +++ b/src/components/lists/GalleryList.tsx @@ -0,0 +1,37 @@ +import prisma from "@/lib/prisma"; +import CoverCard from "../cards/CoverCard"; + +export default async function GalleryList() { + const galleries = await prisma.gallery.findMany({ + include: { + coverImage: { + include: { + colors: { + include: { color: true }, + }, + }, + }, + }, + }); + + if (!galleries.length) return

There are no galleries here!

; + + return ( +
+
+ {galleries.map((gallery) => ( + + ))} +
+
+ ); +} diff --git a/src/components/lists/ImageList.tsx b/src/components/lists/ImageList.tsx new file mode 100644 index 0000000..5eeb3ed --- /dev/null +++ b/src/components/lists/ImageList.tsx @@ -0,0 +1,16 @@ +import { Image } from "@/generated/prisma"; +import ImageCard from "../cards/ImageCard"; + +export default function ImageList({ images, gallerySlug, albumSlug }: { images: Image[], gallerySlug: string, albumSlug: string }) { + if (!images.length) return

There are no images here!

; + + return ( +
+
+ {images.map((image) => ( + + ))} +
+
+ ); +} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/src/hooks/useGlobalSettings.ts b/src/hooks/useGlobalSettings.ts new file mode 100644 index 0000000..204d0df --- /dev/null +++ b/src/hooks/useGlobalSettings.ts @@ -0,0 +1,15 @@ +import { create } from "zustand"; + +type GlobalSettings = { + showNSFW: boolean; + setShowNSFW: (val: boolean) => void; + animateGlow: boolean; + setAnimateGlow: (val: boolean) => void; +}; + +export const useGlobalSettings = create((set) => ({ + showNSFW: false, + setShowNSFW: (val) => set({ showNSFW: val }), + animateGlow: true, + setAnimateGlow: (val) => set({ animateGlow: val }), +}));