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, '
')
- // 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 ?
: 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}
+
+
+