full upgrade

This commit is contained in:
2026-01-07 23:13:25 +01:00
parent 4cd3f60c98
commit c5efd28383
23 changed files with 693 additions and 226 deletions

View File

@@ -1,21 +1,41 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { 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(); const body = await request.json();
// Log performance metrics (you can extend this to store in database) // Log performance metrics (you can extend this to store in database)
console.log('Performance Metric:', { if (process.env.NODE_ENV === 'development') {
timestamp: new Date().toISOString(), console.log('Performance Metric:', {
...body, timestamp: new Date().toISOString(),
}); ...body,
});
}
// You could store this in a database or send to external service // 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 // For now, we'll just log it since Umami handles the main analytics
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error) { } catch (error) {
console.error('Analytics API Error:', error); if (process.env.NODE_ENV === 'development') {
console.error('Analytics API Error:', error);
}
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to process analytics data' }, { error: 'Failed to process analytics data' },
{ status: 500 } { status: 500 }

View File

@@ -1,5 +1,7 @@
import { type NextRequest, NextResponse } from "next/server"; import { type NextRequest, NextResponse } from "next/server";
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@@ -8,6 +10,21 @@ export async function PUT(
{ params }: { params: Promise<{ id: string }> } { params }: { params: Promise<{ id: string }> }
) { ) {
try { 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 resolvedParams = await params;
const id = parseInt(resolvedParams.id); const id = parseInt(resolvedParams.id);
const body = await request.json(); const body = await request.json();
@@ -35,7 +52,20 @@ export async function PUT(
}); });
} catch (error) { } 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( return NextResponse.json(
{ error: 'Failed to update contact' }, { error: 'Failed to update contact' },
{ status: 500 } { status: 500 }
@@ -48,6 +78,21 @@ export async function DELETE(
{ params }: { params: Promise<{ id: string }> } { params }: { params: Promise<{ id: string }> }
) { ) {
try { 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 resolvedParams = await params;
const id = parseInt(resolvedParams.id); const id = parseInt(resolvedParams.id);
@@ -67,7 +112,20 @@ export async function DELETE(
}); });
} catch (error) { } 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( return NextResponse.json(
{ error: 'Failed to delete contact' }, { error: 'Failed to delete contact' },
{ status: 500 } { status: 500 }

View File

@@ -1,5 +1,7 @@
import { type NextRequest, NextResponse } from "next/server"; import { type NextRequest, NextResponse } from "next/server";
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@@ -40,7 +42,21 @@ export async function GET(request: NextRequest) {
}); });
} catch (error) { } 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( return NextResponse.json(
{ error: 'Failed to fetch contacts' }, { error: 'Failed to fetch contacts' },
{ status: 500 } { status: 500 }
@@ -50,6 +66,21 @@ export async function GET(request: NextRequest) {
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { 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 body = await request.json();
const { name, email, subject, message } = body; const { name, email, subject, message } = body;
@@ -86,7 +117,20 @@ export async function POST(request: NextRequest) {
}, { status: 201 }); }, { status: 201 });
} catch (error) { } 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( return NextResponse.json(
{ error: 'Failed to create contact' }, { error: 'Failed to create contact' },
{ status: 500 } { status: 500 }

View File

@@ -45,7 +45,7 @@ export async function POST(request: NextRequest) {
const subject = sanitizeInput(body.subject || '', 200); const subject = sanitizeInput(body.subject || '', 200);
const message = sanitizeInput(body.message || '', 5000); const message = sanitizeInput(body.message || '', 5000);
console.log('📧 Email request received:', { email, name, subject, messageLength: message.length }); // Email request received
// Validate input // Validate input
if (!email || !name || !subject || !message) { if (!email || !name || !subject || !message) {
@@ -121,12 +121,7 @@ export async function POST(request: NextRequest) {
} }
}; };
console.log('🚀 Creating transport with options:', { // Creating transport with configured options
host: transportOptions.host,
port: transportOptions.port,
secure: transportOptions.secure,
user: user.split('@')[0] + '@***' // Hide full email in logs
});
const transport = nodemailer.createTransport(transportOptions); const transport = nodemailer.createTransport(transportOptions);
@@ -138,15 +133,17 @@ export async function POST(request: NextRequest) {
while (verificationAttempts < maxVerificationAttempts && !verificationSuccess) { while (verificationAttempts < maxVerificationAttempts && !verificationSuccess) {
try { try {
verificationAttempts++; verificationAttempts++;
console.log(`🔍 SMTP verification attempt ${verificationAttempts}/${maxVerificationAttempts}`);
await transport.verify(); await transport.verify();
console.log('✅ SMTP connection verified successfully');
verificationSuccess = true; verificationSuccess = true;
} catch (verifyError) { } 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) { 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( return NextResponse.json(
{ error: "E-Mail-Server-Verbindung fehlgeschlagen" }, { error: "E-Mail-Server-Verbindung fehlgeschlagen" },
{ status: 500 }, { 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 // Email sending with retry logic
let sendAttempts = 0; let sendAttempts = 0;
@@ -279,16 +276,18 @@ Diese E-Mail wurde automatisch von deinem Portfolio generiert.
while (sendAttempts < maxSendAttempts && !sendSuccess) { while (sendAttempts < maxSendAttempts && !sendSuccess) {
try { try {
sendAttempts++; sendAttempts++;
console.log(`📤 Email send attempt ${sendAttempts}/${maxSendAttempts}`); // Email send attempt
const sendMailPromise = () => const sendMailPromise = () =>
new Promise<string>((resolve, reject) => { new Promise<string>((resolve, reject) => {
transport.sendMail(mailOptions, function (err, info) { transport.sendMail(mailOptions, function (err, info) {
if (!err) { if (!err) {
console.log('✅ Email sent successfully:', info.response); // Email sent successfully
resolve(info.response); resolve(info.response);
} else { } else {
console.error("❌ Error sending email:", err); if (process.env.NODE_ENV === 'development') {
console.error("Error sending email:", err);
}
reject(err.message); reject(err.message);
} }
}); });
@@ -296,12 +295,16 @@ Diese E-Mail wurde automatisch von deinem Portfolio generiert.
result = await sendMailPromise(); result = await sendMailPromise();
sendSuccess = true; sendSuccess = true;
console.log('🎉 Email process completed successfully'); // Email process completed successfully
} catch (sendError) { } 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) { 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}`); 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 responded: false
} }
}); });
console.log('✅ Contact saved to database'); // Contact saved to database
} catch (dbError) { } 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 // Don't fail the email send if DB save fails
} }

View File

@@ -33,6 +33,26 @@ interface ActivityStatusRow {
export async function GET() { export async function GET() {
try { try {
// Check if table exists first
const tableCheck = await prisma.$queryRawUnsafe<Array<{ exists: boolean }>>(
`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 // Fetch from activity_status table
const result = await prisma.$queryRawUnsafe<ActivityStatusRow[]>( const result = await prisma.$queryRawUnsafe<ActivityStatusRow[]>(
`SELECT * FROM activity_status WHERE id = 1 LIMIT 1`, `SELECT * FROM activity_status WHERE id = 1 LIMIT 1`,
@@ -118,7 +138,10 @@ export async function GET() {
}, },
); );
} catch (error) { } 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 empty state on error (graceful degradation)
return NextResponse.json( return NextResponse.json(

View File

@@ -1,6 +1,8 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
import { apiCache } from '@/lib/cache'; import { apiCache } from '@/lib/cache';
import { checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
export async function GET( export async function GET(
request: NextRequest, request: NextRequest,
@@ -23,7 +25,20 @@ export async function GET(
return NextResponse.json(project); return NextResponse.json(project);
} catch (error) { } 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( return NextResponse.json(
{ error: 'Failed to fetch project' }, { error: 'Failed to fetch project' },
{ status: 500 } { status: 500 }
@@ -36,6 +51,21 @@ export async function PUT(
{ params }: { params: Promise<{ id: string }> } { params }: { params: Promise<{ id: string }> }
) { ) {
try { 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 // Check if this is an admin request
const isAdminRequest = request.headers.get('x-admin-request') === 'true'; const isAdminRequest = request.headers.get('x-admin-request') === 'true';
if (!isAdminRequest) { if (!isAdminRequest) {
@@ -68,7 +98,20 @@ export async function PUT(
return NextResponse.json(project); return NextResponse.json(project);
} catch (error) { } 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( return NextResponse.json(
{ error: 'Failed to update project', details: error instanceof Error ? error.message : 'Unknown error' }, { error: 'Failed to update project', details: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 } { status: 500 }
@@ -81,6 +124,30 @@ export async function DELETE(
{ params }: { params: Promise<{ id: string }> } { params }: { params: Promise<{ id: string }> }
) { ) {
try { 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: idParam } = await params;
const id = parseInt(idParam); const id = parseInt(idParam);
@@ -94,7 +161,20 @@ export async function DELETE(
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error) { } 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( return NextResponse.json(
{ error: 'Failed to delete project' }, { error: 'Failed to delete project' },
{ status: 500 } { status: 500 }

View File

@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
import { apiCache } from '@/lib/cache'; import { apiCache } from '@/lib/cache';
import { requireSessionAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth'; import { requireSessionAuth, checkRateLimit, getRateLimitHeaders } from '@/lib/auth';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
@@ -96,7 +97,22 @@ export async function GET(request: NextRequest) {
return NextResponse.json(result); return NextResponse.json(result);
} catch (error) { } 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( return NextResponse.json(
{ error: 'Failed to fetch projects' }, { error: 'Failed to fetch projects' },
{ status: 500 } { status: 500 }
@@ -106,6 +122,21 @@ export async function GET(request: NextRequest) {
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { 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 // Check if this is an admin request
const isAdminRequest = request.headers.get('x-admin-request') === 'true'; const isAdminRequest = request.headers.get('x-admin-request') === 'true';
if (!isAdminRequest) { if (!isAdminRequest) {
@@ -136,7 +167,20 @@ export async function POST(request: NextRequest) {
return NextResponse.json(project); return NextResponse.json(project);
} catch (error) { } 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( return NextResponse.json(
{ error: 'Failed to create project', details: error instanceof Error ? error.message : 'Unknown error' }, { error: 'Failed to create project', details: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 } { status: 500 }

View File

@@ -2,7 +2,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { motion } from "framer-motion"; 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 // Smooth animation configuration
const smoothTransition = { 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: Code, text: "Self-Hosting & DevOps" },
{ icon: Gamepad2, text: "Gaming" }, { icon: Gamepad2, text: "Gaming" },
{ icon: Server, text: "Setting up Game Servers" }, { icon: Server, text: "Setting up Game Servers" },
{ icon: Activity, text: "Jogging to clear my mind and stay active" },
]; ];
if (!mounted) return null; if (!mounted) return null;
@@ -113,11 +114,24 @@ const About = () => {
experimenting with new tech like game servers or automation experimenting with new tech like game servers or automation
workflows with <strong>n8n</strong>. workflows with <strong>n8n</strong>.
</p> </p>
<p className="text-sm italic text-stone-500 bg-stone-50 p-4 rounded-lg border-l-4 border-liquid-mint"> <motion.div
💡 Fun fact: Even though I automate a lot, I still use pen and variants={fadeInUp}
paper for my calendar and notes it helps me clear my head and className="relative overflow-hidden bg-gradient-to-br from-liquid-mint/15 via-liquid-sky/10 to-liquid-lavender/15 border-2 border-liquid-mint/30 rounded-xl p-5 backdrop-blur-sm"
stay focused. >
</p> <div className="flex items-start gap-3">
<Lightbulb size={20} className="text-stone-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-semibold text-stone-800 mb-1">
Fun Fact
</p>
<p className="text-sm text-stone-700 leading-relaxed">
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.
</p>
</div>
</div>
</motion.div>
</motion.div> </motion.div>
</motion.div> </motion.div>
@@ -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" ? "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 : 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-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"
}`} }`}
> >
<hobby.icon size={20} className="text-stone-600" /> <hobby.icon size={20} className="text-stone-600" />
@@ -218,19 +234,6 @@ const About = () => {
</span> </span>
</motion.div> </motion.div>
))} ))}
<motion.div
variants={fadeInUp}
whileHover={{
x: 8,
scale: 1.02,
transition: { duration: 0.4, ease: "easeOut" },
}}
className="p-4 rounded-xl bg-gradient-to-r from-liquid-lime/15 to-liquid-teal/15 border-2 border-liquid-lime/40 hover:border-liquid-lime/60 hover:from-liquid-lime/20 hover:to-liquid-teal/20 transition-all duration-500 ease-out"
>
<p className="text-sm text-stone-600">
🏃 Jogging to clear my mind and stay active
</p>
</motion.div>
</div> </div>
</div> </div>
</motion.div> </motion.div>

View File

@@ -119,7 +119,7 @@ const SoundWaves = () => {
); );
}; };
// Running animation // Running animation with smooth wavy motion
const RunningAnimation = () => { const RunningAnimation = () => {
return ( return (
<div className="absolute inset-0 overflow-hidden pointer-events-none"> <div className="absolute inset-0 overflow-hidden pointer-events-none">
@@ -127,16 +127,34 @@ const RunningAnimation = () => {
className="absolute bottom-2 text-4xl" className="absolute bottom-2 text-4xl"
animate={{ animate={{
x: ["-10%", "110%"], x: ["-10%", "110%"],
y: [0, -10, -5, -12, -3, -10, 0, -8, -2, -10, 0],
}} }}
transition={{ transition={{
duration: 3, x: {
repeat: Infinity, duration: 1.2,
ease: "linear", repeat: Infinity,
ease: "linear",
},
y: {
duration: 0.4,
repeat: Infinity,
ease: [0.25, 0.1, 0.25, 1], // Smooth cubic bezier for wavy effect
},
}} }}
> >
🏃 🏃
</motion.div> </motion.div>
<div className="absolute bottom-2 left-0 right-0 h-0.5 bg-liquid-lime/30" /> <motion.div
className="absolute bottom-2 left-0 right-0 h-0.5 bg-liquid-lime/30"
animate={{
opacity: [0.3, 0.6, 0.3],
}}
transition={{
duration: 0.4,
repeat: Infinity,
ease: "easeInOut",
}}
/>
</div> </div>
); );
}; };
@@ -264,7 +282,9 @@ export const ActivityFeed = () => {
setData(json); setData(json);
} }
} catch (e) { } catch (e) {
console.error("Failed to fetch activity", e); if (process.env.NODE_ENV === 'development') {
console.error("Failed to fetch activity", e);
}
} }
}; };
fetchData(); fetchData();
@@ -301,7 +321,9 @@ export const ActivityFeed = () => {
throw new Error("Chat API failed"); throw new Error("Chat API failed");
} }
} catch (error) { } catch (error) {
console.error("Chat error:", error); if (process.env.NODE_ENV === 'development') {
console.error("Chat error:", error);
}
setChatHistory((prev) => [ setChatHistory((prev) => [
...prev, ...prev,
{ {
@@ -527,7 +549,7 @@ export const ActivityFeed = () => {
<div <div
className={`max-w-[85%] p-3 rounded-2xl text-sm ${ className={`max-w-[85%] p-3 rounded-2xl text-sm ${
msg.role === "user" msg.role === "user"
? "bg-gradient-to-br from-stone-800 to-stone-900 text-white rounded-tr-none shadow-md" ? "bg-gradient-to-br from-stone-700 to-stone-600 text-white rounded-tr-none shadow-md"
: "bg-gradient-to-br from-white to-stone-50 text-stone-900 shadow-md rounded-tl-none border-2 border-stone-100" : "bg-gradient-to-br from-white to-stone-50 text-stone-900 shadow-md rounded-tl-none border-2 border-stone-100"
}`} }`}
> >
@@ -561,14 +583,14 @@ export const ActivityFeed = () => {
onChange={(e) => setChatMessage(e.target.value)} onChange={(e) => setChatMessage(e.target.value)}
placeholder="Ask me anything..." placeholder="Ask me anything..."
disabled={isLoading} 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"
/> />
<motion.button <motion.button
type="submit" type="submit"
disabled={isLoading || !chatMessage.trim()} disabled={isLoading || !chatMessage.trim()}
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
className="p-3 bg-gradient-to-br from-stone-900 to-stone-800 text-white rounded-xl hover:from-black hover:to-stone-900 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg" className="p-3 bg-gradient-to-br from-stone-700 to-stone-600 text-white rounded-xl hover:from-stone-600 hover:to-stone-500 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg"
> >
<Send size={18} /> <Send size={18} />
</motion.button> </motion.button>

View File

@@ -90,7 +90,9 @@ const Contact = () => {
); );
} }
} catch (error) { } catch (error) {
console.error("Error sending email:", error); if (process.env.NODE_ENV === 'development') {
console.error("Error sending email:", error);
}
showEmailError( showEmailError(
"Network error. Please check your connection and try again.", "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] }} 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" className="glass-card p-8 rounded-3xl bg-white/50 border border-white/70"
> >
<h3 className="text-2xl font-bold text-stone-800 mb-6"> <h3 className="text-2xl font-bold text-gray-800 mb-6">
Send Message Send Message
</h3> </h3>
@@ -404,7 +406,7 @@ const Contact = () => {
) : ( ) : (
<> <>
<Send size={20} /> <Send size={20} />
<span>Send Message</span> <span className="text-cream">Send Message</span>
</> </>
)} )}
</motion.button> </motion.button>

View File

@@ -58,13 +58,16 @@ const Header = () => {
scrolled ? "w-full max-w-5xl" : "w-full max-w-7xl" scrolled ? "w-full max-w-5xl" : "w-full max-w-7xl"
}`} }`}
> >
<div <motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}
className={` className={`
backdrop-blur-xl transition-all duration-500 backdrop-blur-xl transition-all duration-500
${ ${
scrolled scrolled
? "bg-white/95 border border-stone-300 shadow-[0_8px_30px_rgba(0,0,0,0.12)] rounded-full px-6 py-3" ? "bg-white/95 border border-stone-200/50 shadow-[0_8px_30px_rgba(0,0,0,0.12)] rounded-full px-6 py-3"
: "bg-white/85 border border-stone-200 shadow-[0_4px_24px_rgba(0,0,0,0.08)] px-4 py-4 rounded-full" : "bg-white/85 border border-stone-200/30 shadow-[0_4px_24px_rgba(0,0,0,0.08)] px-4 py-4 rounded-full"
} }
flex justify-between items-center flex justify-between items-center
`} `}
@@ -105,7 +108,16 @@ const Header = () => {
}} }}
> >
{item.name} {item.name}
<span className="absolute -bottom-1 left-0 right-0 h-0.5 bg-gradient-to-r from-liquid-mint to-liquid-lavender transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300 origin-center rounded-full"></span> <motion.span
className="absolute -bottom-1 left-0 right-0 h-0.5 bg-gradient-to-r from-liquid-mint to-liquid-lavender rounded-full"
initial={{ scaleX: 0, opacity: 0 }}
whileHover={{ scaleX: 1, opacity: 1 }}
transition={{
duration: 0.4,
ease: [0.25, 0.1, 0.25, 1],
}}
style={{ transformOrigin: "left center" }}
/>
</Link> </Link>
</motion.div> </motion.div>
))} ))}
@@ -134,7 +146,7 @@ const Header = () => {
> >
{isOpen ? <X size={24} /> : <Menu size={24} />} {isOpen ? <X size={24} /> : <Menu size={24} />}
</motion.button> </motion.button>
</div> </motion.div>
</div> </div>
<AnimatePresence> <AnimatePresence>

View File

@@ -226,9 +226,9 @@ const Hero = () => {
whileHover={{ scale: 1.03, y: -2 }} whileHover={{ scale: 1.03, y: -2 }}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
transition={{ duration: 0.3, ease: "easeOut" }} 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"
> >
<span>View My Work</span> <span className="text-cream">View My Work</span>
<ArrowDown size={18} /> <ArrowDown size={18} />
</motion.a> </motion.a>

View File

@@ -68,7 +68,9 @@ const Projects = () => {
setProjects(data.projects || []); setProjects(data.projects || []);
} }
} catch (error) { } catch (error) {
console.error("Error loading projects:", error); if (process.env.NODE_ENV === 'development') {
console.error("Error loading projects:", error);
}
} }
}; };
loadProjects(); loadProjects();

View File

@@ -3,6 +3,7 @@
import React, { useState, useEffect, useRef, useCallback, Suspense } from 'react'; import React, { useState, useEffect, useRef, useCallback, Suspense } from 'react';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import ReactMarkdown from 'react-markdown';
import { import {
ArrowLeft, ArrowLeft,
Save, Save,
@@ -68,15 +69,11 @@ function EditorPageContent() {
const loadProject = useCallback(async (id: string) => { const loadProject = useCallback(async (id: string) => {
try { try {
console.log('Fetching projects...');
const response = await fetch('/api/projects'); const response = await fetch('/api/projects');
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
console.log('Projects loaded:', data);
const foundProject = data.projects.find((p: Project) => p.id.toString() === id); const foundProject = data.projects.find((p: Project) => p.id.toString() === id);
console.log('Found project:', foundProject);
if (foundProject) { if (foundProject) {
setProject(foundProject); setProject(foundProject);
@@ -92,15 +89,16 @@ function EditorPageContent() {
live: foundProject.live || '', live: foundProject.live || '',
image: foundProject.image || '' image: foundProject.image || ''
}); });
console.log('Form data set for project:', foundProject.title);
} else {
console.log('Project not found with ID:', id);
} }
} else { } 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) { } 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 authStatus = sessionStorage.getItem('admin_authenticated');
const sessionToken = sessionStorage.getItem('admin_session_token'); const sessionToken = sessionStorage.getItem('admin_session_token');
console.log('Editor Auth check:', { authStatus, hasSessionToken: !!sessionToken, projectId });
if (authStatus === 'true' && sessionToken) { if (authStatus === 'true' && sessionToken) {
console.log('User is authenticated');
setIsAuthenticated(true); setIsAuthenticated(true);
// Load project if editing // Load project if editing
if (projectId) { if (projectId) {
console.log('Loading project with ID:', projectId);
await loadProject(projectId); await loadProject(projectId);
} else { } else {
console.log('Creating new project');
setIsCreating(true); setIsCreating(true);
} }
} else { } else {
console.log('User not authenticated');
setIsAuthenticated(false); setIsAuthenticated(false);
} }
} catch (error) { } catch (error) {
console.error('Error in init:', error); if (process.env.NODE_ENV === 'development') {
console.error('Error in init:', error);
}
setIsAuthenticated(false); setIsAuthenticated(false);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@@ -175,8 +169,6 @@ function EditorPageContent() {
date: new Date().toISOString().split('T')[0] // Current date in YYYY-MM-DD format 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, { const response = await fetch(url, {
method, method,
headers: { headers: {
@@ -188,7 +180,6 @@ function EditorPageContent() {
if (response.ok) { if (response.ok) {
const savedProject = await response.json(); const savedProject = await response.json();
console.log('Project saved successfully:', savedProject);
// Update local state with the saved project data // Update local state with the saved project data
setProject(savedProject); setProject(savedProject);
@@ -213,11 +204,15 @@ function EditorPageContent() {
}, 1000); }, 1000);
} else { } else {
const errorData = await response.json(); 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'}`); alert(`Error saving project: ${errorData.error || 'Unknown error'}`);
} }
} catch (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'}`); alert(`Error saving project: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally { } finally {
setIsSaving(false); setIsSaving(false);
@@ -239,40 +234,27 @@ function EditorPageContent() {
})); }));
}; };
// Simple markdown to HTML converter // Markdown components for react-markdown with security
const parseMarkdown = (text: string) => { const markdownComponents = {
if (!text) return ''; a: ({ node, ...props }: { node?: unknown; href?: string; children?: React.ReactNode }) => {
// Validate URLs to prevent javascript: and data: protocols
return text const href = props.href || '';
// Headers const isSafe = href && !href.startsWith('javascript:') && !href.startsWith('data:');
.replace(/^### (.*$)/gim, '<h3>$1</h3>') return (
.replace(/^## (.*$)/gim, '<h2>$1</h2>') <a
.replace(/^# (.*$)/gim, '<h1>$1</h1>') {...props}
// Bold href={isSafe ? href : '#'}
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') target={isSafe && href.startsWith('http') ? '_blank' : undefined}
// Italic rel={isSafe && href.startsWith('http') ? 'noopener noreferrer' : undefined}
.replace(/\*(.*?)\*/g, '<em>$1</em>') />
// Code blocks );
.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>') },
// Inline code img: ({ node, ...props }: { node?: unknown; src?: string; alt?: string }) => {
.replace(/`(.*?)`/g, '<code>$1</code>') // Validate image URLs
// Links const src = props.src || '';
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>') const isSafe = src && !src.startsWith('javascript:') && !src.startsWith('data:');
// Images return isSafe ? <img {...props} src={src} alt={props.alt || ''} /> : null;
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" />') },
// Ensure all images have alt attributes
.replace(/<img([^>]*?)(?:\s+alt\s*=\s*["'][^"']*["'])?([^>]*?)>/g, (match, before, after) => {
if (match.includes('alt=')) return match;
return `<img${before} alt=""${after}>`;
})
// Lists
.replace(/^\* (.*$)/gim, '<li>$1</li>')
.replace(/^- (.*$)/gim, '<li>$1</li>')
.replace(/^(\d+)\. (.*$)/gim, '<li>$2</li>')
// Blockquotes
.replace(/^> (.*$)/gim, '<blockquote>$1</blockquote>')
// Line breaks
.replace(/\n/g, '<br>');
}; };
// Rich text editor functions // Rich text editor functions
@@ -855,10 +837,11 @@ function EditorPageContent() {
<div className="border-t border-white/10 pt-6"> <div className="border-t border-white/10 pt-6">
<h3 className="text-xl font-semibold gradient-text mb-4">Content</h3> <h3 className="text-xl font-semibold gradient-text mb-4">Content</h3>
<div className="prose prose-invert max-w-none"> <div className="prose prose-invert max-w-none">
<div <div className="markdown text-gray-300 leading-relaxed">
className="markdown text-gray-300 leading-relaxed" <ReactMarkdown components={markdownComponents}>
dangerouslySetInnerHTML={{ __html: parseMarkdown(formData.content) }} {formData.content}
/> </ReactMarkdown>
</div>
</div> </div>
</div> </div>
)} )}

View File

@@ -4,8 +4,8 @@ import { Inter } from "next/font/google";
import React from "react"; import React from "react";
import { ToastProvider } from "@/components/Toast"; import { ToastProvider } from "@/components/Toast";
import { AnalyticsProvider } from "@/components/AnalyticsProvider"; import { AnalyticsProvider } from "@/components/AnalyticsProvider";
import { PerformanceDashboard } from "@/components/PerformanceDashboard";
import { BackgroundBlobs } from "@/components/BackgroundBlobs"; import { BackgroundBlobs } from "@/components/BackgroundBlobs";
import { ErrorBoundary } from "@/components/ErrorBoundary";
const inter = Inter({ const inter = Inter({
variable: "--font-inter", variable: "--font-inter",
@@ -29,13 +29,14 @@ export default function RootLayout({
<title>Dennis Konkol&#39;s Portfolio</title> <title>Dennis Konkol&#39;s Portfolio</title>
</head> </head>
<body className={inter.variable}> <body className={inter.variable}>
<AnalyticsProvider> <ErrorBoundary>
<ToastProvider> <AnalyticsProvider>
<BackgroundBlobs /> <ToastProvider>
<div className="relative z-10">{children}</div> <BackgroundBlobs />
<PerformanceDashboard /> <div className="relative z-10">{children}</div>
</ToastProvider> </ToastProvider>
</AnalyticsProvider> </AnalyticsProvider>
</ErrorBoundary>
</body> </body>
</html> </html>
); );

View File

@@ -11,7 +11,7 @@ export default function LegalNotice() {
return ( return (
<div className="min-h-screen animated-bg"> <div className="min-h-screen animated-bg">
<Header /> <Header />
<main className="max-w-4xl mx-auto px-4 py-20"> <main className="max-w-4xl mx-auto px-4 pt-32 pb-20">
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@@ -37,21 +37,21 @@ export default function LegalNotice() {
transition={{ duration: 0.8, delay: 0.2 }} transition={{ duration: 0.8, delay: 0.2 }}
className="glass-card p-8 rounded-2xl space-y-6" className="glass-card p-8 rounded-2xl space-y-6"
> >
<div> <div className="text-gray-300 leading-relaxed">
<h2 className="text-2xl font-semibold text-white mb-4"> <h2 className="text-2xl font-semibold mb-4">
Verantwortlicher für die Inhalte dieser Website Verantwortlicher für die Inhalte dieser Website
</h2> </h2>
<div className="space-y-2 text-gray-300"> <div className="space-y-2 text-gray-300">
<p><strong>Name:</strong> Dennis Konkol</p> <p><strong>Name:</strong> Dennis Konkol</p>
<p><strong>Adresse:</strong> Auf dem Ziegenbrink 2B, 49082 Osnabrück, Deutschland</p> <p><strong>Adresse:</strong> Auf dem Ziegenbrink 2B, 49082 Osnabrück, Deutschland</p>
<p><strong>E-Mail:</strong> <Link href="mailto:info@dki.one" className="text-blue-400 hover:text-blue-300 transition-colors">info@dki.one</Link></p> <p><strong>E-Mail:</strong> <Link href="mailto:info@dki.one" className="text-blue-400 hover:text-blue-300 transition-colors">info@dk0.dev</Link></p>
<p><strong>Website:</strong> <Link href="https://www.dki.one" className="text-blue-400 hover:text-blue-300 transition-colors">dki.one</Link></p> <p><strong>Website:</strong> <Link href="https://www.dk0.dev" className="text-blue-400 hover:text-blue-300 transition-colors">dk0.dev</Link></p>
</div> </div>
</div> </div>
<div> <div className="text-gray-300">
<h2 className="text-2xl font-semibold text-white mb-4">Haftung für Links</h2> <h2 className="text-2xl font-semiboldmb-4">Haftung für Links</h2>
<p className="text-gray-300 leading-relaxed"> <p className="leading-relaxed">
Meine Website enthält Links auf externe Websites. Ich habe keinen Einfluss auf die Inhalte dieser Websites 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 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 Anbieter der Seiten verantwortlich. Jedoch überprüfe ich die verlinkten Seiten zum Zeitpunkt der Verlinkung
@@ -59,17 +59,17 @@ export default function LegalNotice() {
</p> </p>
</div> </div>
<div> <div className="text-gray-300">
<h2 className="text-2xl font-semibold text-white mb-4">Urheberrecht</h2> <h2 className="text-2xl font-semibold mb-4">Urheberrecht</h2>
<p className="text-gray-300 leading-relaxed"> <p className="leading-relaxed">
Alle Inhalte dieser Website, einschließlich Texte, Fotos und Designs, stehen unter Urheberrechtsschutz. Alle Inhalte dieser Website, einschließlich Texte, Fotos und Designs, stehen unter Urheberrechtsschutz.
Jegliche Nutzung ohne vorherige schriftliche Zustimmung des Urhebers ist verboten. Jegliche Nutzung ohne vorherige schriftliche Zustimmung des Urhebers ist verboten.
</p> </p>
</div> </div>
<div> <div className="text-gray-300">
<h2 className="text-2xl font-semibold text-white mb-4">Gewährleistung</h2> <h2 className="text-2xl font-semibold mb-4">Gewährleistung</h2>
<p className="text-gray-300 leading-relaxed"> <p className="leading-relaxed">
Die Nutzung der Inhalte dieser Website erfolgt auf eigene Gefahr. Als Diensteanbieter kann ich keine 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. Gewähr übernehmen für Schäden, die entstehen können, durch den Zugriff oder die Nutzung dieser Website.
</p> </p>

View File

@@ -52,7 +52,9 @@ export default function Home() {
<motion.path <motion.path
d="M0,64 C240,96 480,32 720,64 C960,96 1200,32 1440,64 L1440,120 L0,120 Z" d="M0,64 C240,96 480,32 720,64 C960,96 1200,32 1440,64 L1440,120 L0,120 Z"
fill="url(#gradient1)" fill="url(#gradient1)"
initial={{ opacity: 0 }}
animate={{ animate={{
opacity: 1,
d: [ d: [
"M0,64 C240,96 480,32 720,64 C960,96 1200,32 1440,64 L1440,120 L0,120 Z", "M0,64 C240,96 480,32 720,64 C960,96 1200,32 1440,64 L1440,120 L0,120 Z",
"M0,32 C240,64 480,96 720,32 C960,64 1200,96 1440,32 L1440,120 L0,120 Z", "M0,32 C240,64 480,96 720,32 C960,64 1200,96 1440,32 L1440,120 L0,120 Z",
@@ -60,9 +62,12 @@ export default function Home() {
], ],
}} }}
transition={{ transition={{
duration: 12, opacity: { duration: 0.8, delay: 0.3 },
repeat: Infinity, d: {
ease: "easeInOut", duration: 12,
repeat: Infinity,
ease: "easeInOut",
},
}} }}
/> />
<defs> <defs>
@@ -87,7 +92,9 @@ export default function Home() {
<motion.path <motion.path
d="M0,32 C240,64 480,96 720,32 C960,64 1200,96 1440,32 L1440,120 L0,120 Z" d="M0,32 C240,64 480,96 720,32 C960,64 1200,96 1440,32 L1440,120 L0,120 Z"
fill="url(#gradient2)" fill="url(#gradient2)"
initial={{ opacity: 0 }}
animate={{ animate={{
opacity: 1,
d: [ d: [
"M0,32 C240,64 480,96 720,32 C960,64 1200,96 1440,32 L1440,120 L0,120 Z", "M0,32 C240,64 480,96 720,32 C960,64 1200,96 1440,32 L1440,120 L0,120 Z",
"M0,96 C240,32 480,64 720,96 C960,32 1200,64 1440,96 L1440,120 L0,120 Z", "M0,96 C240,32 480,64 720,96 C960,32 1200,64 1440,96 L1440,120 L0,120 Z",
@@ -95,9 +102,12 @@ export default function Home() {
], ],
}} }}
transition={{ transition={{
duration: 14, opacity: { duration: 0.8, delay: 0.3 },
repeat: Infinity, d: {
ease: "easeInOut", duration: 14,
repeat: Infinity,
ease: "easeInOut",
},
}} }}
/> />
<defs> <defs>
@@ -122,7 +132,9 @@ export default function Home() {
<motion.path <motion.path
d="M0,96 C240,32 480,64 720,96 C960,32 1200,64 1440,96 L1440,120 L0,120 Z" d="M0,96 C240,32 480,64 720,96 C960,32 1200,64 1440,96 L1440,120 L0,120 Z"
fill="url(#gradient3)" fill="url(#gradient3)"
initial={{ opacity: 0 }}
animate={{ animate={{
opacity: 1,
d: [ d: [
"M0,96 C240,32 480,64 720,96 C960,32 1200,64 1440,96 L1440,120 L0,120 Z", "M0,96 C240,32 480,64 720,96 C960,32 1200,64 1440,96 L1440,120 L0,120 Z",
"M0,64 C240,96 480,32 720,64 C960,96 1200,32 1440,64 L1440,120 L0,120 Z", "M0,64 C240,96 480,32 720,64 C960,96 1200,32 1440,64 L1440,120 L0,120 Z",
@@ -130,9 +142,12 @@ export default function Home() {
], ],
}} }}
transition={{ transition={{
duration: 16, opacity: { duration: 0.8, delay: 0.3 },
repeat: Infinity, d: {
ease: "easeInOut", duration: 16,
repeat: Infinity,
ease: "easeInOut",
},
}} }}
/> />
<defs> <defs>

View File

@@ -11,7 +11,7 @@ export default function PrivacyPolicy() {
return ( return (
<div className="min-h-screen animated-bg"> <div className="min-h-screen animated-bg">
<Header /> <Header />
<main className="max-w-4xl mx-auto px-4 py-20"> <main className="max-w-4xl mx-auto px-4 pt-32 pb-20">
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@@ -37,24 +37,24 @@ export default function PrivacyPolicy() {
transition={{ duration: 0.8, delay: 0.2 }} transition={{ duration: 0.8, delay: 0.2 }}
className="glass-card p-8 rounded-2xl space-y-6 text-white" className="glass-card p-8 rounded-2xl space-y-6 text-white"
> >
<div> <div className="text-gray-300 leading-relaxed">
<p className="text-gray-300 leading-relaxed"> <p>
Der Schutz Ihrer persönlichen Daten ist mir wichtig. In dieser Datenschutzerklärung informiere ich Sie 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. über die Verarbeitung personenbezogener Daten im Rahmen meines Internet-Angebots.
</p> </p>
</div> </div>
<div> <div className="text-gray-300 leading-relaxed">
<h2 className="text-2xl font-semibold text-white mb-4"> <h2 className="text-2xl font-semibold mb-4">
Verantwortlicher für die Datenverarbeitung Verantwortlicher für die Datenverarbeitung
</h2> </h2>
<div className="space-y-2 text-gray-300"> <div className="space-y-2 text-gray-300">
<p><strong>Name:</strong> Dennis Konkol</p> <p><strong>Name:</strong> Dennis Konkol</p>
<p><strong>Adresse:</strong> Auf dem Ziegenbrink 2B, 49082 Osnabrück, Deutschland</p> <p><strong>Adresse:</strong> Auf dem Ziegenbrink 2B, 49082 Osnabrück, Deutschland</p>
<p><strong>E-Mail:</strong> <Link className="text-blue-400 hover:text-blue-300 transition-colors" href="mailto:info@dki.one">info@dki.one</Link></p> <p><strong>E-Mail:</strong> <Link className="text-blue-400 hover:text-blue-300 transition-colors" href="mailto:info@dk0.dev">info@dk0.dev</Link></p>
<p><strong>Website:</strong> <Link className="text-blue-400 hover:text-blue-300 transition-colors" href="https://www.dki.one">dki.one</Link></p> <p><strong>Website:</strong> <Link className="text-blue-400 hover:text-blue-300 transition-colors" href="https://www.dk0.dev">dk0.dev</Link></p>
</div> </div>
<p className="text-gray-300 leading-relaxed mt-4"> <p className="mt-4">
Diese Datenschutzerklärung gilt für die Verarbeitung personenbezogener Daten durch den oben genannten Verantwortlichen. Diese Datenschutzerklärung gilt für die Verarbeitung personenbezogener Daten durch den oben genannten Verantwortlichen.
</p> </p>
</div> </div>
@@ -214,10 +214,10 @@ export default function PrivacyPolicy() {
<p className="mt-2"> <p className="mt-2">
Bei Fragen zur Datenschutzerklärung kontaktieren Sie mich unter{" "} Bei Fragen zur Datenschutzerklärung kontaktieren Sie mich unter{" "}
<Link <Link
href="mailto:info@dki.one" href="mailto:info@dk0.dev"
className="text-blue-700 transition-underline" className="text-blue-700 transition-underline"
> >
info@dki.one info@dk0.dev
</Link>{" "} </Link>{" "}
oder nutzen Sie das Kontaktformular auf meiner Website. oder nutzen Sie das Kontaktformular auf meiner Website.
</p> </p>

View File

@@ -35,11 +35,11 @@ const ProjectDetail = () => {
if (data.projects && data.projects.length > 0) { if (data.projects && data.projects.length > 0) {
setProject(data.projects[0]); setProject(data.projects[0]);
} }
} else {
console.error('Failed to fetch project from API');
} }
} catch (error) { } 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 ( return (
<div className="min-h-screen animated-bg"> <div className="min-h-screen animated-bg">
<div className="max-w-4xl mx-auto px-4 py-20"> <div className="max-w-4xl mx-auto px-4 pt-32 pb-20">
{/* Header */} {/* Header */}
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}

View File

@@ -30,11 +30,11 @@ const ProjectsPage = () => {
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
setProjects(data.projects || []); setProjects(data.projects || []);
} else {
console.error('Failed to fetch projects from API');
} }
} catch (error) { } 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
: projects.filter(project => project.category === selectedCategory); : projects.filter(project => project.category === selectedCategory);
console.log('Selected category:', selectedCategory);
console.log('Filtered projects:', filteredProjects);
if (!mounted) { if (!mounted) {
return null; return null;
} }
return ( return (
<div className="min-h-screen animated-bg"> <div className="min-h-screen animated-bg">
<div className="max-w-7xl mx-auto px-4 py-20"> <div className="max-w-7xl mx-auto px-4 pt-32 pb-20">
{/* Header */} {/* Header */}
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
@@ -105,7 +102,7 @@ const ProjectsPage = () => {
onClick={() => setSelectedCategory(category)} onClick={() => setSelectedCategory(category)}
className={`px-6 py-3 rounded-lg font-medium transition-all duration-200 ${ className={`px-6 py-3 rounded-lg font-medium transition-all duration-200 ${
selectedCategory === category 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' : 'bg-gray-800/50 text-gray-300 hover:bg-gray-700/50 hover:text-white'
}`} }`}
> >

View File

@@ -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<Props, State> {
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 (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-stone-900 via-stone-800 to-stone-900 p-4">
<div className="max-w-md w-full bg-stone-800/50 backdrop-blur-sm border border-stone-700/50 rounded-xl p-8 shadow-2xl">
<div className="flex items-center justify-center mb-6">
<AlertTriangle className="w-16 h-16 text-yellow-500" />
</div>
<h2 className="text-2xl font-bold text-white mb-4 text-center">
Something went wrong
</h2>
<p className="text-stone-300 mb-6 text-center">
We encountered an unexpected error. Please try refreshing the page.
</p>
{process.env.NODE_ENV === 'development' && this.state.error && (
<details className="mt-4">
<summary className="text-stone-400 cursor-pointer text-sm mb-2">
Error details (development only)
</summary>
<pre className="text-xs text-stone-500 bg-stone-900/50 p-3 rounded overflow-auto max-h-40">
{this.state.error.toString()}
</pre>
</details>
)}
<button
onClick={() => {
this.setState({ hasError: false, error: null });
window.location.reload();
}}
className="w-full mt-6 px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-lg font-semibold hover:from-blue-700 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl"
>
Refresh Page
</button>
</div>
</div>
);
}
return this.props.children;
}
}

View File

@@ -1,35 +1,101 @@
import { createClient } from 'redis'; import { createClient } from 'redis';
let redisClient: ReturnType<typeof createClient> | null = null; let redisClient: ReturnType<typeof createClient> | 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 () => { 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) { if (!redisClient) {
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'; const redisUrl = process.env.REDIS_URL;
redisClient = createClient({ try {
url: redisUrl, redisClient = createClient({
socket: { url: redisUrl,
reconnectStrategy: (retries) => Math.min(retries * 50, 1000) 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 = null;
}
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();
} }
return redisClient; return redisClient;
@@ -47,10 +113,11 @@ export const cache = {
async get(key: string) { async get(key: string) {
try { try {
const client = await getRedisClient(); const client = await getRedisClient();
if (!client) return null;
const value = await client.get(key); const value = await client.get(key);
return value ? JSON.parse(value) : null; return value ? JSON.parse(value) : null;
} catch (error) { } catch (error) {
console.error('Redis GET error:', error); // Silently fail if Redis is not available
return null; return null;
} }
}, },
@@ -58,10 +125,11 @@ export const cache = {
async set(key: string, value: unknown, ttlSeconds = 3600) { async set(key: string, value: unknown, ttlSeconds = 3600) {
try { try {
const client = await getRedisClient(); const client = await getRedisClient();
if (!client) return false;
await client.setEx(key, ttlSeconds, JSON.stringify(value)); await client.setEx(key, ttlSeconds, JSON.stringify(value));
return true; return true;
} catch (error) { } catch (error) {
console.error('Redis SET error:', error); // Silently fail if Redis is not available
return false; return false;
} }
}, },
@@ -69,10 +137,11 @@ export const cache = {
async del(key: string) { async del(key: string) {
try { try {
const client = await getRedisClient(); const client = await getRedisClient();
if (!client) return false;
await client.del(key); await client.del(key);
return true; return true;
} catch (error) { } catch (error) {
console.error('Redis DEL error:', error); // Silently fail if Redis is not available
return false; return false;
} }
}, },
@@ -80,9 +149,10 @@ export const cache = {
async exists(key: string) { async exists(key: string) {
try { try {
const client = await getRedisClient(); const client = await getRedisClient();
if (!client) return false;
return await client.exists(key); return await client.exists(key);
} catch (error) { } catch (error) {
console.error('Redis EXISTS error:', error); // Silently fail if Redis is not available
return false; return false;
} }
}, },
@@ -90,10 +160,11 @@ export const cache = {
async flush() { async flush() {
try { try {
const client = await getRedisClient(); const client = await getRedisClient();
if (!client) return false;
await client.flushAll(); await client.flushAll();
return true; return true;
} catch (error) { } catch (error) {
console.error('Redis FLUSH error:', error); // Silently fail if Redis is not available
return false; return false;
} }
} }
@@ -146,13 +217,14 @@ export const analyticsCache = {
async clearAll() { async clearAll() {
try { try {
const client = await getRedisClient(); const client = await getRedisClient();
if (!client) return;
// Clear all analytics-related keys // Clear all analytics-related keys
const keys = await client.keys('analytics:*'); const keys = await client.keys('analytics:*');
if (keys.length > 0) { if (keys.length > 0) {
await client.del(keys); await client.del(keys);
} }
} catch (error) { } catch (error) {
console.error('Error clearing analytics cache:', error); // Silently fail if Redis is not available
} }
} }
}; };

14
package-lock.json generated
View File

@@ -2265,9 +2265,9 @@
} }
}, },
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "15.5.7", "version": "15.5.9",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz",
"integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", "integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@next/eslint-plugin-next": { "node_modules/@next/eslint-plugin-next": {
@@ -9383,12 +9383,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/next": { "node_modules/next": {
"version": "15.5.7", "version": "15.5.9",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz",
"integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@next/env": "15.5.7", "@next/env": "15.5.9",
"@swc/helpers": "0.5.15", "@swc/helpers": "0.5.15",
"caniuse-lite": "^1.0.30001579", "caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31", "postcss": "8.4.31",