full upgrade to dev

This commit is contained in:
2026-01-07 14:30:00 +01:00
parent 4dc727fcd6
commit 26a8610aa7
34 changed files with 6890 additions and 920 deletions

86
app/api/n8n/chat/route.ts Normal file
View File

@@ -0,0 +1,86 @@
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
try {
const { message } = await request.json();
if (!message || typeof message !== 'string') {
return NextResponse.json(
{ error: 'Message is required' },
{ status: 400 }
);
}
// Call your n8n chat webhook
const n8nWebhookUrl = process.env.N8N_WEBHOOK_URL;
if (!n8nWebhookUrl) {
console.error('N8N_WEBHOOK_URL not configured');
// Return fallback response
return NextResponse.json({
reply: getFallbackResponse(message)
});
}
const response = await fetch(`${n8nWebhookUrl}/webhook/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(process.env.N8N_API_KEY && {
'Authorization': `Bearer ${process.env.N8N_API_KEY}`
}),
},
body: JSON.stringify({ message }),
});
if (!response.ok) {
throw new Error(`n8n webhook failed: ${response.status}`);
}
const data = await response.json();
return NextResponse.json({ reply: data.reply || data.message || data.response });
} catch (error) {
console.error('Chat API error:', error);
// Fallback to mock responses if n8n is down
const { message } = await request.json();
return NextResponse.json(
{ reply: getFallbackResponse(message) }
);
}
}
function getFallbackResponse(message: string): string {
const lowerMessage = message.toLowerCase();
if (lowerMessage.includes('skill') || lowerMessage.includes('tech')) {
return "Dennis specializes in full-stack development with Next.js, Flutter for mobile, and DevOps with Docker Swarm. He's passionate about self-hosting and runs his own infrastructure!";
}
if (lowerMessage.includes('project')) {
return "Dennis has built Clarity (a Flutter app for people with dyslexia) and runs a complete self-hosted infrastructure with Docker Swarm, Traefik, and automated CI/CD pipelines. Check out the Projects section for more!";
}
if (lowerMessage.includes('contact') || lowerMessage.includes('email') || lowerMessage.includes('reach')) {
return "You can reach Dennis via the contact form on this site or email him at contact@dk0.dev. He's always open to discussing new opportunities and interesting projects!";
}
if (lowerMessage.includes('location') || lowerMessage.includes('where')) {
return "Dennis is based in Osnabrück, Germany. He's a student who's passionate about technology and self-hosting.";
}
if (lowerMessage.includes('hobby') || lowerMessage.includes('free time')) {
return "When Dennis isn't coding or managing servers, he enjoys gaming, jogging, and experimenting with new technologies. He also uses pen and paper for notes despite automating everything else!";
}
if (lowerMessage.includes('devops') || lowerMessage.includes('docker') || lowerMessage.includes('infrastructure')) {
return "Dennis runs his own infrastructure on IONOS and OVHcloud using Docker Swarm, Traefik for reverse proxy, and custom CI/CD pipelines. He loves self-hosting and managing game servers!";
}
if (lowerMessage.includes('student') || lowerMessage.includes('study')) {
return "Yes, Dennis is currently a student in Osnabrück while also working on various tech projects and managing his own infrastructure. He's always learning and exploring new technologies!";
}
// Default response
return "That's a great question! Dennis is a full-stack developer and DevOps enthusiast who loves building things with Next.js, Flutter, and Docker. Feel free to ask me more specific questions about his skills, projects, or experience!";
}

View File

@@ -0,0 +1,176 @@
import { NextRequest, NextResponse } from "next/server";
/**
* POST /api/n8n/generate-image
*
* Triggers AI image generation for a project via n8n workflow
*
* Body:
* {
* projectId: number;
* regenerate?: boolean; // Force regenerate even if image exists
* }
*/
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { projectId, regenerate = false } = body;
// Validate input
if (!projectId) {
return NextResponse.json(
{ error: "projectId is required" },
{ status: 400 },
);
}
// Check environment variables
const n8nWebhookUrl = process.env.N8N_WEBHOOK_URL;
const n8nSecretToken = process.env.N8N_SECRET_TOKEN;
if (!n8nWebhookUrl) {
return NextResponse.json(
{
error: "N8N_WEBHOOK_URL not configured",
message:
"AI image generation is not set up. Please configure n8n webhooks.",
},
{ status: 503 },
);
}
// Optional: Check if project already has an image
if (!regenerate) {
const checkResponse = await fetch(
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"}/api/projects/${projectId}`,
{
method: "GET",
cache: "no-store",
},
);
if (checkResponse.ok) {
const project = await checkResponse.json();
if (project.imageUrl && project.imageUrl !== "") {
return NextResponse.json(
{
success: true,
message:
"Project already has an image. Use regenerate=true to force regeneration.",
projectId: projectId,
existingImageUrl: project.imageUrl,
regenerated: false,
},
{ status: 200 },
);
}
}
}
// Call n8n webhook to trigger AI image generation
const n8nResponse = await fetch(`${n8nWebhookUrl}/ai-image-generation`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(n8nSecretToken && {
Authorization: `Bearer ${n8nSecretToken}`,
}),
},
body: JSON.stringify({
projectId: projectId,
regenerate: regenerate,
triggeredBy: "api",
timestamp: new Date().toISOString(),
}),
});
if (!n8nResponse.ok) {
const errorText = await n8nResponse.text();
console.error("n8n webhook error:", errorText);
return NextResponse.json(
{
error: "Failed to trigger image generation",
message: "n8n workflow failed to execute",
details: errorText,
},
{ status: 500 },
);
}
const result = await n8nResponse.json();
return NextResponse.json(
{
success: true,
message: "AI image generation started successfully",
projectId: projectId,
imageUrl: result.imageUrl,
generatedAt: result.generatedAt,
fileSize: result.fileSize,
regenerated: regenerate,
},
{ status: 200 },
);
} catch (error) {
console.error("Error in generate-image API:", error);
return NextResponse.json(
{
error: "Internal server error",
message: error instanceof Error ? error.message : "Unknown error",
},
{ status: 500 },
);
}
}
/**
* GET /api/n8n/generate-image?projectId=123
*
* Check the status of image generation for a project
*/
export async function GET(req: NextRequest) {
try {
const searchParams = req.nextUrl.searchParams;
const projectId = searchParams.get("projectId");
if (!projectId) {
return NextResponse.json(
{ error: "projectId query parameter is required" },
{ status: 400 },
);
}
// Fetch project to check image status
const projectResponse = await fetch(
`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"}/api/projects/${projectId}`,
{
method: "GET",
cache: "no-store",
},
);
if (!projectResponse.ok) {
return NextResponse.json({ error: "Project not found" }, { status: 404 });
}
const project = await projectResponse.json();
return NextResponse.json({
projectId: parseInt(projectId),
title: project.title,
hasImage: !!project.imageUrl,
imageUrl: project.imageUrl || null,
updatedAt: project.updatedAt,
});
} catch (error) {
console.error("Error checking image status:", error);
return NextResponse.json(
{
error: "Internal server error",
message: error instanceof Error ? error.message : "Unknown error",
},
{ status: 500 },
);
}
}

View File

@@ -1,19 +1,115 @@
import { NextResponse } from 'next/server';
import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export const dynamic = "force-dynamic";
export const revalidate = 0;
export async function GET() {
// Mock data - in real integration this would fetch from n8n webhook or database
return NextResponse.json({
activity: {
type: 'coding', // coding, listening, watching
details: 'Portfolio Website',
timestamp: new Date().toISOString(),
},
music: {
isPlaying: true,
track: 'Midnight City',
artist: 'M83',
platform: 'spotify'
},
watching: null
});
try {
// Fetch from activity_status table
const result = await prisma.$queryRawUnsafe<any[]>(
`SELECT * FROM activity_status WHERE id = 1 LIMIT 1`,
);
if (!result || result.length === 0) {
return NextResponse.json({
activity: null,
music: null,
watching: null,
gaming: null,
status: null,
});
}
const data = result[0];
// Check if activity is recent (within last 2 hours)
const lastUpdate = new Date(data.updated_at);
const now = new Date();
const hoursSinceUpdate =
(now.getTime() - lastUpdate.getTime()) / (1000 * 60 * 60);
const isRecent = hoursSinceUpdate < 2;
return NextResponse.json(
{
activity:
data.activity_type && isRecent
? {
type: data.activity_type,
details: data.activity_details,
project: data.activity_project,
language: data.activity_language,
repo: data.activity_repo,
link: data.activity_repo, // Use repo URL as link
timestamp: data.updated_at,
}
: null,
music: data.music_playing
? {
isPlaying: data.music_playing,
track: data.music_track,
artist: data.music_artist,
album: data.music_album,
platform: data.music_platform || "spotify",
progress: data.music_progress,
albumArt: data.music_album_art,
spotifyUrl: data.music_track
? `https://open.spotify.com/search/${encodeURIComponent(data.music_track + " " + data.music_artist)}`
: null,
}
: null,
watching: data.watching_title
? {
title: data.watching_title,
platform: data.watching_platform || "youtube",
type: data.watching_type || "video",
}
: null,
gaming: data.gaming_game
? {
game: data.gaming_game,
platform: data.gaming_platform || "steam",
status: data.gaming_status || "playing",
}
: null,
status: data.status_mood
? {
mood: data.status_mood,
customMessage: data.status_message,
}
: null,
},
{
headers: {
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
Pragma: "no-cache",
},
},
);
} catch (error) {
console.error("Error fetching activity status:", error);
// Return empty state on error (graceful degradation)
return NextResponse.json(
{
activity: null,
music: null,
watching: null,
gaming: null,
status: null,
},
{
status: 200, // Return 200 to prevent frontend errors
headers: {
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
},
},
);
}
}

View File

@@ -1,9 +1,34 @@
"use client";
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Code, Terminal, Cpu, Globe } from 'lucide-react';
import { LiquidHeading } from '@/components/LiquidHeading';
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { Globe, Server, Wrench, Shield, Gamepad2, Code } from "lucide-react";
// Smooth animation configuration
const smoothTransition = {
duration: 1,
ease: [0.25, 0.1, 0.25, 1],
};
const staggerContainer = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.2,
},
},
};
const fadeInUp = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: smoothTransition,
},
};
const About = () => {
const [mounted, setMounted] = useState(false);
@@ -14,81 +39,205 @@ const About = () => {
const techStack = [
{
category: 'Frontend',
category: "Frontend & Mobile",
icon: Globe,
items: ['React', 'TypeScript', 'Tailwind', 'Next.js']
items: ["Next.js", "Tailwind CSS", "Flutter"],
},
{
category: 'Backend',
icon: Terminal,
items: ['Node.js', 'PostgreSQL', 'Prisma', 'API Design']
category: "Backend & DevOps",
icon: Server,
items: ["Docker Swarm", "Traefik", "Nginx Proxy Manager", "Redis"],
},
{
category: 'Tools',
icon: Cpu,
items: ['Git', 'Docker', 'VS Code', 'Figma']
}
category: "Tools & Automation",
icon: Wrench,
items: ["Git", "CI/CD", "n8n", "Self-hosted Services"],
},
{
category: "Security & Admin",
icon: Shield,
items: ["CrowdSec", "Suricata", "Mailcow"],
},
];
const hobbies = [
{ icon: Code, text: "Self-Hosting & DevOps" },
{ icon: Gamepad2, text: "Gaming" },
{ icon: Server, text: "Setting up Game Servers" },
];
if (!mounted) return null;
return (
<section id="about" className="py-24 px-4 bg-white relative overflow-hidden">
<section
id="about"
className="py-24 px-4 bg-gradient-to-br from-liquid-sky/15 via-liquid-lavender/10 to-liquid-pink/15 relative overflow-hidden"
>
<div className="max-w-6xl mx-auto relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-start">
{/* Text Content */}
<div className="space-y-8">
<LiquidHeading
text="About Me"
level={2}
className="text-4xl md:text-5xl font-bold text-stone-900"
/>
<div className="prose prose-stone prose-lg text-stone-600">
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
variants={staggerContainer}
className="space-y-8"
>
<motion.h2
variants={fadeInUp}
className="text-4xl md:text-5xl font-bold text-stone-900"
>
About Me
</motion.h2>
<motion.div
variants={fadeInUp}
className="prose prose-stone prose-lg text-stone-700 space-y-4"
>
<p>
Hi, I&apos;m Dennis. I&apos;m a software engineer who likes building things that work well and look good.
Hi, I&apos;m Dennis a student and passionate self-hoster based
in Osnabrück, Germany.
</p>
<p>
I&apos;m currently based in Osnabrück, Germany. My journey in tech is driven by curiosityI love figuring out how things work and how to make them better.
I love building full-stack web applications with{" "}
<strong>Next.js</strong> and mobile apps with{" "}
<strong>Flutter</strong>. But what really excites me is{" "}
<strong>DevOps</strong>: I run my own infrastructure on{" "}
<strong>IONOS</strong> and <strong>OVHcloud</strong>, managing
everything with <strong>Docker Swarm</strong>,{" "}
<strong>Traefik</strong>, and automated CI/CD pipelines with my
own runners.
</p>
<p>
When I&apos;m not in front of a screen, you can find me listening to music, exploring new ideas, or just relaxing.
When I&apos;m not coding or tinkering with servers, you&apos;ll
find me <strong>gaming</strong>, <strong>jogging</strong>, or
experimenting with new tech like game servers or automation
workflows with <strong>n8n</strong>.
</p>
</div>
</div>
<p className="text-sm italic text-stone-500 bg-stone-50 p-4 rounded-lg border-l-4 border-liquid-mint">
💡 Fun fact: Even though I automate a lot, I still use pen and
paper for my calendar and notes it helps me clear my head and
stay focused.
</p>
</motion.div>
</motion.div>
{/* Simplified Skills / Tech Stack */}
<div className="grid grid-cols-1 gap-6">
<h3 className="text-xl font-bold text-stone-900 mb-2">My Toolbox</h3>
{techStack.map((stack, idx) => (
<motion.div
key={stack.category}
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.1 }}
viewport={{ once: true }}
className="p-6 rounded-xl bg-stone-50 border border-stone-100 hover:border-stone-200 transition-colors"
{/* Tech Stack & Hobbies */}
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
variants={staggerContainer}
className="space-y-8"
>
<div>
<motion.h3
variants={fadeInUp}
className="text-2xl font-bold text-stone-900 mb-6"
>
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-white rounded-lg shadow-sm text-stone-700">
<stack.icon size={20} />
</div>
<h4 className="font-semibold text-stone-800">{stack.category}</h4>
</div>
<div className="flex flex-wrap gap-2">
{stack.items.map(item => (
<span key={item} className="px-3 py-1 bg-white rounded-md border border-stone-200 text-sm text-stone-600">
{item}
My Tech Stack
</motion.h3>
<div className="grid grid-cols-1 gap-4">
{techStack.map((stack, idx) => (
<motion.div
key={`${stack.category}-${idx}`}
variants={fadeInUp}
whileHover={{
scale: 1.02,
transition: { duration: 0.4, ease: "easeOut" },
}}
className={`p-5 rounded-xl border-2 transition-all duration-500 ease-out ${
idx === 0
? "bg-gradient-to-br from-liquid-sky/10 to-liquid-mint/10 border-liquid-sky/30 hover:border-liquid-sky/50 hover:from-liquid-sky/15 hover:to-liquid-mint/15"
: idx === 1
? "bg-gradient-to-br from-liquid-peach/10 to-liquid-coral/10 border-liquid-peach/30 hover:border-liquid-peach/50 hover:from-liquid-peach/15 hover:to-liquid-coral/15"
: idx === 2
? "bg-gradient-to-br 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-br from-liquid-teal/10 to-liquid-lime/10 border-liquid-teal/30 hover:border-liquid-teal/50 hover:from-liquid-teal/15 hover:to-liquid-lime/15"
}`}
>
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-white rounded-lg shadow-sm text-stone-700">
<stack.icon size={18} />
</div>
<h4 className="font-semibold text-stone-800">
{stack.category}
</h4>
</div>
<div className="flex flex-wrap gap-2">
{stack.items.map((item, itemIdx) => (
<span
key={`${stack.category}-${item}-${itemIdx}`}
className={`px-3 py-1.5 rounded-lg border-2 text-sm text-stone-700 font-medium transition-all duration-400 ease-out ${
itemIdx % 4 === 0
? "bg-liquid-mint/10 border-liquid-mint/30 hover:bg-liquid-mint/20 hover:border-liquid-mint/50"
: itemIdx % 4 === 1
? "bg-liquid-lavender/10 border-liquid-lavender/30 hover:bg-liquid-lavender/20 hover:border-liquid-lavender/50"
: itemIdx % 4 === 2
? "bg-liquid-rose/10 border-liquid-rose/30 hover:bg-liquid-rose/20 hover:border-liquid-rose/50"
: "bg-liquid-sky/10 border-liquid-sky/30 hover:bg-liquid-sky/20 hover:border-liquid-sky/50"
}`}
>
{item}
</span>
))}
</div>
</motion.div>
))}
</div>
</div>
{/* Hobbies */}
<div>
<motion.h3
variants={fadeInUp}
className="text-xl font-bold text-stone-900 mb-4"
>
When I&apos;m Not Coding
</motion.h3>
<div className="space-y-3">
{hobbies.map((hobby, idx) => (
<motion.div
key={`hobby-${hobby.text}-${idx}`}
variants={fadeInUp}
whileHover={{
x: 8,
scale: 1.02,
transition: { duration: 0.4, ease: "easeOut" },
}}
className={`flex items-center gap-3 p-4 rounded-xl border-2 transition-all duration-500 ease-out ${
idx === 0
? "bg-gradient-to-r from-liquid-mint/10 to-liquid-sky/10 border-liquid-mint/30 hover:border-liquid-mint/50 hover:from-liquid-mint/15 hover:to-liquid-sky/15"
: idx === 1
? "bg-gradient-to-r from-liquid-coral/10 to-liquid-peach/10 border-liquid-coral/30 hover:border-liquid-coral/50 hover:from-liquid-coral/15 hover:to-liquid-peach/15"
: "bg-gradient-to-r from-liquid-lavender/10 to-liquid-pink/10 border-liquid-lavender/30 hover:border-liquid-lavender/50 hover:from-liquid-lavender/15 hover:to-liquid-pink/15"
}`}
>
<hobby.icon size={20} className="text-stone-600" />
<span className="text-stone-700 font-medium">
{hobby.text}
</span>
))}
</div>
</motion.div>
))}
</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>
</motion.div>
</div>
</div>
</section>
);
};
export default About;
export default About;

View File

@@ -1,48 +1,270 @@
'use client';
"use client";
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Music, Code, Monitor, MessageSquare, Send, X } from 'lucide-react';
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Music,
Code,
Monitor,
MessageSquare,
Send,
X,
Loader2,
Github,
Tv,
Gamepad2,
Coffee,
Headphones,
Terminal,
Sparkles,
ExternalLink,
Activity,
Waves,
Zap,
} from "lucide-react";
interface ActivityData {
activity: {
type: 'coding' | 'listening' | 'watching';
type:
| "coding"
| "listening"
| "watching"
| "gaming"
| "reading"
| "running";
details: string;
timestamp: string;
project?: string;
language?: string;
repo?: string;
link?: string;
} | null;
music: {
isPlaying: boolean;
track: string;
artist: string;
platform: 'spotify' | 'apple';
album?: string;
platform: "spotify" | "apple";
progress?: number;
albumArt?: string;
spotifyUrl?: string;
} | null;
watching: {
title: string;
platform: 'youtube' | 'netflix';
platform: "youtube" | "netflix" | "twitch";
type: "video" | "stream" | "movie" | "series";
} | null;
gaming: {
game: string;
platform: "steam" | "playstation" | "xbox";
status: "playing" | "idle";
} | null;
status: {
mood: string;
customMessage?: string;
} | null;
}
// Matrix rain effect for coding
const MatrixRain = () => {
const chars = "01";
return (
<div className="absolute inset-0 overflow-hidden opacity-20 pointer-events-none">
{[...Array(15)].map((_, i) => (
<motion.div
key={i}
className="absolute text-liquid-mint font-mono text-xs"
style={{ left: `${(i / 15) * 100}%` }}
animate={{
y: ["-100%", "200%"],
opacity: [0, 1, 0],
}}
transition={{
duration: Math.random() * 3 + 2,
repeat: Infinity,
delay: Math.random() * 2,
ease: "linear",
}}
>
{[...Array(20)].map((_, j) => (
<div key={j}>{chars[Math.floor(Math.random() * chars.length)]}</div>
))}
</motion.div>
))}
</div>
);
};
// Sound waves for music
const SoundWaves = () => {
return (
<div className="absolute inset-0 flex items-center justify-center overflow-hidden pointer-events-none">
{[...Array(5)].map((_, i) => (
<motion.div
key={i}
className="absolute w-1 bg-gradient-to-t from-liquid-rose to-liquid-coral rounded-full"
style={{ left: `${20 + i * 15}%` }}
animate={{
height: ["20%", "80%", "20%"],
}}
transition={{
duration: 0.8,
repeat: Infinity,
delay: i * 0.1,
ease: "easeInOut",
}}
/>
))}
</div>
);
};
// Running animation
const RunningAnimation = () => {
return (
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute bottom-2 text-4xl"
animate={{
x: ["-10%", "110%"],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "linear",
}}
>
🏃
</motion.div>
<div className="absolute bottom-2 left-0 right-0 h-0.5 bg-liquid-lime/30" />
</div>
);
};
// Gaming particles
const GamingParticles = () => {
return (
<div className="absolute inset-0 overflow-hidden pointer-events-none">
{[...Array(10)].map((_, i) => (
<motion.div
key={i}
className="absolute w-2 h-2 bg-liquid-peach/60 rounded-full"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
}}
animate={{
scale: [0, 1, 0],
opacity: [0, 1, 0],
}}
transition={{
duration: 2,
repeat: Infinity,
delay: Math.random() * 2,
ease: "easeInOut",
}}
/>
))}
</div>
);
};
// TV scan lines
const TVScanLines = () => {
return (
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute inset-0 bg-gradient-to-b from-transparent via-white/10 to-transparent h-8"
animate={{
y: ["-100%", "200%"],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "linear",
}}
/>
</div>
);
};
const activityIcons = {
coding: Terminal,
listening: Headphones,
watching: Tv,
gaming: Gamepad2,
reading: Coffee,
running: Activity,
};
const activityColors = {
coding: {
bg: "from-liquid-mint/20 to-liquid-sky/20",
border: "border-liquid-mint/40",
text: "text-liquid-mint",
pulse: "bg-green-500",
},
listening: {
bg: "from-liquid-rose/20 to-liquid-coral/20",
border: "border-liquid-rose/40",
text: "text-liquid-rose",
pulse: "bg-red-500",
},
watching: {
bg: "from-liquid-lavender/20 to-liquid-pink/20",
border: "border-liquid-lavender/40",
text: "text-liquid-lavender",
pulse: "bg-purple-500",
},
gaming: {
bg: "from-liquid-peach/20 to-liquid-yellow/20",
border: "border-liquid-peach/40",
text: "text-liquid-peach",
pulse: "bg-orange-500",
},
reading: {
bg: "from-liquid-teal/20 to-liquid-lime/20",
border: "border-liquid-teal/40",
text: "text-liquid-teal",
pulse: "bg-teal-500",
},
running: {
bg: "from-liquid-lime/20 to-liquid-mint/20",
border: "border-liquid-lime/40",
text: "text-liquid-lime",
pulse: "bg-lime-500",
},
};
export const ActivityFeed = () => {
const [data, setData] = useState<ActivityData | null>(null);
const [showChat, setShowChat] = useState(false);
const [chatMessage, setChatMessage] = useState('');
const [chatHistory, setChatHistory] = useState<{
role: 'user' | 'ai';
text: string;
}[]>([
{ role: 'ai', text: 'Hi! I am Dennis\'s AI assistant. Ask me anything about him!' }
const [chatMessage, setChatMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [chatHistory, setChatHistory] = useState<
{
role: "user" | "ai";
text: string;
timestamp: number;
}[]
>([
{
role: "ai",
text: "Hi! I'm Dennis's AI assistant. Ask me anything about his work, skills, or projects! 🚀",
timestamp: Date.now(),
},
]);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch('/api/n8n/status');
const res = await fetch("/api/n8n/status");
if (res.ok) {
const json = await res.json();
setData(json);
}
} catch (e) {
console.error('Failed to fetch activity', e);
console.error("Failed to fetch activity", e);
}
};
fetchData();
@@ -50,67 +272,306 @@ export const ActivityFeed = () => {
return () => clearInterval(interval);
}, []);
const handleSendMessage = (e: React.FormEvent) => {
const handleSendMessage = async (e: React.FormEvent) => {
e.preventDefault();
if (!chatMessage.trim()) return;
if (!chatMessage.trim() || isLoading) return;
const userMsg = chatMessage;
setChatHistory(prev => [...prev, { role: 'user', text: userMsg }]);
setChatMessage('');
setChatHistory((prev) => [
...prev,
{ role: "user", text: userMsg, timestamp: Date.now() },
]);
setChatMessage("");
setIsLoading(true);
// Mock AI response - would connect to n8n webhook
setTimeout(() => {
setChatHistory(prev => [...prev, { role: 'ai', text: `That's a great question about "${userMsg}"! I'll ask Dennis to add more info about that.` }]);
}, 1000);
try {
const response = await fetch("/api/n8n/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: userMsg }),
});
if (response.ok) {
const data = await response.json();
setChatHistory((prev) => [
...prev,
{ role: "ai", text: data.reply, timestamp: Date.now() },
]);
} else {
throw new Error("Chat API failed");
}
} catch (error) {
console.error("Chat error:", error);
setChatHistory((prev) => [
...prev,
{
role: "ai",
text: "Sorry, I encountered an error. Please try again later.",
timestamp: Date.now(),
},
]);
} finally {
setIsLoading(false);
}
};
if (!data) return null;
const renderActivityBubble = () => {
if (!data?.activity) return null;
const { type, details, project, language, link } = data.activity;
const Icon = activityIcons[type];
const colors = activityColors[type];
return (
<motion.div
initial={{ x: 50, opacity: 0, scale: 0.8 }}
animate={{ x: 0, opacity: 1, scale: 1 }}
exit={{ x: 50, opacity: 0, scale: 0.8 }}
transition={{ duration: 0.8, ease: [0.25, 0.1, 0.25, 1] }}
className={`relative bg-gradient-to-r ${colors.bg} backdrop-blur-md border-2 ${colors.border} shadow-lg rounded-2xl px-5 py-3 flex items-start gap-3 text-sm text-stone-800 max-w-xs overflow-hidden`}
>
{/* Background Animation based on activity type */}
{type === "coding" && <MatrixRain />}
{type === "running" && <RunningAnimation />}
{type === "gaming" && <GamingParticles />}
{type === "watching" && <TVScanLines />}
<div className="relative z-10 flex-shrink-0 mt-1">
<span className="relative flex h-3 w-3">
<span
className={`animate-ping absolute inline-flex h-full w-full rounded-full ${colors.pulse} opacity-75`}
></span>
<span
className={`relative inline-flex rounded-full h-3 w-3 ${colors.pulse}`}
></span>
</span>
</div>
<div className="relative z-10 flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<motion.div
animate={
type === "coding"
? { rotate: [0, 360] }
: type === "running"
? { scale: [1, 1.2, 1] }
: {}
}
transition={{
duration: type === "coding" ? 2 : 1,
repeat: Infinity,
ease: "linear",
}}
>
<Icon size={16} className={colors.text} />
</motion.div>
<span className="font-semibold capitalize">{type}</span>
</div>
<p className="text-stone-900 font-medium truncate">{details}</p>
{project && (
<p className="text-stone-600 text-xs mt-1 flex items-center gap-1">
<Github size={12} />
{project}
</p>
)}
{language && (
<span className="inline-block mt-2 px-2 py-0.5 bg-white/60 rounded text-xs text-stone-700 border border-stone-200 font-mono">
{language}
</span>
)}
{link && (
<a
href={link}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 mt-2 text-xs text-stone-700 hover:text-stone-900 underline"
>
View <ExternalLink size={10} />
</a>
)}
</div>
</motion.div>
);
};
const renderMusicBubble = () => {
if (!data?.music?.isPlaying) return null;
const { track, artist, album, progress, albumArt, spotifyUrl } = data.music;
return (
<motion.div
initial={{ x: 50, opacity: 0, scale: 0.8 }}
animate={{ x: 0, opacity: 1, scale: 1 }}
exit={{ x: 50, opacity: 0, scale: 0.8 }}
transition={{ duration: 0.8, delay: 0.15, ease: [0.25, 0.1, 0.25, 1] }}
className="relative bg-gradient-to-r from-liquid-rose/20 to-liquid-coral/20 backdrop-blur-md border-2 border-liquid-rose/40 shadow-lg rounded-2xl px-5 py-3 flex items-center gap-3 text-sm text-stone-800 max-w-xs overflow-hidden"
>
{/* Animated sound waves background */}
<SoundWaves />
{albumArt && (
<motion.div
animate={{ rotate: [0, 360] }}
transition={{ duration: 10, repeat: Infinity, ease: "linear" }}
className="relative z-10 w-14 h-14 rounded-full overflow-hidden flex-shrink-0 border-2 border-white shadow-md"
>
<img
src={albumArt}
alt={album || track}
className="w-full h-full object-cover"
/>
</motion.div>
)}
<div className="relative z-10 flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<motion.div
animate={{ scale: [1, 1.2, 1] }}
transition={{ duration: 1, repeat: Infinity, ease: "easeInOut" }}
>
<Headphones size={16} className="text-liquid-rose" />
</motion.div>
<span className="font-semibold">Now Playing</span>
</div>
<p className="text-stone-900 font-bold text-sm truncate">{track}</p>
<p className="text-stone-600 text-xs truncate">{artist}</p>
{progress !== undefined && (
<div className="mt-2 w-full bg-white/50 rounded-full h-1.5 overflow-hidden">
<motion.div
className="h-full bg-gradient-to-r from-liquid-rose to-liquid-coral"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.5 }}
/>
</div>
)}
{spotifyUrl && (
<a
href={spotifyUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 mt-2 text-xs text-stone-700 hover:text-stone-900 underline"
>
<Waves size={10} />
Listen with me
</a>
)}
</div>
</motion.div>
);
};
const renderStatusBubble = () => {
if (!data?.status) return null;
const { mood, customMessage } = data.status;
return (
<motion.div
initial={{ x: 50, opacity: 0, scale: 0.8 }}
animate={{ x: 0, opacity: 1, scale: 1 }}
exit={{ x: 50, opacity: 0, scale: 0.8 }}
transition={{ duration: 0.8, delay: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
className="relative bg-gradient-to-r from-liquid-lavender/20 to-liquid-pink/20 backdrop-blur-md border-2 border-liquid-lavender/40 shadow-lg rounded-2xl px-5 py-3 flex items-center gap-3 text-sm text-stone-800 max-w-xs overflow-hidden"
>
<motion.div
animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
className="text-3xl flex-shrink-0"
>
{mood}
</motion.div>
<div className="flex-1 min-w-0">
{customMessage && (
<p className="text-stone-900 font-medium text-sm">
{customMessage}
</p>
)}
</div>
</motion.div>
);
};
return (
<div className="fixed bottom-6 right-6 z-40 flex flex-col items-end gap-4 pointer-events-none">
<div className="fixed bottom-6 right-6 z-[9999] flex flex-col items-end gap-4 pointer-events-none">
{/* Chat Window */}
<AnimatePresence>
{showChat && (
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="pointer-events-auto bg-white/80 backdrop-blur-xl border border-white/60 shadow-2xl rounded-2xl w-80 overflow-hidden"
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ duration: 0.5, ease: [0.25, 0.1, 0.25, 1] }}
className="pointer-events-auto bg-white/95 backdrop-blur-xl border-2 border-stone-200 shadow-2xl rounded-2xl w-96 max-w-[calc(100vw-3rem)] overflow-hidden"
>
<div className="p-4 border-b border-white/50 flex justify-between items-center bg-white/40">
<span className="font-semibold text-stone-800 flex items-center gap-2">
<MessageSquare size={16} />
Ask me anything
<div className="p-4 border-b-2 border-stone-200 flex justify-between items-center bg-gradient-to-r from-liquid-mint/10 to-liquid-sky/10">
<span className="font-bold text-stone-900 flex items-center gap-2">
<Sparkles size={18} className="text-liquid-mint" />
AI Assistant
</span>
<button onClick={() => setShowChat(false)} className="text-stone-500 hover:text-stone-800">
<X size={16} />
<button
onClick={() => setShowChat(false)}
className="text-stone-500 hover:text-stone-900 transition-colors duration-300 p-1 hover:bg-stone-100 rounded-lg"
>
<X size={18} />
</button>
</div>
<div className="h-64 overflow-y-auto p-4 space-y-3 bg-white/20">
<div className="h-96 overflow-y-auto p-4 space-y-3 bg-gradient-to-b from-stone-50/50 to-white/50">
{chatHistory.map((msg, i) => (
<div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[85%] p-3 rounded-xl text-sm ${
msg.role === 'user'
? 'bg-stone-800 text-white rounded-tr-none'
: 'bg-white text-stone-800 shadow-sm rounded-tl-none'
}`}>
<motion.div
key={`chat-${msg.timestamp}-${i}`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: i * 0.05 }}
className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[85%] p-3 rounded-2xl text-sm ${
msg.role === "user"
? "bg-gradient-to-br from-stone-800 to-stone-900 text-white rounded-tr-none shadow-md"
: "bg-gradient-to-br from-white to-stone-50 text-stone-900 shadow-md rounded-tl-none border-2 border-stone-100"
}`}
>
{msg.text}
</div>
</div>
</motion.div>
))}
{isLoading && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex justify-start"
>
<div className="max-w-[85%] p-3 rounded-2xl text-sm bg-gradient-to-br from-white to-stone-50 text-stone-900 shadow-md rounded-tl-none border-2 border-stone-100 flex items-center gap-2">
<Loader2
size={14}
className="animate-spin text-liquid-mint"
/>
<span>Thinking...</span>
</div>
</motion.div>
)}
</div>
<form onSubmit={handleSendMessage} className="p-3 border-t border-white/50 bg-white/40 flex gap-2">
<input
type="text"
<form
onSubmit={handleSendMessage}
className="p-4 border-t-2 border-stone-200 bg-gradient-to-r from-liquid-mint/5 to-liquid-sky/5 flex gap-2"
>
<input
type="text"
value={chatMessage}
onChange={(e) => setChatMessage(e.target.value)}
placeholder="Type a message..."
className="flex-1 bg-white/60 border border-white/60 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-stone-400"
placeholder="Ask me anything..."
disabled={isLoading}
className="flex-1 bg-white border-2 border-stone-200 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-liquid-mint focus:border-transparent disabled:opacity-50 transition-all duration-300"
/>
<button type="submit" className="p-2 bg-stone-800 text-white rounded-lg hover:bg-black transition-colors">
<Send size={16} />
</button>
<motion.button
type="submit"
disabled={isLoading || !chatMessage.trim()}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="p-3 bg-gradient-to-br from-stone-900 to-stone-800 text-white rounded-xl hover:from-black hover:to-stone-900 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg"
>
<Send size={18} />
</motion.button>
</form>
</motion.div>
)}
@@ -118,41 +579,40 @@ export const ActivityFeed = () => {
{/* Activity Bubbles */}
<div className="flex flex-col items-end gap-2 pointer-events-auto">
{data.activity?.type === 'coding' && (
<motion.div
initial={{ x: 50, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
className="bg-white/80 backdrop-blur-md border border-white/60 shadow-lg rounded-full px-4 py-2 flex items-center gap-2 text-sm text-stone-700"
>
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
<Code size={14} />
<span>Working on <strong>{data.activity.details}</strong></span>
</motion.div>
)}
<AnimatePresence mode="wait">
{renderActivityBubble()}
{renderMusicBubble()}
{renderStatusBubble()}
</AnimatePresence>
{data.music?.isPlaying && (
<motion.div
initial={{ x: 50, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
className="bg-white/80 backdrop-blur-md border border-white/60 shadow-lg rounded-full px-4 py-2 flex items-center gap-2 text-sm text-stone-700"
>
<Music size={14} className="animate-pulse text-liquid-rose" />
<span>Listening to <strong>{data.music.track}</strong></span>
</motion.div>
)}
{/* Chat Toggle Button */}
{/* Chat Toggle Button with Notification Indicator */}
<motion.button
whileHover={{ scale: 1.05 }}
whileHover={{ scale: 1.08, rotate: 5 }}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.4, ease: "easeOut" }}
onClick={() => setShowChat(!showChat)}
className="bg-stone-900 text-white rounded-full p-4 shadow-xl hover:bg-black transition-all"
className="relative bg-stone-900 text-white rounded-full p-4 shadow-xl hover:bg-stone-950 transition-all duration-500 ease-out"
title="Ask me anything about Dennis"
>
<MessageSquare size={20} />
{!showChat && (
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.5, ease: "easeOut", delay: 0.2 }}
className="absolute -top-1 -right-1 w-3 h-3 bg-liquid-mint rounded-full border-2 border-white"
>
<motion.span
animate={{ scale: [1, 1.3, 1] }}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
className="absolute inset-0 bg-liquid-mint rounded-full"
/>
</motion.span>
)}
</motion.button>
</div>
</div>

View File

@@ -1,10 +1,9 @@
"use client";
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Mail, MapPin, Send } from 'lucide-react';
import { useToast } from '@/components/Toast';
import { LiquidHeading } from '@/components/LiquidHeading';
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { Mail, MapPin, Send } from "lucide-react";
import { useToast } from "@/components/Toast";
const Contact = () => {
const [mounted, setMounted] = useState(false);
@@ -15,10 +14,10 @@ const Contact = () => {
}, []);
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: ''
name: "",
email: "",
subject: "",
message: "",
});
const [errors, setErrors] = useState<Record<string, string>>({});
@@ -29,27 +28,27 @@ const Contact = () => {
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
newErrors.name = "Name is required";
} else if (formData.name.trim().length < 2) {
newErrors.name = 'Name must be at least 2 characters';
newErrors.name = "Name must be at least 2 characters";
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
newErrors.email = "Email is required";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = 'Please enter a valid email address';
newErrors.email = "Please enter a valid email address";
}
if (!formData.subject.trim()) {
newErrors.subject = 'Subject is required';
newErrors.subject = "Subject is required";
} else if (formData.subject.trim().length < 3) {
newErrors.subject = 'Subject must be at least 3 characters';
newErrors.subject = "Subject must be at least 3 characters";
}
if (!formData.message.trim()) {
newErrors.message = 'Message is required';
newErrors.message = "Message is required";
} else if (formData.message.trim().length < 10) {
newErrors.message = 'Message must be at least 10 characters';
newErrors.message = "Message must be at least 10 characters";
}
setErrors(newErrors);
@@ -58,18 +57,18 @@ const Contact = () => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
try {
const response = await fetch('/api/email', {
method: 'POST',
const response = await fetch("/api/email", {
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify({
name: formData.name,
@@ -81,41 +80,49 @@ const Contact = () => {
if (response.ok) {
showEmailSent(formData.email);
setFormData({ name: '', email: '', subject: '', message: '' });
setFormData({ name: "", email: "", subject: "", message: "" });
setTouched({});
setErrors({});
} else {
const errorData = await response.json();
showEmailError(errorData.error || 'Failed to send message. Please try again.');
showEmailError(
errorData.error || "Failed to send message. Please try again.",
);
}
} catch (error) {
console.error('Error sending email:', error);
showEmailError('Network error. Please check your connection and try again.');
console.error("Error sending email:", error);
showEmailError(
"Network error. Please check your connection and try again.",
);
} finally {
setIsSubmitting(false);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value
[name]: value,
});
// Clear error when user starts typing
if (errors[name]) {
setErrors({
...errors,
[name]: ''
[name]: "",
});
}
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const handleBlur = (
e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
setTouched({
...touched,
[e.target.name]: true
[e.target.name]: true,
});
validateForm();
};
@@ -123,53 +130,61 @@ const Contact = () => {
const contactInfo = [
{
icon: Mail,
title: 'Email',
value: 'contact@dk0.dev',
href: 'mailto:contact@dk0.dev'
title: "Email",
value: "contact@dk0.dev",
href: "mailto:contact@dk0.dev",
},
{
icon: MapPin,
title: 'Location',
value: 'Osnabrück, Germany',
}
title: "Location",
value: "Osnabrück, Germany",
},
];
if (!mounted) {
return null;
}
return (
<section id="contact" className="py-24 px-4 relative bg-cream">
<section
id="contact"
className="py-24 px-4 relative bg-gradient-to-br from-liquid-teal/15 via-liquid-mint/10 to-liquid-lime/15"
>
<div className="max-w-7xl mx-auto">
{/* Section Header */}
<div className="text-center mb-16">
<LiquidHeading
text="Contact Me"
level={2}
className="text-4xl md:text-5xl font-bold mb-6 text-stone-800"
/>
<p className="text-xl text-stone-500 max-w-2xl mx-auto mt-4">
Interested in working together or have questions about my projects? Feel free to reach out!
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 1, ease: [0.25, 0.1, 0.25, 1] }}
className="text-center mb-16"
>
<h2 className="text-4xl md:text-5xl font-bold mb-6 text-stone-900">
Contact Me
</h2>
<p className="text-xl text-stone-700 max-w-2xl mx-auto mt-4">
Interested in working together or have questions about my projects?
Feel free to reach out!
</p>
</div>
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Contact Information */}
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 1, ease: [0.25, 0.1, 0.25, 1] }}
className="space-y-8"
>
<div>
<h3 className="text-2xl font-bold text-stone-800 mb-6">
<h3 className="text-2xl font-bold text-stone-900 mb-6">
Get In Touch
</h3>
<p className="text-stone-600 leading-relaxed">
I&apos;m always available to discuss new opportunities, interesting projects,
or simply chat about technology and innovation.
<p className="text-stone-700 leading-relaxed">
I&apos;m always available to discuss new opportunities,
interesting projects, or simply chat about technology and
innovation.
</p>
</div>
@@ -182,37 +197,50 @@ const Contact = () => {
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.1 }}
whileHover={{ x: 5 }}
className="flex items-center space-x-4 p-4 rounded-2xl glass-card hover:bg-white/60 transition-colors group border-transparent hover:border-white/60"
transition={{
duration: 0.8,
delay: index * 0.15,
ease: [0.25, 0.1, 0.25, 1],
}}
whileHover={{
x: 8,
transition: { duration: 0.4, ease: "easeOut" },
}}
className="flex items-center space-x-4 p-4 rounded-2xl glass-card hover:bg-white/80 transition-all duration-500 ease-out group border-transparent hover:border-white/70"
>
<div className="p-3 bg-white rounded-xl shadow-sm group-hover:shadow-md transition-all">
<info.icon className="w-6 h-6 text-stone-700" />
</div>
<div>
<h4 className="font-semibold text-stone-800">{info.title}</h4>
<h4 className="font-semibold text-stone-800">
{info.title}
</h4>
<p className="text-stone-500">{info.value}</p>
</div>
</motion.a>
))}
</div>
</motion.div>
{/* Contact Form */}
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
className="glass-card p-8 rounded-3xl bg-white/40 border border-white/60"
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 1, ease: [0.25, 0.1, 0.25, 1] }}
className="glass-card p-8 rounded-3xl bg-white/50 border border-white/70"
>
<h3 className="text-2xl font-bold text-stone-800 mb-6">Send Message</h3>
<h3 className="text-2xl font-bold text-stone-800 mb-6">
Send Message
</h3>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-stone-600 mb-2">
<label
htmlFor="name"
className="block text-sm font-medium text-stone-600 mb-2"
>
Name <span className="text-liquid-rose">*</span>
</label>
<input
@@ -225,20 +253,29 @@ const Contact = () => {
required
className={`w-full px-4 py-3 bg-white/50 backdrop-blur-sm border rounded-xl text-stone-800 placeholder-stone-400 focus:outline-none focus:ring-2 transition-all ${
errors.name && touched.name
? 'border-red-400 focus:ring-red-400'
: 'border-white/60 focus:ring-liquid-blue focus:border-transparent'
? "border-red-400 focus:ring-red-400"
: "border-white/60 focus:ring-liquid-blue focus:border-transparent"
}`}
placeholder="Your name"
aria-invalid={errors.name && touched.name ? 'true' : 'false'}
aria-describedby={errors.name && touched.name ? 'name-error' : undefined}
aria-invalid={
errors.name && touched.name ? "true" : "false"
}
aria-describedby={
errors.name && touched.name ? "name-error" : undefined
}
/>
{errors.name && touched.name && (
<p id="name-error" className="mt-1 text-sm text-red-500">{errors.name}</p>
<p id="name-error" className="mt-1 text-sm text-red-500">
{errors.name}
</p>
)}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-stone-600 mb-2">
<label
htmlFor="email"
className="block text-sm font-medium text-stone-600 mb-2"
>
Email <span className="text-liquid-rose">*</span>
</label>
<input
@@ -251,21 +288,30 @@ const Contact = () => {
required
className={`w-full px-4 py-3 bg-white/50 backdrop-blur-sm border rounded-xl text-stone-800 placeholder-stone-400 focus:outline-none focus:ring-2 transition-all ${
errors.email && touched.email
? 'border-red-400 focus:ring-red-400'
: 'border-white/60 focus:ring-liquid-blue focus:border-transparent'
? "border-red-400 focus:ring-red-400"
: "border-white/60 focus:ring-liquid-blue focus:border-transparent"
}`}
placeholder="your@email.com"
aria-invalid={errors.email && touched.email ? 'true' : 'false'}
aria-describedby={errors.email && touched.email ? 'email-error' : undefined}
aria-invalid={
errors.email && touched.email ? "true" : "false"
}
aria-describedby={
errors.email && touched.email ? "email-error" : undefined
}
/>
{errors.email && touched.email && (
<p id="email-error" className="mt-1 text-sm text-red-500">{errors.email}</p>
<p id="email-error" className="mt-1 text-sm text-red-500">
{errors.email}
</p>
)}
</div>
</div>
<div>
<label htmlFor="subject" className="block text-sm font-medium text-stone-600 mb-2">
<label
htmlFor="subject"
className="block text-sm font-medium text-stone-600 mb-2"
>
Subject <span className="text-liquid-rose">*</span>
</label>
<input
@@ -278,20 +324,31 @@ const Contact = () => {
required
className={`w-full px-4 py-3 bg-white/50 backdrop-blur-sm border rounded-xl text-stone-800 placeholder-stone-400 focus:outline-none focus:ring-2 transition-all ${
errors.subject && touched.subject
? 'border-red-400 focus:ring-red-400'
: 'border-white/60 focus:ring-liquid-blue focus:border-transparent'
? "border-red-400 focus:ring-red-400"
: "border-white/60 focus:ring-liquid-blue focus:border-transparent"
}`}
placeholder="What's this about?"
aria-invalid={errors.subject && touched.subject ? 'true' : 'false'}
aria-describedby={errors.subject && touched.subject ? 'subject-error' : undefined}
aria-invalid={
errors.subject && touched.subject ? "true" : "false"
}
aria-describedby={
errors.subject && touched.subject
? "subject-error"
: undefined
}
/>
{errors.subject && touched.subject && (
<p id="subject-error" className="mt-1 text-sm text-red-500">{errors.subject}</p>
<p id="subject-error" className="mt-1 text-sm text-red-500">
{errors.subject}
</p>
)}
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-stone-600 mb-2">
<label
htmlFor="message"
className="block text-sm font-medium text-stone-600 mb-2"
>
Message <span className="text-liquid-rose">*</span>
</label>
<textarea
@@ -304,16 +361,24 @@ const Contact = () => {
rows={6}
className={`w-full px-4 py-3 bg-white/50 backdrop-blur-sm border rounded-xl text-stone-800 placeholder-stone-400 focus:outline-none focus:ring-2 transition-all resize-none ${
errors.message && touched.message
? 'border-red-400 focus:ring-red-400'
: 'border-white/60 focus:ring-liquid-blue focus:border-transparent'
? "border-red-400 focus:ring-red-400"
: "border-white/60 focus:ring-liquid-blue focus:border-transparent"
}`}
placeholder="Tell me more about your project or question..."
aria-invalid={errors.message && touched.message ? 'true' : 'false'}
aria-describedby={errors.message && touched.message ? 'message-error' : undefined}
aria-invalid={
errors.message && touched.message ? "true" : "false"
}
aria-describedby={
errors.message && touched.message
? "message-error"
: undefined
}
/>
<div className="flex justify-between items-center mt-1">
{errors.message && touched.message ? (
<p id="message-error" className="text-sm text-red-500">{errors.message}</p>
<p id="message-error" className="text-sm text-red-500">
{errors.message}
</p>
) : (
<span></span>
)}
@@ -328,7 +393,8 @@ const Contact = () => {
disabled={isSubmitting}
whileHover={!isSubmitting ? { scale: 1.02, y: -2 } : {}}
whileTap={!isSubmitting ? { scale: 0.98 } : {}}
className="w-full py-4 text-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2 disabled:hover:scale-100 disabled:hover:translate-y-0 bg-stone-900 text-white rounded-xl hover:bg-black transition-all shadow-lg"
transition={{ duration: 0.3, ease: "easeOut" }}
className="w-full py-4 text-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2 disabled:hover:scale-100 disabled:hover:translate-y-0 bg-stone-900 text-white rounded-xl hover:bg-stone-950 transition-all duration-500 ease-out shadow-lg hover:shadow-xl"
>
{isSubmitting ? (
<>

View File

@@ -1,10 +1,10 @@
"use client";
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Menu, X, Mail } from 'lucide-react';
import { SiGithub, SiLinkedin } from 'react-icons/si';
import Link from 'next/link';
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Menu, X, Mail } from "lucide-react";
import { SiGithub, SiLinkedin } from "react-icons/si";
import Link from "next/link";
const Header = () => {
const [isOpen, setIsOpen] = useState(false);
@@ -20,21 +20,25 @@ const Header = () => {
setScrolled(window.scrollY > 50);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const navItems = [
{ name: 'Home', href: '/' },
{ name: 'About', href: '#about' },
{ name: 'Projects', href: '#projects' },
{ name: 'Contact', href: '#contact' },
{ name: "Home", href: "/" },
{ name: "About", href: "#about" },
{ name: "Projects", href: "#projects" },
{ name: "Contact", href: "#contact" },
];
const socialLinks = [
{ icon: SiGithub, href: 'https://github.com/Denshooter', label: 'GitHub' },
{ icon: SiLinkedin, href: 'https://linkedin.com/in/dkonkol', label: 'LinkedIn' },
{ icon: Mail, href: 'mailto:contact@dk0.dev', label: 'Email' },
{ icon: SiGithub, href: "https://github.com/Denshooter", label: "GitHub" },
{
icon: SiLinkedin,
href: "https://linkedin.com/in/dkonkol",
label: "LinkedIn",
},
{ icon: Mail, href: "mailto:contact@dk0.dev", label: "Email" },
];
if (!mounted) {
@@ -49,22 +53,30 @@ const Header = () => {
transition={{ duration: 0.8, ease: "easeOut" }}
className="fixed top-6 left-0 right-0 z-50 flex justify-center px-4 pointer-events-none"
>
<div className={`pointer-events-auto transition-all duration-500 ease-out ${
scrolled ? 'w-full max-w-5xl' : 'w-full max-w-7xl'
}`}>
<div className={`
<div
className={`pointer-events-auto transition-all duration-500 ease-out ${
scrolled ? "w-full max-w-5xl" : "w-full max-w-7xl"
}`}
>
<div
className={`
backdrop-blur-xl transition-all duration-500
${scrolled
? 'bg-white/95 border border-stone-300 shadow-[0_8px_30px_rgba(0,0,0,0.12)] rounded-full px-6 py-3'
: 'bg-white/85 border border-stone-200 shadow-[0_4px_24px_rgba(0,0,0,0.08)] px-4 py-4 rounded-full'
${
scrolled
? "bg-white/95 border border-stone-300 shadow-[0_8px_30px_rgba(0,0,0,0.12)] rounded-full px-6 py-3"
: "bg-white/85 border border-stone-200 shadow-[0_4px_24px_rgba(0,0,0,0.08)] px-4 py-4 rounded-full"
}
flex justify-between items-center
`}>
`}
>
<motion.div
whileHover={{ scale: 1.05 }}
className="flex items-center space-x-2"
>
<Link href="/" className="text-2xl font-bold font-mono text-stone-800 tracking-tighter liquid-hover">
<Link
href="/"
className="text-2xl font-bold font-mono text-stone-800 tracking-tighter liquid-hover"
>
dk<span className="text-liquid-rose">0</span>
</Link>
</motion.div>
@@ -80,11 +92,14 @@ const Header = () => {
href={item.href}
className="text-stone-600 hover:text-stone-900 transition-colors duration-200 font-medium relative group px-2 py-1 liquid-hover"
onClick={(e) => {
if (item.href.startsWith('#')) {
if (item.href.startsWith("#")) {
e.preventDefault();
const element = document.querySelector(item.href);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
element.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
}
}}
@@ -153,12 +168,15 @@ const Header = () => {
href={item.href}
onClick={(e) => {
setIsOpen(false);
if (item.href.startsWith('#')) {
if (item.href.startsWith("#")) {
e.preventDefault();
setTimeout(() => {
const element = document.querySelector(item.href);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
element.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
}, 100);
}
@@ -169,7 +187,7 @@ const Header = () => {
</Link>
</motion.div>
))}
<div className="pt-6 mt-4 border-t border-stone-200">
<div className="flex justify-center space-x-4">
{socialLinks.map((social, index) => (
@@ -180,7 +198,9 @@ const Header = () => {
rel="noopener noreferrer"
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: (navItems.length + index) * 0.05 }}
transition={{
delay: (navItems.length + index) * 0.05,
}}
whileHover={{ scale: 1.1 }}
className="p-3 rounded-full bg-white/60 text-stone-600 shadow-sm"
aria-label={social.label}

View File

@@ -1,10 +1,9 @@
"use client";
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { ArrowDown, Code, Zap, Rocket } from 'lucide-react';
import Image from 'next/image';
import { LiquidHeading } from '@/components/LiquidHeading';
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { ArrowDown, Code, Zap, Rocket } from "lucide-react";
import Image from "next/image";
const Hero = () => {
const [mounted, setMounted] = useState(false);
@@ -14,97 +13,136 @@ const Hero = () => {
}, []);
const features = [
{ icon: Code, text: 'Full-Stack Development' },
{ icon: Zap, text: 'Modern Technologies' },
{ icon: Rocket, text: 'Innovative Solutions' },
{ icon: Code, text: "Next.js & Flutter" },
{ icon: Zap, text: "Docker Swarm & CI/CD" },
{ icon: Rocket, text: "Self-Hosted Infrastructure" },
];
// Smooth scroll configuration
const smoothTransition = {
type: "spring",
damping: 30,
stiffness: 50,
mass: 1,
};
if (!mounted) {
return null;
}
return (
<section className="relative min-h-screen flex items-center justify-center overflow-hidden pt-20 pb-8 bg-transparent">
<section className="relative min-h-screen flex items-center justify-center overflow-hidden pt-32 pb-16 bg-gradient-to-br from-liquid-mint/10 via-liquid-lavender/10 to-liquid-rose/10">
<div className="relative z-10 text-center px-4 max-w-5xl mx-auto">
{/* Domain Badge */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5 }}
className="mb-8 inline-block"
>
<div className="px-6 py-2 rounded-full glass-panel text-stone-600 font-mono text-sm tracking-wider uppercase">
dk<span className="text-liquid-rose font-bold">0</span>.dev
</div>
</motion.div>
{/* Profile Image with Organic Blob Mask */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 1, delay: 0.7, ease: "easeOut" }}
className="mb-10 flex justify-center relative z-20"
transition={{ duration: 1.2, delay: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
className="mb-12 flex justify-center relative z-20"
>
<div className="relative w-64 h-64 md:w-80 md:h-80 flex items-center justify-center">
{/* Large Rotating Liquid Blobs behind image */}
<motion.div
className="absolute w-[140%] h-[140%] bg-gradient-to-tr from-liquid-mint via-liquid-blue to-liquid-lavender opacity-60 blur-3xl -z-10"
animate={{
borderRadius: ["60% 40% 30% 70%/60% 30% 70% 40%", "30% 60% 70% 40%/50% 60% 30% 60%", "60% 40% 30% 70%/60% 30% 70% 40%"],
rotate: [0, 180, 360],
scale: [1, 1.1, 1]
}}
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
/>
<motion.div
className="absolute w-[120%] h-[120%] bg-gradient-to-bl from-liquid-rose via-purple-200 to-liquid-mint opacity-40 blur-2xl -z-10"
animate={{
borderRadius: ["40% 60% 70% 30%/40% 50% 60% 50%", "60% 30% 40% 70%/60% 40% 70% 30%", "40% 60% 70% 30%/40% 50% 60% 50%"],
rotate: [360, 180, 0],
scale: [1, 0.9, 1]
}}
transition={{ duration: 15, repeat: Infinity, ease: "linear" }}
/>
{/* The Image Container with Organic Border Radius - No hard border, just shadow and glass */}
{/* Large Rotating Liquid Blobs behind image - Very slow and smooth */}
<motion.div
className="absolute inset-0 overflow-hidden shadow-2xl bg-stone-100"
style={{ filter: "drop-shadow(0 20px 40px rgba(0,0,0,0.15))" }}
animate={{
borderRadius: ["60% 40% 30% 70%/60% 30% 70% 40%", "30% 60% 70% 40%/50% 60% 30% 60%", "60% 40% 30% 70%/60% 30% 70% 40%"]
}}
transition={{ duration: 8, repeat: Infinity, ease: "easeInOut" }}
className="absolute w-[150%] h-[150%] bg-gradient-to-tr from-liquid-mint/40 via-liquid-blue/30 to-liquid-lavender/40 blur-3xl -z-10"
animate={{
borderRadius: [
"60% 40% 30% 70%/60% 30% 70% 40%",
"30% 60% 70% 40%/50% 60% 30% 60%",
"60% 40% 30% 70%/60% 30% 70% 40%",
],
rotate: [0, 120, 0],
scale: [1, 1.08, 1],
}}
transition={{
duration: 35,
repeat: Infinity,
ease: "easeInOut",
repeatType: "reverse",
}}
/>
<motion.div
className="absolute w-[130%] h-[130%] bg-gradient-to-bl from-liquid-rose/35 via-purple-200/25 to-liquid-mint/35 blur-2xl -z-10"
animate={{
borderRadius: [
"40% 60% 70% 30%/40% 50% 60% 50%",
"60% 30% 40% 70%/60% 40% 70% 30%",
"40% 60% 70% 30%/40% 50% 60% 50%",
],
rotate: [0, -90, 0],
scale: [1, 1.05, 1],
}}
transition={{
duration: 40,
repeat: Infinity,
ease: "easeInOut",
repeatType: "reverse",
}}
/>
{/* The Image Container with Organic Border Radius */}
<motion.div
className="absolute inset-0 overflow-hidden bg-stone-100"
style={{
filter: "drop-shadow(0 20px 40px rgba(0,0,0,0.1))",
willChange: "border-radius",
}}
animate={{
borderRadius: [
"60% 40% 30% 70%/60% 30% 70% 40%",
"30% 60% 70% 40%/50% 60% 30% 60%",
"60% 40% 30% 70%/60% 30% 70% 40%",
],
}}
transition={{
duration: 12,
repeat: Infinity,
ease: "easeInOut",
repeatType: "reverse",
}}
>
<Image
src="/images/me.jpg"
alt="Dennis Konkol"
fill
className="object-cover scale-105 hover:scale-110 transition-transform duration-700"
priority
/>
{/* Glossy Overlay for Liquid Feel */}
<div className="absolute inset-0 bg-gradient-to-tr from-white/30 via-transparent to-transparent opacity-50 pointer-events-none z-10" />
{/* Inner Border/Highlight */}
<div className="absolute inset-0 border-[3px] border-white/20 rounded-[inherit] pointer-events-none z-20" />
<Image
src="/images/me.jpg"
alt="Dennis Konkol"
fill
className="object-cover scale-105 hover:scale-[1.08] transition-transform duration-1000 ease-out"
priority
/>
{/* Glossy Overlay for Liquid Feel */}
<div className="absolute inset-0 bg-gradient-to-tr from-white/25 via-transparent to-white/10 opacity-60 pointer-events-none z-10" />
{/* Inner Border/Highlight */}
<div className="absolute inset-0 border-[2px] border-white/30 rounded-[inherit] pointer-events-none z-20" />
</motion.div>
{/* Floating Badges */}
{/* Domain Badge - repositioned below image */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 1.5, type: "spring" }}
className="absolute -top-4 right-0 md:-right-4 p-3 bg-white/90 backdrop-blur-md shadow-lg rounded-full text-stone-700 z-30"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.8, ease: "easeOut" }}
className="absolute -bottom-8 left-1/2 -translate-x-1/2 z-30"
>
<div className="px-6 py-2.5 rounded-full glass-panel text-stone-700 font-mono text-sm tracking-wider shadow-lg backdrop-blur-xl border border-white/50">
dk<span className="text-liquid-rose font-bold">0</span>.dev
</div>
</motion.div>
{/* Floating Badges - subtle animations */}
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 1.2, duration: 0.8, ease: "easeOut" }}
whileHover={{ scale: 1.1, rotate: 5 }}
className="absolute -top-4 right-0 md:-right-4 p-3 bg-white/95 backdrop-blur-md shadow-lg rounded-full text-stone-700 z-30"
>
<Code size={24} />
</motion.div>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 1.7, type: "spring" }}
className="absolute bottom-4 -left-4 md:-left-8 p-3 bg-white/90 backdrop-blur-md shadow-lg rounded-full text-stone-700 z-30"
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 1.4, duration: 0.8, ease: "easeOut" }}
whileHover={{ scale: 1.1, rotate: -5 }}
className="absolute bottom-4 -left-4 md:-left-8 p-3 bg-white/95 backdrop-blur-md shadow-lg rounded-full text-stone-700 z-30"
>
<Zap size={24} />
</motion.div>
@@ -112,74 +150,95 @@ const Hero = () => {
</motion.div>
{/* Main Title */}
<div className="mb-6 flex flex-col items-center justify-center relative">
<LiquidHeading
text="Dennis Konkol"
level={1}
className="text-5xl md:text-8xl font-bold tracking-tighter text-stone-800"
/>
<LiquidHeading
text="Software Engineer"
level={2}
className="text-2xl md:text-4xl font-light tracking-wide text-stone-500 mt-2"
/>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.6, ease: [0.25, 0.1, 0.25, 1] }}
className="mb-8 flex flex-col items-center justify-center relative"
>
<h1 className="text-5xl md:text-8xl font-bold tracking-tighter text-stone-900 mb-2">
Dennis Konkol
</h1>
<h2 className="text-2xl md:text-4xl font-light tracking-wide text-stone-600 mt-2">
Software Engineer
</h2>
</motion.div>
{/* Description */}
<motion.p
initial={{ opacity: 0, y: 30 }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 1.2 }}
className="text-lg md:text-xl text-stone-600 mb-12 max-w-2xl mx-auto leading-relaxed"
transition={{ duration: 1, delay: 0.9, ease: [0.25, 0.1, 0.25, 1] }}
className="text-lg md:text-xl text-stone-700 mb-12 max-w-2xl mx-auto leading-relaxed"
>
I craft digital experiences with a focus on <span className="text-stone-900 font-semibold decoration-liquid-mint decoration-2 underline underline-offset-2">design</span>, <span className="text-stone-900 font-semibold decoration-liquid-lavender decoration-2 underline underline-offset-2">performance</span>, and <span className="text-stone-900 font-semibold decoration-liquid-rose decoration-2 underline underline-offset-2">user experience</span>.
Student and passionate{" "}
<span className="text-stone-900 font-semibold decoration-liquid-mint decoration-2 underline underline-offset-4">
self-hoster
</span>{" "}
building full-stack web apps and mobile solutions. I run my own{" "}
<span className="text-stone-900 font-semibold decoration-liquid-lavender decoration-2 underline underline-offset-4">
infrastructure
</span>{" "}
and love exploring{" "}
<span className="text-stone-900 font-semibold decoration-liquid-rose decoration-2 underline underline-offset-4">
DevOps
</span>
.
</motion.p>
{/* Features */}
<motion.div
initial={{ opacity: 0, y: 30 }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 1.4 }}
transition={{ duration: 1, delay: 1.1, ease: [0.25, 0.1, 0.25, 1] }}
className="flex flex-wrap justify-center gap-4 mb-12"
>
{features.map((feature, index) => (
<motion.div
key={feature.text}
initial={{ opacity: 0, scale: 0.8 }}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 1.6 + index * 0.1 }}
whileHover={{ scale: 1.05, y: -2 }}
className="flex items-center space-x-2 px-5 py-2.5 rounded-full bg-white/60 border border-white/80 shadow-sm backdrop-blur-sm liquid-hover"
transition={{
duration: 0.8,
delay: 1.3 + index * 0.15,
ease: [0.25, 0.1, 0.25, 1],
}}
whileHover={{ scale: 1.03, y: -3 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="flex items-center space-x-2 px-5 py-2.5 rounded-full bg-white/70 border border-white/90 shadow-sm backdrop-blur-sm"
>
<feature.icon className="w-4 h-4 text-stone-700" />
<span className="text-stone-700 font-medium text-sm">{feature.text}</span>
<span className="text-stone-700 font-medium text-sm">
{feature.text}
</span>
</motion.div>
))}
</motion.div>
{/* CTA Buttons */}
<motion.div
initial={{ opacity: 0, y: 30 }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 1.8 }}
transition={{ duration: 1, delay: 1.6, ease: [0.25, 0.1, 0.25, 1] }}
className="flex flex-col sm:flex-row gap-5 justify-center items-center"
>
<motion.a
href="#projects"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-8 py-4 bg-stone-900 text-cream rounded-full font-medium shadow-lg hover:shadow-xl hover:bg-black transition-all flex items-center gap-2 liquid-hover"
whileHover={{ scale: 1.03, y: -2 }}
whileTap={{ scale: 0.98 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="px-8 py-4 bg-stone-900 text-cream rounded-full font-medium shadow-lg hover:shadow-xl hover:bg-stone-950 transition-all duration-500 flex items-center gap-2"
>
<span>View My Work</span>
<ArrowDown size={18} />
</motion.a>
<motion.a
href="#contact"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="px-8 py-4 bg-white text-stone-900 border border-stone-200 rounded-full font-medium shadow-sm hover:shadow-md transition-all liquid-hover"
whileHover={{ scale: 1.03, y: -2 }}
whileTap={{ scale: 0.98 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="px-8 py-4 bg-white text-stone-900 border border-stone-200 rounded-full font-medium shadow-sm hover:shadow-md transition-all duration-500"
>
<span>Contact Me</span>
</motion.a>

View File

@@ -1,11 +1,42 @@
"use client";
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { ExternalLink, Github, Calendar, Layers, ArrowRight } from 'lucide-react';
import Link from 'next/link';
import { LiquidHeading } from '@/components/LiquidHeading';
import Image from 'next/image';
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import {
ExternalLink,
Github,
Calendar,
Layers,
ArrowRight,
} from "lucide-react";
import Link from "next/link";
import Image from "next/image";
// Smooth animation configuration
const smoothTransition = {
duration: 0.8,
ease: [0.25, 0.1, 0.25, 1],
};
const fadeInUp = {
hidden: { opacity: 0, y: 40 },
visible: {
opacity: 1,
y: 0,
transition: smoothTransition,
},
};
const staggerContainer = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2,
delayChildren: 0.1,
},
},
};
interface Project {
id: number;
@@ -29,13 +60,15 @@ const Projects = () => {
setMounted(true);
const loadProjects = async () => {
try {
const response = await fetch('/api/projects?featured=true&published=true&limit=6');
const response = await fetch(
"/api/projects?featured=true&published=true&limit=6",
);
if (response.ok) {
const data = await response.json();
setProjects(data.projects || []);
}
} catch (error) {
console.error('Error loading projects:', error);
console.error("Error loading projects:", error);
}
};
loadProjects();
@@ -44,67 +77,93 @@ const Projects = () => {
if (!mounted) return null;
return (
<section id="projects" className="py-24 px-4 relative bg-stone-50/50">
<section
id="projects"
className="py-24 px-4 relative bg-gradient-to-br from-liquid-peach/15 via-liquid-yellow/10 to-liquid-coral/15 overflow-hidden"
>
<div className="max-w-7xl mx-auto">
<div className="text-center mb-20">
<LiquidHeading
text="Selected Works"
level={2}
className="text-4xl md:text-6xl font-bold mb-6 text-stone-900"
/>
<p className="text-lg text-stone-500 max-w-2xl mx-auto mt-4 font-light">
A collection of projects I&apos;ve worked on, ranging from web applications to experiments.
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
variants={fadeInUp}
className="text-center mb-20"
>
<h2 className="text-4xl md:text-6xl font-bold mb-6 text-stone-900">
Selected Works
</h2>
<p className="text-lg text-stone-600 max-w-2xl mx-auto mt-4 font-light">
A collection of projects I&apos;ve worked on, ranging from web
applications to experiments.
</p>
</div>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
variants={staggerContainer}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
>
{projects.map((project, index) => (
<motion.div
key={project.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
whileHover={{ y: -8 }}
className="group relative flex flex-col bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-all duration-300 border border-stone-100"
variants={fadeInUp}
whileHover={{
y: -12,
transition: { duration: 0.5, ease: "easeOut" },
}}
className="group relative flex flex-col bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-700 ease-out border border-stone-100 hover:border-stone-200"
>
{/* Project Cover / Header */}
<div className="relative aspect-[4/3] overflow-hidden bg-stone-100">
<div className="relative aspect-[4/3] overflow-hidden bg-gradient-to-br from-stone-50 to-stone-100">
{project.imageUrl ? (
<Image
src={project.imageUrl}
alt={project.title}
fill
className="object-cover transition-transform duration-500 group-hover:scale-105"
/>
<Image
src={project.imageUrl}
alt={project.title}
fill
className="object-cover transition-transform duration-1000 ease-out group-hover:scale-110"
/>
) : (
<div className="absolute inset-0 bg-gradient-to-br from-stone-100 to-stone-200 flex items-center justify-center p-8 group-hover:from-stone-50 group-hover:to-stone-100 transition-colors">
<div className="w-full h-full border-2 border-dashed border-stone-300 rounded-xl flex items-center justify-center">
<Layers className="text-stone-300 w-12 h-12" />
</div>
<div className="absolute inset-0 bg-gradient-to-tr from-liquid-mint/10 via-transparent to-liquid-rose/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
<div className="absolute inset-0 bg-gradient-to-br from-stone-100 to-stone-200 flex items-center justify-center p-8 group-hover:from-stone-50 group-hover:to-stone-100 transition-colors duration-700 ease-out">
<div className="w-full h-full border-2 border-dashed border-stone-300 rounded-xl flex items-center justify-center">
<Layers className="text-stone-300 w-12 h-12" />
</div>
<div className="absolute inset-0 bg-gradient-to-tr from-liquid-mint/10 via-transparent to-liquid-rose/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
</div>
)}
{/* Overlay Links */}
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-4 backdrop-blur-[2px]">
{project.github && (
<a href={project.github} target="_blank" rel="noopener noreferrer" className="p-3 bg-white rounded-full text-stone-900 hover:scale-110 transition-transform" aria-label="GitHub">
<Github size={20} />
</a>
)}
{project.live && (
<a href={project.live} target="_blank" rel="noopener noreferrer" className="p-3 bg-white rounded-full text-stone-900 hover:scale-110 transition-transform" aria-label="Live Demo">
<ExternalLink size={20} />
</a>
)}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity duration-700 ease-out flex items-center justify-center gap-4 backdrop-blur-sm">
{project.github && (
<a
href={project.github}
target="_blank"
rel="noopener noreferrer"
className="p-3 bg-white rounded-full text-stone-900 hover:scale-110 transition-all duration-500 ease-out hover:shadow-lg"
aria-label="GitHub"
>
<Github size={20} />
</a>
)}
{project.live && (
<a
href={project.live}
target="_blank"
rel="noopener noreferrer"
className="p-3 bg-white rounded-full text-stone-900 hover:scale-110 transition-all duration-500 ease-out hover:shadow-lg"
aria-label="Live Demo"
>
<ExternalLink size={20} />
</a>
)}
</div>
</div>
{/* Content */}
<div className="flex flex-col flex-1 p-6">
<div className="flex justify-between items-start mb-3">
<h3 className="text-xl font-bold text-stone-900 group-hover:text-stone-600 transition-colors">
<h3 className="text-xl font-bold text-stone-900 group-hover:text-stone-700 transition-colors duration-500">
{project.title}
</h3>
<span className="text-xs font-mono text-stone-400 bg-stone-100 px-2 py-1 rounded">
@@ -112,45 +171,60 @@ const Projects = () => {
</span>
</div>
<p className="text-stone-600 text-sm leading-relaxed mb-6 line-clamp-3 flex-1">
<p className="text-stone-700 text-sm leading-relaxed mb-6 line-clamp-3 flex-1">
{project.description}
</p>
<div className="space-y-4 mt-auto">
<div className="flex flex-wrap gap-2">
{project.tags.slice(0, 3).map(tag => (
<span key={tag} className="text-xs px-2.5 py-1 bg-stone-50 border border-stone-100 rounded-md text-stone-500 font-medium">
{project.tags.slice(0, 3).map((tag, tIdx) => (
<span
key={`${project.id}-${tag}-${tIdx}`}
className="text-xs px-2.5 py-1 bg-stone-50 border border-stone-100 rounded-md text-stone-600 font-medium hover:bg-stone-100 hover:border-stone-200 transition-all duration-400 ease-out"
>
{tag}
</span>
))}
{project.tags.length > 3 && (
<span className="text-xs px-2 py-1 text-stone-400">+ {project.tags.length - 3}</span>
<span className="text-xs px-2 py-1 text-stone-400">
+ {project.tags.length - 3}
</span>
)}
</div>
<Link
href={`/projects/${project.title.toLowerCase().replace(/\s+/g, '-')}`}
className="inline-flex items-center text-sm font-semibold text-stone-900 hover:gap-2 transition-all group/link"
<Link
href={`/projects/${project.title.toLowerCase().replace(/\s+/g, "-")}`}
className="inline-flex items-center text-sm font-semibold text-stone-900 hover:gap-3 transition-all duration-500 ease-out group/link"
>
Read more <ArrowRight size={16} className="ml-1 transition-transform group-hover/link:translate-x-1" />
Read more{" "}
<ArrowRight
size={16}
className="ml-1 transition-transform duration-500 ease-out group-hover/link:translate-x-2"
/>
</Link>
</div>
</div>
</motion.div>
))}
</div>
</motion.div>
<div className="mt-16 text-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, delay: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
className="mt-16 text-center"
>
<Link
href="/projects"
className="inline-flex items-center gap-2 px-6 py-3 bg-white border border-stone-200 rounded-full text-stone-600 font-medium hover:bg-stone-50 hover:border-stone-300 transition-all shadow-sm"
className="inline-flex items-center gap-2 px-8 py-4 bg-white border border-stone-200 rounded-full text-stone-700 font-medium hover:bg-stone-50 hover:border-stone-300 hover:gap-3 transition-all duration-500 ease-out shadow-sm hover:shadow-md"
>
Archive <ArrowRight size={16} />
View All Projects <ArrowRight size={16} />
</Link>
</div>
</motion.div>
</div>
</section>
);
};
export default Projects;
export default Projects;

View File

@@ -0,0 +1,240 @@
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Sparkles,
Loader2,
Image as ImageIcon,
RefreshCw,
CheckCircle,
XCircle,
} from "lucide-react";
interface AIImageGeneratorProps {
projectId: number;
projectTitle: string;
currentImageUrl?: string | null;
onImageGenerated?: (imageUrl: string) => void;
}
export default function AIImageGenerator({
projectId,
projectTitle,
currentImageUrl,
onImageGenerated,
}: AIImageGeneratorProps) {
const [isGenerating, setIsGenerating] = useState(false);
const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
const [message, setMessage] = useState("");
const [generatedImageUrl, setGeneratedImageUrl] = useState(
currentImageUrl || null,
);
const [showPreview, setShowPreview] = useState(false);
const handleGenerate = async (regenerate: boolean = false) => {
setIsGenerating(true);
setStatus("idle");
setMessage("Generating AI image...");
try {
const response = await fetch("/api/n8n/generate-image", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
projectId: projectId,
regenerate: regenerate,
}),
});
const data = await response.json();
if (response.ok && data.success) {
setStatus("success");
setMessage(data.message || "Image generated successfully!");
setGeneratedImageUrl(data.imageUrl);
setShowPreview(true);
if (onImageGenerated) {
onImageGenerated(data.imageUrl);
}
} else {
setStatus("error");
setMessage(data.error || data.message || "Failed to generate image");
}
} catch (error) {
setStatus("error");
setMessage(
error instanceof Error ? error.message : "An unexpected error occurred",
);
} finally {
setIsGenerating(false);
}
};
return (
<div className="bg-white rounded-2xl border-2 border-stone-200 p-6 shadow-sm">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-gradient-to-br from-purple-100 to-pink-100 rounded-lg">
<Sparkles className="text-purple-600" size={20} />
</div>
<div>
<h3 className="font-bold text-stone-900">AI Image Generator</h3>
<p className="text-sm text-stone-600">
Generate cover image for:{" "}
<span className="font-semibold">{projectTitle}</span>
</p>
</div>
</div>
{/* Current/Generated Image Preview */}
<AnimatePresence mode="wait">
{(generatedImageUrl || showPreview) && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3 }}
className="mb-4 relative group"
>
<div className="aspect-[4/3] rounded-xl overflow-hidden border-2 border-stone-200 bg-stone-50">
{generatedImageUrl ? (
<img
src={generatedImageUrl}
alt={projectTitle}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<ImageIcon className="text-stone-300" size={48} />
</div>
)}
</div>
{generatedImageUrl && (
<div className="absolute top-2 right-2 bg-white/90 backdrop-blur-sm px-3 py-1 rounded-full text-xs font-medium text-stone-700 border border-stone-200">
Current Image
</div>
)}
</motion.div>
)}
</AnimatePresence>
{/* Status Message */}
<AnimatePresence mode="wait">
{message && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2 }}
className={`mb-4 p-3 rounded-xl border-2 flex items-center gap-2 ${
status === "success"
? "bg-green-50 border-green-200 text-green-800"
: status === "error"
? "bg-red-50 border-red-200 text-red-800"
: "bg-blue-50 border-blue-200 text-blue-800"
}`}
>
{status === "success" && <CheckCircle size={18} />}
{status === "error" && <XCircle size={18} />}
{status === "idle" && isGenerating && (
<Loader2 size={18} className="animate-spin" />
)}
<span className="text-sm font-medium">{message}</span>
</motion.div>
)}
</AnimatePresence>
{/* Action Buttons */}
<div className="flex gap-3">
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => handleGenerate(false)}
disabled={isGenerating || (!regenerate && !!generatedImageUrl)}
className={`flex-1 py-3 px-4 rounded-xl font-semibold text-white transition-all duration-300 flex items-center justify-center gap-2 ${
isGenerating
? "bg-stone-400 cursor-not-allowed"
: generatedImageUrl
? "bg-stone-300 cursor-not-allowed"
: "bg-gradient-to-br from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 shadow-lg hover:shadow-xl"
}`}
>
{isGenerating ? (
<>
<Loader2 size={18} className="animate-spin" />
Generating...
</>
) : (
<>
<Sparkles size={18} />
Generate Image
</>
)}
</motion.button>
{generatedImageUrl && (
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => handleGenerate(true)}
disabled={isGenerating}
className={`py-3 px-4 rounded-xl font-semibold transition-all duration-300 flex items-center justify-center gap-2 border-2 ${
isGenerating
? "bg-stone-100 border-stone-300 text-stone-400 cursor-not-allowed"
: "bg-white border-purple-300 text-purple-700 hover:bg-purple-50 hover:border-purple-400"
}`}
>
<RefreshCw size={18} />
Regenerate
</motion.button>
)}
</div>
{/* Info Box */}
<div className="mt-4 p-3 bg-gradient-to-br from-blue-50 to-purple-50 border border-blue-200 rounded-lg">
<p className="text-xs text-stone-700 leading-relaxed">
<span className="font-semibold">💡 How it works:</span> The AI
analyzes your project&aposs title, description, category, and tech
stack to create a unique cover image using Stable Diffusion.
Generation takes 15-30 seconds.
</p>
</div>
{/* Advanced Options (Optional) */}
<details className="mt-4">
<summary className="cursor-pointer text-sm font-semibold text-stone-700 hover:text-stone-900 transition-colors">
Advanced Options
</summary>
<div className="mt-3 space-y-3 pl-4 border-l-2 border-stone-200">
<div className="text-xs text-stone-600 space-y-1">
<p>
<strong>Image Size:</strong> 1024x768 (4:3 aspect ratio)
</p>
<p>
<strong>Quality:</strong> High (30 steps, CFG 7)
</p>
<p>
<strong>Sampler:</strong> DPM++ 2M Karras
</p>
<p>
<strong>Model:</strong> SDXL Base / Category-specific
</p>
</div>
<button
onClick={() =>
window.open("/docs/ai-image-generation/SETUP.md", "_blank")
}
className="text-xs text-purple-600 hover:text-purple-700 font-medium underline"
>
View Full Documentation
</button>
</div>
</details>
</div>
);
}

View File

@@ -2,152 +2,181 @@
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap");
:root {
/* Organic Modern Palette */
--background: #FDFCF8; /* Cream */
--foreground: #292524; /* Warm Grey */
--card: rgba(255, 255, 255, 0.6);
--card-foreground: #292524;
--popover: #FFFFFF;
--popover-foreground: #292524;
--primary: #292524;
--primary-foreground: #FDFCF8;
--secondary: #E7E5E4;
--secondary-foreground: #292524;
--muted: #F5F5F4;
--muted-foreground: #78716C;
--accent: #F3F1E7; /* Sand */
--accent-foreground: #292524;
--destructive: #EF4444;
--destructive-foreground: #FDFCF8;
--border: #E7E5E4;
--input: #E7E5E4;
--ring: #A7F3D0; /* Mint ring */
--radius: 1rem;
/* Organic Modern Palette */
--background: #fdfcf8; /* Cream */
--foreground: #292524; /* Warm Grey */
--card: rgba(255, 255, 255, 0.6);
--card-foreground: #292524;
--popover: #ffffff;
--popover-foreground: #292524;
--primary: #292524;
--primary-foreground: #fdfcf8;
--secondary: #e7e5e4;
--secondary-foreground: #292524;
--muted: #f5f5f4;
--muted-foreground: #78716c;
--accent: #f3f1e7; /* Sand */
--accent-foreground: #292524;
--destructive: #ef4444;
--destructive-foreground: #fdfcf8;
--border: #e7e5e4;
--input: #e7e5e4;
--ring: #a7f3d0; /* Mint ring */
--radius: 1rem;
}
body {
background-color: var(--background);
color: var(--foreground);
font-family: 'Inter', sans-serif;
margin: 0;
padding: 0;
overflow-x: hidden;
background-color: var(--background);
color: var(--foreground);
font-family: "Inter", sans-serif;
margin: 0;
padding: 0;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
/* Custom Selection */
::selection {
background: #A7F3D0; /* Mint */
color: #292524;
background: #a7f3d0; /* Mint */
color: #292524;
}
/* Smooth Scrolling */
html {
scroll-behavior: smooth;
scroll-behavior: smooth;
}
/* Liquid Glass Effects */
.glass-panel {
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(12px) saturate(120%);
-webkit-backdrop-filter: blur(12px) saturate(120%);
border: 1px solid rgba(255, 255, 255, 0.6);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05);
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(12px) saturate(120%);
-webkit-backdrop-filter: blur(12px) saturate(120%);
border: 1px solid rgba(255, 255, 255, 0.7);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
will-change: backdrop-filter;
}
.glass-card {
background: rgba(255, 255, 255, 0.65);
backdrop-filter: blur(24px) saturate(180%);
-webkit-backdrop-filter: blur(24px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.8);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.02),
0 2px 4px -1px rgba(0, 0, 0, 0.02),
inset 0 0 20px rgba(255, 255, 255, 0.5);
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(24px) saturate(180%);
-webkit-backdrop-filter: blur(24px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.85);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.03),
0 2px 4px -1px rgba(0, 0, 0, 0.02),
inset 0 0 20px rgba(255, 255, 255, 0.5);
transition: all 0.6s cubic-bezier(0.25, 0.1, 0.25, 1);
will-change: transform, box-shadow;
}
.glass-card:hover {
background: rgba(255, 255, 255, 0.75);
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.05),
0 10px 10px -5px rgba(0, 0, 0, 0.01),
inset 0 0 20px rgba(255, 255, 255, 0.8);
transform: translateY(-4px) scale(1.005);
border-color: #ffffff;
background: rgba(255, 255, 255, 0.8);
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.08),
0 10px 10px -5px rgba(0, 0, 0, 0.02),
inset 0 0 20px rgba(255, 255, 255, 0.8);
transform: translateY(-4px) scale(1.005);
border-color: #ffffff;
}
/* Typography & Headings */
h1, h2, h3, h4, h5, h6 {
letter-spacing: -0.02em;
h1,
h2,
h3,
h4,
h5,
h6 {
letter-spacing: -0.02em;
font-weight: 700;
color: #292524;
}
/* Improve text contrast */
p,
span,
div {
color: #44403c;
}
/* Utility for the liquid melt effect container */
.liquid-container {
filter: url('#goo');
}
/* Liquid container removed - no filters applied */
/* Hide scrollbar but keep functionality */
::-webkit-scrollbar {
width: 8px;
width: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #D6D3D1;
border-radius: 4px;
background: #d6d3d1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #A8A29E;
background: #a8a29e;
}
/* Animations */
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
.animate-float {
animation: float 5s ease-in-out infinite;
animation: float 8s ease-in-out infinite;
will-change: transform;
}
@keyframes liquid-pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
/* Liquid Blobs Background */
.liquid-bg-blob {
position: absolute;
filter: blur(80px);
opacity: 0.6;
z-index: -1;
animation: float 10s ease-in-out infinite;
position: absolute;
filter: blur(80px);
opacity: 0.6;
z-index: -1;
animation: float 10s ease-in-out infinite;
}
/* Markdown Specifics for Blog/Projects */
.markdown h1 {
@apply text-4xl font-bold mb-6 text-stone-800 tracking-tight;
@apply text-4xl font-bold mb-6 text-stone-900 tracking-tight;
}
.markdown h2 {
@apply text-2xl font-semibold mt-8 mb-4 text-stone-800 tracking-tight;
@apply text-2xl font-semibold mt-8 mb-4 text-stone-900 tracking-tight;
}
.markdown p {
@apply mb-4 leading-relaxed text-stone-600;
@apply mb-4 leading-relaxed text-stone-700;
}
.markdown a {
@apply text-stone-800 underline decoration-liquid-mint decoration-2 underline-offset-2 hover:text-black transition-colors;
@apply text-stone-900 underline decoration-liquid-mint decoration-2 underline-offset-2 hover:text-black transition-colors duration-300;
}
.markdown ul {
@apply list-disc list-inside mb-4 space-y-2 text-stone-600;
@apply list-disc list-inside mb-4 space-y-2 text-stone-700;
}
.markdown code {
@apply bg-stone-100 px-1.5 py-0.5 rounded text-sm text-stone-800 font-mono;
@apply bg-stone-100 px-1.5 py-0.5 rounded text-sm text-stone-900 font-mono;
}
.markdown pre {
@apply bg-stone-900 text-stone-50 p-4 rounded-xl overflow-x-auto mb-6;
}
@apply bg-stone-900 text-stone-50 p-4 rounded-xl overflow-x-auto mb-6;
}

View File

@@ -5,7 +5,6 @@ import React from "react";
import { ToastProvider } from "@/components/Toast";
import { AnalyticsProvider } from "@/components/AnalyticsProvider";
import { PerformanceDashboard } from "@/components/PerformanceDashboard";
import { GooFilter } from "@/components/GooFilter";
import { BackgroundBlobs } from "@/components/BackgroundBlobs";
const inter = Inter({
@@ -21,18 +20,19 @@ export default function RootLayout({
return (
<html lang="en">
<head>
<script defer src="https://analytics.dk0.dev/script.js" data-website-id="b3665829-927a-4ada-b9bb-fcf24171061e"></script>
<meta charSet="utf-8"/>
<script
defer
src="https://analytics.dk0.dev/script.js"
data-website-id="b3665829-927a-4ada-b9bb-fcf24171061e"
></script>
<meta charSet="utf-8" />
<title>Dennis Konkol&#39;s Portfolio</title>
</head>
<body className={inter.variable}>
<AnalyticsProvider>
<ToastProvider>
<GooFilter />
<BackgroundBlobs />
<div className="relative z-10">
{children}
</div>
<div className="relative z-10">{children}</div>
<PerformanceDashboard />
</ToastProvider>
</AnalyticsProvider>
@@ -43,12 +43,14 @@ export default function RootLayout({
export const metadata: Metadata = {
title: "Dennis Konkol | Portfolio",
description: "Portfolio of Dennis Konkol, a student and software engineer based in Osnabrück, Germany. Passionate about technology, coding, and solving real-world problems.",
description:
"Portfolio of Dennis Konkol, a student and software engineer based in Osnabrück, Germany. Passionate about technology, coding, and solving real-world problems.",
keywords: ["Dennis Konkol", "Software Engineer", "Portfolio", "Student"],
authors: [{name: "Dennis Konkol", url: "https://dk0.dev"}],
authors: [{ name: "Dennis Konkol", url: "https://dk0.dev" }],
openGraph: {
title: "Dennis Konkol | Portfolio",
description: "Explore my projects and contact me for collaboration opportunities!",
description:
"Explore my projects and contact me for collaboration opportunities!",
url: "https://dk0.dev",
siteName: "Dennis Konkol Portfolio",
images: [

View File

@@ -8,6 +8,7 @@ import Contact from "./components/Contact";
import Footer from "./components/Footer";
import Script from "next/script";
import { ActivityFeed } from "./components/ActivityFeed";
import { motion } from "framer-motion";
export default function Home() {
return (
@@ -36,10 +37,114 @@ export default function Home() {
/>
<ActivityFeed />
<Header />
{/* Spacer to prevent navbar overlap */}
<div className="h-24 md:h-32" aria-hidden="true"></div>
<main className="relative">
<Hero />
{/* Wavy Separator 1 - Hero to About */}
<div className="relative h-24 overflow-hidden">
<svg
className="absolute inset-0 w-full h-full"
viewBox="0 0 1440 120"
preserveAspectRatio="none"
>
<motion.path
d="M0,64 C240,96 480,32 720,64 C960,96 1200,32 1440,64 L1440,120 L0,120 Z"
fill="url(#gradient1)"
animate={{
d: [
"M0,64 C240,96 480,32 720,64 C960,96 1200,32 1440,64 L1440,120 L0,120 Z",
"M0,32 C240,64 480,96 720,32 C960,64 1200,96 1440,32 L1440,120 L0,120 Z",
"M0,64 C240,96 480,32 720,64 C960,96 1200,32 1440,64 L1440,120 L0,120 Z",
],
}}
transition={{
duration: 12,
repeat: Infinity,
ease: "easeInOut",
}}
/>
<defs>
<linearGradient id="gradient1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#BAE6FD" stopOpacity="0.4" />
<stop offset="50%" stopColor="#DDD6FE" stopOpacity="0.4" />
<stop offset="100%" stopColor="#FBCFE8" stopOpacity="0.4" />
</linearGradient>
</defs>
</svg>
</div>
<About />
{/* Wavy Separator 2 - About to Projects */}
<div className="relative h-24 overflow-hidden">
<svg
className="absolute inset-0 w-full h-full"
viewBox="0 0 1440 120"
preserveAspectRatio="none"
>
<motion.path
d="M0,32 C240,64 480,96 720,32 C960,64 1200,96 1440,32 L1440,120 L0,120 Z"
fill="url(#gradient2)"
animate={{
d: [
"M0,32 C240,64 480,96 720,32 C960,64 1200,96 1440,32 L1440,120 L0,120 Z",
"M0,96 C240,32 480,64 720,96 C960,32 1200,64 1440,96 L1440,120 L0,120 Z",
"M0,32 C240,64 480,96 720,32 C960,64 1200,96 1440,32 L1440,120 L0,120 Z",
],
}}
transition={{
duration: 14,
repeat: Infinity,
ease: "easeInOut",
}}
/>
<defs>
<linearGradient id="gradient2" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#FED7AA" stopOpacity="0.4" />
<stop offset="50%" stopColor="#FDE68A" stopOpacity="0.4" />
<stop offset="100%" stopColor="#FCA5A5" stopOpacity="0.4" />
</linearGradient>
</defs>
</svg>
</div>
<Projects />
{/* Wavy Separator 3 - Projects to Contact */}
<div className="relative h-24 overflow-hidden">
<svg
className="absolute inset-0 w-full h-full"
viewBox="0 0 1440 120"
preserveAspectRatio="none"
>
<motion.path
d="M0,96 C240,32 480,64 720,96 C960,32 1200,64 1440,96 L1440,120 L0,120 Z"
fill="url(#gradient3)"
animate={{
d: [
"M0,96 C240,32 480,64 720,96 C960,32 1200,64 1440,96 L1440,120 L0,120 Z",
"M0,64 C240,96 480,32 720,64 C960,96 1200,32 1440,64 L1440,120 L0,120 Z",
"M0,96 C240,32 480,64 720,96 C960,32 1200,64 1440,96 L1440,120 L0,120 Z",
],
}}
transition={{
duration: 16,
repeat: Infinity,
ease: "easeInOut",
}}
/>
<defs>
<linearGradient id="gradient3" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#99F6E4" stopOpacity="0.4" />
<stop offset="50%" stopColor="#A7F3D0" stopOpacity="0.4" />
<stop offset="100%" stopColor="#D9F99D" stopOpacity="0.4" />
</linearGradient>
</defs>
</svg>
</div>
<Contact />
</main>
<Footer />