Add different gallery layouts

This commit is contained in:
2025-07-12 23:30:52 +02:00
parent 505e1e28b8
commit f0d44ff807
18 changed files with 2449 additions and 4 deletions

View File

@ -12,11 +12,21 @@ export default function TopNav() {
<Link href="/">Home</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
<Link href="/portfolio">Portfolio</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
<Link href="/commissions">Commissions</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
<Link href="/ych">YCH / Custom offers</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
<Link href="/tos">Terms of Service</Link>

View File

@ -0,0 +1,86 @@
"use client";
import type { Color, ImageColor, ImageVariant, PortfolioImage } from "@/generated/prisma";
import { cn } from "@/lib/utils";
import Image from "next/image";
// ---------- Type Definitions ----------
// Prisma-based image (square layout)
type SquareImage = PortfolioImage & {
variants: ImageVariant[];
colors?: (ImageColor & { color: Color })[];
};
// Flattened image for mosaic layout
type MosaicImage = {
id: string;
url: string;
altText: string;
backgroundColor: string;
};
// Union Props
type ImageCardProps =
| {
variant: "square";
image: SquareImage;
backgroundColor?: string;
}
| {
variant: "mosaic";
image: MosaicImage;
};
export function ImageCard(props: ImageCardProps) {
const { variant } = props;
const isSquare = variant === "square";
const isMosaic = variant === "mosaic";
const backgroundColor =
isSquare
? props.backgroundColor ?? "#e5e7eb"
: props.image.backgroundColor;
const altText =
isSquare
? props.image.altText ?? props.image.name ?? "Image"
: props.image.altText;
const src =
isSquare
? `/api/image/thumbnail/${props.image.fileKey}.webp`
: props.image.url;
return (
<div
className={cn(
"overflow-hidden rounded-md",
isSquare && "aspect-square shadow-sm hover:scale-[1.01] transition-transform",
isMosaic && "w-full h-full"
)}
style={{ backgroundColor }}
>
<div
className={cn(
"relative w-full h-full",
isSquare && "flex items-center justify-center"
)}
>
<Image
src={src}
alt={altText}
fill={isMosaic}
width={isSquare ? 400 : undefined}
height={isSquare ? 400 : undefined}
className={cn(
isSquare && "object-contain max-w-full max-h-full",
isMosaic && "object-cover"
)}
loading="lazy"
/>
</div>
</div>
);
}

View File

@ -0,0 +1,59 @@
"use client";
import {
JustifiedImage,
justifyPortfolioImages,
} from "@/utils/justifyPortfolioImages";
import { useEffect, useRef, useState } from "react";
import { ImageCard } from "./ImageCard";
interface Props {
images: JustifiedImage[];
rowHeight?: number;
gap?: number;
}
export function JustifiedGallery({ images, rowHeight = 200, gap = 4 }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const [containerWidth, setContainerWidth] = useState(1200);
const [rows, setRows] = useState<JustifiedImage[][]>([]);
useEffect(() => {
if (!containerRef.current) return;
const observer = new ResizeObserver(([entry]) => {
setContainerWidth(entry.contentRect.width);
});
observer.observe(containerRef.current);
return () => observer.disconnect();
}, []);
useEffect(() => {
const newRows = justifyPortfolioImages(images, containerWidth, rowHeight, gap);
setRows(newRows);
}, [images, containerWidth, rowHeight, gap]);
return (
<div ref={containerRef} className="w-full">
{rows.length === 0 && (
<p className="text-muted-foreground text-center py-20">No images to display</p>
)}
{rows.map((row, i) => (
<div key={i} className="flex gap-[4px] mb-[4px]">
{row.map((img) => (
<div key={img.id} style={{ width: img.width, height: img.height }}>
<ImageCard
image={{
id: img.id,
altText: img.altText,
url: img.url,
backgroundColor: img.backgroundColor,
}}
variant="mosaic"
/>
</div>
))}
</div>
))}
</div>
);
}