This commit is contained in:
echo 2026-07-24 04:11:40 +02:00
parent db88626eeb
commit c51ff09cdc
8 changed files with 71 additions and 85 deletions

View File

@ -2,17 +2,27 @@ import { prisma } from "@/lib/prisma";
import { renderTemplate } from "@/lib/templates"; import { renderTemplate } from "@/lib/templates";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { unstable_cache } from "next/cache";
interface Props { interface Props {
params: Promise<{ subdomain: string }>; params: Promise<{ subdomain: string }>;
} }
export async function generateMetadata({ params }: Props): Promise<Metadata> { const getMemorial = unstable_cache(
const { subdomain } = await params; async (subdomain: string) => {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { subdomain }, where: { subdomain },
include: { images: { orderBy: { order: "asc" } } }, include: { images: { orderBy: { order: "asc" } } },
}); });
return user;
},
["memorial"],
{ revalidate: 3600, tags: ["memorial"] }
);
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { subdomain } = await params;
const user = await getMemorial(subdomain);
if (!user || !user.published) { if (!user || !user.published) {
return { title: "Спомен страницата не е пронајдена" }; return { title: "Спомен страницата не е пронајдена" };
@ -36,10 +46,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
export default async function MemorialPage({ params }: Props) { export default async function MemorialPage({ params }: Props) {
const { subdomain } = await params; const { subdomain } = await params;
const user = await prisma.user.findUnique({ const user = await getMemorial(subdomain);
where: { subdomain },
include: { images: { orderBy: { order: "asc" } } },
});
if (!user || !user.published) { if (!user || !user.published) {
notFound(); notFound();

View File

@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server"; import { prisma } from "@/lib/prisma";
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
const slug = req.nextUrl.searchParams.get("slug"); const slug = req.nextUrl.searchParams.get("slug");
@ -8,7 +8,10 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ available: false }); return NextResponse.json({ available: false });
} }
const { prisma } = await import("@/lib/prisma");
const existing = await prisma.user.findUnique({ where: { subdomain: slug } }); const existing = await prisma.user.findUnique({ where: { subdomain: slug } });
return NextResponse.json({ available: !existing });
return NextResponse.json(
{ available: !existing },
{ headers: { "Cache-Control": "no-store" } }
);
} }

View File

@ -1,17 +1,7 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; import { GetObjectCommand } from "@aws-sdk/client-s3";
import { Readable } from "stream";
const s3Client = new S3Client({ import { s3Client, S3_BUCKET } from "@/lib/s3";
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION || "eu-2",
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
},
forcePathStyle: true,
});
const S3_BUCKET = process.env.S3_BUCKET_NAME || "monuments-images";
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
const key = req.nextUrl.searchParams.get("key"); const key = req.nextUrl.searchParams.get("key");
@ -32,13 +22,16 @@ export async function GET(req: NextRequest) {
}) })
); );
const body = await result.Body!.transformToByteArray();
const contentType = result.ContentType || "application/octet-stream"; const contentType = result.ContentType || "application/octet-stream";
const contentLength = result.ContentLength;
return new NextResponse(Buffer.from(body), { const stream = result.Body as Readable;
return new NextResponse(Readable.toWeb(stream) as ReadableStream, {
headers: { headers: {
"Content-Type": contentType, "Content-Type": contentType,
"Cache-Control": "public, max-age=31536000, immutable", "Cache-Control": "public, max-age=31536000, immutable",
...(contentLength && { "Content-Length": String(contentLength) }),
}, },
}); });
} catch (error: unknown) { } catch (error: unknown) {

View File

@ -1,27 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ subdomain: string }> }
) {
const { subdomain } = await params;
const user = await prisma.user.findUnique({
where: { subdomain },
include: { images: { orderBy: { order: "asc" } } },
});
if (!user || !user.published) {
return NextResponse.json({ error: "Monument not found" }, { status: 404 });
}
return NextResponse.json({
id: user.id,
title: user.title,
description: user.description,
subdomain: user.subdomain,
templateId: user.templateId,
images: user.images,
});
}

View File

@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server"; import { auth } from "@clerk/nextjs/server";
import { revalidateTag } from "next/cache";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { generateMonumentQR } from "@/lib/qrcode"; import { generateMonumentQR } from "@/lib/qrcode";
import { getPublicUrl } from "@/lib/upload"; import { getPublicUrl } from "@/lib/upload";
@ -39,6 +40,9 @@ export async function POST(req: NextRequest) {
let user; let user;
if (existingUser) { if (existingUser) {
if (existingUser.subdomain !== subdomain) {
revalidateTag(`memorial-${existingUser.subdomain}`);
}
await prisma.image.deleteMany({ where: { userId: existingUser.id } }); await prisma.image.deleteMany({ where: { userId: existingUser.id } });
user = await prisma.user.update({ user = await prisma.user.update({
where: { clerkId: userId }, where: { clerkId: userId },
@ -83,6 +87,9 @@ export async function POST(req: NextRequest) {
}); });
} }
revalidateTag("memorial");
revalidateTag(`memorial-${subdomain}`);
const qrCode = await generateMonumentQR(subdomain); const qrCode = await generateMonumentQR(subdomain);
return NextResponse.json({ return NextResponse.json({

View File

@ -1,25 +1,9 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server"; import { auth } from "@clerk/nextjs/server";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; import { PutObjectCommand } from "@aws-sdk/client-s3";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { s3Client, S3_BUCKET, getPublicUrl } from "@/lib/s3";
const s3Client = new S3Client({ import { MAX_FILE_SIZE, ALLOWED_TYPES } from "@/lib/upload";
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION || "eu-2",
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
},
forcePathStyle: true,
});
const S3_BUCKET = process.env.S3_BUCKET_NAME || "monuments-images";
const MAX_FILE_SIZE = 5 * 1024 * 1024;
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"];
function getPublicUrl(key: string): string {
return `/api/image?key=${encodeURIComponent(key)}`;
}
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
const { userId } = await auth(); const { userId } = await auth();

View File

@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server"; import { auth } from "@clerk/nextjs/server";
import { revalidateTag } from "next/cache";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { deleteS3Object } from "@/lib/s3"; import { deleteS3Object } from "@/lib/s3";
@ -37,12 +38,17 @@ export async function DELETE(
orderBy: { order: "asc" }, orderBy: { order: "asc" },
}); });
for (let i = 0; i < remainingImages.length; i++) { await prisma.$transaction(
await prisma.image.update({ remainingImages.map((img, i) =>
where: { id: remainingImages[i].id }, prisma.image.update({
where: { id: img.id },
data: { order: i + 1 }, data: { order: i + 1 },
}); })
} )
);
revalidateTag("memorial");
revalidateTag(`memorial-${image.user.subdomain}`);
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error) { } catch (error) {

View File

@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server"; import { auth } from "@clerk/nextjs/server";
import { revalidateTag } from "next/cache";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { deleteS3Object } from "@/lib/s3"; import { deleteS3Object } from "@/lib/s3";
@ -19,16 +20,19 @@ export async function DELETE(req: NextRequest) {
return NextResponse.json({ error: "Споменот не е пронајден" }, { status: 404 }); return NextResponse.json({ error: "Споменот не е пронајден" }, { status: 404 });
} }
for (const image of user.images) { await Promise.all(
try { user.images.map((image) =>
await deleteS3Object(image.key); deleteS3Object(image.key).catch((e) =>
} catch (e) { console.error("Не успеа бришењето на S3 објект:", image.key, e)
console.error("Не успеа бришењето на S3 објект:", image.key, e); )
} )
} );
await prisma.user.delete({ where: { id: user.id } }); await prisma.user.delete({ where: { id: user.id } });
revalidateTag("memorial");
revalidateTag(`memorial-${user.subdomain}`);
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error) { } catch (error) {
console.error("Delete monument error:", error); console.error("Delete monument error:", error);
@ -64,6 +68,9 @@ export async function PUT(req: NextRequest) {
const body = await req.json(); const body = await req.json();
const { title, description, bornDate, passedDate, templateId, published } = body; const { title, description, bornDate, passedDate, templateId, published } = body;
const existingUser = await prisma.user.findUnique({ where: { clerkId: userId } });
const oldSubdomain = existingUser?.subdomain;
const user = await prisma.user.update({ const user = await prisma.user.update({
where: { clerkId: userId }, where: { clerkId: userId },
data: { data: {
@ -77,6 +84,12 @@ export async function PUT(req: NextRequest) {
include: { images: { orderBy: { order: "asc" } } }, include: { images: { orderBy: { order: "asc" } } },
}); });
revalidateTag("memorial");
revalidateTag(`memorial-${user.subdomain}`);
if (oldSubdomain && oldSubdomain !== user.subdomain) {
revalidateTag(`memorial-${oldSubdomain}`);
}
return NextResponse.json({ user }); return NextResponse.json({ user });
} catch (error) { } catch (error) {
console.error("Update error:", error); console.error("Update error:", error);