feat: integrate feedback module

This commit is contained in:
2026-02-08 21:48:55 +01:00
parent 453a603392
commit 7ec826dae3
48 changed files with 4413 additions and 1168 deletions

View File

@@ -9,10 +9,10 @@ import { getOGImageMetadata } from '@/lib/metadata';
import { SITE_URL } from '@/lib/schema';
interface PageProps {
params: {
params: Promise<{
locale: string;
slug: string;
};
}>;
}
export async function generateStaticParams() {
@@ -29,7 +29,8 @@ export async function generateStaticParams() {
return params;
}
export async function generateMetadata({ params: { locale, slug } }: PageProps): Promise<Metadata> {
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { locale, slug } = await params;
const pageData = await getPageBySlug(slug, locale);
if (!pageData) return {};
@@ -59,7 +60,8 @@ export async function generateMetadata({ params: { locale, slug } }: PageProps):
};
}
export default async function StandardPage({ params: { locale, slug } }: PageProps) {
export default async function StandardPage({ params }: PageProps) {
const { locale, slug } = await params;
const pageData = await getPageBySlug(slug, locale);
const t = await getTranslations('StandardPage');

View File

@@ -14,15 +14,14 @@ import { Heading } from '@/components/ui';
import { getOGImageMetadata } from '@/lib/metadata';
interface BlogPostProps {
params: {
params: Promise<{
locale: string;
slug: string;
};
}>;
}
export async function generateMetadata({
params: { locale, slug },
}: BlogPostProps): Promise<Metadata> {
export async function generateMetadata({ params }: BlogPostProps): Promise<Metadata> {
const { locale, slug } = await params;
const post = await getPostBySlug(slug, locale);
if (!post) return {};
@@ -56,7 +55,8 @@ export async function generateMetadata({
};
}
export default async function BlogPost({ params: { locale, slug } }: BlogPostProps) {
export default async function BlogPost({ params }: BlogPostProps) {
const { locale, slug } = await params;
const post = await getPostBySlug(slug, locale);
const { prev, next } = await getAdjacentPosts(slug, locale);

View File

@@ -7,12 +7,13 @@ import { getOGImageMetadata } from '@/lib/metadata';
import { SITE_URL } from '@/lib/schema';
interface BlogIndexProps {
params: {
params: Promise<{
locale: string;
};
}>;
}
export async function generateMetadata({ params: { locale } }: BlogIndexProps) {
export async function generateMetadata({ params }: BlogIndexProps) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
return {
title: t('title'),
@@ -39,7 +40,8 @@ export async function generateMetadata({ params: { locale } }: BlogIndexProps) {
};
}
export default async function BlogIndex({ params: { locale } }: BlogIndexProps) {
export default async function BlogIndex({ params }: BlogIndexProps) {
const { locale } = await params;
const t = await getTranslations('Blog');
const posts = await getAllPosts(locale);

View File

@@ -3,6 +3,7 @@ import Header from '@/components/Header';
import JsonLd from '@/components/JsonLd';
import AnalyticsProvider from '@/components/analytics/AnalyticsProvider';
import CMSConnectivityNotice from '@/components/CMSConnectivityNotice';
import { FeedbackOverlay } from '@/components/feedback/FeedbackOverlay';
import { Metadata, Viewport } from 'next';
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
@@ -32,27 +33,38 @@ export const viewport: Viewport = {
export default async function LocaleLayout({
children,
params: { locale },
params,
}: {
children: React.ReactNode;
params: { locale: string };
params: Promise<{ locale: string }>;
}) {
// Providing all messages to the client
// side is the easiest way to get started
const messages = await getMessages();
const { locale } = await params;
// Ensure locale is a valid string, fallback to 'en'
const supportedLocales = ['en', 'de'];
const localeStr = (typeof locale === 'string' ? locale : '').trim();
const safeLocale = supportedLocales.includes(localeStr) ? localeStr : 'en';
let messages = {};
try {
messages = await getMessages();
} catch (error) {
console.error(`Failed to load messages for locale '${safeLocale}':`, error);
messages = {};
}
return (
<html lang={locale} className="scroll-smooth overflow-x-hidden">
<html lang={safeLocale} className="scroll-smooth overflow-x-hidden">
<body className="flex flex-col min-h-screen font-sans selection:bg-accent selection:text-primary-dark antialiased overflow-x-hidden">
<NextIntlClientProvider messages={messages} locale={locale}>
<NextIntlClientProvider messages={messages} locale={safeLocale}>
<JsonLd />
<Header />
<main className="flex-grow animate-fade-in overflow-visible">{children}</main>
<Footer />
<CMSConnectivityNotice />
{/* Sends pageviews for client-side navigations */}
<AnalyticsProvider />
{config.feedbackEnabled && <FeedbackOverlay />}
</NextIntlClientProvider>
</body>
</html>

View File

@@ -15,7 +15,12 @@ import { getTranslations } from 'next-intl/server';
import { Metadata } from 'next';
import { getOGImageMetadata } from '@/lib/metadata';
export default function HomePage({ params: { locale } }: { params: { locale: string } }) {
export default async function HomePage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return (
<div className="flex flex-col min-h-screen">
<JsonLd
@@ -55,10 +60,11 @@ export default function HomePage({ params: { locale } }: { params: { locale: str
}
export async function generateMetadata({
params: { locale },
params,
}: {
params: { locale: string };
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
// Use translations for meta where available (namespace: Index.meta)
// Fallback to a sensible default if translation keys are missing.
let t;

View File

@@ -9,12 +9,13 @@ import Reveal from '@/components/Reveal';
import Gallery from '@/components/team/Gallery';
interface TeamPageProps {
params: {
params: Promise<{
locale: string;
};
}>;
}
export async function generateMetadata({ params: { locale } }: TeamPageProps): Promise<Metadata> {
export async function generateMetadata({ params }: TeamPageProps): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Team' });
const title = t('meta.title') || t('hero.subtitle');
const description = t('meta.description') || t('hero.title');
@@ -43,7 +44,8 @@ export async function generateMetadata({ params: { locale } }: TeamPageProps): P
};
}
export default async function TeamPage({ params: { locale } }: TeamPageProps) {
export default async function TeamPage({ params }: TeamPageProps) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Team' });
return (

79
app/api/feedback/route.ts Normal file
View File

@@ -0,0 +1,79 @@
import { NextRequest, NextResponse } from 'next/server';
import { createDirectus, rest, authentication, staticToken, createItem, readItems } from '@directus/sdk';
import { config } from '@/lib/config';
async function getAuthenticatedClient() {
const { url, token: rawToken } = config.infraCMS;
const effectiveUrl = url;
const token = rawToken?.trim();
if (!token) {
throw new Error('INFRA_DIRECTUS_TOKEN is not configured');
}
const client = createDirectus(effectiveUrl)
.with(staticToken(token))
.with(rest());
return client;
}
export async function GET() {
try {
const client = await getAuthenticatedClient();
const items = await client.request(readItems('visual_feedback', {
fields: ['*'],
sort: ['-date_created'],
}));
return NextResponse.json(items);
} catch (error: any) {
const errMsg = error.errors?.[0]?.message || error.message || 'Unknown Directus Error';
console.error('Error fetching feedback:', {
msg: errMsg,
url: config.infraCMS.url,
status: error.response?.status,
errors: error.errors
});
return NextResponse.json({ error: errMsg }, { status: 500 });
}
}
export async function POST(req: NextRequest) {
try {
const client = await getAuthenticatedClient();
const body = await req.json();
const { action, ...data } = body;
if (action === 'reply') {
const reply = await client.request(createItem('visual_feedback_comments', {
feedback_id: data.feedbackId,
user_name: data.userName,
text: data.text,
}));
return NextResponse.json(reply);
}
const feedback = await client.request(createItem('visual_feedback', {
project: 'klz-cables',
url: data.url,
selector: data.selector,
x: data.x,
y: data.y,
type: data.type,
text: data.text,
user_name: data.userName,
user_identity: data.userIdentity,
}));
return NextResponse.json(feedback);
} catch (error: any) {
const errMsg = error.errors?.[0]?.message || error.message || 'Unknown Directus Error';
console.error('Error saving feedback:', {
msg: errMsg,
url: config.infraCMS.url,
status: error.response?.status,
errors: error.errors
});
return NextResponse.json({ error: errMsg }, { status: 500 });
}
}

43
app/api/whoami/route.ts Normal file
View File

@@ -0,0 +1,43 @@
import { NextRequest, NextResponse } from 'next/server';
import { envSchema, getRawEnv } from '@/lib/env';
export async function GET(req: NextRequest) {
const env = envSchema.parse(getRawEnv());
const gatekeeperUrl = env.GATEKEEPER_URL;
const host = req.headers.get('host') || '';
const { searchParams } = new URL(req.url);
const hasBypassParam = searchParams.get('gatekeeper_bypass') === 'true';
const isLocal = host.includes('localhost') || host.includes('127.0.0.1') || host.includes('klz.localhost');
const isBypassEnabled = hasBypassParam || env.GATEKEEPER_BYPASS_ENABLED || (env.NODE_ENV === 'development' && isLocal);
// If bypass is enabled or we are in local development, use "Dev-Admin" identity.
if (isBypassEnabled) {
return NextResponse.json({
authenticated: true,
identity: 'Dev-Admin',
isDevFallback: true
});
}
try {
// We forward the cookie header to gatekeeper so it can identify the session
const response = await fetch(`${gatekeeperUrl}/api/whoami`, {
headers: {
cookie: req.headers.get('cookie') || '',
},
cache: 'no-store',
});
if (!response.ok) {
return NextResponse.json({ authenticated: false, identity: 'Guest' });
}
const data = await response.json();
return NextResponse.json(data);
} catch (error: any) {
console.error('Error proxying to gatekeeper:', error);
return NextResponse.json({ authenticated: false, identity: 'Guest (Auth Error)' });
}
}