Add different gallery layouts
This commit is contained in:
@ -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>
|
||||
|
||||
86
src/components/portfolio/ImageCard.tsx
Normal file
86
src/components/portfolio/ImageCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
59
src/components/portfolio/JustifiedGallery.tsx
Normal file
59
src/components/portfolio/JustifiedGallery.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user