full upgrade

This commit is contained in:
2026-01-10 00:52:08 +01:00
parent b487f4ba75
commit ae37294b06
14 changed files with 2680 additions and 1340 deletions

View File

@@ -3,412 +3,199 @@ import nodemailer from "nodemailer";
import SMTPTransport from "nodemailer/lib/smtp-transport";
import Mail from "nodemailer/lib/mailer";
// Email templates with beautiful designs
const BRAND = {
siteUrl: "https://dk0.dev",
email: "contact@dk0.dev",
bg: "#FDFCF8",
sand: "#F3F1E7",
border: "#E7E5E4",
text: "#292524",
muted: "#78716C",
mint: "#A7F3D0",
red: "#EF4444",
};
function escapeHtml(input: string): string {
return input
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function nl2br(input: string): string {
return input.replace(/\r\n|\r|\n/g, "<br>");
}
function baseEmail(opts: { title: string; subtitle: string; bodyHtml: string }) {
const sentAt = new Date().toLocaleString("de-DE", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
return `
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${escapeHtml(opts.title)}</title>
</head>
<body style="margin:0;padding:0;background-color:${BRAND.bg};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;color:${BRAND.text};">
<div style="max-width:640px;margin:0 auto;padding:28px 14px;">
<div style="background:#ffffff;border:1px solid ${BRAND.border};border-radius:20px;overflow:hidden;box-shadow:0 18px 50px rgba(0,0,0,0.08);">
<div style="background:${BRAND.text};padding:22px 26px;">
<div style="display:flex;align-items:center;justify-content:space-between;gap:16px;">
<div style="font-weight:800;font-size:16px;color:${BRAND.bg};">Dennis Konkol</div>
<div style="font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;font-weight:800;font-size:14px;color:${BRAND.bg};">
dk<span style="color:${BRAND.red};">0</span>.dev
</div>
</div>
<div style="margin-top:10px;">
<div style="font-size:22px;font-weight:900;letter-spacing:-0.02em;color:${BRAND.bg};">${escapeHtml(opts.title)}</div>
<div style="margin-top:4px;font-size:13px;color:#d6d3d1;">${escapeHtml(opts.subtitle)}${sentAt}</div>
</div>
<div style="height:3px;background:${BRAND.mint};margin-top:18px;border-radius:999px;"></div>
</div>
<div style="padding:26px;">
${opts.bodyHtml}
</div>
<div style="padding:18px 26px;background:${BRAND.bg};border-top:1px solid ${BRAND.border};">
<div style="font-size:12px;color:${BRAND.muted};line-height:1.5;">
Automatisch generiert von <a href="${BRAND.siteUrl}" style="color:${BRAND.text};text-decoration:underline;">dk0.dev</a> •
<a href="mailto:${BRAND.email}" style="color:${BRAND.text};text-decoration:underline;">${BRAND.email}</a>
</div>
</div>
</div>
</div>
</body>
</html>
`.trim();
}
const emailTemplates = {
welcome: {
subject: "Vielen Dank für deine Nachricht! 👋",
template: (name: string, originalMessage: string) => `
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Willkommen - Dennis Konkol</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f8fafc; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
<div style="max-width: 600px; margin: 0 auto; background-color: #ffffff; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">
template: (name: string, originalMessage: string) => {
const safeName = escapeHtml(name);
const safeMsg = nl2br(escapeHtml(originalMessage));
return baseEmail({
title: `Danke, ${safeName}!`,
subtitle: "Nachricht erhalten",
bodyHtml: `
<div style="font-size:15px;line-height:1.65;color:${BRAND.text};">
Hey ${safeName},<br><br>
danke für deine Nachricht — ich habe sie erhalten und melde mich so schnell wie möglich bei dir zurück.
</div>
<!-- Header -->
<div style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 40px 30px; text-align: center;">
<h1 style="color: #ffffff; margin: 0; font-size: 28px; font-weight: 600; letter-spacing: -0.5px;">
👋 Hallo ${name}!
</h1>
<p style="color: #d1fae5; margin: 8px 0 0 0; font-size: 16px; opacity: 0.9;">
Vielen Dank für deine Nachricht
</p>
<div style="margin-top:18px;background:${BRAND.bg};border:1px solid ${BRAND.border};border-radius:16px;overflow:hidden;">
<div style="padding:14px 16px;background:${BRAND.sand};border-bottom:1px solid ${BRAND.border};">
<div style="font-size:12px;letter-spacing:0.08em;text-transform:uppercase;font-weight:800;color:#57534e;">Deine Nachricht</div>
</div>
<div style="padding:16px;line-height:1.65;color:${BRAND.text};font-size:14px;border-left:4px solid ${BRAND.mint};">
${safeMsg}
</div>
</div>
<!-- Content -->
<div style="padding: 40px 30px;">
<!-- Welcome Message -->
<div style="background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); padding: 30px; border-radius: 12px; margin-bottom: 30px; border: 1px solid #bbf7d0;">
<div style="text-align: center; margin-bottom: 20px;">
<div style="width: 60px; height: 60px; background: linear-gradient(135deg, #10b981 0%, #059669 100%); border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 15px;">
<span style="color: #ffffff; font-size: 24px;">✓</span>
</div>
<h2 style="color: #065f46; margin: 0; font-size: 22px; font-weight: 600;">Nachricht erhalten!</h2>
</div>
<p style="color: #047857; margin: 0; text-align: center; line-height: 1.6; font-size: 16px;">
Vielen Dank für deine Nachricht! Ich habe sie erhalten und werde mich so schnell wie möglich bei dir melden.
</p>
</div>
<!-- Original Message Reference -->
<div style="background: #ffffff; padding: 25px; border-radius: 12px; border: 1px solid #e5e7eb; margin-bottom: 30px;">
<h3 style="color: #374151; margin: 0 0 15px 0; font-size: 16px; font-weight: 600; display: flex; align-items: center;">
<span style="width: 6px; height: 6px; background: #6b7280; border-radius: 50%; margin-right: 10px;"></span>
Deine ursprüngliche Nachricht
</h3>
<div style="background: #f9fafb; padding: 20px; border-radius: 8px; border-left: 4px solid #10b981;">
<p style="color: #4b5563; margin: 0; line-height: 1.6; font-style: italic; white-space: pre-wrap;">${originalMessage}</p>
</div>
</div>
<!-- Next Steps -->
<div style="background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); padding: 30px; border-radius: 12px; border: 1px solid #bfdbfe;">
<h3 style="color: #1e40af; margin: 0 0 20px 0; font-size: 18px; font-weight: 600; text-align: center;">
🚀 Was passiert als nächstes?
</h3>
<div style="display: grid; gap: 15px;">
<div style="display: flex; align-items: center; padding: 15px; background: #ffffff; border-radius: 8px; border-left: 4px solid #3b82f6;">
<span style="color: #3b82f6; font-size: 20px; margin-right: 15px;">📧</span>
<div>
<h4 style="color: #1e40af; margin: 0 0 4px 0; font-size: 14px; font-weight: 600;">Schnelle Antwort</h4>
<p style="color: #4b5563; margin: 0; font-size: 14px;">Ich antworte normalerweise innerhalb von 24 Stunden</p>
</div>
</div>
<div style="display: flex; align-items: center; padding: 15px; background: #ffffff; border-radius: 8px; border-left: 4px solid #8b5cf6;">
<span style="color: #8b5cf6; font-size: 20px; margin-right: 15px;">💼</span>
<div>
<h4 style="color: #7c3aed; margin: 0 0 4px 0; font-size: 14px; font-weight: 600;">Projekt-Diskussion</h4>
<p style="color: #4b5563; margin: 0; font-size: 14px;">Gerne besprechen wir dein Projekt im Detail</p>
</div>
</div>
<div style="display: flex; align-items: center; padding: 15px; background: #ffffff; border-radius: 8px; border-left: 4px solid #f59e0b;">
<span style="color: #f59e0b; font-size: 20px; margin-right: 15px;">🤝</span>
<div>
<h4 style="color: #d97706; margin: 0 0 4px 0; font-size: 14px; font-weight: 600;">Zusammenarbeit</h4>
<p style="color: #4b5563; margin: 0; font-size: 14px;">Lass uns gemeinsam etwas Großartiges schaffen!</p>
</div>
</div>
</div>
</div>
<!-- Portfolio Links -->
<div style="text-align: center; margin-top: 30px;">
<h3 style="color: #374151; margin: 0 0 20px 0; font-size: 18px; font-weight: 600;">Entdecke mehr von mir</h3>
<div style="display: flex; justify-content: center; gap: 15px; flex-wrap: wrap;">
<a href="https://dk0.dev" style="display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 8px; font-weight: 600; font-size: 14px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);">
🌐 Portfolio
<div style="margin-top:20px;text-align:center;">
<a href="${BRAND.siteUrl}" style="display:inline-block;background:${BRAND.text};color:${BRAND.bg};text-decoration:none;padding:12px 18px;border-radius:999px;font-weight:800;font-size:14px;">
Portfolio ansehen
</a>
<a href="https://github.com/denniskonkol" style="display: inline-block; background: linear-gradient(135deg, #374151 0%, #111827 100%); color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 8px; font-weight: 600; font-size: 14px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);">
💻 GitHub
</a>
<a href="https://linkedin.com/in/denniskonkol" style="display: inline-block; background: linear-gradient(135deg, #0077b5 0%, #005885 100%); color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 8px; font-weight: 600; font-size: 14px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);">
💼 LinkedIn
</a>
</div>
</div>
</div>
<!-- Footer -->
<div style="background: #f8fafc; padding: 30px; text-align: center; border-top: 1px solid #e5e7eb;">
<div style="margin-bottom: 15px;">
<span style="display: inline-block; width: 40px; height: 2px; background: linear-gradient(135deg, #10b981 0%, #059669 100%); border-radius: 1px;"></span>
</div>
<p style="color: #6b7280; margin: 0; font-size: 14px; line-height: 1.5;">
<strong>Dennis Konkol</strong> • Software Engineer & Student<br>
<a href="https://dk0.dev" style="color: #10b981; text-decoration: none; font-family: 'Monaco', 'Menlo', 'Consolas', monospace; font-weight: bold;">dk<span style="color: #ef4444;">0</span>.dev</a> •
<a href="mailto:contact@dk0.dev" style="color: #10b981; text-decoration: none;">contact@dk0.dev</a>
</p>
<p style="color: #9ca3af; margin: 10px 0 0 0; font-size: 12px;">
${new Date().toLocaleString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</p>
</div>
</div>
</body>
</html>
`
</div>
`.trim(),
});
},
},
project: {
subject: "Projekt-Anfrage erhalten! 🚀",
template: (name: string, originalMessage: string) => `
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Projekt-Anfrage - Dennis Konkol</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f8fafc; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
<div style="max-width: 600px; margin: 0 auto; background-color: #ffffff; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">
template: (name: string, originalMessage: string) => {
const safeName = escapeHtml(name);
const safeMsg = nl2br(escapeHtml(originalMessage));
return baseEmail({
title: `Projekt-Anfrage: danke, ${safeName}!`,
subtitle: "Ich melde mich zeitnah",
bodyHtml: `
<div style="font-size:15px;line-height:1.65;color:${BRAND.text};">
Hey ${safeName},<br><br>
mega — danke für die Projekt-Anfrage. Ich schaue mir deine Nachricht an und komme mit Rückfragen/Ideen auf dich zu.
</div>
<!-- Header -->
<div style="background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); padding: 40px 30px; text-align: center;">
<h1 style="color: #ffffff; margin: 0; font-size: 28px; font-weight: 600; letter-spacing: -0.5px;">
🚀 Projekt-Anfrage erhalten!
</h1>
<p style="color: #e9d5ff; margin: 8px 0 0 0; font-size: 16px; opacity: 0.9;">
Hallo ${name}, lass uns etwas Großartiges schaffen!
</p>
<div style="margin-top:18px;background:${BRAND.bg};border:1px solid ${BRAND.border};border-radius:16px;overflow:hidden;">
<div style="padding:14px 16px;background:${BRAND.sand};border-bottom:1px solid ${BRAND.border};">
<div style="font-size:12px;letter-spacing:0.08em;text-transform:uppercase;font-weight:800;color:#57534e;">Deine Projekt-Nachricht</div>
</div>
<div style="padding:16px;line-height:1.65;color:${BRAND.text};font-size:14px;border-left:4px solid ${BRAND.mint};">
${safeMsg}
</div>
</div>
<!-- Content -->
<div style="padding: 40px 30px;">
<!-- Project Message -->
<div style="background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%); padding: 30px; border-radius: 12px; margin-bottom: 30px; border: 1px solid #e9d5ff;">
<div style="text-align: center; margin-bottom: 20px;">
<div style="width: 60px; height: 60px; background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 15px;">
<span style="color: #ffffff; font-size: 24px;">💼</span>
</div>
<h2 style="color: #6b21a8; margin: 0; font-size: 22px; font-weight: 600;">Bereit für dein Projekt!</h2>
</div>
<p style="color: #7c2d12; margin: 0; text-align: center; line-height: 1.6; font-size: 16px;">
Vielen Dank für deine Projekt-Anfrage! Ich bin gespannt darauf, mehr über deine Ideen zu erfahren und wie wir sie gemeinsam umsetzen können.
</p>
</div>
<!-- Original Message -->
<div style="background: #ffffff; padding: 25px; border-radius: 12px; border: 1px solid #e5e7eb; margin-bottom: 30px;">
<h3 style="color: #374151; margin: 0 0 15px 0; font-size: 16px; font-weight: 600; display: flex; align-items: center;">
<span style="width: 6px; height: 6px; background: #8b5cf6; border-radius: 50%; margin-right: 10px;"></span>
Deine Projekt-Nachricht
</h3>
<div style="background: #f9fafb; padding: 20px; border-radius: 8px; border-left: 4px solid #8b5cf6;">
<p style="color: #4b5563; margin: 0; line-height: 1.6; font-style: italic; white-space: pre-wrap;">${originalMessage}</p>
</div>
</div>
<!-- Process Steps -->
<div style="background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); padding: 30px; border-radius: 12px; border: 1px solid #bfdbfe;">
<h3 style="color: #1e40af; margin: 0 0 20px 0; font-size: 18px; font-weight: 600; text-align: center;">
🎯 Mein Arbeitsprozess
</h3>
<div style="display: grid; gap: 15px;">
<div style="display: flex; align-items: center; padding: 15px; background: #ffffff; border-radius: 8px; border-left: 4px solid #3b82f6;">
<span style="color: #3b82f6; font-size: 20px; margin-right: 15px;">💬</span>
<div>
<h4 style="color: #1e40af; margin: 0 0 4px 0; font-size: 14px; font-weight: 600;">1. Erstgespräch</h4>
<p style="color: #4b5563; margin: 0; font-size: 14px;">Wir besprechen deine Anforderungen im Detail</p>
</div>
</div>
<div style="display: flex; align-items: center; padding: 15px; background: #ffffff; border-radius: 8px; border-left: 4px solid #8b5cf6;">
<span style="color: #8b5cf6; font-size: 20px; margin-right: 15px;">📋</span>
<div>
<h4 style="color: #7c3aed; margin: 0 0 4px 0; font-size: 14px; font-weight: 600;">2. Konzept & Planung</h4>
<p style="color: #4b5563; margin: 0; font-size: 14px;">Ich erstelle ein detailliertes Konzept für dein Projekt</p>
</div>
</div>
<div style="display: flex; align-items: center; padding: 15px; background: #ffffff; border-radius: 8px; border-left: 4px solid #10b981;">
<span style="color: #10b981; font-size: 20px; margin-right: 15px;">⚡</span>
<div>
<h4 style="color: #059669; margin: 0 0 4px 0; font-size: 14px; font-weight: 600;">3. Entwicklung</h4>
<p style="color: #4b5563; margin: 0; font-size: 14px;">Agile Entwicklung mit regelmäßigen Updates</p>
</div>
</div>
<div style="display: flex; align-items: center; padding: 15px; background: #ffffff; border-radius: 8px; border-left: 4px solid #f59e0b;">
<span style="color: #f59e0b; font-size: 20px; margin-right: 15px;">🎉</span>
<div>
<h4 style="color: #d97706; margin: 0 0 4px 0; font-size: 14px; font-weight: 600;">4. Launch & Support</h4>
<p style="color: #4b5563; margin: 0; font-size: 14px;">Deployment und kontinuierlicher Support</p>
</div>
</div>
</div>
</div>
<!-- CTA -->
<div style="text-align: center; margin-top: 30px;">
<a href="mailto:contact@dk0.dev?subject=Projekt-Diskussion mit ${name}" style="display: inline-block; background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); color: #ffffff; text-decoration: none; padding: 15px 30px; border-radius: 8px; font-weight: 600; font-size: 16px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">
💬 Projekt besprechen
<div style="margin-top:20px;text-align:center;">
<a href="mailto:${BRAND.email}" style="display:inline-block;background:${BRAND.text};color:${BRAND.bg};text-decoration:none;padding:12px 18px;border-radius:999px;font-weight:800;font-size:14px;">
Kontakt aufnehmen
</a>
</div>
</div>
<!-- Footer -->
<div style="background: #f8fafc; padding: 30px; text-align: center; border-top: 1px solid #e5e7eb;">
<div style="margin-bottom: 15px;">
<span style="display: inline-block; width: 40px; height: 2px; background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); border-radius: 1px;"></span>
</div>
<p style="color: #6b7280; margin: 0; font-size: 14px; line-height: 1.5;">
<strong>Dennis Konkol</strong> • Software Engineer & Student<br>
<a href="https://dki.one" style="color: #8b5cf6; text-decoration: none;">dki.one</a> •
<a href="mailto:contact@dk0.dev" style="color: #8b5cf6; text-decoration: none;">contact@dk0.dev</a>
</p>
</div>
</div>
</body>
</html>
`
</div>
`.trim(),
});
},
},
quick: {
subject: "Danke für deine Nachricht! ⚡",
template: (name: string, originalMessage: string) => `
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quick Response - Dennis Konkol</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f8fafc; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
<div style="max-width: 600px; margin: 0 auto; background-color: #ffffff; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">
template: (name: string, originalMessage: string) => {
const safeName = escapeHtml(name);
const safeMsg = nl2br(escapeHtml(originalMessage));
return baseEmail({
title: `Danke, ${safeName}!`,
subtitle: "Kurze Bestätigung",
bodyHtml: `
<div style="font-size:15px;line-height:1.65;color:${BRAND.text};">
Hey ${safeName},<br><br>
kurze Bestätigung: deine Nachricht ist angekommen. Ich melde mich bald zurück.
</div>
<!-- Header -->
<div style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); padding: 40px 30px; text-align: center;">
<h1 style="color: #ffffff; margin: 0; font-size: 28px; font-weight: 600; letter-spacing: -0.5px;">
⚡ Schnelle Antwort!
</h1>
<p style="color: #fef3c7; margin: 8px 0 0 0; font-size: 16px; opacity: 0.9;">
Hallo ${name}, danke für deine Nachricht!
</p>
<div style="margin-top:18px;background:${BRAND.bg};border:1px solid ${BRAND.border};border-radius:16px;overflow:hidden;">
<div style="padding:14px 16px;background:${BRAND.sand};border-bottom:1px solid ${BRAND.border};">
<div style="font-size:12px;letter-spacing:0.08em;text-transform:uppercase;font-weight:800;color:#57534e;">Deine Nachricht</div>
</div>
<!-- Content -->
<div style="padding: 40px 30px;">
<!-- Quick Response -->
<div style="background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); padding: 30px; border-radius: 12px; margin-bottom: 30px; border: 1px solid #fde68a;">
<div style="text-align: center;">
<div style="width: 60px; height: 60px; background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 15px;">
<span style="color: #ffffff; font-size: 24px;">⚡</span>
<div style="padding:16px;line-height:1.65;color:${BRAND.text};font-size:14px;border-left:4px solid ${BRAND.mint};">
${safeMsg}
</div>
<h2 style="color: #92400e; margin: 0 0 15px 0; font-size: 22px; font-weight: 600;">Nachricht erhalten!</h2>
<p style="color: #a16207; margin: 0; line-height: 1.6; font-size: 16px;">
Vielen Dank für deine Nachricht! Ich werde mich so schnell wie möglich bei dir melden.
</p>
</div>
</div>
<!-- Original Message -->
<div style="background: #ffffff; padding: 25px; border-radius: 12px; border: 1px solid #e5e7eb; margin-bottom: 30px;">
<h3 style="color: #374151; margin: 0 0 15px 0; font-size: 16px; font-weight: 600; display: flex; align-items: center;">
<span style="width: 6px; height: 6px; background: #f59e0b; border-radius: 50%; margin-right: 10px;"></span>
Deine Nachricht
</h3>
<div style="background: #f9fafb; padding: 20px; border-radius: 8px; border-left: 4px solid #f59e0b;">
<p style="color: #4b5563; margin: 0; line-height: 1.6; font-style: italic; white-space: pre-wrap;">${originalMessage}</p>
</div>
</div>
<!-- Quick Info -->
<div style="background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); padding: 25px; border-radius: 12px; border: 1px solid #bfdbfe;">
<h3 style="color: #1e40af; margin: 0 0 15px 0; font-size: 16px; font-weight: 600; text-align: center;">
📞 Kontakt
</h3>
<p style="color: #1e40af; margin: 0; text-align: center; line-height: 1.6; font-size: 14px;">
<strong>E-Mail:</strong> <a href="mailto:contact@dk0.dev" style="color: #1e40af; text-decoration: none;">contact@dk0.dev</a><br>
<strong>Portfolio:</strong> <a href="https://dki.one" style="color: #1e40af; text-decoration: none;">dki.one</a>
</p>
</div>
</div>
<!-- Footer -->
<div style="background: #f8fafc; padding: 30px; text-align: center; border-top: 1px solid #e5e7eb;">
<div style="margin-bottom: 15px;">
<span style="display: inline-block; width: 40px; height: 2px; background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); border-radius: 1px;"></span>
</div>
<p style="color: #6b7280; margin: 0; font-size: 14px; line-height: 1.5;">
<strong>Dennis Konkol</strong> • Software Engineer & Student<br>
<a href="https://dki.one" style="color: #f59e0b; text-decoration: none;">dki.one</a>
</p>
</div>
</div>
</body>
</html>
`
</div>
`.trim(),
});
},
},
reply: {
subject: "Antwort auf deine Nachricht 📧",
template: (name: string, originalMessage: string) => `
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Antwort - Dennis Konkol</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f8fafc; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
<div style="max-width: 600px; margin: 0 auto; background-color: #ffffff; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">
template: (name: string, originalMessage: string) => {
const safeName = escapeHtml(name);
const safeMsg = nl2br(escapeHtml(originalMessage));
return baseEmail({
title: `Antwort für ${safeName}`,
subtitle: "Neue Nachricht",
bodyHtml: `
<div style="font-size:15px;line-height:1.65;color:${BRAND.text};">
Hey ${safeName},<br><br>
hier ist meine Antwort:
</div>
<!-- Header -->
<div style="background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); padding: 40px 30px; text-align: center;">
<h1 style="color: #ffffff; margin: 0; font-size: 28px; font-weight: 600; letter-spacing: -0.5px;">
📧 Hallo ${name}!
</h1>
<p style="color: #dbeafe; margin: 8px 0 0 0; font-size: 16px; opacity: 0.9;">
Hier ist meine Antwort auf deine Nachricht
</p>
<div style="margin-top:14px;background:${BRAND.bg};border:1px solid ${BRAND.border};border-radius:16px;overflow:hidden;">
<div style="padding:14px 16px;background:${BRAND.sand};border-bottom:1px solid ${BRAND.border};">
<div style="font-size:12px;letter-spacing:0.08em;text-transform:uppercase;font-weight:800;color:#57534e;">Antwort</div>
</div>
<!-- Content -->
<div style="padding: 40px 30px;">
<!-- Reply Message -->
<div style="background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); padding: 30px; border-radius: 12px; margin-bottom: 30px; border: 1px solid #93c5fd;">
<div style="text-align: center; margin-bottom: 20px;">
<div style="width: 60px; height: 60px; background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 15px;">
<span style="color: #ffffff; font-size: 24px;">💬</span>
<div style="padding:16px;line-height:1.65;color:${BRAND.text};font-size:14px;border-left:4px solid ${BRAND.mint};">
${safeMsg}
</div>
<h2 style="color: #1e40af; margin: 0; font-size: 22px; font-weight: 600;">Meine Antwort</h2>
</div>
<div style="background: #ffffff; padding: 20px; border-radius: 8px; border-left: 4px solid #3b82f6;">
<p style="color: #1e40af; margin: 0; line-height: 1.6; font-size: 16px; white-space: pre-wrap;">${originalMessage}</p>
</div>
</div>
<!-- Original Message Reference -->
<div style="background: #ffffff; padding: 25px; border-radius: 12px; border: 1px solid #e5e7eb; margin-bottom: 30px;">
<h3 style="color: #374151; margin: 0 0 15px 0; font-size: 16px; font-weight: 600; display: flex; align-items: center;">
<span style="width: 6px; height: 6px; background: #6b7280; border-radius: 50%; margin-right: 10px;"></span>
Deine ursprüngliche Nachricht
</h3>
<div style="background: #f9fafb; padding: 20px; border-radius: 8px; border-left: 4px solid #3b82f6;">
<p style="color: #4b5563; margin: 0; line-height: 1.6; font-style: italic; white-space: pre-wrap;">${originalMessage}</p>
</div>
</div>
<!-- Contact Info -->
<div style="background: #f8fafc; padding: 25px; border-radius: 12px; text-align: center; border: 1px solid #e2e8f0;">
<h3 style="color: #374151; margin: 0 0 15px 0; font-size: 18px; font-weight: 600;">Weitere Fragen?</h3>
<p style="color: #6b7280; margin: 0 0 20px 0; line-height: 1.6;">
Falls du weitere Fragen hast oder mehr über meine Projekte erfahren möchtest, zögere nicht, mir zu schreiben!
</p>
<div style="display: flex; justify-content: center; gap: 20px; flex-wrap: wrap;">
<a href="https://dki.one" style="display: inline-flex; align-items: center; padding: 12px 24px; background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); color: #ffffff; text-decoration: none; border-radius: 8px; font-weight: 500; transition: all 0.2s;">
🌐 Portfolio besuchen
</a>
<a href="mailto:contact@dk0.dev" style="display: inline-flex; align-items: center; padding: 12px 24px; background: #ffffff; color: #3b82f6; text-decoration: none; border-radius: 8px; font-weight: 500; border: 2px solid #3b82f6; transition: all 0.2s;">
📧 Direkt antworten
</a>
</div>
</div>
</div>
<!-- Footer -->
<div style="background: #f8fafc; padding: 30px; text-align: center; border-top: 1px solid #e5e7eb;">
<p style="color: #6b7280; margin: 0 0 10px 0; font-size: 14px; font-weight: 500;">
<strong>Dennis Konkol</strong> • <a href="https://dki.one" style="color: #3b82f6; text-decoration: none;">dki.one</a>
</p>
<p style="color: #9ca3af; margin: 10px 0 0 0; font-size: 12px;">
${new Date().toLocaleString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</p>
</div>
</div>
</body>
</html>
`
}
</div>
`.trim(),
});
},
},
};
export async function POST(request: NextRequest) {

View File

@@ -15,6 +15,15 @@ function sanitizeInput(input: string, maxLength: number = 10000): string {
.trim();
}
function escapeHtml(input: string): string {
return input
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
export async function POST(request: NextRequest) {
try {
// Rate limiting (defensive: headers may be undefined in tests)
@@ -155,6 +164,22 @@ export async function POST(request: NextRequest) {
}
}
const brandUrl = "https://dk0.dev";
const sentAt = new Date().toLocaleString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
const safeName = escapeHtml(name);
const safeEmail = escapeHtml(email);
const safeSubject = escapeHtml(subject);
const safeMessageHtml = escapeHtml(message).replace(/\n/g, "<br>");
const initial = (name.trim()[0] || "?").toUpperCase();
const replyHref = `mailto:${email}?subject=${encodeURIComponent(`Re: ${subject}`)}`;
const mailOptions: Mail.Options = {
from: `"Portfolio Contact" <${user}>`,
to: "contact@dk0.dev", // Send to your contact email
@@ -168,85 +193,79 @@ export async function POST(request: NextRequest) {
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neue Kontaktanfrage - Portfolio</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f8fafc; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
<div style="max-width: 600px; margin: 0 auto; background-color: #ffffff; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">
<!-- Header -->
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 40px 30px; text-align: center;">
<h1 style="color: #ffffff; margin: 0; font-size: 28px; font-weight: 600; letter-spacing: -0.5px;">
📧 Neue Kontaktanfrage
</h1>
<p style="color: #e2e8f0; margin: 8px 0 0 0; font-size: 16px; opacity: 0.9;">
Von deinem Portfolio
</p>
<body style="margin:0;padding:0;background-color:#fdfcf8;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;color:#292524;">
<div style="max-width:640px;margin:0 auto;padding:28px 14px;">
<div style="background:#ffffff;border:1px solid #e7e5e4;border-radius:20px;overflow:hidden;box-shadow:0 18px 50px rgba(0,0,0,0.08);">
<!-- Top bar -->
<div style="background:#292524;padding:22px 26px;">
<div style="display:flex;align-items:center;justify-content:space-between;gap:16px;">
<div style="font-weight:700;font-size:16px;letter-spacing:-0.01em;color:#fdfcf8;">
Dennis Konkol
</div>
<div style="font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;font-weight:700;font-size:14px;color:#fdfcf8;">
dk<span style="color:#ef4444;">0</span>.dev
</div>
</div>
<div style="margin-top:10px;">
<div style="font-size:22px;font-weight:800;letter-spacing:-0.02em;color:#fdfcf8;">
Neue Kontaktanfrage
</div>
<div style="margin-top:4px;font-size:13px;color:#d6d3d1;">
Eingegangen am ${sentAt}
</div>
</div>
<div style="height:3px;background:#a7f3d0;margin-top:18px;border-radius:999px;"></div>
</div>
<!-- Content -->
<div style="padding: 40px 30px;">
<!-- Contact Info Card -->
<div style="background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); padding: 30px; border-radius: 12px; margin-bottom: 30px; border: 1px solid #e2e8f0;">
<div style="display: flex; align-items: center; margin-bottom: 20px;">
<div style="width: 50px; height: 50px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-right: 15px;">
<span style="color: #ffffff; font-size: 20px; font-weight: bold;">${name.charAt(0).toUpperCase()}</span>
<div style="padding:26px;">
<!-- Sender -->
<div style="display:flex;align-items:flex-start;gap:14px;">
<div style="width:44px;height:44px;border-radius:14px;background:#f3f1e7;border:1px solid #e7e5e4;display:flex;align-items:center;justify-content:center;font-weight:800;color:#292524;">
${escapeHtml(initial)}
</div>
<div>
<h2 style="color: #1e293b; margin: 0; font-size: 24px; font-weight: 600;">${name}</h2>
<p style="color: #64748b; margin: 4px 0 0 0; font-size: 14px;">Kontaktanfrage</p>
<div style="flex:1;min-width:0;">
<div style="font-size:18px;font-weight:800;letter-spacing:-0.01em;color:#292524;line-height:1.2;">
${safeName}
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;">
<div style="background: #ffffff; padding: 20px; border-radius: 8px; border-left: 4px solid #10b981;">
<h4 style="color: #059669; margin: 0 0 8px 0; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">E-Mail</h4>
<p style="color: #374151; margin: 0; font-size: 16px; font-weight: 500;">${email}</p>
</div>
<div style="background: #ffffff; padding: 20px; border-radius: 8px; border-left: 4px solid #3b82f6;">
<h4 style="color: #2563eb; margin: 0 0 8px 0; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Betreff</h4>
<p style="color: #374151; margin: 0; font-size: 16px; font-weight: 500;">${subject}</p>
<div style="margin-top:6px;font-size:13px;color:#78716c;line-height:1.4;">
<span style="font-weight:700;color:#44403c;">E-Mail:</span> ${safeEmail}<br>
<span style="font-weight:700;color:#44403c;">Betreff:</span> ${safeSubject}
</div>
</div>
</div>
<!-- Message Card -->
<div style="background: #ffffff; padding: 30px; border-radius: 12px; border: 1px solid #e2e8f0; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);">
<div style="display: flex; align-items: center; margin-bottom: 20px;">
<div style="width: 8px; height: 8px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; margin-right: 12px;"></div>
<h3 style="color: #1e293b; margin: 0; font-size: 18px; font-weight: 600;">Nachricht</h3>
<!-- Message -->
<div style="margin-top:18px;background:#fdfcf8;border:1px solid #e7e5e4;border-radius:16px;overflow:hidden;">
<div style="padding:14px 16px;background:#f3f1e7;border-bottom:1px solid #e7e5e4;">
<div style="font-size:12px;letter-spacing:0.08em;text-transform:uppercase;font-weight:800;color:#57534e;">
Nachricht
</div>
<div style="background: #f8fafc; padding: 25px; border-radius: 8px; border-left: 4px solid #667eea;">
<p style="color: #374151; margin: 0; line-height: 1.7; font-size: 16px; white-space: pre-wrap;">${message}</p>
</div>
<div style="padding:16px;line-height:1.65;color:#292524;font-size:15px;border-left:4px solid #a7f3d0;">
${safeMessageHtml}
</div>
</div>
<!-- Action Button -->
<div style="text-align: center; margin-top: 30px;">
<a href="mailto:${email}?subject=Re: ${subject}" style="display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #ffffff; text-decoration: none; padding: 15px 30px; border-radius: 8px; font-weight: 600; font-size: 16px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); transition: all 0.2s;">
📬 Antworten
<!-- CTA -->
<div style="margin-top:22px;text-align:center;">
<a href="${escapeHtml(replyHref)}"
style="display:inline-block;background:#292524;color:#fdfcf8;text-decoration:none;padding:12px 18px;border-radius:999px;font-weight:800;font-size:14px;">
Antworten
</a>
<div style="margin-top:10px;font-size:12px;color:#78716c;">
Oder antworte direkt auf diese E-Mail.
</div>
</div>
</div>
<!-- Footer -->
<div style="background: #f8fafc; padding: 30px; text-align: center; border-top: 1px solid #e2e8f0;">
<div style="margin-bottom: 15px;">
<span style="display: inline-block; width: 40px; height: 2px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 1px;"></span>
<div style="padding:18px 26px;background:#fdfcf8;border-top:1px solid #e7e5e4;">
<div style="font-size:12px;color:#78716c;line-height:1.5;">
Automatisch generiert von <a href="${brandUrl}" style="color:#292524;text-decoration:underline;">dk0.dev</a>
</div>
</div>
<p style="color: #64748b; margin: 0; font-size: 14px; line-height: 1.5;">
Diese E-Mail wurde automatisch von deinem Portfolio generiert.<br>
<strong>Dennis Konkol Portfolio</strong> • <a href="https://dki.one" style="color: #667eea; text-decoration: none;">dki.one</a>
</p>
<p style="color: #94a3b8; margin: 10px 0 0 0; font-size: 12px;">
${new Date().toLocaleString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</p>
</div>
</div>
</body>
</html>
@@ -261,7 +280,7 @@ Nachricht:
${message}
---
Diese E-Mail wurde automatisch von deinem Portfolio generiert.
Diese E-Mail wurde automatisch von dk0.dev generiert.
`,
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,17 @@
"use client";
import dynamic from "next/dynamic";
import React from "react";
// Dynamically import the heavy framer-motion component on the client only
const BackgroundBlobs = dynamic(() => import("@/components/BackgroundBlobs"), { ssr: false });
import React, { useEffect, useState } from "react";
import BackgroundBlobs from "@/components/BackgroundBlobs";
export default function BackgroundBlobsClient() {
// Avoid SSR/webpack bailout issues from `next/dynamic({ ssr:false })`
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return <BackgroundBlobs />;
}

View File

@@ -53,8 +53,8 @@ export default function ChatWidget() {
// Helper function to decode HTML entities
const decodeHtmlEntities = (text: string): string => {
if (!text || typeof text !== 'string') return text;
const textarea = document.createElement('textarea');
if (!text || typeof text !== "string") return text;
const textarea = document.createElement("textarea");
textarea.innerHTML = text;
return textarea.value;
};
@@ -129,24 +129,27 @@ export default function ChatWidget() {
});
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
const errorText = await response.text().catch(() => "Unknown error");
console.error("Chat API error:", {
status: response.status,
statusText: response.statusText,
error: errorText,
});
throw new Error(`Failed to get response: ${response.status} - ${errorText.substring(0, 100)}`);
throw new Error(
`Failed to get response: ${response.status} - ${errorText.substring(0, 100)}`,
);
}
const data = await response.json();
// Log response for debugging (only in development)
if (process.env.NODE_ENV === 'development') {
if (process.env.NODE_ENV === "development") {
console.log("Chat API response:", data);
}
// Decode HTML entities in the reply
let replyText = data.reply || "Sorry, I couldn't process that. Please try again.";
let replyText =
data.reply || "Sorry, I couldn't process that. Please try again.";
// Decode HTML entities client-side (double safety)
replyText = decodeHtmlEntities(replyText);
@@ -218,11 +221,11 @@ export default function ChatWidget() {
setIsOpen(true);
}
}}
className="fixed bottom-20 left-4 md:bottom-6 md:left-6 z-30 bg-gradient-to-br from-blue-500 to-purple-600 text-white p-3 rounded-full shadow-2xl hover:shadow-blue-500/50 hover:scale-110 transition-all duration-300 group cursor-pointer"
className="fixed bottom-20 left-4 md:bottom-6 md:left-6 z-30 bg-[#5A4E42]/90 backdrop-blur-md text-white p-3 rounded-full shadow-2xl hover:bg-[#4A3F35]/90 hover:scale-110 transition-all duration-300 group cursor-pointer border border-white/10"
aria-label="Open chat"
>
<MessageCircle size={20} />
<span className="absolute -top-1 -right-1 w-3 h-3 bg-green-400 rounded-full animate-pulse" />
<span className="absolute -top-1 -right-1 w-3 h-3 bg-[#8B7D6F] rounded-full animate-pulse shadow-lg" />
{/* Tooltip */}
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-1.5 bg-black/90 text-white text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-[100] shadow-lg">
@@ -236,26 +239,29 @@ export default function ChatWidget() {
<AnimatePresence>
{isOpen && (
<motion.div
data-chat-widget
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="fixed bottom-20 left-4 md:bottom-6 md:left-6 z-30 w-[300px] sm:w-[340px] md:w-[380px] max-w-[calc(100vw-2rem)] h-[450px] sm:h-[500px] md:h-[550px] max-h-[calc(100vh-10rem)] bg-white dark:bg-gray-900 rounded-2xl shadow-2xl flex flex-col overflow-hidden border border-gray-200 dark:border-gray-800"
className="fixed bottom-20 left-4 md:bottom-6 md:left-6 z-30 w-[300px] sm:w-[340px] md:w-[380px] max-w-[calc(100vw-2rem)] h-[450px] sm:h-[500px] md:h-[550px] max-h-[calc(100vh-10rem)] bg-[#f5f1e8]/85 backdrop-blur-xl rounded-2xl shadow-2xl flex flex-col overflow-hidden border border-[#8B7D6F]/30 [&_a]:text-inherit [&_a]:no-underline [&_a]:text-[#2A241F] [&_a:hover]:text-[#2A241F] [&_a:visited]:text-[#2A241F] [&_a:active]:text-[#2A241F] [&_*]:outline-none [&_*:focus]:outline-none [&_*:focus-visible]:outline-none"
>
{/* Header */}
<div className="bg-gradient-to-br from-blue-500 to-purple-600 text-white p-3 md:p-4 flex items-center justify-between">
<div className="bg-gradient-to-r from-[#5A4E42]/90 to-[#4A3F35]/90 backdrop-blur-md text-white p-3 md:p-4 flex items-center justify-between border-b border-[#6B5D4F]/30">
<div className="flex items-center gap-3">
<div className="relative">
<div className="w-10 h-10 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center">
<Sparkles size={20} />
<div className="w-10 h-10 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center ring-2 ring-white/20 shadow-lg">
<Sparkles size={20} className="text-white" />
</div>
<span className="absolute bottom-0 right-0 w-3 h-3 bg-green-400 rounded-full border-2 border-white" />
<span className="absolute bottom-0 right-0 w-3 h-3 bg-[#8B7D6F] rounded-full border-2 border-[#5A4E42] shadow-lg" />
</div>
<div className="min-w-0 flex-1">
<h3 className="font-bold text-sm truncate">
Dennis{'\''}s AI Assistant
<h3 className="font-bold text-sm truncate text-white">
Dennis{"'"}s AI Assistant
</h3>
<p className="text-xs text-white/80 truncate">Always online</p>
<p className="text-xs text-white/90 truncate">
Always online
</p>
</div>
</div>
@@ -278,7 +284,7 @@ export default function ChatWidget() {
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-3 md:p-4 space-y-3 md:space-y-4 bg-gray-50 dark:bg-gray-950">
<div className="flex-1 overflow-y-auto scrollbar-hide p-3 md:p-4 space-y-3 md:space-y-4 bg-transparent">
{messages.map((message) => (
<motion.div
key={message.id}
@@ -287,20 +293,22 @@ export default function ChatWidget() {
className={`flex ${message.sender === "user" ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[80%] rounded-2xl px-4 py-2 ${
className={`max-w-[80%] rounded-2xl px-4 py-2.5 backdrop-blur-sm ${
message.sender === "user"
? "bg-gradient-to-br from-blue-500 to-purple-600 text-white"
: "bg-white dark:bg-gray-800 text-gray-900 dark:text-white border border-gray-200 dark:border-gray-700"
? "bg-gradient-to-br from-[#5A4E42] to-[#4A3F35] text-white shadow-lg"
: "bg-white/90 backdrop-blur-sm text-[#2A241F] border border-[#8B7D6F]/30 shadow-md"
}`}
>
<p className="text-sm whitespace-pre-wrap break-words">
<p className={`text-sm whitespace-pre-wrap break-words leading-relaxed [&_a]:text-inherit [&_a]:no-underline [&_a]:text-current [&_a:hover]:text-current [&_a:visited]:text-current [&_a:active]:text-current ${
message.sender === "user" ? "text-white" : "text-[#2A241F]"
}`}>
{message.text}
</p>
<p
className={`text-[10px] mt-1 ${
className={`text-[10px] mt-1.5 ${
message.sender === "user"
? "text-white/60"
: "text-gray-500 dark:text-gray-400"
? "text-white/80"
: "text-[#5A4E42]/70"
}`}
>
{message.timestamp.toLocaleTimeString([], {
@@ -319,10 +327,10 @@ export default function ChatWidget() {
animate={{ opacity: 1, y: 0 }}
className="flex justify-start"
>
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-2xl px-4 py-3">
<div className="flex gap-1">
<div className="bg-white/90 backdrop-blur-sm border border-[#8B7D6F]/30 rounded-2xl px-4 py-3 shadow-md">
<div className="flex gap-1.5">
<motion.div
className="w-2 h-2 bg-gray-400 rounded-full"
className="w-2 h-2 bg-[#5A4E42] rounded-full"
animate={{ y: [0, -8, 0] }}
transition={{
duration: 0.6,
@@ -331,7 +339,7 @@ export default function ChatWidget() {
}}
/>
<motion.div
className="w-2 h-2 bg-gray-400 rounded-full"
className="w-2 h-2 bg-[#5A4E42] rounded-full"
animate={{ y: [0, -8, 0] }}
transition={{
duration: 0.6,
@@ -340,7 +348,7 @@ export default function ChatWidget() {
}}
/>
<motion.div
className="w-2 h-2 bg-gray-400 rounded-full"
className="w-2 h-2 bg-[#5A4E42] rounded-full"
animate={{ y: [0, -8, 0] }}
transition={{
duration: 0.6,
@@ -357,7 +365,7 @@ export default function ChatWidget() {
</div>
{/* Input */}
<div className="p-3 md:p-4 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800">
<div className="p-3 md:p-4 bg-[#f5f1e8]/60 backdrop-blur-md border-t border-[#8B7D6F]/25">
<div className="flex gap-2">
<input
ref={inputRef}
@@ -367,12 +375,12 @@ export default function ChatWidget() {
onKeyPress={handleKeyPress}
placeholder="Ask anything..."
disabled={isLoading}
className="flex-1 px-3 md:px-4 py-2 text-sm bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white rounded-full border border-gray-200 dark:border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
className="flex-1 px-4 py-2.5 text-sm bg-white/90 backdrop-blur-sm text-[#2A241F] rounded-full border border-[#8B7D6F]/40 focus:outline-none focus:ring-2 focus:ring-[#5A4E42]/20 focus:border-[#5A4E42]/50 focus:bg-white disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-[#8B7D6F]/70 transition-all focus-visible:ring-[#5A4E42]/20 focus-visible:border-[#5A4E42]/50"
/>
<button
onClick={handleSend}
disabled={!inputValue.trim() || isLoading}
className="p-2 bg-gradient-to-br from-blue-500 to-purple-600 text-white rounded-full hover:shadow-lg hover:scale-110 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
className="p-2.5 bg-gradient-to-br from-[#5A4E42] to-[#4A3F35] text-white rounded-full hover:from-[#6B5D4F] hover:to-[#5A4E42] hover:shadow-xl hover:scale-110 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 shadow-lg"
aria-label="Send message"
>
{isLoading ? (
@@ -384,7 +392,7 @@ export default function ChatWidget() {
</div>
{/* Quick Actions */}
<div className="flex gap-2 mt-2 overflow-x-auto pb-1 scrollbar-hide">
<div className="flex gap-2 mt-2.5 overflow-x-auto pb-1 scrollbar-hide">
{[
"What are Dennis's skills?",
"Tell me about his projects",
@@ -397,7 +405,7 @@ export default function ChatWidget() {
inputRef.current?.focus();
}}
disabled={isLoading}
className="px-2 md:px-3 py-1 text-[10px] md:text-xs bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors whitespace-nowrap disabled:opacity-50 flex-shrink-0"
className="px-3 py-1.5 text-[10px] md:text-xs bg-white/80 backdrop-blur-sm text-[#2A241F] rounded-full hover:bg-white/95 border border-[#8B7D6F]/30 transition-all whitespace-nowrap disabled:opacity-50 flex-shrink-0 shadow-sm"
>
{suggestion}
</button>

View File

@@ -1,8 +1,8 @@
"use client";
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
export function ClientOnly({ children }: { children: React.ReactNode }) {
export default function ClientOnly({ children }: { children: React.ReactNode }) {
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {

View File

@@ -0,0 +1,52 @@
"use client";
import React, { useEffect, useState, Suspense, lazy } from "react";
import { usePathname } from "next/navigation";
import { ToastProvider } from "@/components/Toast";
import { AnalyticsProvider } from "@/components/AnalyticsProvider";
// Lazy load heavy components to avoid webpack issues
const BackgroundBlobs = lazy(() => import("@/components/BackgroundBlobs"));
const ChatWidget = lazy(() => import("./ChatWidget"));
export default function ClientProviders({
children,
}: {
children: React.ReactNode;
}) {
const [mounted, setMounted] = useState(false);
const [is404Page, setIs404Page] = useState(false);
useEffect(() => {
setMounted(true);
// Check if we're on a 404 page by looking for the data attribute
const check404 = () => {
if (typeof window !== "undefined") {
const has404Component = document.querySelector('[data-404-page]');
setIs404Page(!!has404Component);
}
};
// Check immediately and after a short delay
check404();
const timeout = setTimeout(check404, 100);
return () => clearTimeout(timeout);
}, []);
return (
<AnalyticsProvider>
<ToastProvider>
{mounted && (
<Suspense fallback={null}>
<BackgroundBlobs />
</Suspense>
)}
<div className="relative z-10">{children}</div>
{mounted && !is404Page && (
<Suspense fallback={null}>
<ChatWidget />
</Suspense>
)}
</ToastProvider>
</AnalyticsProvider>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
"use client";
import React, { useEffect, useState } from "react";
// Lazy load providers to avoid webpack module resolution issues
const AnalyticsProvider = React.lazy(() =>
import("@/components/AnalyticsProvider").then((mod) => ({
default: mod.AnalyticsProvider,
}))
);
const ToastProvider = React.lazy(() =>
import("@/components/Toast").then((mod) => ({
default: mod.ToastProvider,
}))
);
const BackgroundBlobs = React.lazy(() =>
import("@/components/BackgroundBlobs")
);
const ChatWidget = React.lazy(() => import("./ChatWidget"));
export default function RootProviders({
children,
}: {
children: React.ReactNode;
}) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <div className="relative z-10">{children}</div>;
}
return (
<React.Suspense fallback={<div className="relative z-10">{children}</div>}>
<AnalyticsProvider>
<ToastProvider>
<BackgroundBlobs />
<div className="relative z-10">{children}</div>
<ChatWidget />
</ToastProvider>
</AnalyticsProvider>
</React.Suspense>
);
}

View File

@@ -121,6 +121,14 @@ div {
background: #a8a29e;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* Animations */
@keyframes float {
0%,

View File

@@ -2,11 +2,7 @@ import "./globals.css";
import { Metadata } from "next";
import { Inter } from "next/font/google";
import React from "react";
import { ToastProvider } from "@/components/Toast";
import { AnalyticsProvider } from "@/components/AnalyticsProvider";
import { ClientOnly } from "./components/ClientOnly";
import BackgroundBlobsClient from "./components/BackgroundBlobsClient";
import ChatWidget from "./components/ChatWidget";
import ClientProviders from "./components/ClientProviders";
const inter = Inter({
variable: "--font-inter",
@@ -29,16 +25,8 @@ export default function RootLayout({
<meta charSet="utf-8" />
<title>Dennis Konkol&#39;s Portfolio</title>
</head>
<body className={inter.variable}>
<AnalyticsProvider>
<ToastProvider>
<ClientOnly>
<BackgroundBlobsClient />
</ClientOnly>
<div className="relative z-10">{children}</div>
<ChatWidget />
</ToastProvider>
</AnalyticsProvider>
<body className={inter.variable} suppressHydrationWarning>
<ClientProviders>{children}</ClientProviders>
</body>
</html>
);

View File

@@ -1,5 +1,30 @@
import KernelPanic404 from './components/KernelPanic404';
"use client";
import { Suspense } from "react";
import dynamic from "next/dynamic";
// Dynamically import KernelPanic404 to avoid SSR issues
const KernelPanic404 = dynamic(() => import("./components/KernelPanic404"), {
ssr: false,
loading: () => (
<div className="flex items-center justify-center min-h-screen bg-black text-[#33ff00] font-mono">
<div>Loading terminal...</div>
</div>
),
});
export default function NotFound() {
return <KernelPanic404 />;
return (
<main className="min-h-screen w-full bg-black overflow-hidden relative">
<Suspense
fallback={
<div className="flex items-center justify-center min-h-screen bg-black text-[#33ff00] font-mono">
<div>Loading terminal...</div>
</div>
}
>
<KernelPanic404 />
</Suspense>
</main>
);
}

View File

@@ -139,10 +139,27 @@ interface ToastContextType {
const ToastContext = createContext<ToastContextType | undefined>(undefined);
// No-op fallback for SSR or when outside provider
const noopToast: ToastContextType = {
addToast: () => {},
showToast: () => {},
showSuccess: () => {},
showError: () => {},
showWarning: () => {},
showInfo: () => {},
showEmailSent: () => {},
showEmailError: () => {},
showProjectSaved: () => {},
showProjectDeleted: () => {},
showImportSuccess: () => {},
showImportError: () => {},
};
export const useToast = () => {
const context = useContext(ToastContext);
// Return no-op fallback during SSR or if used outside provider
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
return noopToast;
}
return context;
};

View File

@@ -29,9 +29,14 @@ const nextConfig: NextConfig = {
},
// Performance optimizations
experimental: {
// NOTE: `optimizePackageImports` can cause dev-time webpack runtime issues with some setups.
// Keep it enabled for production builds only.
experimental:
process.env.NODE_ENV === "production"
? {
optimizePackageImports: ["lucide-react", "framer-motion"],
},
}
: {},
// Image optimization
images: {
@@ -54,7 +59,7 @@ const nextConfig: NextConfig = {
},
// Webpack configuration
webpack: (config, { isServer, dev, webpack }) => {
webpack: (config) => {
// Fix for module resolution issues
config.resolve.fallback = {
...config.resolve.fallback,
@@ -63,24 +68,6 @@ const nextConfig: NextConfig = {
tls: false,
};
// Safari + React 19 + Next.js 15 compatibility fixes
if (dev && !isServer) {
// Disable module concatenation to prevent factory initialization issues
config.optimization = {
...config.optimization,
concatenateModules: false,
providedExports: false,
usedExports: false,
};
// Add DefinePlugin to ensure proper environment detection
config.plugins.push(
new webpack.DefinePlugin({
"process.env.__NEXT_DISABLE_REACT_STRICT_MODE": JSON.stringify(false),
}),
);
}
return config;
},