From c5efd2838359ca0a708c7a078dd8a04fc2231eff Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 7 Jan 2026 23:13:25 +0100 Subject: [PATCH] full upgrade --- app/api/analytics/route.ts | 30 ++++++-- app/api/contacts/[id]/route.tsx | 62 ++++++++++++++- app/api/contacts/route.tsx | 48 +++++++++++- app/api/email/route.tsx | 45 ++++++----- app/api/n8n/status/route.ts | 25 +++++- app/api/projects/[id]/route.ts | 86 ++++++++++++++++++++- app/api/projects/route.ts | 48 +++++++++++- app/components/About.tsx | 45 ++++++----- app/components/ActivityFeed.tsx | 42 +++++++--- app/components/Contact.tsx | 8 +- app/components/Header.tsx | 22 ++++-- app/components/Hero.tsx | 4 +- app/components/Projects.tsx | 4 +- app/editor/page.tsx | 101 ++++++++++-------------- app/layout.tsx | 17 ++-- app/legal-notice/page.tsx | 28 +++---- app/page.tsx | 33 +++++--- app/privacy-policy/page.tsx | 20 ++--- app/projects/[slug]/page.tsx | 8 +- app/projects/page.tsx | 13 ++-- components/ErrorBoundary.tsx | 84 ++++++++++++++++++++ lib/redis.ts | 132 ++++++++++++++++++++++++-------- package-lock.json | 14 ++-- 23 files changed, 693 insertions(+), 226 deletions(-) create mode 100644 components/ErrorBoundary.tsx diff --git a/app/api/analytics/route.ts b/app/api/analytics/route.ts index 6d3b813..650f4a6 100644 --- a/app/api/analytics/route.ts +++ b/app/api/analytics/route.ts @@ -1,21 +1,41 @@ import { NextRequest, NextResponse } from 'next/server'; +import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; export async function POST(request: NextRequest) { try { + // Rate limiting for POST requests + const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; + if (!checkRateLimit(ip, 30, 60000)) { // 30 requests per minute for analytics + return new NextResponse( + JSON.stringify({ error: 'Rate limit exceeded' }), + { + status: 429, + headers: { + 'Content-Type': 'application/json', + ...getRateLimitHeaders(ip, 30, 60000) + } + } + ); + } + const body = await request.json(); // Log performance metrics (you can extend this to store in database) - console.log('Performance Metric:', { - timestamp: new Date().toISOString(), - ...body, - }); + if (process.env.NODE_ENV === 'development') { + console.log('Performance Metric:', { + timestamp: new Date().toISOString(), + ...body, + }); + } // You could store this in a database or send to external service // For now, we'll just log it since Umami handles the main analytics return NextResponse.json({ success: true }); } catch (error) { - console.error('Analytics API Error:', error); + if (process.env.NODE_ENV === 'development') { + console.error('Analytics API Error:', error); + } return NextResponse.json( { error: 'Failed to process analytics data' }, { status: 500 } diff --git a/app/api/contacts/[id]/route.tsx b/app/api/contacts/[id]/route.tsx index 5092965..cd6646a 100644 --- a/app/api/contacts/[id]/route.tsx +++ b/app/api/contacts/[id]/route.tsx @@ -1,5 +1,7 @@ import { type NextRequest, NextResponse } from "next/server"; import { PrismaClient } from '@prisma/client'; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; +import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; const prisma = new PrismaClient(); @@ -8,6 +10,21 @@ export async function PUT( { params }: { params: Promise<{ id: string }> } ) { try { + // Rate limiting for PUT requests + const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; + if (!checkRateLimit(ip, 5, 60000)) { // 5 requests per minute + return new NextResponse( + JSON.stringify({ error: 'Rate limit exceeded' }), + { + status: 429, + headers: { + 'Content-Type': 'application/json', + ...getRateLimitHeaders(ip, 5, 60000) + } + } + ); + } + const resolvedParams = await params; const id = parseInt(resolvedParams.id); const body = await request.json(); @@ -35,7 +52,20 @@ export async function PUT( }); } catch (error) { - console.error('Error updating contact:', error); + // Handle missing database table gracefully + if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') { + if (process.env.NODE_ENV === 'development') { + console.warn('Contact table does not exist.'); + } + return NextResponse.json( + { error: 'Database table not found. Please run migrations.' }, + { status: 503 } + ); + } + + if (process.env.NODE_ENV === 'development') { + console.error('Error updating contact:', error); + } return NextResponse.json( { error: 'Failed to update contact' }, { status: 500 } @@ -48,6 +78,21 @@ export async function DELETE( { params }: { params: Promise<{ id: string }> } ) { try { + // Rate limiting for DELETE requests + const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; + if (!checkRateLimit(ip, 3, 60000)) { // 3 requests per minute for DELETE (more restrictive) + return new NextResponse( + JSON.stringify({ error: 'Rate limit exceeded' }), + { + status: 429, + headers: { + 'Content-Type': 'application/json', + ...getRateLimitHeaders(ip, 3, 60000) + } + } + ); + } + const resolvedParams = await params; const id = parseInt(resolvedParams.id); @@ -67,7 +112,20 @@ export async function DELETE( }); } catch (error) { - console.error('Error deleting contact:', error); + // Handle missing database table gracefully + if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') { + if (process.env.NODE_ENV === 'development') { + console.warn('Contact table does not exist.'); + } + return NextResponse.json( + { error: 'Database table not found. Please run migrations.' }, + { status: 503 } + ); + } + + if (process.env.NODE_ENV === 'development') { + console.error('Error deleting contact:', error); + } return NextResponse.json( { error: 'Failed to delete contact' }, { status: 500 } diff --git a/app/api/contacts/route.tsx b/app/api/contacts/route.tsx index f9b2a62..d674293 100644 --- a/app/api/contacts/route.tsx +++ b/app/api/contacts/route.tsx @@ -1,5 +1,7 @@ import { type NextRequest, NextResponse } from "next/server"; import { PrismaClient } from '@prisma/client'; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; +import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; const prisma = new PrismaClient(); @@ -40,7 +42,21 @@ export async function GET(request: NextRequest) { }); } catch (error) { - console.error('Error fetching contacts:', error); + // Handle missing database table gracefully + if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') { + if (process.env.NODE_ENV === 'development') { + console.warn('Contact table does not exist. Returning empty result.'); + } + return NextResponse.json({ + contacts: [], + total: 0, + hasMore: false + }); + } + + if (process.env.NODE_ENV === 'development') { + console.error('Error fetching contacts:', error); + } return NextResponse.json( { error: 'Failed to fetch contacts' }, { status: 500 } @@ -50,6 +66,21 @@ export async function GET(request: NextRequest) { export async function POST(request: NextRequest) { try { + // Rate limiting for POST requests + const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; + if (!checkRateLimit(ip, 5, 60000)) { // 5 requests per minute + return new NextResponse( + JSON.stringify({ error: 'Rate limit exceeded' }), + { + status: 429, + headers: { + 'Content-Type': 'application/json', + ...getRateLimitHeaders(ip, 5, 60000) + } + } + ); + } + const body = await request.json(); const { name, email, subject, message } = body; @@ -86,7 +117,20 @@ export async function POST(request: NextRequest) { }, { status: 201 }); } catch (error) { - console.error('Error creating contact:', error); + // Handle missing database table gracefully + if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') { + if (process.env.NODE_ENV === 'development') { + console.warn('Contact table does not exist.'); + } + return NextResponse.json( + { error: 'Database table not found. Please run migrations.' }, + { status: 503 } + ); + } + + if (process.env.NODE_ENV === 'development') { + console.error('Error creating contact:', error); + } return NextResponse.json( { error: 'Failed to create contact' }, { status: 500 } diff --git a/app/api/email/route.tsx b/app/api/email/route.tsx index 223aefc..1f18f89 100644 --- a/app/api/email/route.tsx +++ b/app/api/email/route.tsx @@ -45,7 +45,7 @@ export async function POST(request: NextRequest) { const subject = sanitizeInput(body.subject || '', 200); const message = sanitizeInput(body.message || '', 5000); - console.log('📧 Email request received:', { email, name, subject, messageLength: message.length }); + // Email request received // Validate input if (!email || !name || !subject || !message) { @@ -121,12 +121,7 @@ export async function POST(request: NextRequest) { } }; - console.log('🚀 Creating transport with options:', { - host: transportOptions.host, - port: transportOptions.port, - secure: transportOptions.secure, - user: user.split('@')[0] + '@***' // Hide full email in logs - }); + // Creating transport with configured options const transport = nodemailer.createTransport(transportOptions); @@ -138,15 +133,17 @@ export async function POST(request: NextRequest) { while (verificationAttempts < maxVerificationAttempts && !verificationSuccess) { try { verificationAttempts++; - console.log(`🔍 SMTP verification attempt ${verificationAttempts}/${maxVerificationAttempts}`); await transport.verify(); - console.log('✅ SMTP connection verified successfully'); verificationSuccess = true; } catch (verifyError) { - console.error(`❌ SMTP verification attempt ${verificationAttempts} failed:`, verifyError); + if (process.env.NODE_ENV === 'development') { + console.error(`SMTP verification attempt ${verificationAttempts} failed:`, verifyError); + } if (verificationAttempts >= maxVerificationAttempts) { - console.error('❌ All SMTP verification attempts failed'); + if (process.env.NODE_ENV === 'development') { + console.error('All SMTP verification attempts failed'); + } return NextResponse.json( { error: "E-Mail-Server-Verbindung fehlgeschlagen" }, { status: 500 }, @@ -268,7 +265,7 @@ Diese E-Mail wurde automatisch von deinem Portfolio generiert. `, }; - console.log('📤 Sending email...'); + // Sending email // Email sending with retry logic let sendAttempts = 0; @@ -279,16 +276,18 @@ Diese E-Mail wurde automatisch von deinem Portfolio generiert. while (sendAttempts < maxSendAttempts && !sendSuccess) { try { sendAttempts++; - console.log(`📤 Email send attempt ${sendAttempts}/${maxSendAttempts}`); + // Email send attempt const sendMailPromise = () => new Promise((resolve, reject) => { transport.sendMail(mailOptions, function (err, info) { if (!err) { - console.log('✅ Email sent successfully:', info.response); + // Email sent successfully resolve(info.response); } else { - console.error("❌ Error sending email:", err); + if (process.env.NODE_ENV === 'development') { + console.error("Error sending email:", err); + } reject(err.message); } }); @@ -296,12 +295,16 @@ Diese E-Mail wurde automatisch von deinem Portfolio generiert. result = await sendMailPromise(); sendSuccess = true; - console.log('🎉 Email process completed successfully'); + // Email process completed successfully } catch (sendError) { - console.error(`❌ Email send attempt ${sendAttempts} failed:`, sendError); + if (process.env.NODE_ENV === 'development') { + console.error(`Email send attempt ${sendAttempts} failed:`, sendError); + } if (sendAttempts >= maxSendAttempts) { - console.error('❌ All email send attempts failed'); + if (process.env.NODE_ENV === 'development') { + console.error('All email send attempts failed'); + } throw new Error(`Failed to send email after ${maxSendAttempts} attempts: ${sendError}`); } @@ -321,9 +324,11 @@ Diese E-Mail wurde automatisch von deinem Portfolio generiert. responded: false } }); - console.log('✅ Contact saved to database'); + // Contact saved to database } catch (dbError) { - console.error('❌ Error saving contact to database:', dbError); + if (process.env.NODE_ENV === 'development') { + console.error('Error saving contact to database:', dbError); + } // Don't fail the email send if DB save fails } diff --git a/app/api/n8n/status/route.ts b/app/api/n8n/status/route.ts index 9e2c189..8dbabc6 100644 --- a/app/api/n8n/status/route.ts +++ b/app/api/n8n/status/route.ts @@ -33,6 +33,26 @@ interface ActivityStatusRow { export async function GET() { try { + // Check if table exists first + const tableCheck = await prisma.$queryRawUnsafe>( + `SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'activity_status' + ) as exists` + ); + + if (!tableCheck || !tableCheck[0]?.exists) { + // Table doesn't exist, return empty state + return NextResponse.json({ + activity: null, + music: null, + watching: null, + gaming: null, + status: null, + }); + } + // Fetch from activity_status table const result = await prisma.$queryRawUnsafe( `SELECT * FROM activity_status WHERE id = 1 LIMIT 1`, @@ -118,7 +138,10 @@ export async function GET() { }, ); } catch (error) { - console.error("Error fetching activity status:", error); + // Only log non-table-missing errors + if (error instanceof Error && !error.message.includes('does not exist')) { + console.error("Error fetching activity status:", error); + } // Return empty state on error (graceful degradation) return NextResponse.json( diff --git a/app/api/projects/[id]/route.ts b/app/api/projects/[id]/route.ts index 9134235..6b55d41 100644 --- a/app/api/projects/[id]/route.ts +++ b/app/api/projects/[id]/route.ts @@ -1,6 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; import { apiCache } from '@/lib/cache'; +import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; export async function GET( request: NextRequest, @@ -23,7 +25,20 @@ export async function GET( return NextResponse.json(project); } catch (error) { - console.error('Error fetching project:', error); + // Handle missing database table gracefully + if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') { + if (process.env.NODE_ENV === 'development') { + console.warn('Project table does not exist. Returning 404.'); + } + return NextResponse.json( + { error: 'Project not found' }, + { status: 404 } + ); + } + + if (process.env.NODE_ENV === 'development') { + console.error('Error fetching project:', error); + } return NextResponse.json( { error: 'Failed to fetch project' }, { status: 500 } @@ -36,6 +51,21 @@ export async function PUT( { params }: { params: Promise<{ id: string }> } ) { try { + // Rate limiting for PUT requests + const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; + if (!checkRateLimit(ip, 5, 60000)) { // 5 requests per minute for PUT + return new NextResponse( + JSON.stringify({ error: 'Rate limit exceeded' }), + { + status: 429, + headers: { + 'Content-Type': 'application/json', + ...getRateLimitHeaders(ip, 5, 60000) + } + } + ); + } + // Check if this is an admin request const isAdminRequest = request.headers.get('x-admin-request') === 'true'; if (!isAdminRequest) { @@ -68,7 +98,20 @@ export async function PUT( return NextResponse.json(project); } catch (error) { - console.error('Error updating project:', error); + // Handle missing database table gracefully + if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') { + if (process.env.NODE_ENV === 'development') { + console.warn('Project table does not exist.'); + } + return NextResponse.json( + { error: 'Database table not found. Please run migrations.' }, + { status: 503 } + ); + } + + if (process.env.NODE_ENV === 'development') { + console.error('Error updating project:', error); + } return NextResponse.json( { error: 'Failed to update project', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 } @@ -81,6 +124,30 @@ export async function DELETE( { params }: { params: Promise<{ id: string }> } ) { try { + // Rate limiting for DELETE requests + const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; + if (!checkRateLimit(ip, 3, 60000)) { // 3 requests per minute for DELETE (more restrictive) + return new NextResponse( + JSON.stringify({ error: 'Rate limit exceeded' }), + { + status: 429, + headers: { + 'Content-Type': 'application/json', + ...getRateLimitHeaders(ip, 3, 60000) + } + } + ); + } + + // Check if this is an admin request + const isAdminRequest = request.headers.get('x-admin-request') === 'true'; + if (!isAdminRequest) { + return NextResponse.json( + { error: 'Admin access required' }, + { status: 403 } + ); + } + const { id: idParam } = await params; const id = parseInt(idParam); @@ -94,7 +161,20 @@ export async function DELETE( return NextResponse.json({ success: true }); } catch (error) { - console.error('Error deleting project:', error); + // Handle missing database table gracefully + if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') { + if (process.env.NODE_ENV === 'development') { + console.warn('Project table does not exist.'); + } + return NextResponse.json( + { error: 'Database table not found. Please run migrations.' }, + { status: 503 } + ); + } + + if (process.env.NODE_ENV === 'development') { + console.error('Error deleting project:', error); + } return NextResponse.json( { error: 'Failed to delete project' }, { status: 500 } diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts index 8153b50..9812114 100644 --- a/app/api/projects/route.ts +++ b/app/api/projects/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; import { apiCache } from '@/lib/cache'; import { requireSessionAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; export async function GET(request: NextRequest) { try { @@ -96,7 +97,22 @@ export async function GET(request: NextRequest) { return NextResponse.json(result); } catch (error) { - console.error('Error fetching projects:', error); + // Handle missing database table gracefully + if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') { + if (process.env.NODE_ENV === 'development') { + console.warn('Project table does not exist. Returning empty result.'); + } + return NextResponse.json({ + projects: [], + total: 0, + pages: 0, + currentPage: 1 + }); + } + + if (process.env.NODE_ENV === 'development') { + console.error('Error fetching projects:', error); + } return NextResponse.json( { error: 'Failed to fetch projects' }, { status: 500 } @@ -106,6 +122,21 @@ export async function GET(request: NextRequest) { export async function POST(request: NextRequest) { try { + // Rate limiting for POST requests + const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; + if (!checkRateLimit(ip, 5, 60000)) { // 5 requests per minute for POST + return new NextResponse( + JSON.stringify({ error: 'Rate limit exceeded' }), + { + status: 429, + headers: { + 'Content-Type': 'application/json', + ...getRateLimitHeaders(ip, 5, 60000) + } + } + ); + } + // Check if this is an admin request const isAdminRequest = request.headers.get('x-admin-request') === 'true'; if (!isAdminRequest) { @@ -136,7 +167,20 @@ export async function POST(request: NextRequest) { return NextResponse.json(project); } catch (error) { - console.error('Error creating project:', error); + // Handle missing database table gracefully + if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') { + if (process.env.NODE_ENV === 'development') { + console.warn('Project table does not exist.'); + } + return NextResponse.json( + { error: 'Database table not found. Please run migrations.' }, + { status: 503 } + ); + } + + if (process.env.NODE_ENV === 'development') { + console.error('Error creating project:', error); + } return NextResponse.json( { error: 'Failed to create project', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 } diff --git a/app/components/About.tsx b/app/components/About.tsx index 736853c..d48df4f 100644 --- a/app/components/About.tsx +++ b/app/components/About.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { motion } from "framer-motion"; -import { Globe, Server, Wrench, Shield, Gamepad2, Code } from "lucide-react"; +import { Globe, Server, Wrench, Shield, Gamepad2, Code, Activity, Lightbulb } from "lucide-react"; // Smooth animation configuration const smoothTransition = { @@ -60,10 +60,11 @@ const About = () => { }, ]; - const hobbies = [ + const hobbies: Array<{ icon: typeof Code; text: string }> = [ { icon: Code, text: "Self-Hosting & DevOps" }, { icon: Gamepad2, text: "Gaming" }, { icon: Server, text: "Setting up Game Servers" }, + { icon: Activity, text: "Jogging to clear my mind and stay active" }, ]; if (!mounted) return null; @@ -113,11 +114,24 @@ const About = () => { experimenting with new tech like game servers or automation workflows with n8n.

-

- 💡 Fun fact: Even though I automate a lot, I still use pen and - paper for my calendar and notes – it helps me clear my head and - stay focused. -

+ +
+ +
+

+ Fun Fact +

+

+ Even though I automate a lot, I still use pen and paper + for my calendar and notes – it helps me clear my head and + stay focused. +

+
+
+
@@ -209,7 +223,9 @@ const About = () => { ? "bg-gradient-to-r from-liquid-mint/10 to-liquid-sky/10 border-liquid-mint/30 hover:border-liquid-mint/50 hover:from-liquid-mint/15 hover:to-liquid-sky/15" : idx === 1 ? "bg-gradient-to-r from-liquid-coral/10 to-liquid-peach/10 border-liquid-coral/30 hover:border-liquid-coral/50 hover:from-liquid-coral/15 hover:to-liquid-peach/15" - : "bg-gradient-to-r from-liquid-lavender/10 to-liquid-pink/10 border-liquid-lavender/30 hover:border-liquid-lavender/50 hover:from-liquid-lavender/15 hover:to-liquid-pink/15" + : idx === 2 + ? "bg-gradient-to-r from-liquid-lavender/10 to-liquid-pink/10 border-liquid-lavender/30 hover:border-liquid-lavender/50 hover:from-liquid-lavender/15 hover:to-liquid-pink/15" + : "bg-gradient-to-r from-liquid-lime/10 to-liquid-teal/10 border-liquid-lime/30 hover:border-liquid-lime/50 hover:from-liquid-lime/15 hover:to-liquid-teal/15" }`} > @@ -218,19 +234,6 @@ const About = () => { ))} - -

- 🏃 Jogging to clear my mind and stay active -

-
diff --git a/app/components/ActivityFeed.tsx b/app/components/ActivityFeed.tsx index 8fde83a..163679d 100644 --- a/app/components/ActivityFeed.tsx +++ b/app/components/ActivityFeed.tsx @@ -119,7 +119,7 @@ const SoundWaves = () => { ); }; -// Running animation +// Running animation with smooth wavy motion const RunningAnimation = () => { return (
@@ -127,16 +127,34 @@ const RunningAnimation = () => { className="absolute bottom-2 text-4xl" animate={{ x: ["-10%", "110%"], + y: [0, -10, -5, -12, -3, -10, 0, -8, -2, -10, 0], }} transition={{ - duration: 3, - repeat: Infinity, - ease: "linear", + x: { + duration: 1.2, + repeat: Infinity, + ease: "linear", + }, + y: { + duration: 0.4, + repeat: Infinity, + ease: [0.25, 0.1, 0.25, 1], // Smooth cubic bezier for wavy effect + }, }} > 🏃 -
+
); }; @@ -264,7 +282,9 @@ export const ActivityFeed = () => { setData(json); } } catch (e) { - console.error("Failed to fetch activity", e); + if (process.env.NODE_ENV === 'development') { + console.error("Failed to fetch activity", e); + } } }; fetchData(); @@ -301,7 +321,9 @@ export const ActivityFeed = () => { throw new Error("Chat API failed"); } } catch (error) { - console.error("Chat error:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Chat error:", error); + } setChatHistory((prev) => [ ...prev, { @@ -527,7 +549,7 @@ export const ActivityFeed = () => {
@@ -561,14 +583,14 @@ export const ActivityFeed = () => { onChange={(e) => setChatMessage(e.target.value)} placeholder="Ask me anything..." disabled={isLoading} - className="flex-1 bg-white border-2 border-stone-200 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-liquid-mint focus:border-transparent disabled:opacity-50 transition-all duration-300" + className="flex-1 bg-white border-2 border-stone-200 rounded-xl px-4 py-3 text-sm text-stone-900 placeholder:text-stone-400 focus:outline-none focus:ring-2 focus:ring-liquid-mint focus:border-transparent disabled:opacity-50 transition-all duration-300" /> diff --git a/app/components/Contact.tsx b/app/components/Contact.tsx index 9d8a6aa..4fce26a 100644 --- a/app/components/Contact.tsx +++ b/app/components/Contact.tsx @@ -90,7 +90,9 @@ const Contact = () => { ); } } catch (error) { - console.error("Error sending email:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Error sending email:", error); + } showEmailError( "Network error. Please check your connection and try again.", ); @@ -230,7 +232,7 @@ const Contact = () => { transition={{ duration: 1, ease: [0.25, 0.1, 0.25, 1] }} className="glass-card p-8 rounded-3xl bg-white/50 border border-white/70" > -

+

Send Message

@@ -404,7 +406,7 @@ const Contact = () => { ) : ( <> - Send Message + Send Message )} diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 1ec17df..1957cbf 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -58,13 +58,16 @@ const Header = () => { scrolled ? "w-full max-w-5xl" : "w-full max-w-7xl" }`} > -
{ }} > {item.name} - + ))} @@ -134,7 +146,7 @@ const Header = () => { > {isOpen ? : } -
+
diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index a153a1f..f03b815 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -226,9 +226,9 @@ const Hero = () => { whileHover={{ scale: 1.03, y: -2 }} whileTap={{ scale: 0.98 }} transition={{ duration: 0.3, ease: "easeOut" }} - className="px-8 py-4 bg-stone-900 text-cream rounded-full font-medium shadow-lg hover:shadow-xl hover:bg-stone-950 transition-all duration-500 flex items-center gap-2" + className="px-8 py-4 bg-stone-900 text-cream rounded-full shadow-lg hover:shadow-xl hover:bg-stone-950 transition-all duration-500 flex items-center gap-2" > - View My Work + View My Work diff --git a/app/components/Projects.tsx b/app/components/Projects.tsx index 0ec24d9..d1aa06f 100644 --- a/app/components/Projects.tsx +++ b/app/components/Projects.tsx @@ -68,7 +68,9 @@ const Projects = () => { setProjects(data.projects || []); } } catch (error) { - console.error("Error loading projects:", error); + if (process.env.NODE_ENV === 'development') { + console.error("Error loading projects:", error); + } } }; loadProjects(); diff --git a/app/editor/page.tsx b/app/editor/page.tsx index b40869d..6f1c41d 100644 --- a/app/editor/page.tsx +++ b/app/editor/page.tsx @@ -3,6 +3,7 @@ import React, { useState, useEffect, useRef, useCallback, Suspense } from 'react'; import { useSearchParams } from 'next/navigation'; import { motion, AnimatePresence } from 'framer-motion'; +import ReactMarkdown from 'react-markdown'; import { ArrowLeft, Save, @@ -68,15 +69,11 @@ function EditorPageContent() { const loadProject = useCallback(async (id: string) => { try { - console.log('Fetching projects...'); const response = await fetch('/api/projects'); if (response.ok) { const data = await response.json(); - console.log('Projects loaded:', data); - const foundProject = data.projects.find((p: Project) => p.id.toString() === id); - console.log('Found project:', foundProject); if (foundProject) { setProject(foundProject); @@ -92,15 +89,16 @@ function EditorPageContent() { live: foundProject.live || '', image: foundProject.image || '' }); - console.log('Form data set for project:', foundProject.title); - } else { - console.log('Project not found with ID:', id); } } else { - console.error('Failed to fetch projects:', response.status); + if (process.env.NODE_ENV === 'development') { + console.error('Failed to fetch projects:', response.status); + } } } catch (error) { - console.error('Error loading project:', error); + if (process.env.NODE_ENV === 'development') { + console.error('Error loading project:', error); + } } }, []); @@ -112,26 +110,22 @@ function EditorPageContent() { const authStatus = sessionStorage.getItem('admin_authenticated'); const sessionToken = sessionStorage.getItem('admin_session_token'); - console.log('Editor Auth check:', { authStatus, hasSessionToken: !!sessionToken, projectId }); - if (authStatus === 'true' && sessionToken) { - console.log('User is authenticated'); setIsAuthenticated(true); // Load project if editing if (projectId) { - console.log('Loading project with ID:', projectId); await loadProject(projectId); } else { - console.log('Creating new project'); setIsCreating(true); } } else { - console.log('User not authenticated'); setIsAuthenticated(false); } } catch (error) { - console.error('Error in init:', error); + if (process.env.NODE_ENV === 'development') { + console.error('Error in init:', error); + } setIsAuthenticated(false); } finally { setIsLoading(false); @@ -175,8 +169,6 @@ function EditorPageContent() { date: new Date().toISOString().split('T')[0] // Current date in YYYY-MM-DD format }; - console.log('Saving project:', { url, method, saveData }); - const response = await fetch(url, { method, headers: { @@ -188,7 +180,6 @@ function EditorPageContent() { if (response.ok) { const savedProject = await response.json(); - console.log('Project saved successfully:', savedProject); // Update local state with the saved project data setProject(savedProject); @@ -213,11 +204,15 @@ function EditorPageContent() { }, 1000); } else { const errorData = await response.json(); - console.error('Error saving project:', response.status, errorData); + if (process.env.NODE_ENV === 'development') { + console.error('Error saving project:', response.status, errorData); + } alert(`Error saving project: ${errorData.error || 'Unknown error'}`); } } catch (error) { - console.error('Error saving project:', error); + if (process.env.NODE_ENV === 'development') { + console.error('Error saving project:', error); + } alert(`Error saving project: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { setIsSaving(false); @@ -239,40 +234,27 @@ function EditorPageContent() { })); }; - // Simple markdown to HTML converter - const parseMarkdown = (text: string) => { - if (!text) return ''; - - return text - // Headers - .replace(/^### (.*$)/gim, '

$1

') - .replace(/^## (.*$)/gim, '

$1

') - .replace(/^# (.*$)/gim, '

$1

') - // Bold - .replace(/\*\*(.*?)\*\*/g, '$1') - // Italic - .replace(/\*(.*?)\*/g, '$1') - // Code blocks - .replace(/```([\s\S]*?)```/g, '
$1
') - // Inline code - .replace(/`(.*?)`/g, '$1') - // Links - .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') - // Images - .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1') - // Ensure all images have alt attributes - .replace(/]*?)(?:\s+alt\s*=\s*["'][^"']*["'])?([^>]*?)>/g, (match, before, after) => { - if (match.includes('alt=')) return match; - return ``; - }) - // Lists - .replace(/^\* (.*$)/gim, '
  • $1
  • ') - .replace(/^- (.*$)/gim, '
  • $1
  • ') - .replace(/^(\d+)\. (.*$)/gim, '
  • $2
  • ') - // Blockquotes - .replace(/^> (.*$)/gim, '
    $1
    ') - // Line breaks - .replace(/\n/g, '
    '); + // Markdown components for react-markdown with security + const markdownComponents = { + a: ({ node, ...props }: { node?: unknown; href?: string; children?: React.ReactNode }) => { + // Validate URLs to prevent javascript: and data: protocols + const href = props.href || ''; + const isSafe = href && !href.startsWith('javascript:') && !href.startsWith('data:'); + return ( + + ); + }, + img: ({ node, ...props }: { node?: unknown; src?: string; alt?: string }) => { + // Validate image URLs + const src = props.src || ''; + const isSafe = src && !src.startsWith('javascript:') && !src.startsWith('data:'); + return isSafe ? {props.alt : null; + }, }; // Rich text editor functions @@ -855,10 +837,11 @@ function EditorPageContent() {

    Content

    -
    +
    + + {formData.content} + +
    )} diff --git a/app/layout.tsx b/app/layout.tsx index a9f2ebb..9ba9ebc 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,8 +4,8 @@ import { Inter } from "next/font/google"; import React from "react"; import { ToastProvider } from "@/components/Toast"; import { AnalyticsProvider } from "@/components/AnalyticsProvider"; -import { PerformanceDashboard } from "@/components/PerformanceDashboard"; import { BackgroundBlobs } from "@/components/BackgroundBlobs"; +import { ErrorBoundary } from "@/components/ErrorBoundary"; const inter = Inter({ variable: "--font-inter", @@ -29,13 +29,14 @@ export default function RootLayout({ Dennis Konkol's Portfolio - - - -
    {children}
    - -
    -
    + + + + +
    {children}
    +
    +
    +
    ); diff --git a/app/legal-notice/page.tsx b/app/legal-notice/page.tsx index 818fb5a..782b249 100644 --- a/app/legal-notice/page.tsx +++ b/app/legal-notice/page.tsx @@ -11,7 +11,7 @@ export default function LegalNotice() { return (
    -
    +
    -
    -

    +
    +

    Verantwortlicher für die Inhalte dieser Website

    Name: Dennis Konkol

    Adresse: Auf dem Ziegenbrink 2B, 49082 Osnabrück, Deutschland

    -

    E-Mail: info@dki.one

    -

    Website: dki.one

    +

    E-Mail: info@dk0.dev

    +

    Website: dk0.dev

    -
    -

    Haftung für Links

    -

    +

    +

    Haftung für Links

    +

    Meine Website enthält Links auf externe Websites. Ich habe keinen Einfluss auf die Inhalte dieser Websites und kann daher keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der Betreiber oder Anbieter der Seiten verantwortlich. Jedoch überprüfe ich die verlinkten Seiten zum Zeitpunkt der Verlinkung @@ -59,17 +59,17 @@ export default function LegalNotice() {

    -
    -

    Urheberrecht

    -

    +

    +

    Urheberrecht

    +

    Alle Inhalte dieser Website, einschließlich Texte, Fotos und Designs, stehen unter Urheberrechtsschutz. Jegliche Nutzung ohne vorherige schriftliche Zustimmung des Urhebers ist verboten.

    -
    -

    Gewährleistung

    -

    +

    +

    Gewährleistung

    +

    Die Nutzung der Inhalte dieser Website erfolgt auf eigene Gefahr. Als Diensteanbieter kann ich keine Gewähr übernehmen für Schäden, die entstehen können, durch den Zugriff oder die Nutzung dieser Website.

    diff --git a/app/page.tsx b/app/page.tsx index 07a7bb4..b434793 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -52,7 +52,9 @@ export default function Home() { @@ -87,7 +92,9 @@ export default function Home() { @@ -122,7 +132,9 @@ export default function Home() { diff --git a/app/privacy-policy/page.tsx b/app/privacy-policy/page.tsx index 1749583..ff79d61 100644 --- a/app/privacy-policy/page.tsx +++ b/app/privacy-policy/page.tsx @@ -11,7 +11,7 @@ export default function PrivacyPolicy() { return (
    -
    +
    -
    -

    +

    +

    Der Schutz Ihrer persönlichen Daten ist mir wichtig. In dieser Datenschutzerklärung informiere ich Sie über die Verarbeitung personenbezogener Daten im Rahmen meines Internet-Angebots.

    -
    -

    +
    +

    Verantwortlicher für die Datenverarbeitung

    Name: Dennis Konkol

    Adresse: Auf dem Ziegenbrink 2B, 49082 Osnabrück, Deutschland

    -

    E-Mail: info@dki.one

    -

    Website: dki.one

    +

    E-Mail: info@dk0.dev

    +

    Website: dk0.dev

    -

    +

    Diese Datenschutzerklärung gilt für die Verarbeitung personenbezogener Daten durch den oben genannten Verantwortlichen.

    @@ -214,10 +214,10 @@ export default function PrivacyPolicy() {

    Bei Fragen zur Datenschutzerklärung kontaktieren Sie mich unter{" "} - info@dki.one + info@dk0.dev {" "} oder nutzen Sie das Kontaktformular auf meiner Website.

    diff --git a/app/projects/[slug]/page.tsx b/app/projects/[slug]/page.tsx index 1eb573c..1132ed0 100644 --- a/app/projects/[slug]/page.tsx +++ b/app/projects/[slug]/page.tsx @@ -35,11 +35,11 @@ const ProjectDetail = () => { if (data.projects && data.projects.length > 0) { setProject(data.projects[0]); } - } else { - console.error('Failed to fetch project from API'); } } catch (error) { - console.error('Error loading project:', error); + if (process.env.NODE_ENV === 'development') { + console.error('Error loading project:', error); + } } }; @@ -59,7 +59,7 @@ const ProjectDetail = () => { return (
    -
    +
    {/* Header */} { if (response.ok) { const data = await response.json(); setProjects(data.projects || []); - } else { - console.error('Failed to fetch projects from API'); } } catch (error) { - console.error('Error loading projects:', error); + if (process.env.NODE_ENV === 'development') { + console.error('Error loading projects:', error); + } } }; @@ -57,16 +57,13 @@ const ProjectsPage = () => { ? projects : projects.filter(project => project.category === selectedCategory); - console.log('Selected category:', selectedCategory); - console.log('Filtered projects:', filteredProjects); - if (!mounted) { return null; } return (
    -
    +
    {/* Header */} { onClick={() => setSelectedCategory(category)} className={`px-6 py-3 rounded-lg font-medium transition-all duration-200 ${ selectedCategory === category - ? 'bg-blue-600 text-white shadow-lg' + ? 'bg-gray-800 text-cream shadow-lg' : 'bg-gray-800/50 text-gray-300 hover:bg-gray-700/50 hover:text-white' }`} > diff --git a/components/ErrorBoundary.tsx b/components/ErrorBoundary.tsx new file mode 100644 index 0000000..31a50c0 --- /dev/null +++ b/components/ErrorBoundary.tsx @@ -0,0 +1,84 @@ +'use client'; + +import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { AlertTriangle } from 'lucide-react'; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { + hasError: false, + error: null, + }; + } + + static getDerivedStateFromError(error: Error): State { + return { + hasError: true, + error, + }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // Log error to console in development + if (process.env.NODE_ENV === 'development') { + console.error('ErrorBoundary caught an error:', error, errorInfo); + } + // In production, you could log to an error reporting service + } + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( +
    +
    +
    + +
    +

    + Something went wrong +

    +

    + We encountered an unexpected error. Please try refreshing the page. +

    + {process.env.NODE_ENV === 'development' && this.state.error && ( +
    + + Error details (development only) + +
    +                  {this.state.error.toString()}
    +                
    +
    + )} + +
    +
    + ); + } + + return this.props.children; + } +} diff --git a/lib/redis.ts b/lib/redis.ts index cf03ff0..4cbd14e 100644 --- a/lib/redis.ts +++ b/lib/redis.ts @@ -1,35 +1,101 @@ import { createClient } from 'redis'; let redisClient: ReturnType | null = null; +let connectionFailed = false; // Track if connection has permanently failed + +// Helper to check if error is connection refused +const isConnectionRefused = (err: any): boolean => { + if (!err) return false; + + // Check direct properties + if (err.code === 'ECONNREFUSED' || err.message?.includes('ECONNREFUSED')) { + return true; + } + + // Check AggregateError + if (err.errors && Array.isArray(err.errors)) { + return err.errors.some((e: any) => e?.code === 'ECONNREFUSED' || e?.message?.includes('ECONNREFUSED')); + } + + // Check nested error + if (err.cause) { + return isConnectionRefused(err.cause); + } + + return false; +}; export const getRedisClient = async () => { + // If Redis URL is not configured, return null instead of trying to connect + if (!process.env.REDIS_URL) { + return null; + } + + // If connection has already failed, don't try again + if (connectionFailed) { + return null; + } + if (!redisClient) { - const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'; + const redisUrl = process.env.REDIS_URL; - redisClient = createClient({ - url: redisUrl, - socket: { - reconnectStrategy: (retries) => Math.min(retries * 50, 1000) + try { + redisClient = createClient({ + url: redisUrl, + socket: { + reconnectStrategy: (retries) => { + // Stop trying after 1 attempt to avoid spam + if (retries > 1) { + connectionFailed = true; + return false; + } + return false; // Don't reconnect automatically + } + } + }); + + redisClient.on('error', (err: any) => { + // Silently handle connection refused errors - Redis is optional + if (isConnectionRefused(err)) { + connectionFailed = true; + return; // Don't log connection refused errors + } + // Only log non-connection-refused errors + console.error('Redis Client Error:', err); + }); + + redisClient.on('connect', () => { + console.log('Redis Client Connected'); + connectionFailed = false; // Reset on successful connection + }); + + redisClient.on('ready', () => { + console.log('Redis Client Ready'); + connectionFailed = false; // Reset on ready + }); + + redisClient.on('end', () => { + console.log('Redis Client Disconnected'); + }); + + await redisClient.connect().catch((err: any) => { + // Connection failed + if (isConnectionRefused(err)) { + connectionFailed = true; + // Silently handle connection refused - Redis is optional + } else { + // Only log non-connection-refused errors + console.error('Redis connection failed:', err); + } + redisClient = null; + }); + } catch (error: any) { + // If connection fails, set to null + if (isConnectionRefused(error)) { + connectionFailed = true; } - }); - - redisClient.on('error', (err) => { - console.error('Redis Client Error:', err); - }); - - redisClient.on('connect', () => { - console.log('Redis Client Connected'); - }); - - redisClient.on('ready', () => { - console.log('Redis Client Ready'); - }); - - redisClient.on('end', () => { - console.log('Redis Client Disconnected'); - }); - - await redisClient.connect(); + redisClient = null; + } } return redisClient; @@ -47,10 +113,11 @@ export const cache = { async get(key: string) { try { const client = await getRedisClient(); + if (!client) return null; const value = await client.get(key); return value ? JSON.parse(value) : null; } catch (error) { - console.error('Redis GET error:', error); + // Silently fail if Redis is not available return null; } }, @@ -58,10 +125,11 @@ export const cache = { async set(key: string, value: unknown, ttlSeconds = 3600) { try { const client = await getRedisClient(); + if (!client) return false; await client.setEx(key, ttlSeconds, JSON.stringify(value)); return true; } catch (error) { - console.error('Redis SET error:', error); + // Silently fail if Redis is not available return false; } }, @@ -69,10 +137,11 @@ export const cache = { async del(key: string) { try { const client = await getRedisClient(); + if (!client) return false; await client.del(key); return true; } catch (error) { - console.error('Redis DEL error:', error); + // Silently fail if Redis is not available return false; } }, @@ -80,9 +149,10 @@ export const cache = { async exists(key: string) { try { const client = await getRedisClient(); + if (!client) return false; return await client.exists(key); } catch (error) { - console.error('Redis EXISTS error:', error); + // Silently fail if Redis is not available return false; } }, @@ -90,10 +160,11 @@ export const cache = { async flush() { try { const client = await getRedisClient(); + if (!client) return false; await client.flushAll(); return true; } catch (error) { - console.error('Redis FLUSH error:', error); + // Silently fail if Redis is not available return false; } } @@ -146,13 +217,14 @@ export const analyticsCache = { async clearAll() { try { const client = await getRedisClient(); + if (!client) return; // Clear all analytics-related keys const keys = await client.keys('analytics:*'); if (keys.length > 0) { await client.del(keys); } } catch (error) { - console.error('Error clearing analytics cache:', error); + // Silently fail if Redis is not available } } }; diff --git a/package-lock.json b/package-lock.json index 9ff62fc..ae31d65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2265,9 +2265,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", - "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz", + "integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -9383,12 +9383,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", - "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", + "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", "dependencies": { - "@next/env": "15.5.7", + "@next/env": "15.5.9", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31",