Changed a lot of things
This commit is contained in:
@ -14,6 +14,193 @@ datasource db {
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// Portfolio
|
||||
model PortfolioImage {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
fileKey String @unique
|
||||
originalFile String @unique
|
||||
fileType String
|
||||
name String
|
||||
fileSize Int
|
||||
needsWork Boolean @default(true)
|
||||
nsfw Boolean @default(false)
|
||||
published Boolean @default(false)
|
||||
setAsHeader Boolean @default(false)
|
||||
|
||||
altText String?
|
||||
description String?
|
||||
month Int?
|
||||
year Int?
|
||||
creationDate DateTime?
|
||||
|
||||
albumId String?
|
||||
typeId String?
|
||||
album PortfolioAlbum? @relation(fields: [albumId], references: [id])
|
||||
type PortfolioType? @relation(fields: [typeId], references: [id])
|
||||
|
||||
metadata ImageMetadata?
|
||||
|
||||
categories PortfolioCategory[]
|
||||
colors ImageColor[]
|
||||
sortContexts PortfolioSortContext[]
|
||||
tags PortfolioTag[]
|
||||
variants ImageVariant[]
|
||||
}
|
||||
|
||||
model PortfolioAlbum {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String @unique
|
||||
slug String @unique
|
||||
|
||||
description String?
|
||||
|
||||
images PortfolioImage[]
|
||||
}
|
||||
|
||||
model PortfolioType {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String @unique
|
||||
slug String @unique
|
||||
|
||||
description String?
|
||||
|
||||
images PortfolioImage[]
|
||||
}
|
||||
|
||||
model PortfolioCategory {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String @unique
|
||||
slug String @unique
|
||||
|
||||
description String?
|
||||
|
||||
images PortfolioImage[]
|
||||
}
|
||||
|
||||
model PortfolioTag {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String @unique
|
||||
slug String @unique
|
||||
|
||||
description String?
|
||||
|
||||
images PortfolioImage[]
|
||||
}
|
||||
|
||||
model PortfolioSortContext {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
year String
|
||||
albumId String
|
||||
type String
|
||||
group String
|
||||
sortOrder Int
|
||||
|
||||
imageId String
|
||||
image PortfolioImage @relation(fields: [imageId], references: [id])
|
||||
|
||||
@@unique([imageId, year, albumId, type, group])
|
||||
}
|
||||
|
||||
model Color {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String @unique
|
||||
type String
|
||||
|
||||
hex String?
|
||||
blue Int?
|
||||
green Int?
|
||||
red Int?
|
||||
|
||||
images ImageColor[]
|
||||
}
|
||||
|
||||
model ImageColor {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String
|
||||
colorId String
|
||||
type String
|
||||
|
||||
image PortfolioImage @relation(fields: [imageId], references: [id])
|
||||
color Color @relation(fields: [colorId], references: [id])
|
||||
|
||||
@@unique([imageId, type])
|
||||
}
|
||||
|
||||
model ImageMetadata {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String @unique
|
||||
depth String
|
||||
format String
|
||||
space String
|
||||
channels Int
|
||||
height Int
|
||||
width Int
|
||||
|
||||
autoOrientH Int?
|
||||
autoOrientW Int?
|
||||
bitsPerSample Int?
|
||||
density Int?
|
||||
hasAlpha Boolean?
|
||||
hasProfile Boolean?
|
||||
isPalette Boolean?
|
||||
isProgressive Boolean?
|
||||
|
||||
image PortfolioImage @relation(fields: [imageId], references: [id])
|
||||
}
|
||||
|
||||
model ImageVariant {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String
|
||||
s3Key String
|
||||
type String
|
||||
height Int
|
||||
width Int
|
||||
|
||||
fileExtension String?
|
||||
mimeType String?
|
||||
url String?
|
||||
sizeBytes Int?
|
||||
|
||||
image PortfolioImage @relation(fields: [imageId], references: [id])
|
||||
|
||||
@@unique([imageId, type])
|
||||
}
|
||||
|
||||
model CommissionType {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
@ -149,137 +336,3 @@ model CommissionRequest {
|
||||
option CommissionOption? @relation(fields: [optionId], references: [id])
|
||||
type CommissionType? @relation(fields: [typeId], references: [id])
|
||||
}
|
||||
|
||||
model PortfolioImage {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
fileKey String @unique
|
||||
originalFile String @unique
|
||||
nsfw Boolean @default(false)
|
||||
published Boolean @default(false)
|
||||
setAsHeader Boolean @default(false)
|
||||
|
||||
altText String?
|
||||
description String?
|
||||
fileType String?
|
||||
name String?
|
||||
slug String?
|
||||
type String?
|
||||
fileSize Int?
|
||||
creationDate DateTime?
|
||||
|
||||
metadata ImageMetadata?
|
||||
|
||||
categories PortfolioCategory[]
|
||||
colors ImageColor[]
|
||||
tags PortfolioTag[]
|
||||
variants ImageVariant[]
|
||||
}
|
||||
|
||||
model PortfolioCategory {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String @unique
|
||||
|
||||
slug String?
|
||||
description String?
|
||||
|
||||
images PortfolioImage[]
|
||||
}
|
||||
|
||||
model PortfolioTag {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sortIndex Int @default(0)
|
||||
|
||||
name String @unique
|
||||
|
||||
slug String?
|
||||
description String?
|
||||
|
||||
images PortfolioImage[]
|
||||
}
|
||||
|
||||
model Color {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
name String @unique
|
||||
type String
|
||||
|
||||
hex String?
|
||||
blue Int?
|
||||
green Int?
|
||||
red Int?
|
||||
|
||||
images ImageColor[]
|
||||
}
|
||||
|
||||
model ImageColor {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String
|
||||
colorId String
|
||||
type String
|
||||
|
||||
image PortfolioImage @relation(fields: [imageId], references: [id])
|
||||
color Color @relation(fields: [colorId], references: [id])
|
||||
|
||||
@@unique([imageId, type])
|
||||
}
|
||||
|
||||
model ImageMetadata {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String @unique
|
||||
depth String
|
||||
format String
|
||||
space String
|
||||
channels Int
|
||||
height Int
|
||||
width Int
|
||||
|
||||
autoOrientH Int?
|
||||
autoOrientW Int?
|
||||
bitsPerSample Int?
|
||||
density Int?
|
||||
hasAlpha Boolean?
|
||||
hasProfile Boolean?
|
||||
isPalette Boolean?
|
||||
isProgressive Boolean?
|
||||
|
||||
image PortfolioImage @relation(fields: [imageId], references: [id])
|
||||
}
|
||||
|
||||
model ImageVariant {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
imageId String
|
||||
s3Key String
|
||||
type String
|
||||
height Int
|
||||
width Int
|
||||
|
||||
fileExtension String?
|
||||
mimeType String?
|
||||
url String?
|
||||
sizeBytes Int?
|
||||
|
||||
image PortfolioImage @relation(fields: [imageId], references: [id])
|
||||
|
||||
@@unique([imageId, type])
|
||||
}
|
||||
|
@ -1,40 +1,53 @@
|
||||
"use server";
|
||||
// "use server";
|
||||
|
||||
// import prisma from "@/lib/prisma";
|
||||
|
||||
// export async function getJustifiedImages() {
|
||||
// const images = await prisma.portfolioImage.findMany({
|
||||
// where: {
|
||||
// variants: {
|
||||
// some: { type: "resized" },
|
||||
// },
|
||||
// },
|
||||
// include: {
|
||||
// variants: true,
|
||||
// colors: { include: { color: true } },
|
||||
// },
|
||||
// });
|
||||
|
||||
// return images
|
||||
// .map((img) => {
|
||||
// const variant = img.variants.find((v) => v.type === "resized");
|
||||
// if (!variant || !variant.width || !variant.height) return null;
|
||||
|
||||
// const bg = img.colors.find((c) => c.type === "Vibrant")?.color.hex ?? "#e5e7eb";
|
||||
|
||||
// return {
|
||||
// id: img.id,
|
||||
// fileKey: img.fileKey,
|
||||
// altText: img.altText ?? img.name ?? "",
|
||||
// backgroundColor: bg,
|
||||
// width: variant.width,
|
||||
// height: variant.height,
|
||||
// url: variant.url ?? `/api/image/resized/${img.fileKey}.webp`,
|
||||
// };
|
||||
// })
|
||||
// .filter(Boolean) as JustifiedInputImage[];
|
||||
// }
|
||||
|
||||
// export interface JustifiedInputImage {
|
||||
// id: string;
|
||||
// url: string;
|
||||
// altText: string;
|
||||
// backgroundColor: string;
|
||||
// width: number;
|
||||
// height: number;
|
||||
// }
|
||||
|
||||
"use server"
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export async function getJustifiedImages() {
|
||||
const images = await prisma.portfolioImage.findMany({
|
||||
where: {
|
||||
variants: {
|
||||
some: { type: "resized" },
|
||||
},
|
||||
},
|
||||
include: {
|
||||
variants: true,
|
||||
colors: { include: { color: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return images
|
||||
.map((img) => {
|
||||
const variant = img.variants.find((v) => v.type === "resized");
|
||||
if (!variant || !variant.width || !variant.height) return null;
|
||||
|
||||
const bg = img.colors.find((c) => c.type === "Vibrant")?.color.hex ?? "#e5e7eb";
|
||||
|
||||
return {
|
||||
id: img.id,
|
||||
fileKey: img.fileKey,
|
||||
altText: img.altText ?? img.name ?? "",
|
||||
backgroundColor: bg,
|
||||
width: variant.width,
|
||||
height: variant.height,
|
||||
url: variant.url ?? `/api/image/resized/${img.fileKey}.webp`,
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as JustifiedInputImage[];
|
||||
}
|
||||
|
||||
export interface JustifiedInputImage {
|
||||
id: string;
|
||||
url: string;
|
||||
@ -42,4 +55,165 @@ export interface JustifiedInputImage {
|
||||
backgroundColor: string;
|
||||
width: number;
|
||||
height: number;
|
||||
fileKey: string;
|
||||
}
|
||||
|
||||
interface Variant {
|
||||
type: string;
|
||||
url?: string | null;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface Color {
|
||||
type: string;
|
||||
color: {
|
||||
hex: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface PortfolioImage {
|
||||
id: string;
|
||||
fileKey: string;
|
||||
name?: string | null;
|
||||
altText?: string | null;
|
||||
variants: Variant[];
|
||||
colors: Color[];
|
||||
}
|
||||
|
||||
interface PortfolioImageSortContext {
|
||||
image: PortfolioImage;
|
||||
group: "highlighted" | "featured" | "default" | string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
// const groupPriority = {
|
||||
// highlighted: 0,
|
||||
// featured: 1,
|
||||
// default: 2,
|
||||
// } as const;
|
||||
|
||||
function shuffleImages<T>(arr: T[]): T[] {
|
||||
const shuffled = [...arr];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
// Stub: Replace this with your actual DB or API call
|
||||
async function getImageSortContext(params: {
|
||||
year?: string;
|
||||
albumId?: string;
|
||||
type: string;
|
||||
}): Promise<PortfolioImageSortContext[]> {
|
||||
const { year, albumId, type } = params;
|
||||
|
||||
const typeEntry = await prisma.portfolioType.findUnique({
|
||||
where: { slug: type },
|
||||
// select: { id: true },
|
||||
});
|
||||
if (!typeEntry) return [];
|
||||
const typeId = typeEntry.id;
|
||||
|
||||
const sortContexts = await prisma.portfolioSortContext.findMany({
|
||||
where: {
|
||||
type: typeId,
|
||||
...(year ? { year: year } : {}),
|
||||
...(albumId ? { albumId } : {}),
|
||||
image: {
|
||||
// published: true,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
image: {
|
||||
include: {
|
||||
variants: true,
|
||||
colors: {
|
||||
include: {
|
||||
color: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return sortContexts;
|
||||
}
|
||||
|
||||
export async function getJustifiedImages(
|
||||
year: string | null | undefined,
|
||||
albumId: string | null | undefined,
|
||||
type: string,
|
||||
shouldShuffle = false
|
||||
): Promise<JustifiedInputImage[] | null> {
|
||||
if (!year && !albumId) return null;
|
||||
|
||||
const sortContexts = await getImageSortContext({
|
||||
year: year || undefined,
|
||||
albumId: albumId || undefined,
|
||||
type,
|
||||
});
|
||||
|
||||
// const validImages = sortContexts
|
||||
// .filter((ctx) => ctx.image && ctx.image.variants.some((v) => v.type === "resized"))
|
||||
// .sort((a, b) => {
|
||||
// const groupA = groupPriority[a.group as keyof typeof groupPriority] ?? 3;
|
||||
// const groupB = groupPriority[b.group as keyof typeof groupPriority] ?? 3;
|
||||
// if (groupA !== groupB) return groupA - groupB;
|
||||
// return a.sortOrder - b.sortOrder;
|
||||
// });
|
||||
|
||||
type GroupName = "highlighted" | "featured" | "default";
|
||||
|
||||
const grouped: Record<GroupName, PortfolioImageSortContext[]> = {
|
||||
highlighted: [],
|
||||
featured: [],
|
||||
default: [],
|
||||
};
|
||||
|
||||
for (const ctx of sortContexts) {
|
||||
const rawGroup = ctx.group?.toLowerCase() ?? "default";
|
||||
const group = ["highlighted", "featured", "default"].includes(rawGroup)
|
||||
? (rawGroup as GroupName)
|
||||
: "default";
|
||||
|
||||
grouped[group].push(ctx);
|
||||
}
|
||||
|
||||
const processGroup = (list: PortfolioImageSortContext[]) =>
|
||||
shouldShuffle
|
||||
? shuffleImages(list)
|
||||
: [...list].sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
|
||||
const finalList = [
|
||||
...processGroup(grouped.highlighted),
|
||||
...processGroup(grouped.featured),
|
||||
...processGroup(grouped.default),
|
||||
];
|
||||
|
||||
const output: JustifiedInputImage[] = [];
|
||||
|
||||
for (const ctx of finalList) {
|
||||
const img = ctx.image;
|
||||
const variant = img.variants.find((v) => v.type === "resized");
|
||||
|
||||
if (!variant || !variant.width || !variant.height) continue;
|
||||
|
||||
const bg = img.colors.find((c) => c.type === "Vibrant")?.color.hex ?? "#e5e7eb";
|
||||
|
||||
output.push({
|
||||
id: img.id,
|
||||
fileKey: img.fileKey,
|
||||
altText: img.altText ?? img.name ?? "",
|
||||
backgroundColor: bg,
|
||||
width: variant.width,
|
||||
height: variant.height,
|
||||
url: variant.url ?? `/api/image/resized/${img.fileKey}.webp`,
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
42
src/actions/portfolio/getPortfolioFilters.ts
Normal file
42
src/actions/portfolio/getPortfolioFilters.ts
Normal file
@ -0,0 +1,42 @@
|
||||
"use server"
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export async function getPortfolioFilters() {
|
||||
const albums = await prisma.portfolioAlbum.findMany({
|
||||
where: {
|
||||
// images: {
|
||||
// some: {
|
||||
// published: true,
|
||||
// },
|
||||
// },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
orderBy: {
|
||||
sortIndex: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
const yearsRaw = await prisma.portfolioImage.findMany({
|
||||
where: {
|
||||
// published: true,
|
||||
year: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
distinct: ["year"],
|
||||
select: {
|
||||
year: true,
|
||||
},
|
||||
});
|
||||
|
||||
const years = yearsRaw
|
||||
.map((y) => y.year!)
|
||||
.sort((a, b) => b - a)
|
||||
.map(String);
|
||||
|
||||
return { albums, years };
|
||||
}
|
@ -42,13 +42,13 @@ export default function RootLayout({
|
||||
<div>
|
||||
<Banner />
|
||||
</div>
|
||||
<header className="sticky top-0 z-50 h-14 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 px-4 py-2">
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<Header />
|
||||
</header>
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<main className="container mx-auto">
|
||||
{children}
|
||||
</main>
|
||||
<footer className="mt-auto px-4 py-2 h-14 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 ">
|
||||
<footer className="mt-auto p-4 h-14 border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 ">
|
||||
<Footer />
|
||||
</footer>
|
||||
<Toaster />
|
||||
|
@ -45,7 +45,7 @@ const sections = [
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
<div className="flex flex-col gap-10 px-4 py-8">
|
||||
{/* Hero Section */}
|
||||
<div className="text-center flex flex-col items-center justify-center mt-10 px-4">
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { getJustifiedImages } from "@/actions/portfolio/getJustifiedImages";
|
||||
import { JustifiedGallery } from "@/components/portfolio/JustifiedGallery";
|
||||
|
||||
export default async function PortfolioTwoPage() {
|
||||
const images = await getJustifiedImages();
|
||||
export default async function PortfolioArtworksPage({ searchParams }: { searchParams: { year?: string; album?: string } }) {
|
||||
const { year, album } = await searchParams;
|
||||
const images = await getJustifiedImages(year, album, "art", false);
|
||||
|
||||
if (!images) return null
|
||||
|
||||
return (
|
||||
<main className="p-2 mx-auto max-w-screen-2xl">
|
5
src/app/portfolio/artfight/page.tsx
Normal file
5
src/app/portfolio/artfight/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export default function PortfolioArtfightPage() {
|
||||
return (
|
||||
<div>PortfolioArtfightPage</div>
|
||||
);
|
||||
}
|
16
src/app/portfolio/layout.tsx
Normal file
16
src/app/portfolio/layout.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import PortfolioSubNav from "@/components/portfolio/PortfolioSubNav"
|
||||
|
||||
export default function PortfolioLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<PortfolioSubNav />
|
||||
<div className="px-4 py-8">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
5
src/app/portfolio/minis/page.tsx
Normal file
5
src/app/portfolio/minis/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export default function PortfolioMiniaturesPage() {
|
||||
return (
|
||||
<div>PortfolioMiniaturesPage</div>
|
||||
);
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function PortfolioPage() {
|
||||
return (
|
||||
<div>
|
||||
<Link href="/portfolio/one">Variant One</Link>
|
||||
<Link href="/portfolio/two">Variant Two</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -4,11 +4,9 @@ import TopNav from "./TopNav";
|
||||
export default function Header() {
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between px-4 md:px-8 py-2">
|
||||
<TopNav />
|
||||
</div>
|
||||
<div>
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,8 +9,9 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "../u
|
||||
|
||||
const links = [
|
||||
{ href: "/", label: "Home" },
|
||||
{ href: "/portfolio", label: "Art Portfolio" },
|
||||
{ href: "/miniatures", label: "Miniatures" },
|
||||
{ href: "/portfolio/art", label: "Artworks" },
|
||||
{ href: "/portfolio/artfight", label: "Artfight" },
|
||||
{ href: "/portfolio/minis", label: "Miniatures" },
|
||||
{ href: "/commissions", label: "Commissions" },
|
||||
{ href: "/ych", label: "YCH / Custom offers" },
|
||||
{ href: "/tos", label: "Terms of Service" },
|
||||
|
@ -54,7 +54,7 @@ export function ImageCard(props: ImageCardProps) {
|
||||
? `/api/image/thumbnail/${props.image.fileKey}.webp`
|
||||
: props.image.url;
|
||||
|
||||
console.log(props.image);
|
||||
// console.log(props.image);
|
||||
|
||||
return (
|
||||
<Link href={`/portfolio/${props.image.id}`}>
|
||||
|
192
src/components/portfolio/PortfolioSubNav.tsx
Normal file
192
src/components/portfolio/PortfolioSubNav.tsx
Normal file
@ -0,0 +1,192 @@
|
||||
"use client";
|
||||
|
||||
import { getPortfolioFilters } from "@/actions/portfolio/getPortfolioFilters";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import clsx from "clsx";
|
||||
import { CalendarIcon, ImagesIcon } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Album {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default function PortfolioSubNav({
|
||||
onFilterSelect,
|
||||
}: {
|
||||
onFilterSelect?: (value: string) => void;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [mode, setMode] = useState<"year" | "album">("year");
|
||||
const [selectedYear, setSelectedYear] = useState<string | null>(null);
|
||||
const [selectedAlbum, setSelectedAlbum] = useState<string | null>(null);
|
||||
|
||||
const [years, setYears] = useState<string[]>([]);
|
||||
const [albums, setAlbums] = useState<Album[]>([]);
|
||||
|
||||
// useEffect(() => {
|
||||
// const year = searchParams.get("year");
|
||||
// const album = searchParams.get("album");
|
||||
|
||||
// if (year) {
|
||||
// setMode("year");
|
||||
// setSelectedYear(year);
|
||||
// } else if (album) {
|
||||
// setMode("album");
|
||||
// setSelectedAlbum(album);
|
||||
// }
|
||||
// }, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFilters = async () => {
|
||||
const { years, albums } = await getPortfolioFilters();
|
||||
setYears(years);
|
||||
setAlbums(albums);
|
||||
|
||||
const urlYear = searchParams.get("year");
|
||||
const urlAlbum = searchParams.get("album");
|
||||
|
||||
if (urlYear) {
|
||||
setMode("year");
|
||||
setSelectedYear(urlYear);
|
||||
onFilterSelect?.(urlYear);
|
||||
} else if (urlAlbum) {
|
||||
setMode("album");
|
||||
setSelectedAlbum(urlAlbum);
|
||||
onFilterSelect?.(urlAlbum);
|
||||
} else if (years.length > 0) {
|
||||
const defaultYear = years[0];
|
||||
setMode("year");
|
||||
setSelectedYear(defaultYear);
|
||||
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set("year", defaultYear);
|
||||
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
|
||||
|
||||
onFilterSelect?.(defaultYear);
|
||||
}
|
||||
};
|
||||
fetchFilters();
|
||||
}, [onFilterSelect, pathname, router, searchParams]);
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
if (mode === "year") {
|
||||
setSelectedYear(value);
|
||||
setSelectedAlbum(null);
|
||||
params.set("year", value);
|
||||
params.delete("album");
|
||||
} else {
|
||||
setSelectedAlbum(value);
|
||||
setSelectedYear(null);
|
||||
params.set("album", value);
|
||||
params.delete("year");
|
||||
}
|
||||
|
||||
router.push(`${pathname}?${params.toString()}`, { scroll: false });
|
||||
|
||||
onFilterSelect?.(value);
|
||||
};
|
||||
|
||||
const activeList =
|
||||
mode === "year"
|
||||
? years.map((y) => ({ id: y, name: y }))
|
||||
: albums.map((a) => ({ id: a.id, name: a.name }));
|
||||
|
||||
const selected = mode === "year" ? selectedYear : selectedAlbum;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center gap-4 px-4 md:px-8 py-2 overflow-x-auto">
|
||||
{/* Toggle icons */}
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<ModeButton
|
||||
icon={<CalendarIcon className="w-4 h-4" />}
|
||||
isActive={mode === "year"}
|
||||
onClick={() => setMode("year")}
|
||||
label="Filter by year"
|
||||
/>
|
||||
<ModeButton
|
||||
icon={<ImagesIcon className="w-4 h-4" />}
|
||||
isActive={mode === "album"}
|
||||
onClick={() => setMode("album")}
|
||||
label="Filter by album"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="h-5 w-px bg-border" />
|
||||
|
||||
{/* Filter buttons */}
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{activeList.map((item) => (
|
||||
<FilterButton
|
||||
key={item.id}
|
||||
label={item.name}
|
||||
isActive={selected === item.id}
|
||||
onClick={() => handleSelect(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ModeButton({
|
||||
icon,
|
||||
isActive,
|
||||
onClick,
|
||||
label,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClick}
|
||||
aria-label={label}
|
||||
className={clsx(
|
||||
"transition-colors",
|
||||
isActive ? "bg-secondary text-secondary-foreground" : "hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function FilterButton({
|
||||
label,
|
||||
isActive,
|
||||
onClick,
|
||||
}: {
|
||||
label: string;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClick}
|
||||
className={clsx(
|
||||
"text-sm px-3 py-1 transition-colors",
|
||||
isActive
|
||||
? "bg-secondary text-secondary-foreground"
|
||||
: "hover:bg-muted text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user