Implement public audit pages
This commit is contained in:
25
app/api/internal/revalidate-public-audit/route.ts
Normal file
25
app/api/internal/revalidate-public-audit/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { revalidatePublicAudit } from "@/lib/audits/public-audit-revalidation";
|
||||
import { parsePublicAuditSlug } from "@/lib/audits/slugs";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const secret = process.env.PUBLIC_AUDIT_REVALIDATION_SECRET;
|
||||
const authorization = request.headers.get("authorization");
|
||||
|
||||
if (!secret || authorization !== `Bearer ${secret}`) {
|
||||
return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = (await request.json().catch(() => null)) as { slug?: unknown } | null;
|
||||
const normalizedSlug =
|
||||
typeof body?.slug === "string" ? parsePublicAuditSlug(body.slug) : null;
|
||||
|
||||
if (!normalizedSlug) {
|
||||
return NextResponse.json({ ok: false, error: "Invalid slug" }, { status: 400 });
|
||||
}
|
||||
|
||||
revalidatePublicAudit(normalizedSlug);
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -1,27 +1,66 @@
|
||||
import { FileText } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import { Suspense } from "react";
|
||||
import { cacheLife, cacheTag } from "next/cache";
|
||||
import { fetchQuery } from "convex/nextjs";
|
||||
|
||||
export default async function PublicAuditPage({
|
||||
params,
|
||||
}: {
|
||||
import { PublicAuditPage } from "@/components/public-audit/public-audit-page";
|
||||
import { PublicAuditStatus } from "@/components/public-audit/public-audit-status";
|
||||
import { api } from "@/convex/_generated/api";
|
||||
import { publicAuditCacheTag } from "@/lib/audits/public-audit-cache";
|
||||
import { toPublicAuditRenderState } from "@/lib/audits/public-audit-presenter";
|
||||
import type { PublicAuditLookupResult } from "@/lib/audits/public-audit-types";
|
||||
import { parsePublicAuditSlug } from "@/lib/audits/slugs";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Website-Audit",
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
googleBot: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type PublicAuditRouteProps = {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
};
|
||||
|
||||
async function getCachedPublicAudit(slug: string): Promise<PublicAuditLookupResult> {
|
||||
"use cache";
|
||||
|
||||
const normalizedSlug = parsePublicAuditSlug(slug);
|
||||
if (!normalizedSlug) {
|
||||
return null;
|
||||
}
|
||||
|
||||
cacheTag(publicAuditCacheTag(normalizedSlug));
|
||||
cacheLife("days");
|
||||
|
||||
return await fetchQuery(api.audits.getPublicBySlug, { slug: normalizedSlug });
|
||||
}
|
||||
|
||||
async function PublicAuditContent({ params }: PublicAuditRouteProps) {
|
||||
const { slug } = await params;
|
||||
const result = await getCachedPublicAudit(slug);
|
||||
const renderState = toPublicAuditRenderState(result);
|
||||
|
||||
if (renderState.kind === "pending") {
|
||||
return <PublicAuditStatus status="pending" />;
|
||||
}
|
||||
|
||||
if (renderState.kind === "unavailable") {
|
||||
return <PublicAuditStatus status="unavailable" />;
|
||||
}
|
||||
|
||||
return <PublicAuditPage audit={renderState.audit} />;
|
||||
}
|
||||
|
||||
export default function PublicAuditRoute({ params }: PublicAuditRouteProps) {
|
||||
return (
|
||||
<main className="flex min-h-dvh items-center justify-center bg-background px-6 py-12">
|
||||
<section className="w-full max-w-2xl rounded-lg border bg-card p-6 text-card-foreground">
|
||||
<FileText className="mb-5 size-6 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Audit: {slug}
|
||||
</p>
|
||||
<h1 className="mt-3 text-3xl font-semibold tracking-normal">
|
||||
Dieser Audit ist noch nicht freigegeben
|
||||
</h1>
|
||||
<p className="mt-4 text-sm leading-6 text-muted-foreground">
|
||||
Sobald der Bericht manuell geprueft und veroeffentlicht wurde,
|
||||
erscheinen hier die freigegebenen Beobachtungen und Empfehlungen.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
<Suspense fallback={<PublicAuditStatus status="pending" />}>
|
||||
<PublicAuditContent params={params} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
7
app/audit/layout.tsx
Normal file
7
app/audit/layout.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function AuditLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return <div className="bg-slate-50 text-slate-950">{children}</div>;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Suspense } from "react";
|
||||
import { ConvexClientProvider } from "@/components/convex-client-provider";
|
||||
import { getToken } from "@/lib/auth-server";
|
||||
import "./globals.css";
|
||||
@@ -19,20 +20,30 @@ export const metadata: Metadata = {
|
||||
description: "Interner Akquise-Agent fuer lokale Webdesign-Leads",
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
async function AuthenticatedConvexProvider({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const token = await getToken();
|
||||
|
||||
return <ConvexClientProvider initialToken={token}>{children}</ConvexClientProvider>;
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="de"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">
|
||||
<ConvexClientProvider initialToken={token}>{children}</ConvexClientProvider>
|
||||
<Suspense fallback={null}>
|
||||
<AuthenticatedConvexProvider>{children}</AuthenticatedConvexProvider>
|
||||
</Suspense>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
14
app/sitemap.ts
Normal file
14
app/sitemap.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
return [
|
||||
{
|
||||
url: siteUrl,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly",
|
||||
priority: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user