Changed a lot of things
This commit is contained in:
@ -14,6 +14,193 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
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 {
|
model CommissionType {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@ -149,137 +336,3 @@ model CommissionRequest {
|
|||||||
option CommissionOption? @relation(fields: [optionId], references: [id])
|
option CommissionOption? @relation(fields: [optionId], references: [id])
|
||||||
type CommissionType? @relation(fields: [typeId], 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";
|
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 {
|
export interface JustifiedInputImage {
|
||||||
id: string;
|
id: string;
|
||||||
url: string;
|
url: string;
|
||||||
@ -42,4 +55,165 @@ export interface JustifiedInputImage {
|
|||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: 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>
|
<div>
|
||||||
<Banner />
|
<Banner />
|
||||||
</div>
|
</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 />
|
||||||
</header>
|
</header>
|
||||||
<main className="container mx-auto px-4 py-8">
|
<main className="container mx-auto">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</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 />
|
||||||
</footer>
|
</footer>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
@ -45,7 +45,7 @@ const sections = [
|
|||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10 px-4 py-8">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<div className="text-center flex flex-col items-center justify-center mt-10 px-4">
|
<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">
|
<h1 className="text-4xl md:text-5xl font-bold mb-4">
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { getJustifiedImages } from "@/actions/portfolio/getJustifiedImages";
|
import { getJustifiedImages } from "@/actions/portfolio/getJustifiedImages";
|
||||||
import { JustifiedGallery } from "@/components/portfolio/JustifiedGallery";
|
import { JustifiedGallery } from "@/components/portfolio/JustifiedGallery";
|
||||||
|
|
||||||
export default async function PortfolioTwoPage() {
|
export default async function PortfolioArtworksPage({ searchParams }: { searchParams: { year?: string; album?: string } }) {
|
||||||
const images = await getJustifiedImages();
|
const { year, album } = await searchParams;
|
||||||
|
const images = await getJustifiedImages(year, album, "art", false);
|
||||||
|
|
||||||
|
if (!images) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="p-2 mx-auto max-w-screen-2xl">
|
<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() {
|
export default function Header() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="w-full">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between px-4 md:px-8 py-2">
|
||||||
<TopNav />
|
<TopNav />
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,8 +9,9 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "../u
|
|||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{ href: "/", label: "Home" },
|
{ href: "/", label: "Home" },
|
||||||
{ href: "/portfolio", label: "Art Portfolio" },
|
{ href: "/portfolio/art", label: "Artworks" },
|
||||||
{ href: "/miniatures", label: "Miniatures" },
|
{ href: "/portfolio/artfight", label: "Artfight" },
|
||||||
|
{ href: "/portfolio/minis", label: "Miniatures" },
|
||||||
{ href: "/commissions", label: "Commissions" },
|
{ href: "/commissions", label: "Commissions" },
|
||||||
{ href: "/ych", label: "YCH / Custom offers" },
|
{ href: "/ych", label: "YCH / Custom offers" },
|
||||||
{ href: "/tos", label: "Terms of Service" },
|
{ href: "/tos", label: "Terms of Service" },
|
||||||
|
@ -54,7 +54,7 @@ export function ImageCard(props: ImageCardProps) {
|
|||||||
? `/api/image/thumbnail/${props.image.fileKey}.webp`
|
? `/api/image/thumbnail/${props.image.fileKey}.webp`
|
||||||
: props.image.url;
|
: props.image.url;
|
||||||
|
|
||||||
console.log(props.image);
|
// console.log(props.image);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/portfolio/${props.image.id}`}>
|
<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