feat: integrate feedback module
This commit is contained in:
@@ -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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
79
app/api/feedback/route.ts
Normal 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
43
app/api/whoami/route.ts
Normal 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)' });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user