diff --git a/src/app/[subdomain]/page.tsx b/src/app/[subdomain]/page.tsx index 2e1b99d..287924f 100644 --- a/src/app/[subdomain]/page.tsx +++ b/src/app/[subdomain]/page.tsx @@ -2,17 +2,27 @@ import { prisma } from "@/lib/prisma"; import { renderTemplate } from "@/lib/templates"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; +import { unstable_cache } from "next/cache"; interface Props { params: Promise<{ subdomain: string }>; } +const getMemorial = unstable_cache( + async (subdomain: string) => { + const user = await prisma.user.findUnique({ + where: { subdomain }, + include: { images: { orderBy: { order: "asc" } } }, + }); + return user; + }, + ["memorial"], + { revalidate: 3600, tags: ["memorial"] } +); + export async function generateMetadata({ params }: Props): Promise { const { subdomain } = await params; - const user = await prisma.user.findUnique({ - where: { subdomain }, - include: { images: { orderBy: { order: "asc" } } }, - }); + const user = await getMemorial(subdomain); if (!user || !user.published) { return { title: "Спомен страницата не е пронајдена" }; @@ -36,10 +46,7 @@ export async function generateMetadata({ params }: Props): Promise { export default async function MemorialPage({ params }: Props) { const { subdomain } = await params; - const user = await prisma.user.findUnique({ - where: { subdomain }, - include: { images: { orderBy: { order: "asc" } } }, - }); + const user = await getMemorial(subdomain); if (!user || !user.published) { notFound(); diff --git a/src/app/api/check-subdomain/route.ts b/src/app/api/check-subdomain/route.ts index 3aac807..b064ab6 100644 --- a/src/app/api/check-subdomain/route.ts +++ b/src/app/api/check-subdomain/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { auth } from "@clerk/nextjs/server"; +import { prisma } from "@/lib/prisma"; export async function GET(req: NextRequest) { const slug = req.nextUrl.searchParams.get("slug"); @@ -8,7 +8,10 @@ export async function GET(req: NextRequest) { return NextResponse.json({ available: false }); } - const { prisma } = await import("@/lib/prisma"); const existing = await prisma.user.findUnique({ where: { subdomain: slug } }); - return NextResponse.json({ available: !existing }); + + return NextResponse.json( + { available: !existing }, + { headers: { "Cache-Control": "no-store" } } + ); } \ No newline at end of file diff --git a/src/app/api/image/route.ts b/src/app/api/image/route.ts index 426d6ba..a5d245c 100644 --- a/src/app/api/image/route.ts +++ b/src/app/api/image/route.ts @@ -1,17 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; -import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; - -const s3Client = new S3Client({ - 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"; +import { GetObjectCommand } from "@aws-sdk/client-s3"; +import { Readable } from "stream"; +import { s3Client, S3_BUCKET } from "@/lib/s3"; export async function GET(req: NextRequest) { 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 contentLength = result.ContentLength; - return new NextResponse(Buffer.from(body), { + const stream = result.Body as Readable; + + return new NextResponse(Readable.toWeb(stream) as ReadableStream, { headers: { "Content-Type": contentType, "Cache-Control": "public, max-age=31536000, immutable", + ...(contentLength && { "Content-Length": String(contentLength) }), }, }); } catch (error: unknown) { diff --git a/src/app/api/monument/[subdomain]/route.ts b/src/app/api/monument/[subdomain]/route.ts deleted file mode 100644 index 426ed20..0000000 --- a/src/app/api/monument/[subdomain]/route.ts +++ /dev/null @@ -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, - }); -} \ No newline at end of file diff --git a/src/app/api/publish/route.ts b/src/app/api/publish/route.ts index 6681512..b8449ef 100644 --- a/src/app/api/publish/route.ts +++ b/src/app/api/publish/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { auth } from "@clerk/nextjs/server"; +import { revalidateTag } from "next/cache"; import { prisma } from "@/lib/prisma"; import { generateMonumentQR } from "@/lib/qrcode"; import { getPublicUrl } from "@/lib/upload"; @@ -39,6 +40,9 @@ export async function POST(req: NextRequest) { let user; if (existingUser) { + if (existingUser.subdomain !== subdomain) { + revalidateTag(`memorial-${existingUser.subdomain}`); + } await prisma.image.deleteMany({ where: { userId: existingUser.id } }); user = await prisma.user.update({ where: { clerkId: userId }, @@ -83,6 +87,9 @@ export async function POST(req: NextRequest) { }); } + revalidateTag("memorial"); + revalidateTag(`memorial-${subdomain}`); + const qrCode = await generateMonumentQR(subdomain); return NextResponse.json({ diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index faf8f65..381106a 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -1,25 +1,9 @@ import { NextRequest, NextResponse } from "next/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"; - -const s3Client = new S3Client({ - 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)}`; -} +import { s3Client, S3_BUCKET, getPublicUrl } from "@/lib/s3"; +import { MAX_FILE_SIZE, ALLOWED_TYPES } from "@/lib/upload"; export async function POST(req: NextRequest) { const { userId } = await auth(); diff --git a/src/app/api/user/monument/image/[imageId]/route.ts b/src/app/api/user/monument/image/[imageId]/route.ts index 8210fc7..144195a 100644 --- a/src/app/api/user/monument/image/[imageId]/route.ts +++ b/src/app/api/user/monument/image/[imageId]/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { auth } from "@clerk/nextjs/server"; +import { revalidateTag } from "next/cache"; import { prisma } from "@/lib/prisma"; import { deleteS3Object } from "@/lib/s3"; @@ -37,12 +38,17 @@ export async function DELETE( orderBy: { order: "asc" }, }); - for (let i = 0; i < remainingImages.length; i++) { - await prisma.image.update({ - where: { id: remainingImages[i].id }, - data: { order: i + 1 }, - }); - } + await prisma.$transaction( + remainingImages.map((img, i) => + prisma.image.update({ + where: { id: img.id }, + data: { order: i + 1 }, + }) + ) + ); + + revalidateTag("memorial"); + revalidateTag(`memorial-${image.user.subdomain}`); return NextResponse.json({ success: true }); } catch (error) { diff --git a/src/app/api/user/monument/route.ts b/src/app/api/user/monument/route.ts index f338417..e3b18ad 100644 --- a/src/app/api/user/monument/route.ts +++ b/src/app/api/user/monument/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { auth } from "@clerk/nextjs/server"; +import { revalidateTag } from "next/cache"; import { prisma } from "@/lib/prisma"; import { deleteS3Object } from "@/lib/s3"; @@ -19,16 +20,19 @@ export async function DELETE(req: NextRequest) { return NextResponse.json({ error: "Споменот не е пронајден" }, { status: 404 }); } - for (const image of user.images) { - try { - await deleteS3Object(image.key); - } catch (e) { - console.error("Не успеа бришењето на S3 објект:", image.key, e); - } - } + await Promise.all( + user.images.map((image) => + deleteS3Object(image.key).catch((e) => + console.error("Не успеа бришењето на S3 објект:", image.key, e) + ) + ) + ); await prisma.user.delete({ where: { id: user.id } }); + revalidateTag("memorial"); + revalidateTag(`memorial-${user.subdomain}`); + return NextResponse.json({ success: true }); } catch (error) { console.error("Delete monument error:", error); @@ -64,6 +68,9 @@ export async function PUT(req: NextRequest) { const body = await req.json(); 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({ where: { clerkId: userId }, data: { @@ -77,6 +84,12 @@ export async function PUT(req: NextRequest) { include: { images: { orderBy: { order: "asc" } } }, }); + revalidateTag("memorial"); + revalidateTag(`memorial-${user.subdomain}`); + if (oldSubdomain && oldSubdomain !== user.subdomain) { + revalidateTag(`memorial-${oldSubdomain}`); + } + return NextResponse.json({ user }); } catch (error) { console.error("Update error:", error);