full upgrade
This commit is contained in:
@@ -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)
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.log('Performance Metric:', {
|
console.log('Performance Metric:', {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
...body,
|
...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) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.error('Analytics API Error:', error);
|
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 }
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
// 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);
|
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) {
|
||||||
|
// 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);
|
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 }
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
// 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);
|
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) {
|
||||||
|
// 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);
|
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 }
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
// Only log non-table-missing errors
|
||||||
|
if (error instanceof Error && !error.message.includes('does not exist')) {
|
||||||
console.error("Error fetching activity status:", error);
|
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(
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
// 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);
|
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) {
|
||||||
|
// 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);
|
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) {
|
||||||
|
// 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);
|
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 }
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
// 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);
|
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) {
|
||||||
|
// 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);
|
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 }
|
||||||
|
|||||||
@@ -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"
|
||||||
|
>
|
||||||
|
<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.
|
stay focused.
|
||||||
</p>
|
</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>
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
duration: 1.2,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
ease: "linear",
|
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,8 +282,10 @@ export const ActivityFeed = () => {
|
|||||||
setData(json);
|
setData(json);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.error("Failed to fetch activity", e);
|
console.error("Failed to fetch activity", e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
const interval = setInterval(fetchData, 30000); // Poll every 30s
|
const interval = setInterval(fetchData, 30000); // Poll every 30s
|
||||||
@@ -301,7 +321,9 @@ export const ActivityFeed = () => {
|
|||||||
throw new Error("Chat API failed");
|
throw new Error("Chat API failed");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.error("Chat error:", error);
|
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>
|
||||||
|
|||||||
@@ -90,7 +90,9 @@ const Contact = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.error("Error sending email:", error);
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -68,8 +68,10 @@ const Projects = () => {
|
|||||||
setProjects(data.projects || []);
|
setProjects(data.projects || []);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.error("Error loading projects:", error);
|
console.error("Error loading projects:", error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
loadProjects();
|
loadProjects();
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -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,16 +89,17 @@ 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 {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.error('Failed to fetch projects:', response.status);
|
console.error('Failed to fetch projects:', response.status);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.error('Error loading project:', error);
|
console.error('Error loading project:', error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Check authentication and load project
|
// Check authentication and load project
|
||||||
@@ -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) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.error('Error in init:', error);
|
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();
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.error('Error saving project:', response.status, errorData);
|
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) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.error('Error saving project:', error);
|
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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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's Portfolio</title>
|
<title>Dennis Konkol's Portfolio</title>
|
||||||
</head>
|
</head>
|
||||||
<body className={inter.variable}>
|
<body className={inter.variable}>
|
||||||
|
<ErrorBoundary>
|
||||||
<AnalyticsProvider>
|
<AnalyticsProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<BackgroundBlobs />
|
<BackgroundBlobs />
|
||||||
<div className="relative z-10">{children}</div>
|
<div className="relative z-10">{children}</div>
|
||||||
<PerformanceDashboard />
|
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</AnalyticsProvider>
|
</AnalyticsProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
15
app/page.tsx
15
app/page.tsx
@@ -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={{
|
||||||
|
opacity: { duration: 0.8, delay: 0.3 },
|
||||||
|
d: {
|
||||||
duration: 12,
|
duration: 12,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
ease: "easeInOut",
|
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={{
|
||||||
|
opacity: { duration: 0.8, delay: 0.3 },
|
||||||
|
d: {
|
||||||
duration: 14,
|
duration: 14,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
ease: "easeInOut",
|
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={{
|
||||||
|
opacity: { duration: 0.8, delay: 0.3 },
|
||||||
|
d: {
|
||||||
duration: 16,
|
duration: 16,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
ease: "easeInOut",
|
ease: "easeInOut",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<defs>
|
<defs>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ 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) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.error('Error loading project:', error);
|
console.error('Error loading project:', error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadProject();
|
loadProject();
|
||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ 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) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.error('Error loading projects:', error);
|
console.error('Error loading projects:', error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadProjects();
|
loadProjects();
|
||||||
@@ -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'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
84
components/ErrorBoundary.tsx
Normal file
84
components/ErrorBoundary.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
94
lib/redis.ts
94
lib/redis.ts
@@ -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 (!redisClient) {
|
// If Redis URL is not configured, return null instead of trying to connect
|
||||||
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
|
if (!process.env.REDIS_URL) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If connection has already failed, don't try again
|
||||||
|
if (connectionFailed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!redisClient) {
|
||||||
|
const redisUrl = process.env.REDIS_URL;
|
||||||
|
|
||||||
|
try {
|
||||||
redisClient = createClient({
|
redisClient = createClient({
|
||||||
url: redisUrl,
|
url: redisUrl,
|
||||||
socket: {
|
socket: {
|
||||||
reconnectStrategy: (retries) => Math.min(retries * 50, 1000)
|
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) => {
|
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);
|
console.error('Redis Client Error:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
redisClient.on('connect', () => {
|
redisClient.on('connect', () => {
|
||||||
console.log('Redis Client Connected');
|
console.log('Redis Client Connected');
|
||||||
|
connectionFailed = false; // Reset on successful connection
|
||||||
});
|
});
|
||||||
|
|
||||||
redisClient.on('ready', () => {
|
redisClient.on('ready', () => {
|
||||||
console.log('Redis Client Ready');
|
console.log('Redis Client Ready');
|
||||||
|
connectionFailed = false; // Reset on ready
|
||||||
});
|
});
|
||||||
|
|
||||||
redisClient.on('end', () => {
|
redisClient.on('end', () => {
|
||||||
console.log('Redis Client Disconnected');
|
console.log('Redis Client Disconnected');
|
||||||
});
|
});
|
||||||
|
|
||||||
await redisClient.connect();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
14
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user