full upgrade
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<string>((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
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,26 @@ interface ActivityStatusRow {
|
||||
|
||||
export async function GET() {
|
||||
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
|
||||
const result = await prisma.$queryRawUnsafe<ActivityStatusRow[]>(
|
||||
`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(
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 <strong>n8n</strong>.
|
||||
</p>
|
||||
<p className="text-sm italic text-stone-500 bg-stone-50 p-4 rounded-lg border-l-4 border-liquid-mint">
|
||||
💡 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.
|
||||
</p>
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
|
||||
@@ -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"
|
||||
}`}
|
||||
>
|
||||
<hobby.icon size={20} className="text-stone-600" />
|
||||
@@ -218,19 +234,6 @@ const About = () => {
|
||||
</span>
|
||||
</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>
|
||||
</motion.div>
|
||||
|
||||
@@ -119,7 +119,7 @@ const SoundWaves = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// Running animation
|
||||
// Running animation with smooth wavy motion
|
||||
const RunningAnimation = () => {
|
||||
return (
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
@@ -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
|
||||
},
|
||||
}}
|
||||
>
|
||||
🏃
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -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 = () => {
|
||||
<div
|
||||
className={`max-w-[85%] p-3 rounded-2xl text-sm ${
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
@@ -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"
|
||||
/>
|
||||
<motion.button
|
||||
type="submit"
|
||||
disabled={isLoading || !chatMessage.trim()}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
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} />
|
||||
</motion.button>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<h3 className="text-2xl font-bold text-stone-800 mb-6">
|
||||
<h3 className="text-2xl font-bold text-gray-800 mb-6">
|
||||
Send Message
|
||||
</h3>
|
||||
|
||||
@@ -404,7 +406,7 @@ const Contact = () => {
|
||||
) : (
|
||||
<>
|
||||
<Send size={20} />
|
||||
<span>Send Message</span>
|
||||
<span className="text-cream">Send Message</span>
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
@@ -58,13 +58,16 @@ const Header = () => {
|
||||
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={`
|
||||
backdrop-blur-xl transition-all duration-500
|
||||
${
|
||||
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/85 border border-stone-200 shadow-[0_4px_24px_rgba(0,0,0,0.08)] px-4 py-4 rounded-full"
|
||||
? "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/30 shadow-[0_4px_24px_rgba(0,0,0,0.08)] px-4 py-4 rounded-full"
|
||||
}
|
||||
flex justify-between items-center
|
||||
`}
|
||||
@@ -105,7 +108,16 @@ const Header = () => {
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
</motion.div>
|
||||
))}
|
||||
@@ -134,7 +146,7 @@ const Header = () => {
|
||||
>
|
||||
{isOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<span>View My Work</span>
|
||||
<span className="text-cream">View My Work</span>
|
||||
<ArrowDown size={18} />
|
||||
</motion.a>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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, '<h3>$1</h3>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
// Bold
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
// Italic
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
// Code blocks
|
||||
.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
|
||||
// Inline code
|
||||
.replace(/`(.*?)`/g, '<code>$1</code>')
|
||||
// Links
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
||||
// Images
|
||||
.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>');
|
||||
// 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 (
|
||||
<a
|
||||
{...props}
|
||||
href={isSafe ? href : '#'}
|
||||
target={isSafe && href.startsWith('http') ? '_blank' : undefined}
|
||||
rel={isSafe && href.startsWith('http') ? 'noopener noreferrer' : undefined}
|
||||
/>
|
||||
);
|
||||
},
|
||||
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 ? <img {...props} src={src} alt={props.alt || ''} /> : null;
|
||||
},
|
||||
};
|
||||
|
||||
// Rich text editor functions
|
||||
@@ -855,10 +837,11 @@ function EditorPageContent() {
|
||||
<div className="border-t border-white/10 pt-6">
|
||||
<h3 className="text-xl font-semibold gradient-text mb-4">Content</h3>
|
||||
<div className="prose prose-invert max-w-none">
|
||||
<div
|
||||
className="markdown text-gray-300 leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: parseMarkdown(formData.content) }}
|
||||
/>
|
||||
<div className="markdown text-gray-300 leading-relaxed">
|
||||
<ReactMarkdown components={markdownComponents}>
|
||||
{formData.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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({
|
||||
<title>Dennis Konkol's Portfolio</title>
|
||||
</head>
|
||||
<body className={inter.variable}>
|
||||
<AnalyticsProvider>
|
||||
<ToastProvider>
|
||||
<BackgroundBlobs />
|
||||
<div className="relative z-10">{children}</div>
|
||||
<PerformanceDashboard />
|
||||
</ToastProvider>
|
||||
</AnalyticsProvider>
|
||||
<ErrorBoundary>
|
||||
<AnalyticsProvider>
|
||||
<ToastProvider>
|
||||
<BackgroundBlobs />
|
||||
<div className="relative z-10">{children}</div>
|
||||
</ToastProvider>
|
||||
</AnalyticsProvider>
|
||||
</ErrorBoundary>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function LegalNotice() {
|
||||
return (
|
||||
<div className="min-h-screen animated-bg">
|
||||
<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
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
@@ -37,21 +37,21 @@ export default function LegalNotice() {
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
className="glass-card p-8 rounded-2xl space-y-6"
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-white mb-4">
|
||||
<div className="text-gray-300 leading-relaxed">
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Verantwortlicher für die Inhalte dieser Website
|
||||
</h2>
|
||||
<div className="space-y-2 text-gray-300">
|
||||
<p><strong>Name:</strong> Dennis Konkol</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>Website:</strong> <Link href="https://www.dki.one" className="text-blue-400 hover:text-blue-300 transition-colors">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.dk0.dev" className="text-blue-400 hover:text-blue-300 transition-colors">dk0.dev</Link></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-white mb-4">Haftung für Links</h2>
|
||||
<p className="text-gray-300 leading-relaxed">
|
||||
<div className="text-gray-300">
|
||||
<h2 className="text-2xl font-semiboldmb-4">Haftung für Links</h2>
|
||||
<p className="leading-relaxed">
|
||||
Meine Website enthält Links auf externe Websites. Ich habe keinen Einfluss auf die Inhalte dieser Websites
|
||||
und kann daher keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der Betreiber oder
|
||||
Anbieter der Seiten verantwortlich. Jedoch überprüfe ich die verlinkten Seiten zum Zeitpunkt der Verlinkung
|
||||
@@ -59,17 +59,17 @@ export default function LegalNotice() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-white mb-4">Urheberrecht</h2>
|
||||
<p className="text-gray-300 leading-relaxed">
|
||||
<div className="text-gray-300">
|
||||
<h2 className="text-2xl font-semibold mb-4">Urheberrecht</h2>
|
||||
<p className="leading-relaxed">
|
||||
Alle Inhalte dieser Website, einschließlich Texte, Fotos und Designs, stehen unter Urheberrechtsschutz.
|
||||
Jegliche Nutzung ohne vorherige schriftliche Zustimmung des Urhebers ist verboten.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-white mb-4">Gewährleistung</h2>
|
||||
<p className="text-gray-300 leading-relaxed">
|
||||
<div className="text-gray-300">
|
||||
<h2 className="text-2xl font-semibold mb-4">Gewährleistung</h2>
|
||||
<p className="leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
|
||||
33
app/page.tsx
33
app/page.tsx
@@ -52,7 +52,9 @@ export default function Home() {
|
||||
<motion.path
|
||||
d="M0,64 C240,96 480,32 720,64 C960,96 1200,32 1440,64 L1440,120 L0,120 Z"
|
||||
fill="url(#gradient1)"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
d: [
|
||||
"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",
|
||||
@@ -60,9 +62,12 @@ export default function Home() {
|
||||
],
|
||||
}}
|
||||
transition={{
|
||||
duration: 12,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
opacity: { duration: 0.8, delay: 0.3 },
|
||||
d: {
|
||||
duration: 12,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<defs>
|
||||
@@ -87,7 +92,9 @@ export default function Home() {
|
||||
<motion.path
|
||||
d="M0,32 C240,64 480,96 720,32 C960,64 1200,96 1440,32 L1440,120 L0,120 Z"
|
||||
fill="url(#gradient2)"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
d: [
|
||||
"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",
|
||||
@@ -95,9 +102,12 @@ export default function Home() {
|
||||
],
|
||||
}}
|
||||
transition={{
|
||||
duration: 14,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
opacity: { duration: 0.8, delay: 0.3 },
|
||||
d: {
|
||||
duration: 14,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<defs>
|
||||
@@ -122,7 +132,9 @@ export default function Home() {
|
||||
<motion.path
|
||||
d="M0,96 C240,32 480,64 720,96 C960,32 1200,64 1440,96 L1440,120 L0,120 Z"
|
||||
fill="url(#gradient3)"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
d: [
|
||||
"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",
|
||||
@@ -130,9 +142,12 @@ export default function Home() {
|
||||
],
|
||||
}}
|
||||
transition={{
|
||||
duration: 16,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
opacity: { duration: 0.8, delay: 0.3 },
|
||||
d: {
|
||||
duration: 16,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<defs>
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function PrivacyPolicy() {
|
||||
return (
|
||||
<div className="min-h-screen animated-bg">
|
||||
<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
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
@@ -37,24 +37,24 @@ export default function PrivacyPolicy() {
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
className="glass-card p-8 rounded-2xl space-y-6 text-white"
|
||||
>
|
||||
<div>
|
||||
<p className="text-gray-300 leading-relaxed">
|
||||
<div className="text-gray-300 leading-relaxed">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-white mb-4">
|
||||
<div className="text-gray-300 leading-relaxed">
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
Verantwortlicher für die Datenverarbeitung
|
||||
</h2>
|
||||
<div className="space-y-2 text-gray-300">
|
||||
<p><strong>Name:</strong> Dennis Konkol</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>Website:</strong> <Link className="text-blue-400 hover:text-blue-300 transition-colors" href="https://www.dki.one">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.dk0.dev">dk0.dev</Link></p>
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -214,10 +214,10 @@ export default function PrivacyPolicy() {
|
||||
<p className="mt-2">
|
||||
Bei Fragen zur Datenschutzerklärung kontaktieren Sie mich unter{" "}
|
||||
<Link
|
||||
href="mailto:info@dki.one"
|
||||
href="mailto:info@dk0.dev"
|
||||
className="text-blue-700 transition-underline"
|
||||
>
|
||||
info@dki.one
|
||||
info@dk0.dev
|
||||
</Link>{" "}
|
||||
oder nutzen Sie das Kontaktformular auf meiner Website.
|
||||
</p>
|
||||
|
||||
@@ -35,11 +35,11 @@ const ProjectDetail = () => {
|
||||
if (data.projects && data.projects.length > 0) {
|
||||
setProject(data.projects[0]);
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to fetch project from API');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading project:', error);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error loading project:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -59,7 +59,7 @@ const ProjectDetail = () => {
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
|
||||
@@ -30,11 +30,11 @@ const ProjectsPage = () => {
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setProjects(data.projects || []);
|
||||
} else {
|
||||
console.error('Failed to fetch projects from API');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading projects:', error);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error loading projects:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -57,16 +57,13 @@ const ProjectsPage = () => {
|
||||
? projects
|
||||
: projects.filter(project => project.category === selectedCategory);
|
||||
|
||||
console.log('Selected category:', selectedCategory);
|
||||
console.log('Filtered projects:', filteredProjects);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
@@ -105,7 +102,7 @@ const ProjectsPage = () => {
|
||||
onClick={() => setSelectedCategory(category)}
|
||||
className={`px-6 py-3 rounded-lg font-medium transition-all duration-200 ${
|
||||
selectedCategory === category
|
||||
? 'bg-blue-600 text-white shadow-lg'
|
||||
? 'bg-gray-800 text-cream shadow-lg'
|
||||
: 'bg-gray-800/50 text-gray-300 hover:bg-gray-700/50 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user