diff --git a/src/actions/portfolio/getJustifiedImages.ts b/src/actions/portfolio/getJustifiedImages.ts
index 8b49590..1eccc8a 100644
--- a/src/actions/portfolio/getJustifiedImages.ts
+++ b/src/actions/portfolio/getJustifiedImages.ts
@@ -56,6 +56,9 @@ export interface JustifiedInputImage {
width: number;
height: number;
fileKey: string;
+ fullUrl: string;
+ fullWidth: number;
+ fullHeight: number;
}
interface Variant {
@@ -199,6 +202,7 @@ export async function getJustifiedImages(
for (const ctx of finalList) {
const img = ctx.image;
const variant = img.variants.find((v) => v.type === "resized");
+ const full = img.variants.find((v) => v.type === "modified");
if (!variant || !variant.width || !variant.height) continue;
@@ -212,6 +216,9 @@ export async function getJustifiedImages(
width: variant.width,
height: variant.height,
url: variant.url ?? `/api/image/resized/${img.fileKey}.webp`,
+ fullUrl: full?.url ?? `/api/image/modified/${img.fileKey}.webp`,
+ fullWidth: full?.width ?? 0,
+ fullHeight: full?.height ?? 0,
});
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index c58cac5..799b05c 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -13,13 +13,19 @@ import Link from "next/link";
const sections = [
{
title: "Art Portfolio",
- href: "/portfolio",
+ href: "/portfolio/art",
description: "My artwork gallery",
icon: Palette,
},
+ {
+ title: "Artfight",
+ href: "/portfolio/artfight",
+ description: "Artfight pieces",
+ icon: Palette,
+ },
{
title: "Miniatures",
- href: "/miniatures",
+ href: "/portfolio/minis",
description: "See my painted miniatures",
icon: Brush,
},
diff --git a/src/components/portfolio/ImageCard.tsx b/src/components/portfolio/ImageCard.tsx
index 14c39ee..1655fb7 100644
--- a/src/components/portfolio/ImageCard.tsx
+++ b/src/components/portfolio/ImageCard.tsx
@@ -3,7 +3,6 @@
import type { Color, ImageColor, ImageVariant, PortfolioImage } from "@/generated/prisma";
import { cn } from "@/lib/utils";
import Image from "next/image";
-import Link from "next/link";
// ---------- Type Definitions ----------
@@ -57,40 +56,40 @@ export function ImageCard(props: ImageCardProps) {
// console.log(props.image);
return (
-
+ //
+
-
-
-
+ loading="lazy"
+ />
-
+
+ //
);
}
diff --git a/src/components/portfolio/JustifiedGallery.tsx b/src/components/portfolio/JustifiedGallery.tsx
index 41e844c..a846a29 100644
--- a/src/components/portfolio/JustifiedGallery.tsx
+++ b/src/components/portfolio/JustifiedGallery.tsx
@@ -6,6 +6,7 @@ import {
} from "@/utils/justifyPortfolioImages";
import { useEffect, useRef, useState } from "react";
import { ImageCard } from "./ImageCard";
+import { Lightbox } from "./Lightbox";
interface Props {
images: JustifiedImage[];
@@ -17,6 +18,7 @@ export function JustifiedGallery({ images, rowHeight = 200, gap = 4 }: Props) {
const containerRef = useRef(null);
const [containerWidth, setContainerWidth] = useState(1200);
const [rows, setRows] = useState([]);
+ const [selectedIndex, setSelectedIndex] = useState(null);
useEffect(() => {
if (!containerRef.current) return;
@@ -32,32 +34,54 @@ export function JustifiedGallery({ images, rowHeight = 200, gap = 4 }: Props) {
setRows(newRows);
}, [images, containerWidth, rowHeight, gap]);
+ const openModal = (index: number) => setSelectedIndex(index);
+
return (
-
- {rows.length === 0 && (
-
No images to display
+ <>
+
+ {rows.length === 0 && (
+
No images to display
+ )}
+ {rows.map((row, i) => (
+
+ {row.map((img) => {
+ const index = images.findIndex((i) => i.id === img.id);
+ return (
+
openModal(index)}
+ className="cursor-zoom-in"
+ >
+
+
+ )
+ })}
+
+ ))}
+
+ {selectedIndex !== null && (
+
setSelectedIndex(null)}
+ hasPrev={selectedIndex > 0}
+ hasNext={selectedIndex < images.length - 1}
+ onPrev={() => setSelectedIndex((i) => (i !== null ? i - 1 : null))}
+ onNext={() => setSelectedIndex((i) => (i !== null ? i + 1 : null))}
+ />
)}
- {rows.map((row, i) => (
-
- {row.map((img) => (
-
-
-
- ))}
-
- ))}
-
+ >
);
}
diff --git a/src/components/portfolio/Lightbox.tsx b/src/components/portfolio/Lightbox.tsx
new file mode 100644
index 0000000..2a86dca
--- /dev/null
+++ b/src/components/portfolio/Lightbox.tsx
@@ -0,0 +1,79 @@
+"use client";
+
+import { ChevronLeft, ChevronRight, X } from "lucide-react";
+import Image from "next/image";
+import { useEffect } from "react";
+import { createPortal } from "react-dom";
+
+interface Props {
+ image: {
+ fullUrl: string;
+ altText?: string;
+ fullWidth: number;
+ fullHeight: number;
+ };
+ onClose: () => void;
+ onPrev?: () => void;
+ onNext?: () => void;
+ hasPrev?: boolean;
+ hasNext?: boolean;
+}
+
+export function Lightbox({ image, onClose, onPrev, onNext, hasPrev, hasNext }: Props) {
+ useEffect(() => {
+ const handleKey = (e: KeyboardEvent) => {
+ if (e.key === "Escape") onClose();
+ if (e.key === "ArrowLeft" && hasPrev) onPrev?.();
+ if (e.key === "ArrowRight" && hasNext) onNext?.();
+ };
+ window.addEventListener("keydown", handleKey);
+ return () => window.removeEventListener("keydown", handleKey);
+ }, [hasPrev, hasNext, onClose, onPrev, onNext]);
+
+ return createPortal(
+
+
e.stopPropagation()}
+ >
+
+
+ {/* Close */}
+
+
+ {/* Prev/Next */}
+ {hasPrev && (
+
+ )}
+ {hasNext && (
+
+ )}
+
+
,
+ document.body
+ );
+}
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..d9ccec9
--- /dev/null
+++ b/src/components/ui/dialog.tsx
@@ -0,0 +1,143 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Dialog({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ showCloseButton?: boolean
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ )
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+}