Copilot/setup sentry nextjs (#58)

* Revise portfolio: warm brown theme, elegant typography, optimized analytics tracking (#55)

* Initial plan

* Update color theme to warm brown and off-white, add elegant fonts, fix analytics tracking

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* Fix 404 page integration with warm theme, update admin console colors, fix font loading

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* Address code review feedback: fix navigation, add utils, improve tracking

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* Fix accessibility and memory leak issues from code review

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* chore: Code cleanup, add Sentry.io monitoring, and documentation (#56)

* Initial plan

* Remove unused code and clean up console statements

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* Remove unused components and fix type issues

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* Wrap console.warn in development check

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* Integrate Sentry.io monitoring and add text editing documentation

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* Initial plan

* feat: Add Sentry configuration files and example pages

- Add sentry.server.config.ts and sentry.edge.config.ts
- Update instrumentation.ts with onRequestError export
- Update instrumentation-client.ts with onRouterTransitionStart export
- Update global-error.tsx to capture exceptions with Sentry
- Create Sentry example page at app/sentry-example-page/page.tsx
- Create Sentry example API route at app/api/sentry-example-api/route.ts

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* feat: Update middleware to allow Sentry example page and fix deprecated API

- Update middleware to exclude /sentry-example-page from locale routing
- Remove deprecated startTransaction API from Sentry example page
- Use consistent DSN configuration with fallback values

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* refactor: Improve Sentry configuration with environment-based sampling

- Add comments explaining DSN fallback values
- Use environment-based tracesSampleRate (10% in production, 100% in dev)
- Address code review feedback for production-safe configuration

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
denshooter
2026-01-22 10:05:43 +01:00
committed by GitHub
parent 33f6d47b3e
commit 377631ee50
33 changed files with 3219 additions and 539 deletions

View File

@@ -44,10 +44,12 @@ export async function POST(request: NextRequest) {
// Ensure URL doesn't have trailing slash before adding /webhook/chat
const baseUrl = n8nWebhookUrl.replace(/\/$/, '');
const webhookUrl = `${baseUrl}/webhook/chat`;
console.log(`Sending to n8n: ${webhookUrl}`, {
hasSecretToken: !!process.env.N8N_SECRET_TOKEN,
hasApiKey: !!process.env.N8N_API_KEY,
});
if (process.env.NODE_ENV === 'development') {
console.log(`Sending to n8n: ${webhookUrl}`, {
hasSecretToken: !!process.env.N8N_SECRET_TOKEN,
hasApiKey: !!process.env.N8N_API_KEY,
});
}
// Add timeout to prevent hanging requests
const controller = new AbortController();
@@ -76,20 +78,24 @@ export async function POST(request: NextRequest) {
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
console.error(`n8n webhook failed with status: ${response.status}`, {
status: response.status,
statusText: response.statusText,
error: errorText,
webhookUrl: webhookUrl.replace(/\/\/[^:]+:[^@]+@/, '//***:***@'), // Hide credentials in logs
});
if (process.env.NODE_ENV === 'development') {
console.error(`n8n webhook failed with status: ${response.status}`, {
status: response.status,
statusText: response.statusText,
error: errorText,
webhookUrl: webhookUrl.replace(/\/\/[^:]+:[^@]+@/, '//***:***@'), // Hide credentials in logs
});
}
throw new Error(`n8n webhook failed: ${response.status} - ${errorText.substring(0, 200)}`);
}
const data = await response.json();
console.log("n8n response data (full):", JSON.stringify(data, null, 2));
console.log("n8n response data type:", typeof data);
console.log("n8n response is array:", Array.isArray(data));
if (process.env.NODE_ENV === 'development') {
console.log("n8n response data (full):", JSON.stringify(data, null, 2));
console.log("n8n response data type:", typeof data);
console.log("n8n response is array:", Array.isArray(data));
}
// Try multiple ways to extract the reply
let reply: string | undefined = undefined;

View File

@@ -43,7 +43,9 @@ export async function GET(request: NextRequest) {
// Rufe den n8n Webhook auf
// Add timestamp to query to bypass Cloudflare cache
const webhookUrl = `${n8nWebhookUrl}/webhook/hardcover/currently-reading?t=${Date.now()}`;
console.log(`Fetching currently reading from: ${webhookUrl}`);
if (process.env.NODE_ENV === 'development') {
console.log(`Fetching currently reading from: ${webhookUrl}`);
}
// Add timeout to prevent hanging requests
const controller = new AbortController();

View File

@@ -31,7 +31,9 @@ export async function GET(request: NextRequest) {
const n8nWebhookUrl = process.env.N8N_WEBHOOK_URL;
if (!n8nWebhookUrl) {
console.warn("N8N_WEBHOOK_URL not configured for status endpoint");
if (process.env.NODE_ENV === 'development') {
console.warn("N8N_WEBHOOK_URL not configured for status endpoint");
}
// Return fallback if n8n is not configured
return NextResponse.json({
status: { text: "offline", color: "gray" },
@@ -44,7 +46,9 @@ export async function GET(request: NextRequest) {
// Rufe den n8n Webhook auf
// Add timestamp to query to bypass Cloudflare cache
const statusUrl = `${n8nWebhookUrl}/webhook/denshooter-71242/status?t=${Date.now()}`;
console.log(`Fetching status from: ${statusUrl}`);
if (process.env.NODE_ENV === 'development') {
console.log(`Fetching status from: ${statusUrl}`);
}
// Add timeout to prevent hanging requests
const controller = new AbortController();
@@ -68,7 +72,9 @@ export async function GET(request: NextRequest) {
if (!res.ok) {
const errorText = await res.text().catch(() => 'Unknown error');
console.error(`n8n status webhook failed: ${res.status}`, errorText);
if (process.env.NODE_ENV === 'development') {
console.error(`n8n status webhook failed: ${res.status}`, errorText);
}
throw new Error(`n8n error: ${res.status} - ${errorText}`);
}
@@ -108,20 +114,24 @@ export async function GET(request: NextRequest) {
} catch (fetchError: unknown) {
clearTimeout(timeoutId);
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
console.error("n8n status webhook request timed out");
} else {
console.error("n8n status webhook fetch error:", fetchError);
if (process.env.NODE_ENV === 'development') {
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
console.error("n8n status webhook request timed out");
} else {
console.error("n8n status webhook fetch error:", fetchError);
}
}
throw fetchError;
}
} catch (error: unknown) {
console.error("Error fetching n8n status:", error);
console.error("Error details:", {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
n8nUrl: process.env.N8N_WEBHOOK_URL ? 'configured' : 'missing',
});
if (process.env.NODE_ENV === 'development') {
console.error("Error fetching n8n status:", error);
console.error("Error details:", {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
n8nUrl: process.env.N8N_WEBHOOK_URL ? 'configured' : 'missing',
});
}
// Leeres Fallback-Objekt, damit die Seite nicht abstürzt
return NextResponse.json({
status: { text: "offline", color: "gray" },

View File

@@ -0,0 +1,11 @@
import * as Sentry from "@sentry/nextjs";
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
// A faulty API route to test Sentry's error monitoring
export function GET() {
const testError = new Error("Sentry Example API Route Error");
Sentry.captureException(testError);
return NextResponse.json({ error: "This is a test error from the API route" }, { status: 500 });
}

View File

@@ -1,5 +1,6 @@
"use client";
import * as Sentry from "@sentry/nextjs";
import { useEffect } from "react";
export default function GlobalError({
@@ -10,6 +11,9 @@ export default function GlobalError({
reset: () => void;
}) {
useEffect(() => {
// Capture exception in Sentry
Sentry.captureException(error);
// Log error details to console
console.error("Global Error:", error);
console.error("Error Name:", error.name);

View File

@@ -2,29 +2,27 @@
@tailwind components;
@tailwind utilities;
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap");
:root {
/* Organic Modern Palette */
--background: #fdfcf8; /* Cream */
--foreground: #292524; /* Warm Grey */
--card: rgba(255, 255, 255, 0.6);
--card-foreground: #292524;
--popover: #ffffff;
--popover-foreground: #292524;
--primary: #292524;
--primary-foreground: #fdfcf8;
--secondary: #e7e5e4;
--secondary-foreground: #292524;
--muted: #f5f5f4;
--muted-foreground: #78716c;
--accent: #f3f1e7; /* Sand */
--accent-foreground: #292524;
--destructive: #ef4444;
--destructive-foreground: #fdfcf8;
--border: #e7e5e4;
--input: #e7e5e4;
--ring: #a7f3d0; /* Mint ring */
/* Warm Brown & Off-White Palette */
--background: #faf8f3; /* Warm off-white */
--foreground: #3e2723; /* Rich brown */
--card: rgba(255, 252, 245, 0.7);
--card-foreground: #3e2723;
--popover: #fffcf5;
--popover-foreground: #3e2723;
--primary: #5d4037; /* Medium brown */
--primary-foreground: #faf8f3;
--secondary: #d7ccc8; /* Light taupe */
--secondary-foreground: #3e2723;
--muted: #efebe9; /* Very light brown */
--muted-foreground: #795548; /* Muted brown */
--accent: #bcaaa4; /* Warm taupe */
--accent-foreground: #3e2723;
--destructive: #d84315; /* Warm red-brown */
--destructive-foreground: #faf8f3;
--border: #d7ccc8;
--input: #efebe9;
--ring: #a1887f; /* Warm brown ring */
--radius: 1rem;
}
@@ -42,8 +40,8 @@ body {
/* Custom Selection */
::selection {
background: #a7f3d0; /* Mint */
color: #292524;
background: var(--primary); /* Rich brown for better contrast */
color: var(--primary-foreground); /* Off-white */
}
/* Smooth Scrolling */
@@ -53,35 +51,35 @@ html {
/* Liquid Glass Effects */
.glass-panel {
background: rgba(255, 255, 255, 0.4);
background: rgba(250, 248, 243, 0.5);
backdrop-filter: blur(12px) saturate(120%);
-webkit-backdrop-filter: blur(12px) saturate(120%);
border: 1px solid rgba(255, 255, 255, 0.7);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(215, 204, 200, 0.5);
box-shadow: 0 8px 32px rgba(62, 39, 35, 0.08);
will-change: backdrop-filter;
}
.glass-card {
background: rgba(255, 255, 255, 0.7);
background: rgba(255, 252, 245, 0.8);
backdrop-filter: blur(24px) saturate(180%);
-webkit-backdrop-filter: blur(24px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.85);
border: 1px solid rgba(215, 204, 200, 0.6);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.03),
0 2px 4px -1px rgba(0, 0, 0, 0.02),
inset 0 0 20px rgba(255, 255, 255, 0.5);
0 4px 6px -1px rgba(62, 39, 35, 0.04),
0 2px 4px -1px rgba(62, 39, 35, 0.03),
inset 0 0 20px rgba(255, 252, 245, 0.5);
transition: all 0.6s cubic-bezier(0.25, 0.1, 0.25, 1);
will-change: transform, box-shadow;
}
.glass-card:hover {
background: rgba(255, 255, 255, 0.8);
background: rgba(255, 252, 245, 0.9);
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.08),
0 10px 10px -5px rgba(0, 0, 0, 0.02),
inset 0 0 20px rgba(255, 255, 255, 0.8);
0 20px 25px -5px rgba(62, 39, 35, 0.1),
0 10px 10px -5px rgba(62, 39, 35, 0.04),
inset 0 0 20px rgba(255, 252, 245, 0.8);
transform: translateY(-4px);
border-color: #ffffff;
border-color: rgba(215, 204, 200, 0.8);
}
/* Typography & Headings */
@@ -91,16 +89,17 @@ h3,
h4,
h5,
h6 {
font-family: var(--font-playfair), Georgia, serif;
letter-spacing: -0.02em;
font-weight: 700;
color: #292524;
color: #3e2723;
}
/* Improve text contrast */
/* Improve text contrast - using foreground variable for WCAG AA compliance */
p,
span,
div {
color: #44403c;
color: var(--foreground); /* #3e2723 - meets WCAG AA standards */
}
/* Hide scrollbar but keep functionality */
@@ -111,11 +110,11 @@ div {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #d6d3d1;
background: #bcaaa4;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a29e;
background: #a1887f;
}
.scrollbar-hide::-webkit-scrollbar {
@@ -153,30 +152,40 @@ div {
/* Markdown Specifics for Blog/Projects */
.markdown h1 {
@apply text-4xl font-bold mb-6 text-stone-900 tracking-tight;
@apply text-4xl font-bold mb-6 tracking-tight;
color: #3e2723;
}
.markdown h2 {
@apply text-2xl font-semibold mt-8 mb-4 text-stone-900 tracking-tight;
@apply text-2xl font-semibold mt-8 mb-4 tracking-tight;
color: #3e2723;
}
.markdown p {
@apply mb-4 leading-relaxed text-stone-700;
@apply mb-4 leading-relaxed;
color: #4e342e;
}
.markdown a {
@apply text-stone-900 underline decoration-liquid-mint decoration-2 underline-offset-2 hover:text-black transition-colors duration-300;
@apply underline decoration-2 underline-offset-2 hover:opacity-80 transition-colors duration-300;
color: #5d4037;
text-decoration-color: #a1887f;
}
.markdown ul {
@apply list-disc list-inside mb-4 space-y-2 text-stone-700;
@apply list-disc list-inside mb-4 space-y-2;
color: #4e342e;
}
.markdown code {
@apply bg-stone-100 px-1.5 py-0.5 rounded text-sm text-stone-900 font-mono;
@apply px-1.5 py-0.5 rounded text-sm font-mono;
background: #efebe9;
color: #3e2723;
}
.markdown pre {
@apply bg-stone-900 text-stone-50 p-4 rounded-xl overflow-x-auto mb-6;
@apply p-4 rounded-xl overflow-x-auto mb-6;
background: #3e2723;
color: #faf8f3;
}
/* Admin Dashboard Styles - Organic Modern */
/* Admin Dashboard Styles - Warm Brown Theme */
.animated-bg {
background: #fdfcf8;
background: #faf8f3;
position: fixed;
top: 0;
left: 0;
@@ -186,30 +195,30 @@ div {
}
.admin-glass {
background: rgba(253, 252, 248, 0.9);
background: rgba(250, 248, 243, 0.95);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid #e7e5e4;
color: #292524;
border-bottom: 1px solid #d7ccc8;
color: #3e2723;
}
.admin-glass-light {
background: #ffffff;
border: 1px solid #e7e5e4;
color: #292524;
background: #fffcf5;
border: 1px solid #d7ccc8;
color: #3e2723;
transition: all 0.2s ease;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
box-shadow: 0 1px 2px rgba(62, 39, 35, 0.05);
}
.admin-glass-light:hover {
background: #fdfcf8;
border-color: #d6d3d1;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
background: #faf8f3;
border-color: #bcaaa4;
box-shadow: 0 4px 6px rgba(62, 39, 35, 0.08);
}
.admin-glass-card {
background: #ffffff;
border: 1px solid #e7e5e4;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
color: #292524;
background: #fffcf5;
border: 1px solid #d7ccc8;
box-shadow: 0 4px 6px -1px rgba(62, 39, 35, 0.06);
color: #3e2723;
}

View File

@@ -1,6 +1,6 @@
import "./globals.css";
import { Metadata } from "next";
import { Inter } from "next/font/google";
import { Inter, Playfair_Display } from "next/font/google";
import React from "react";
import ClientProviders from "./components/ClientProviders";
import { cookies } from "next/headers";
@@ -9,6 +9,15 @@ import { getBaseUrl } from "@/lib/seo";
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
display: "swap",
adjustFontFallback: true,
});
const playfair = Playfair_Display({
variable: "--font-playfair",
subsets: ["latin"],
display: "swap",
adjustFontFallback: true,
});
export default async function RootLayout({
@@ -23,7 +32,7 @@ export default async function RootLayout({
<head>
<meta charSet="utf-8" />
</head>
<body className={inter.variable} suppressHydrationWarning>
<body className={`${inter.variable} ${playfair.variable}`} suppressHydrationWarning>
<ClientProviders>{children}</ClientProviders>
</body>
</html>
@@ -32,11 +41,39 @@ export default async function RootLayout({
export const metadata: Metadata = {
metadataBase: new URL(getBaseUrl()),
title: "Dennis Konkol | Portfolio",
title: {
default: "Dennis Konkol | Portfolio",
template: "%s | Dennis Konkol",
},
description:
"Portfolio of Dennis Konkol, a student and software engineer based in Osnabrück, Germany. Passionate about technology, coding, and solving real-world problems.",
keywords: ["Dennis Konkol", "Software Engineer", "Portfolio", "Student"],
keywords: [
"Dennis Konkol",
"Software Engineer",
"Portfolio",
"Student",
"Web Development",
"Full Stack Developer",
"Osnabrück",
"Germany",
"React",
"Next.js",
"TypeScript",
],
authors: [{ name: "Dennis Konkol", url: "https://dk0.dev" }],
creator: "Dennis Konkol",
publisher: "Dennis Konkol",
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
openGraph: {
title: "Dennis Konkol | Portfolio",
description:
@@ -51,6 +88,7 @@ export const metadata: Metadata = {
alt: "Dennis Konkol Portfolio",
},
],
locale: "en_US",
type: "website",
},
twitter: {
@@ -58,5 +96,12 @@ export const metadata: Metadata = {
title: "Dennis Konkol | Portfolio",
description: "Student & Software Engineer based in Osnabrück, Germany.",
images: ["https://dk0.dev/api/og"],
creator: "@denshooter",
},
verification: {
google: process.env.NEXT_PUBLIC_GOOGLE_VERIFICATION,
},
alternates: {
canonical: "https://dk0.dev",
},
};

View File

@@ -259,10 +259,10 @@ const AdminPage = () => {
// Loading state
if (authState.isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-[#fdfcf8]">
<div className="min-h-screen flex items-center justify-center bg-[#faf8f3]">
<div className="text-center">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-stone-600" />
<p className="text-stone-500">Loading...</p>
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-[#795548]" />
<p className="text-[#5d4037]">Loading...</p>
</div>
</div>
);
@@ -271,13 +271,13 @@ const AdminPage = () => {
// Lockout state
if (authState.isLocked) {
return (
<div className="min-h-screen flex items-center justify-center bg-[#fdfcf8]">
<div className="min-h-screen flex items-center justify-center bg-[#faf8f3]">
<div className="text-center">
<div className="w-16 h-16 bg-red-50 rounded-2xl flex items-center justify-center mx-auto mb-6">
<Lock className="w-8 h-8 text-red-500" />
<div className="w-16 h-16 bg-[#fecaca] rounded-2xl flex items-center justify-center mx-auto mb-6">
<Lock className="w-8 h-8 text-[#d84315]" />
</div>
<h2 className="text-2xl font-bold text-stone-900 mb-2">Account Locked</h2>
<p className="text-stone-500">Too many failed attempts. Please try again in 15 minutes.</p>
<h2 className="text-2xl font-bold text-[#3e2723] mb-2">Account Locked</h2>
<p className="text-[#5d4037]">Too many failed attempts. Please try again in 15 minutes.</p>
<button
onClick={() => {
try {
@@ -287,7 +287,7 @@ const AdminPage = () => {
}
window.location.reload();
}}
className="mt-4 px-6 py-2 bg-stone-900 text-stone-50 rounded-xl hover:bg-stone-800 transition-colors"
className="mt-4 px-6 py-2 bg-[#5d4037] text-[#faf8f3] rounded-xl hover:bg-[#3e2723] transition-colors"
>
Try Again
</button>
@@ -299,20 +299,20 @@ const AdminPage = () => {
// Login form
if (authState.showLogin || !authState.isAuthenticated) {
return (
<div className="min-h-screen flex items-center justify-center relative overflow-hidden bg-[#fdfcf8] z-0">
<div className="min-h-screen flex items-center justify-center relative overflow-hidden bg-[#faf8f3] z-0">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="w-full max-w-md p-6"
>
<div className="bg-white/80 backdrop-blur-xl rounded-3xl p-8 border border-stone-200 shadow-2xl relative z-10">
<div className="bg-[#fffcf5] backdrop-blur-xl rounded-3xl p-8 border border-[#d7ccc8] shadow-2xl relative z-10">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-[#f3f1e7] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-sm border border-stone-100">
<Lock className="w-6 h-6 text-stone-600" />
<div className="w-16 h-16 bg-[#efebe9] rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-sm border border-[#d7ccc8]">
<Lock className="w-6 h-6 text-[#5d4037]" />
</div>
<h1 className="text-2xl font-bold text-stone-900 mb-2 tracking-tight">Admin Access</h1>
<p className="text-stone-500">Enter your password to continue</p>
<h1 className="text-2xl font-bold text-[#3e2723] mb-2 tracking-tight">Admin Access</h1>
<p className="text-[#5d4037]">Enter your password to continue</p>
</div>
<form onSubmit={handleLogin} className="space-y-5">
@@ -323,13 +323,13 @@ const AdminPage = () => {
value={authState.password}
onChange={(e) => setAuthState(prev => ({ ...prev, password: e.target.value }))}
placeholder="Enter password"
className="w-full px-4 py-3.5 bg-white border border-stone-200 rounded-xl text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-stone-200 focus:border-stone-400 transition-all shadow-sm"
className="w-full px-4 py-3.5 bg-white border border-[#d7ccc8] rounded-xl text-[#3e2723] placeholder:text-[#a1887f] focus:outline-none focus:ring-2 focus:ring-[#bcaaa4] focus:border-[#5d4037] transition-all shadow-sm"
disabled={authState.isLoading}
/>
<button
type="button"
onClick={() => setAuthState(prev => ({ ...prev, showPassword: !prev.showPassword }))}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-stone-400 hover:text-stone-600 p-1"
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-[#a1887f] hover:text-[#5d4037] p-1"
>
{authState.showPassword ? '👁️' : '👁️‍🗨️'}
</button>
@@ -338,9 +338,9 @@ const AdminPage = () => {
<motion.p
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
className="mt-2 text-red-500 text-sm font-medium flex items-center"
className="mt-2 text-[#d84315] text-sm font-medium flex items-center"
>
<span className="w-1.5 h-1.5 bg-red-500 rounded-full mr-2" />
<span className="w-1.5 h-1.5 bg-[#d84315] rounded-full mr-2" />
{authState.error}
</motion.p>
)}
@@ -349,15 +349,15 @@ const AdminPage = () => {
<button
type="submit"
disabled={authState.isLoading || !authState.password}
className="w-full bg-stone-900 text-stone-50 py-3.5 px-6 rounded-xl font-semibold text-lg hover:bg-stone-800 focus:outline-none focus:ring-2 focus:ring-stone-200 focus:ring-offset-2 focus:ring-offset-white disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-lg flex items-center justify-center"
className="w-full bg-[#5d4037] text-[#faf8f3] py-3.5 px-6 rounded-xl font-semibold text-lg hover:bg-[#3e2723] focus:outline-none focus:ring-2 focus:ring-[#bcaaa4] focus:ring-offset-2 focus:ring-offset-white disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-lg flex items-center justify-center"
>
{authState.isLoading ? (
<div className="flex items-center justify-center space-x-2">
<Loader2 className="w-5 h-5 animate-spin" />
<span className="text-stone-50">Authenticating...</span>
<span className="text-[#faf8f3]">Authenticating...</span>
</div>
) : (
<span className="text-stone-50">Sign In</span>
<span className="text-[#faf8f3]">Sign In</span>
)}
</button>
</form>

View File

@@ -1,32 +1,14 @@
"use client";
import { useEffect, useState } from "react";
import dynamic from "next/dynamic";
// Dynamically import KernelPanic404Wrapper to avoid SSR issues
const KernelPanic404 = dynamic(() => import("./components/KernelPanic404Wrapper"), {
ssr: false,
loading: () => (
<div style={{
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#020202",
color: "#33ff00",
fontFamily: "monospace"
}}>
<div>Loading terminal...</div>
</div>
),
});
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Home, ArrowLeft, Search } from "lucide-react";
export default function NotFound() {
const [mounted, setMounted] = useState(false);
const [input, setInput] = useState("");
const router = useRouter();
useEffect(() => {
setMounted(true);
@@ -43,47 +25,126 @@ export default function NotFound() {
if (!mounted) {
return (
<div style={{
position: "fixed",
top: 0,
left: 0,
width: "100vw",
height: "100vh",
margin: 0,
padding: 0,
overflow: "hidden",
backgroundColor: "#020202",
zIndex: 9998
}}>
<div style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
color: "#33ff00",
fontFamily: "monospace"
}}>
Loading terminal...
<div className="min-h-screen flex items-center justify-center bg-[#faf8f3]">
<div className="text-center">
<div className="text-[#795548]">Loading...</div>
</div>
</div>
);
}
const handleCommand = (cmd: string) => {
const command = cmd.toLowerCase().trim();
if (command === 'home' || command === 'cd ~' || command === 'cd /') {
router.push('/');
} else if (command === 'back' || command === 'cd ..') {
router.back();
} else if (command === 'search') {
router.push('/projects');
}
};
return (
<div style={{
position: "fixed",
top: 0,
left: 0,
width: "100vw",
height: "100vh",
margin: 0,
padding: 0,
overflow: "hidden",
backgroundColor: "#020202",
zIndex: 9998
}}>
<KernelPanic404 />
<div className="min-h-screen flex items-center justify-center bg-[#faf8f3] p-4">
<div className="w-full max-w-2xl">
{/* Terminal-style 404 */}
<div className="bg-[#3e2723] rounded-2xl shadow-2xl overflow-hidden border border-[#5d4037]">
{/* Terminal Header */}
<div className="bg-[#5d4037] px-4 py-3 flex items-center gap-2 border-b border-[#795548]">
<div className="flex gap-2">
<div className="w-3 h-3 rounded-full bg-[#d84315]"></div>
<div className="w-3 h-3 rounded-full bg-[#bcaaa4]"></div>
<div className="w-3 h-3 rounded-full bg-[#a1887f]"></div>
</div>
<div className="ml-4 text-[#faf8f3] text-sm font-mono">
terminal@portfolio ~ 404
</div>
</div>
{/* Terminal Body */}
<div className="p-6 md:p-8 font-mono text-sm md:text-base">
<div className="mb-6">
<div className="text-[#bcaaa4] mb-2">$ cd {mounted ? window.location.pathname : '/unknown'}</div>
<div className="text-[#d84315] mb-4">
<span className="mr-2"></span>
Error: ENOENT: no such file or directory
</div>
<div className="text-[#a1887f] mb-6">
<pre className="whitespace-pre-wrap">
{`
██╗ ██╗ ██████╗ ██╗ ██╗
██║ ██║██╔═████╗██║ ██║
███████║██║██╔██║███████║
╚════██║████╔╝██║╚════██║
██║╚██████╔╝ ██║
╚═╝ ╚═════╝ ╚═╝
`}
</pre>
</div>
<div className="text-[#faf8f3] mb-6">
<p className="mb-3">The page you&apos;re looking for seems to have wandered off.</p>
<p className="text-[#bcaaa4]">Perhaps it never existed, or maybe it&apos;s on a coffee break.</p>
</div>
<div className="mb-6 text-[#a1887f]">
<div className="mb-2">Available commands:</div>
<div className="pl-4 space-y-1 text-sm">
<div> <span className="text-[#faf8f3]">home</span> - Return to homepage</div>
<div> <span className="text-[#faf8f3]">back</span> - Go back to previous page</div>
<div> <span className="text-[#faf8f3]">search</span> - Search the website</div>
</div>
</div>
</div>
{/* Interactive Command Line */}
<div className="flex items-center gap-2 border-t border-[#5d4037] pt-4">
<span className="text-[#a1887f]">$</span>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleCommand(input);
setInput('');
}
}}
placeholder="Type a command..."
className="flex-1 bg-transparent text-[#faf8f3] outline-none placeholder:text-[#795548] font-mono"
autoFocus
/>
</div>
</div>
</div>
{/* Quick Action Buttons */}
<div className="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4">
<Link
href="/"
className="flex items-center justify-center gap-2 bg-[#fffcf5] hover:bg-[#faf8f3] border border-[#d7ccc8] rounded-xl px-6 py-4 transition-all hover:shadow-md group"
>
<Home className="w-5 h-5 text-[#5d4037] group-hover:text-[#3e2723]" />
<span className="text-[#3e2723] font-medium">Home</span>
</Link>
<button
onClick={() => router.back()}
className="flex items-center justify-center gap-2 bg-[#fffcf5] hover:bg-[#faf8f3] border border-[#d7ccc8] rounded-xl px-6 py-4 transition-all hover:shadow-md group"
>
<ArrowLeft className="w-5 h-5 text-[#5d4037] group-hover:text-[#3e2723]" />
<span className="text-[#3e2723] font-medium">Go Back</span>
</button>
<Link
href="/projects"
className="flex items-center justify-center gap-2 bg-[#fffcf5] hover:bg-[#faf8f3] border border-[#d7ccc8] rounded-xl px-6 py-4 transition-all hover:shadow-md group"
>
<Search className="w-5 h-5 text-[#5d4037] group-hover:text-[#3e2723]" />
<span className="text-[#3e2723] font-medium">Explore Projects</span>
</Link>
</div>
</div>
</div>
);
}

View File

@@ -173,6 +173,32 @@ export default function PrivacyPolicy() {
Nutzungsstatistiken erfassen (z.B. Seitenaufrufe, Performance-Metriken),
die erst nach deiner Einwilligung im Consent-Banner aktiviert werden.
</p>
<h2 className="text-2xl font-semibold mt-6">Error Monitoring (Sentry)</h2>
<p className="mt-2">
Um Fehler und Probleme auf dieser Website schnell zu erkennen und zu beheben,
nutze ich Sentry.io, einen Dienst zur Fehlerüberwachung. Dabei werden technische
Informationen wie Browser-Typ, Betriebssystem, URL der aufgerufenen Seite und
Fehlermeldungen an Sentry übermittelt. Diese Daten dienen ausschließlich der
Verbesserung der Website-Stabilität und werden nicht für andere Zwecke verwendet.
<br />
<br />
Anbieter: Functional Software, Inc. (Sentry), 45 Fremont Street, 8th Floor,
San Francisco, CA 94105, USA
<br />
<br />
Rechtsgrundlage: Art. 6 Abs. 1 S. 1 lit. f DSGVO (berechtigtes Interesse an
der Fehleranalyse und Systemstabilität).
<br />
<br />
Weitere Informationen: <Link
className="text-blue-700 transition-underline"
href={"https://sentry.io/privacy/"}
>
Sentry Datenschutzerklärung
</Link>
</p>
<h2 className="text-2xl font-semibold mt-6">Kontaktformular</h2>
<p className="mt-2">
Wenn Sie das Kontaktformular nutzen, werden Ihre Angaben zur

View File

@@ -0,0 +1,81 @@
"use client";
import Head from "next/head";
import * as Sentry from "@sentry/nextjs";
export default function SentryExamplePage() {
return (
<div>
<Head>
<title>Sentry Onboarding</title>
<meta name="description" content="Test Sentry for your Next.js app!" />
</Head>
<main
style={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
padding: "2rem",
}}
>
<h1 style={{ fontSize: "2rem", fontWeight: "bold", marginBottom: "1rem" }}>
Sentry Onboarding
</h1>
<p style={{ marginBottom: "1rem" }}>
Get started by sending us a sample error:
</p>
<button
type="button"
style={{
padding: "0.5rem 1rem",
backgroundColor: "#0070f3",
color: "white",
border: "none",
borderRadius: "0.25rem",
cursor: "pointer",
}}
onClick={async () => {
Sentry.captureException(new Error("This is your first error!"));
try {
const res = await fetch("/api/sentry-example-api");
if (!res.ok) {
throw new Error("Sentry Example API Error");
}
} catch (err) {
Sentry.captureException(err);
}
}}
>
Throw error!
</button>
<p style={{ marginTop: "2rem", fontSize: "0.875rem", color: "#666" }}>
Next, look for the error on the{" "}
<a
style={{ color: "#0070f3", textDecoration: "underline" }}
href="https://dk0.sentry.io/issues/?project=4510751388926032"
target="_blank"
rel="noopener noreferrer"
>
Issues Page
</a>
</p>
<p style={{ fontSize: "0.875rem", color: "#666" }}>
For more information, see{" "}
<a
style={{ color: "#0070f3", textDecoration: "underline" }}
href="https://docs.sentry.io/platforms/javascript/guides/nextjs/"
target="_blank"
rel="noopener noreferrer"
>
https://docs.sentry.io/platforms/javascript/guides/nextjs/
</a>
</p>
</main>
</div>
);
}