Refactr a lot of things
This commit is contained in:
32
package-lock.json
generated
32
package-lock.json
generated
@ -24,7 +24,8 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"sonner": "^2.0.5",
|
"sonner": "^2.0.5",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"zustand": "^5.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
@ -8988,6 +8989,35 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,8 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"sonner": "^2.0.5",
|
"sonner": "^2.0.5",
|
||||||
"tailwind-merge": "^3.3.1"
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"zustand": "^5.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import ImageList from "@/components/albums/ImageList";
|
import ImageList from "@/components/lists/ImageList";
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
export default async function GalleryPage({ params }: { params: { gallerySlug: string, albumSlug: string } }) {
|
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: {
|
include: {
|
||||||
images: true
|
images: { include: { colors: { include: { color: true } } } },
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ export default async function GalleryPage({ params }: { params: { gallerySlug: s
|
|||||||
{album && (
|
{album && (
|
||||||
<>
|
<>
|
||||||
<section className="text-center space-y-4">
|
<section className="text-center space-y-4">
|
||||||
<h1 className="text-4xl font-bold tracking-tight">{album.name}</h1>
|
<h1 className="text-4xl font-bold tracking-tight">Album: {album.name}</h1>
|
||||||
<p className="text-lg text-muted-foreground">
|
<p className="text-lg text-muted-foreground">
|
||||||
{album.description}
|
{album.description}
|
||||||
</p>
|
</p>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import AlbumList from "@/components/galleries/AlbumList";
|
import AlbumList from "@/components/lists/AlbumList";
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
export default async function GalleryPage({ params }: { params: { gallerySlug: string } }) {
|
export default async function GalleryPage({ params }: { params: { gallerySlug: string } }) {
|
||||||
@ -9,7 +9,7 @@ export default async function GalleryPage({ params }: { params: { gallerySlug: s
|
|||||||
slug: gallerySlug
|
slug: gallerySlug
|
||||||
},
|
},
|
||||||
include: {
|
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 && (
|
||||||
<>
|
<>
|
||||||
<section className="text-center space-y-4">
|
<section className="text-center space-y-4">
|
||||||
<h1 className="text-4xl font-bold tracking-tight">{gallery.name}</h1>
|
<h1 className="text-4xl font-bold tracking-tight">Gallery: {gallery.name}</h1>
|
||||||
<p className="text-lg text-muted-foreground">
|
<p className="text-lg text-muted-foreground">
|
||||||
{gallery.description}
|
{gallery.description}
|
||||||
</p>
|
</p>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import GalleryList from "@/components/home/GalleryList";
|
import GalleryList from "@/components/lists/GalleryList";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
|
47
src/components/cards/AlbumCard.tsx
Normal file
47
src/components/cards/AlbumCard.tsx
Normal file
@ -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 (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className="group relative overflow-hidden rounded-lg border bg-background shadow transition hover:shadow-lg"
|
||||||
|
>
|
||||||
|
<div className="aspect-[4/3] w-full relative">
|
||||||
|
{cover ? (
|
||||||
|
<NextImage
|
||||||
|
src={`/api/image/thumbnails/${cover.fileKey}.webp`}
|
||||||
|
alt={cover.imageName}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full w-full bg-muted text-muted-foreground text-sm">
|
||||||
|
No cover image
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Overlay: Album label */}
|
||||||
|
<div className="absolute top-1 left-1 bg-black/60 text-white text-xs px-2 py-0.5 rounded z-10">
|
||||||
|
Album
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom: Album name */}
|
||||||
|
<div className="absolute bottom-0 left-0 w-full bg-black/50 text-white text-sm px-2 py-1 line-clamp-1 truncate">
|
||||||
|
{album.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
93
src/components/cards/CoverCard.tsx
Normal file
93
src/components/cards/CoverCard.tsx
Normal file
@ -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" ? <BookOpenIcon size={12} className="mr-1" /> : <ImagePlusIcon size={12} className="mr-1" />
|
||||||
|
|
||||||
|
const borderColors =
|
||||||
|
coverImage?.colors?.map((c) => c.color?.hex).filter((hex): hex is string => Boolean(hex)) ?? []
|
||||||
|
|
||||||
|
const shouldBlur = coverImage?.nsfw && !showNSFW
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<div className="group rounded-lg border overflow-hidden hover:shadow-lg transition-shadow bg-background relative">
|
||||||
|
<div className="relative aspect-[4/3] w-full bg-muted items-center justify-center">
|
||||||
|
{coverImage?.fileKey ? (
|
||||||
|
<NextImage
|
||||||
|
src={`/api/image/thumbnails/${coverImage.fileKey}.webp`}
|
||||||
|
alt={coverImage.imageName}
|
||||||
|
fill
|
||||||
|
className={clsx(
|
||||||
|
"object-cover transition duration-300",
|
||||||
|
shouldBlur && "blur-md scale-105"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
||||||
|
No cover image
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="absolute top-1 left-1 z-10 flex gap-1">
|
||||||
|
<TagBadge
|
||||||
|
label={badgeText}
|
||||||
|
variant="secondary"
|
||||||
|
className="text-xs px-2 py-0.5 inline-flex items-center"
|
||||||
|
icon={badgeIcon}
|
||||||
|
/>
|
||||||
|
{coverImage?.nsfw && (
|
||||||
|
<TagBadge
|
||||||
|
label="NSFW"
|
||||||
|
variant="destructive"
|
||||||
|
icon={<EyeOffIcon size={12} />}
|
||||||
|
className="text-xs px-2 py-0.5 inline-flex items-center"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<h2 className="text-lg font-semibold truncate text-center">{item.name}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={href}>
|
||||||
|
{coverImage && animateGlow && borderColors.length > 0 ? (
|
||||||
|
<GlowingBorderWrapper colors={borderColors}>{content}</GlowingBorderWrapper>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
42
src/components/cards/GlowingBorderWrapper.tsx
Normal file
42
src/components/cards/GlowingBorderWrapper.tsx
Normal file
@ -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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative rounded-lg p-[2px]",
|
||||||
|
animate && "animate-glow",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${gradientColors})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="rounded-md overflow-hidden">{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
67
src/components/cards/ImageCard.tsx
Normal file
67
src/components/cards/ImageCard.tsx
Normal file
@ -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 = (
|
||||||
|
<div className="group rounded-lg border overflow-hidden hover:shadow-lg transition-shadow bg-background relative">
|
||||||
|
<div className="relative aspect-[4/3] w-full bg-muted items-center justify-center">
|
||||||
|
<NextImage
|
||||||
|
src={`/api/image/thumbnails/${image.fileKey}.webp`}
|
||||||
|
alt={image.imageName}
|
||||||
|
fill
|
||||||
|
className={clsx("object-cover transition duration-300", shouldBlur && "blur-md scale-105")}
|
||||||
|
/>
|
||||||
|
{image.nsfw && (
|
||||||
|
<div className="absolute top-1 left-1 z-10 flex gap-1">
|
||||||
|
<TagBadge
|
||||||
|
label="NSFW"
|
||||||
|
variant="destructive"
|
||||||
|
icon={<EyeOffIcon size={12} />}
|
||||||
|
className="text-xs px-2 py-0.5 inline-flex items-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<h2 className="text-lg font-semibold truncate text-center">{image.imageName}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={href}>
|
||||||
|
{animateGlow && borderColors.length > 0 ? (
|
||||||
|
<GlowingBorderWrapper colors={borderColors}>{content}</GlowingBorderWrapper>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
32
src/components/cards/TagBadge.tsx
Normal file
32
src/components/cards/TagBadge.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { HTMLAttributes, ReactNode } from "react"
|
||||||
|
import { Badge } from "../ui/badge"
|
||||||
|
|
||||||
|
interface TagBadgeProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
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 (
|
||||||
|
<Badge
|
||||||
|
variant={variant}
|
||||||
|
className={cn("inline-flex items-center text-xs px-2 py-0.5 rounded-md", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{label}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
20
src/components/global/AnimateToggle.tsx
Normal file
20
src/components/global/AnimateToggle.tsx
Normal file
@ -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 (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setAnimateGlow(!animateGlow)}
|
||||||
|
aria-label="Toggle glow animation"
|
||||||
|
>
|
||||||
|
{animateGlow ? <Sparkles className="w-5 h-5" /> : <Sparkle className="w-5 h-5 opacity-50" />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
@ -1,11 +1,17 @@
|
|||||||
|
import AnimateToggle from "./AnimateToggle";
|
||||||
import ModeToggle from "./ModeToggle";
|
import ModeToggle from "./ModeToggle";
|
||||||
|
import NSFWToggle from "./NSFWToggle";
|
||||||
import TopNav from "./TopNav";
|
import TopNav from "./TopNav";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<TopNav />
|
<TopNav />
|
||||||
<ModeToggle />
|
<div className="flex gap-4">
|
||||||
|
<AnimateToggle />
|
||||||
|
<NSFWToggle />
|
||||||
|
<ModeToggle />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
20
src/components/global/NSFWToggle.tsx
Normal file
20
src/components/global/NSFWToggle.tsx
Normal file
@ -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 (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setShowNSFW(!showNSFW)}
|
||||||
|
aria-label="Toggle NSFW content"
|
||||||
|
>
|
||||||
|
{showNSFW ? <Eye className="w-5 h-5" /> : <EyeOff className="w-5 h-5 opacity-50" />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
@ -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 (
|
|
||||||
<section>
|
|
||||||
<h1 className="text-2xl font-bold mb-4">Galleries</h1>
|
|
||||||
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
|
||||||
{galleries ? galleries.map((gallery) => (
|
|
||||||
<Link href={`/galleries/${gallery.slug}`} key={gallery.id}>
|
|
||||||
<div className="group rounded-lg border overflow-hidden hover:shadow-lg transition-shadow bg-background">
|
|
||||||
<div className="relative aspect-[4/3] w-full bg-muted">
|
|
||||||
{gallery.coverImage?.fileKey ? (
|
|
||||||
<Image
|
|
||||||
src={`/api/image/thumbnails/${gallery.coverImage.fileKey}.webp`}
|
|
||||||
alt={gallery.coverImage.imageName}
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
||||||
No cover image
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="p-4">
|
|
||||||
<h2 className="text-lg font-semibold truncate text-center">{gallery.name}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)) : "There are no galleries here!"}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
38
src/components/lists/AlbumList.tsx
Normal file
38
src/components/lists/AlbumList.tsx
Normal file
@ -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 <p>There are no albums here!</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||||
|
{albums.map((album) => (
|
||||||
|
<CoverCard
|
||||||
|
key={album.id}
|
||||||
|
item={{
|
||||||
|
id: album.id,
|
||||||
|
name: album.name,
|
||||||
|
slug: album.slug,
|
||||||
|
coverImage: album.coverImage ?? undefined,
|
||||||
|
type: "album",
|
||||||
|
gallerySlug,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
37
src/components/lists/GalleryList.tsx
Normal file
37
src/components/lists/GalleryList.tsx
Normal file
@ -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 <p>There are no galleries here!</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||||
|
{galleries.map((gallery) => (
|
||||||
|
<CoverCard
|
||||||
|
key={gallery.id}
|
||||||
|
item={{
|
||||||
|
id: gallery.id,
|
||||||
|
name: gallery.name,
|
||||||
|
slug: gallery.slug,
|
||||||
|
coverImage: gallery.coverImage ?? undefined,
|
||||||
|
type: "gallery",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
16
src/components/lists/ImageList.tsx
Normal file
16
src/components/lists/ImageList.tsx
Normal file
@ -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 <p>There are no images here!</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="grid gap-6 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
|
||||||
|
{images.map((image) => (
|
||||||
|
<ImageCard key={image.id} image={image} gallerySlug={gallerySlug} albumSlug={albumSlug} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
46
src/components/ui/badge.tsx
Normal file
46
src/components/ui/badge.tsx
Normal file
@ -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<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
15
src/hooks/useGlobalSettings.ts
Normal file
15
src/hooks/useGlobalSettings.ts
Normal file
@ -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<GlobalSettings>((set) => ({
|
||||||
|
showNSFW: false,
|
||||||
|
setShowNSFW: (val) => set({ showNSFW: val }),
|
||||||
|
animateGlow: true,
|
||||||
|
setAnimateGlow: (val) => set({ animateGlow: val }),
|
||||||
|
}));
|
Reference in New Issue
Block a user