full upgrade to dev
This commit is contained in:
86
app/api/n8n/chat/route.ts
Normal file
86
app/api/n8n/chat/route.ts
Normal 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!";
|
||||||
|
}
|
||||||
176
app/api/n8n/generate-image/route.ts
Normal file
176
app/api/n8n/generate-image/route.ts
Normal 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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() {
|
export async function GET() {
|
||||||
// Mock data - in real integration this would fetch from n8n webhook or database
|
try {
|
||||||
return NextResponse.json({
|
// Fetch from activity_status table
|
||||||
activity: {
|
const result = await prisma.$queryRawUnsafe<any[]>(
|
||||||
type: 'coding', // coding, listening, watching
|
`SELECT * FROM activity_status WHERE id = 1 LIMIT 1`,
|
||||||
details: 'Portfolio Website',
|
);
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
if (!result || result.length === 0) {
|
||||||
music: {
|
return NextResponse.json({
|
||||||
isPlaying: true,
|
activity: null,
|
||||||
track: 'Midnight City',
|
music: null,
|
||||||
artist: 'M83',
|
watching: null,
|
||||||
platform: 'spotify'
|
gaming: null,
|
||||||
},
|
status: null,
|
||||||
watching: 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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,34 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import { Code, Terminal, Cpu, Globe } from 'lucide-react';
|
import { Globe, Server, Wrench, Shield, Gamepad2, Code } from "lucide-react";
|
||||||
import { LiquidHeading } from '@/components/LiquidHeading';
|
|
||||||
|
// 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 About = () => {
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
@@ -14,77 +39,201 @@ const About = () => {
|
|||||||
|
|
||||||
const techStack = [
|
const techStack = [
|
||||||
{
|
{
|
||||||
category: 'Frontend',
|
category: "Frontend & Mobile",
|
||||||
icon: Globe,
|
icon: Globe,
|
||||||
items: ['React', 'TypeScript', 'Tailwind', 'Next.js']
|
items: ["Next.js", "Tailwind CSS", "Flutter"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'Backend',
|
category: "Backend & DevOps",
|
||||||
icon: Terminal,
|
icon: Server,
|
||||||
items: ['Node.js', 'PostgreSQL', 'Prisma', 'API Design']
|
items: ["Docker Swarm", "Traefik", "Nginx Proxy Manager", "Redis"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: 'Tools',
|
category: "Tools & Automation",
|
||||||
icon: Cpu,
|
icon: Wrench,
|
||||||
items: ['Git', 'Docker', 'VS Code', 'Figma']
|
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;
|
if (!mounted) return null;
|
||||||
|
|
||||||
return (
|
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="max-w-6xl mx-auto relative z-10">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-start">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
|
||||||
{/* Text Content */}
|
{/* Text Content */}
|
||||||
<div className="space-y-8">
|
<motion.div
|
||||||
<LiquidHeading
|
initial="hidden"
|
||||||
text="About Me"
|
whileInView="visible"
|
||||||
level={2}
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
className="text-4xl md:text-5xl font-bold text-stone-900"
|
variants={staggerContainer}
|
||||||
/>
|
className="space-y-8"
|
||||||
<div className="prose prose-stone prose-lg text-stone-600">
|
>
|
||||||
|
<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>
|
<p>
|
||||||
Hi, I'm Dennis. I'm a software engineer who likes building things that work well and look good.
|
Hi, I'm Dennis – a student and passionate self-hoster based
|
||||||
|
in Osnabrück, Germany.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
I'm currently based in Osnabrück, Germany. My journey in tech is driven by curiosity—I 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>
|
||||||
<p>
|
<p>
|
||||||
When I'm not in front of a screen, you can find me listening to music, exploring new ideas, or just relaxing.
|
When I'm not coding or tinkering with servers, you'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>
|
</p>
|
||||||
</div>
|
<p className="text-sm italic text-stone-500 bg-stone-50 p-4 rounded-lg border-l-4 border-liquid-mint">
|
||||||
</div>
|
💡 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 */}
|
{/* Tech Stack & Hobbies */}
|
||||||
<div className="grid grid-cols-1 gap-6">
|
<motion.div
|
||||||
<h3 className="text-xl font-bold text-stone-900 mb-2">My Toolbox</h3>
|
initial="hidden"
|
||||||
{techStack.map((stack, idx) => (
|
whileInView="visible"
|
||||||
<motion.div
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
key={stack.category}
|
variants={staggerContainer}
|
||||||
initial={{ opacity: 0, x: 20 }}
|
className="space-y-8"
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
>
|
||||||
transition={{ delay: idx * 0.1 }}
|
<div>
|
||||||
viewport={{ once: true }}
|
<motion.h3
|
||||||
className="p-6 rounded-xl bg-stone-50 border border-stone-100 hover:border-stone-200 transition-colors"
|
variants={fadeInUp}
|
||||||
|
className="text-2xl font-bold text-stone-900 mb-6"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 mb-4">
|
My Tech Stack
|
||||||
<div className="p-2 bg-white rounded-lg shadow-sm text-stone-700">
|
</motion.h3>
|
||||||
<stack.icon size={20} />
|
<div className="grid grid-cols-1 gap-4">
|
||||||
</div>
|
{techStack.map((stack, idx) => (
|
||||||
<h4 className="font-semibold text-stone-800">{stack.category}</h4>
|
<motion.div
|
||||||
</div>
|
key={`${stack.category}-${idx}`}
|
||||||
<div className="flex flex-wrap gap-2">
|
variants={fadeInUp}
|
||||||
{stack.items.map(item => (
|
whileHover={{
|
||||||
<span key={item} className="px-3 py-1 bg-white rounded-md border border-stone-200 text-sm text-stone-600">
|
scale: 1.02,
|
||||||
{item}
|
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'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>
|
</span>
|
||||||
))}
|
</motion.div>
|
||||||
</div>
|
))}
|
||||||
</motion.div>
|
<motion.div
|
||||||
))}
|
variants={fadeInUp}
|
||||||
</div>
|
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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,48 +1,270 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { Music, Code, Monitor, MessageSquare, Send, X } from 'lucide-react';
|
import {
|
||||||
|
Music,
|
||||||
|
Code,
|
||||||
|
Monitor,
|
||||||
|
MessageSquare,
|
||||||
|
Send,
|
||||||
|
X,
|
||||||
|
Loader2,
|
||||||
|
Github,
|
||||||
|
Tv,
|
||||||
|
Gamepad2,
|
||||||
|
Coffee,
|
||||||
|
Headphones,
|
||||||
|
Terminal,
|
||||||
|
Sparkles,
|
||||||
|
ExternalLink,
|
||||||
|
Activity,
|
||||||
|
Waves,
|
||||||
|
Zap,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
interface ActivityData {
|
interface ActivityData {
|
||||||
activity: {
|
activity: {
|
||||||
type: 'coding' | 'listening' | 'watching';
|
type:
|
||||||
|
| "coding"
|
||||||
|
| "listening"
|
||||||
|
| "watching"
|
||||||
|
| "gaming"
|
||||||
|
| "reading"
|
||||||
|
| "running";
|
||||||
details: string;
|
details: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
project?: string;
|
||||||
|
language?: string;
|
||||||
|
repo?: string;
|
||||||
|
link?: string;
|
||||||
} | null;
|
} | null;
|
||||||
music: {
|
music: {
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
track: string;
|
track: string;
|
||||||
artist: string;
|
artist: string;
|
||||||
platform: 'spotify' | 'apple';
|
album?: string;
|
||||||
|
platform: "spotify" | "apple";
|
||||||
|
progress?: number;
|
||||||
|
albumArt?: string;
|
||||||
|
spotifyUrl?: string;
|
||||||
} | null;
|
} | null;
|
||||||
watching: {
|
watching: {
|
||||||
title: string;
|
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;
|
} | 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 = () => {
|
export const ActivityFeed = () => {
|
||||||
const [data, setData] = useState<ActivityData | null>(null);
|
const [data, setData] = useState<ActivityData | null>(null);
|
||||||
const [showChat, setShowChat] = useState(false);
|
const [showChat, setShowChat] = useState(false);
|
||||||
const [chatMessage, setChatMessage] = useState('');
|
const [chatMessage, setChatMessage] = useState("");
|
||||||
const [chatHistory, setChatHistory] = useState<{
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
role: 'user' | 'ai';
|
const [chatHistory, setChatHistory] = useState<
|
||||||
text: string;
|
{
|
||||||
}[]>([
|
role: "user" | "ai";
|
||||||
{ role: 'ai', text: 'Hi! I am Dennis\'s AI assistant. Ask me anything about him!' }
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/n8n/status');
|
const res = await fetch("/api/n8n/status");
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
setData(json);
|
setData(json);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to fetch activity', e);
|
console.error("Failed to fetch activity", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -50,67 +272,306 @@ export const ActivityFeed = () => {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSendMessage = (e: React.FormEvent) => {
|
const handleSendMessage = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!chatMessage.trim()) return;
|
if (!chatMessage.trim() || isLoading) return;
|
||||||
|
|
||||||
const userMsg = chatMessage;
|
const userMsg = chatMessage;
|
||||||
setChatHistory(prev => [...prev, { role: 'user', text: userMsg }]);
|
setChatHistory((prev) => [
|
||||||
setChatMessage('');
|
...prev,
|
||||||
|
{ role: "user", text: userMsg, timestamp: Date.now() },
|
||||||
|
]);
|
||||||
|
setChatMessage("");
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
// Mock AI response - would connect to n8n webhook
|
try {
|
||||||
setTimeout(() => {
|
const response = await fetch("/api/n8n/chat", {
|
||||||
setChatHistory(prev => [...prev, { role: 'ai', text: `That's a great question about "${userMsg}"! I'll ask Dennis to add more info about that.` }]);
|
method: "POST",
|
||||||
}, 1000);
|
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 (
|
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 */}
|
{/* Chat Window */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showChat && (
|
{showChat && (
|
||||||
<motion.div
|
<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 }}
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
className="pointer-events-auto bg-white/80 backdrop-blur-xl border border-white/60 shadow-2xl rounded-2xl w-80 overflow-hidden"
|
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">
|
<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-semibold text-stone-800 flex items-center gap-2">
|
<span className="font-bold text-stone-900 flex items-center gap-2">
|
||||||
<MessageSquare size={16} />
|
<Sparkles size={18} className="text-liquid-mint" />
|
||||||
Ask me anything
|
AI Assistant
|
||||||
</span>
|
</span>
|
||||||
<button onClick={() => setShowChat(false)} className="text-stone-500 hover:text-stone-800">
|
<button
|
||||||
<X size={16} />
|
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>
|
</button>
|
||||||
</div>
|
</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) => (
|
{chatHistory.map((msg, i) => (
|
||||||
<div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
<motion.div
|
||||||
<div className={`max-w-[85%] p-3 rounded-xl text-sm ${
|
key={`chat-${msg.timestamp}-${i}`}
|
||||||
msg.role === 'user'
|
initial={{ opacity: 0, y: 10 }}
|
||||||
? 'bg-stone-800 text-white rounded-tr-none'
|
animate={{ opacity: 1, y: 0 }}
|
||||||
: 'bg-white text-stone-800 shadow-sm rounded-tl-none'
|
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}
|
{msg.text}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<form onSubmit={handleSendMessage} className="p-3 border-t border-white/50 bg-white/40 flex gap-2">
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={chatMessage}
|
value={chatMessage}
|
||||||
onChange={(e) => setChatMessage(e.target.value)}
|
onChange={(e) => setChatMessage(e.target.value)}
|
||||||
placeholder="Type a message..."
|
placeholder="Ask me anything..."
|
||||||
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"
|
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">
|
<motion.button
|
||||||
<Send size={16} />
|
type="submit"
|
||||||
</button>
|
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>
|
</form>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@@ -118,41 +579,40 @@ export const ActivityFeed = () => {
|
|||||||
|
|
||||||
{/* Activity Bubbles */}
|
{/* Activity Bubbles */}
|
||||||
<div className="flex flex-col items-end gap-2 pointer-events-auto">
|
<div className="flex flex-col items-end gap-2 pointer-events-auto">
|
||||||
{data.activity?.type === 'coding' && (
|
<AnimatePresence mode="wait">
|
||||||
<motion.div
|
{renderActivityBubble()}
|
||||||
initial={{ x: 50, opacity: 0 }}
|
{renderMusicBubble()}
|
||||||
animate={{ x: 0, opacity: 1 }}
|
{renderStatusBubble()}
|
||||||
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"
|
</AnimatePresence>
|
||||||
>
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{data.music?.isPlaying && (
|
{/* Chat Toggle Button with Notification Indicator */}
|
||||||
<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 */}
|
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.08, rotate: 5 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
|
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||||
onClick={() => setShowChat(!showChat)}
|
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} />
|
<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>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import { Mail, MapPin, Send } from 'lucide-react';
|
import { Mail, MapPin, Send } from "lucide-react";
|
||||||
import { useToast } from '@/components/Toast';
|
import { useToast } from "@/components/Toast";
|
||||||
import { LiquidHeading } from '@/components/LiquidHeading';
|
|
||||||
|
|
||||||
const Contact = () => {
|
const Contact = () => {
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
@@ -15,10 +14,10 @@ const Contact = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: "",
|
||||||
email: '',
|
email: "",
|
||||||
subject: '',
|
subject: "",
|
||||||
message: ''
|
message: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
@@ -29,27 +28,27 @@ const Contact = () => {
|
|||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
if (!formData.name.trim()) {
|
if (!formData.name.trim()) {
|
||||||
newErrors.name = 'Name is required';
|
newErrors.name = "Name is required";
|
||||||
} else if (formData.name.trim().length < 2) {
|
} 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()) {
|
if (!formData.email.trim()) {
|
||||||
newErrors.email = 'Email is required';
|
newErrors.email = "Email is required";
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
} 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()) {
|
if (!formData.subject.trim()) {
|
||||||
newErrors.subject = 'Subject is required';
|
newErrors.subject = "Subject is required";
|
||||||
} else if (formData.subject.trim().length < 3) {
|
} 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()) {
|
if (!formData.message.trim()) {
|
||||||
newErrors.message = 'Message is required';
|
newErrors.message = "Message is required";
|
||||||
} else if (formData.message.trim().length < 10) {
|
} 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);
|
setErrors(newErrors);
|
||||||
@@ -66,10 +65,10 @@ const Contact = () => {
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/email', {
|
const response = await fetch("/api/email", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
@@ -81,41 +80,49 @@ const Contact = () => {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
showEmailSent(formData.email);
|
showEmailSent(formData.email);
|
||||||
setFormData({ name: '', email: '', subject: '', message: '' });
|
setFormData({ name: "", email: "", subject: "", message: "" });
|
||||||
setTouched({});
|
setTouched({});
|
||||||
setErrors({});
|
setErrors({});
|
||||||
} else {
|
} else {
|
||||||
const errorData = await response.json();
|
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) {
|
} catch (error) {
|
||||||
console.error('Error sending email:', error);
|
console.error("Error sending email:", error);
|
||||||
showEmailError('Network error. Please check your connection and try again.');
|
showEmailError(
|
||||||
|
"Network error. Please check your connection and try again.",
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
const handleChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
|
) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
[name]: value
|
[name]: value,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear error when user starts typing
|
// Clear error when user starts typing
|
||||||
if (errors[name]) {
|
if (errors[name]) {
|
||||||
setErrors({
|
setErrors({
|
||||||
...errors,
|
...errors,
|
||||||
[name]: ''
|
[name]: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur = (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
const handleBlur = (
|
||||||
|
e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
|
) => {
|
||||||
setTouched({
|
setTouched({
|
||||||
...touched,
|
...touched,
|
||||||
[e.target.name]: true
|
[e.target.name]: true,
|
||||||
});
|
});
|
||||||
validateForm();
|
validateForm();
|
||||||
};
|
};
|
||||||
@@ -123,53 +130,61 @@ const Contact = () => {
|
|||||||
const contactInfo = [
|
const contactInfo = [
|
||||||
{
|
{
|
||||||
icon: Mail,
|
icon: Mail,
|
||||||
title: 'Email',
|
title: "Email",
|
||||||
value: 'contact@dk0.dev',
|
value: "contact@dk0.dev",
|
||||||
href: 'mailto:contact@dk0.dev'
|
href: "mailto:contact@dk0.dev",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: MapPin,
|
icon: MapPin,
|
||||||
title: 'Location',
|
title: "Location",
|
||||||
value: 'Osnabrück, Germany',
|
value: "Osnabrück, Germany",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
<div className="text-center mb-16">
|
<motion.div
|
||||||
<LiquidHeading
|
initial={{ opacity: 0, y: 30 }}
|
||||||
text="Contact Me"
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
level={2}
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
className="text-4xl md:text-5xl font-bold mb-6 text-stone-800"
|
transition={{ duration: 1, ease: [0.25, 0.1, 0.25, 1] }}
|
||||||
/>
|
className="text-center mb-16"
|
||||||
<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!
|
<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>
|
</p>
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||||
{/* Contact Information */}
|
{/* Contact Information */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: -30 }}
|
initial={{ opacity: 0, x: -30 }}
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
transition={{ duration: 0.8 }}
|
transition={{ duration: 1, ease: [0.25, 0.1, 0.25, 1] }}
|
||||||
className="space-y-8"
|
className="space-y-8"
|
||||||
>
|
>
|
||||||
<div>
|
<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
|
Get In Touch
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-stone-600 leading-relaxed">
|
<p className="text-stone-700 leading-relaxed">
|
||||||
I'm always available to discuss new opportunities, interesting projects,
|
I'm always available to discuss new opportunities,
|
||||||
or simply chat about technology and innovation.
|
interesting projects, or simply chat about technology and
|
||||||
|
innovation.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -182,37 +197,50 @@ const Contact = () => {
|
|||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
transition={{
|
||||||
whileHover={{ x: 5 }}
|
duration: 0.8,
|
||||||
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"
|
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">
|
<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" />
|
<info.icon className="w-6 h-6 text-stone-700" />
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<p className="text-stone-500">{info.value}</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.a>
|
</motion.a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Contact Form */}
|
{/* Contact Form */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: 30 }}
|
initial={{ opacity: 0, x: 30 }}
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
transition={{ duration: 0.8 }}
|
transition={{ duration: 1, ease: [0.25, 0.1, 0.25, 1] }}
|
||||||
className="glass-card p-8 rounded-3xl bg-white/40 border border-white/60"
|
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">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<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>
|
Name <span className="text-liquid-rose">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -225,20 +253,29 @@ const Contact = () => {
|
|||||||
required
|
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 ${
|
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
|
errors.name && touched.name
|
||||||
? 'border-red-400 focus:ring-red-400'
|
? "border-red-400 focus:ring-red-400"
|
||||||
: 'border-white/60 focus:ring-liquid-blue focus:border-transparent'
|
: "border-white/60 focus:ring-liquid-blue focus:border-transparent"
|
||||||
}`}
|
}`}
|
||||||
placeholder="Your name"
|
placeholder="Your name"
|
||||||
aria-invalid={errors.name && touched.name ? 'true' : 'false'}
|
aria-invalid={
|
||||||
aria-describedby={errors.name && touched.name ? 'name-error' : undefined}
|
errors.name && touched.name ? "true" : "false"
|
||||||
|
}
|
||||||
|
aria-describedby={
|
||||||
|
errors.name && touched.name ? "name-error" : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{errors.name && touched.name && (
|
{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>
|
||||||
|
|
||||||
<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>
|
Email <span className="text-liquid-rose">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -251,21 +288,30 @@ const Contact = () => {
|
|||||||
required
|
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 ${
|
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
|
errors.email && touched.email
|
||||||
? 'border-red-400 focus:ring-red-400'
|
? "border-red-400 focus:ring-red-400"
|
||||||
: 'border-white/60 focus:ring-liquid-blue focus:border-transparent'
|
: "border-white/60 focus:ring-liquid-blue focus:border-transparent"
|
||||||
}`}
|
}`}
|
||||||
placeholder="your@email.com"
|
placeholder="your@email.com"
|
||||||
aria-invalid={errors.email && touched.email ? 'true' : 'false'}
|
aria-invalid={
|
||||||
aria-describedby={errors.email && touched.email ? 'email-error' : undefined}
|
errors.email && touched.email ? "true" : "false"
|
||||||
|
}
|
||||||
|
aria-describedby={
|
||||||
|
errors.email && touched.email ? "email-error" : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{errors.email && touched.email && (
|
{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>
|
</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>
|
Subject <span className="text-liquid-rose">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -278,20 +324,31 @@ const Contact = () => {
|
|||||||
required
|
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 ${
|
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
|
errors.subject && touched.subject
|
||||||
? 'border-red-400 focus:ring-red-400'
|
? "border-red-400 focus:ring-red-400"
|
||||||
: 'border-white/60 focus:ring-liquid-blue focus:border-transparent'
|
: "border-white/60 focus:ring-liquid-blue focus:border-transparent"
|
||||||
}`}
|
}`}
|
||||||
placeholder="What's this about?"
|
placeholder="What's this about?"
|
||||||
aria-invalid={errors.subject && touched.subject ? 'true' : 'false'}
|
aria-invalid={
|
||||||
aria-describedby={errors.subject && touched.subject ? 'subject-error' : undefined}
|
errors.subject && touched.subject ? "true" : "false"
|
||||||
|
}
|
||||||
|
aria-describedby={
|
||||||
|
errors.subject && touched.subject
|
||||||
|
? "subject-error"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{errors.subject && touched.subject && (
|
{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>
|
||||||
|
|
||||||
<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>
|
Message <span className="text-liquid-rose">*</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -304,16 +361,24 @@ const Contact = () => {
|
|||||||
rows={6}
|
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 ${
|
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
|
errors.message && touched.message
|
||||||
? 'border-red-400 focus:ring-red-400'
|
? "border-red-400 focus:ring-red-400"
|
||||||
: 'border-white/60 focus:ring-liquid-blue focus:border-transparent'
|
: "border-white/60 focus:ring-liquid-blue focus:border-transparent"
|
||||||
}`}
|
}`}
|
||||||
placeholder="Tell me more about your project or question..."
|
placeholder="Tell me more about your project or question..."
|
||||||
aria-invalid={errors.message && touched.message ? 'true' : 'false'}
|
aria-invalid={
|
||||||
aria-describedby={errors.message && touched.message ? 'message-error' : undefined}
|
errors.message && touched.message ? "true" : "false"
|
||||||
|
}
|
||||||
|
aria-describedby={
|
||||||
|
errors.message && touched.message
|
||||||
|
? "message-error"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between items-center mt-1">
|
<div className="flex justify-between items-center mt-1">
|
||||||
{errors.message && touched.message ? (
|
{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>
|
<span></span>
|
||||||
)}
|
)}
|
||||||
@@ -328,7 +393,8 @@ const Contact = () => {
|
|||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
whileHover={!isSubmitting ? { scale: 1.02, y: -2 } : {}}
|
whileHover={!isSubmitting ? { scale: 1.02, y: -2 } : {}}
|
||||||
whileTap={!isSubmitting ? { scale: 0.98 } : {}}
|
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 ? (
|
{isSubmitting ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { Menu, X, Mail } from 'lucide-react';
|
import { Menu, X, Mail } from "lucide-react";
|
||||||
import { SiGithub, SiLinkedin } from 'react-icons/si';
|
import { SiGithub, SiLinkedin } from "react-icons/si";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -20,21 +20,25 @@ const Header = () => {
|
|||||||
setScrolled(window.scrollY > 50);
|
setScrolled(window.scrollY > 50);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
window.addEventListener("scroll", handleScroll);
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ name: 'Home', href: '/' },
|
{ name: "Home", href: "/" },
|
||||||
{ name: 'About', href: '#about' },
|
{ name: "About", href: "#about" },
|
||||||
{ name: 'Projects', href: '#projects' },
|
{ name: "Projects", href: "#projects" },
|
||||||
{ name: 'Contact', href: '#contact' },
|
{ name: "Contact", href: "#contact" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const socialLinks = [
|
const socialLinks = [
|
||||||
{ icon: SiGithub, href: 'https://github.com/Denshooter', label: 'GitHub' },
|
{ 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: SiLinkedin,
|
||||||
|
href: "https://linkedin.com/in/dkonkol",
|
||||||
|
label: "LinkedIn",
|
||||||
|
},
|
||||||
|
{ icon: Mail, href: "mailto:contact@dk0.dev", label: "Email" },
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
@@ -49,22 +53,30 @@ const Header = () => {
|
|||||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||||
className="fixed top-6 left-0 right-0 z-50 flex justify-center px-4 pointer-events-none"
|
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 ${
|
<div
|
||||||
scrolled ? 'w-full max-w-5xl' : 'w-full max-w-7xl'
|
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={`
|
||||||
backdrop-blur-xl transition-all duration-500
|
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'
|
scrolled
|
||||||
: 'bg-white/85 border border-stone-200 shadow-[0_4px_24px_rgba(0,0,0,0.08)] px-4 py-4 rounded-full'
|
? "bg-white/95 border border-stone-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
|
flex justify-between items-center
|
||||||
`}>
|
`}
|
||||||
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
className="flex items-center space-x-2"
|
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>
|
dk<span className="text-liquid-rose">0</span>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -80,11 +92,14 @@ const Header = () => {
|
|||||||
href={item.href}
|
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"
|
className="text-stone-600 hover:text-stone-900 transition-colors duration-200 font-medium relative group px-2 py-1 liquid-hover"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (item.href.startsWith('#')) {
|
if (item.href.startsWith("#")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const element = document.querySelector(item.href);
|
const element = document.querySelector(item.href);
|
||||||
if (element) {
|
if (element) {
|
||||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
element.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "start",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -153,12 +168,15 @@ const Header = () => {
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
if (item.href.startsWith('#')) {
|
if (item.href.startsWith("#")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const element = document.querySelector(item.href);
|
const element = document.querySelector(item.href);
|
||||||
if (element) {
|
if (element) {
|
||||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
element.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
block: "start",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
@@ -180,7 +198,9 @@ const Header = () => {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
initial={{ scale: 0, opacity: 0 }}
|
initial={{ scale: 0, opacity: 0 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
transition={{ delay: (navItems.length + index) * 0.05 }}
|
transition={{
|
||||||
|
delay: (navItems.length + index) * 0.05,
|
||||||
|
}}
|
||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
className="p-3 rounded-full bg-white/60 text-stone-600 shadow-sm"
|
className="p-3 rounded-full bg-white/60 text-stone-600 shadow-sm"
|
||||||
aria-label={social.label}
|
aria-label={social.label}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import { ArrowDown, Code, Zap, Rocket } from 'lucide-react';
|
import { ArrowDown, Code, Zap, Rocket } from "lucide-react";
|
||||||
import Image from 'next/image';
|
import Image from "next/image";
|
||||||
import { LiquidHeading } from '@/components/LiquidHeading';
|
|
||||||
|
|
||||||
const Hero = () => {
|
const Hero = () => {
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
@@ -14,97 +13,136 @@ const Hero = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
{ icon: Code, text: 'Full-Stack Development' },
|
{ icon: Code, text: "Next.js & Flutter" },
|
||||||
{ icon: Zap, text: 'Modern Technologies' },
|
{ icon: Zap, text: "Docker Swarm & CI/CD" },
|
||||||
{ icon: Rocket, text: 'Innovative Solutions' },
|
{ icon: Rocket, text: "Self-Hosted Infrastructure" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Smooth scroll configuration
|
||||||
|
const smoothTransition = {
|
||||||
|
type: "spring",
|
||||||
|
damping: 30,
|
||||||
|
stiffness: 50,
|
||||||
|
mass: 1,
|
||||||
|
};
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<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 */}
|
{/* Profile Image with Organic Blob Mask */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 1, delay: 0.7, ease: "easeOut" }}
|
transition={{ duration: 1.2, delay: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
|
||||||
className="mb-10 flex justify-center relative z-20"
|
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">
|
<div className="relative w-64 h-64 md:w-80 md:h-80 flex items-center justify-center">
|
||||||
{/* Large Rotating Liquid Blobs behind image */}
|
{/* Large Rotating Liquid Blobs behind image - Very slow and smooth */}
|
||||||
<motion.div
|
<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"
|
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={{
|
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%"],
|
borderRadius: [
|
||||||
rotate: [0, 180, 360],
|
"60% 40% 30% 70%/60% 30% 70% 40%",
|
||||||
scale: [1, 1.1, 1]
|
"30% 60% 70% 40%/50% 60% 30% 60%",
|
||||||
}}
|
"60% 40% 30% 70%/60% 30% 70% 40%",
|
||||||
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
|
],
|
||||||
|
rotate: [0, 120, 0],
|
||||||
|
scale: [1, 1.08, 1],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 35,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut",
|
||||||
|
repeatType: "reverse",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<motion.div
|
<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"
|
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={{
|
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%"],
|
borderRadius: [
|
||||||
rotate: [360, 180, 0],
|
"40% 60% 70% 30%/40% 50% 60% 50%",
|
||||||
scale: [1, 0.9, 1]
|
"60% 30% 40% 70%/60% 40% 70% 30%",
|
||||||
}}
|
"40% 60% 70% 30%/40% 50% 60% 50%",
|
||||||
transition={{ duration: 15, repeat: Infinity, ease: "linear" }}
|
],
|
||||||
|
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 - No hard border, just shadow and glass */}
|
{/* The Image Container with Organic Border Radius */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute inset-0 overflow-hidden shadow-2xl bg-stone-100"
|
className="absolute inset-0 overflow-hidden bg-stone-100"
|
||||||
style={{ filter: "drop-shadow(0 20px 40px rgba(0,0,0,0.15))" }}
|
style={{
|
||||||
animate={{
|
filter: "drop-shadow(0 20px 40px rgba(0,0,0,0.1))",
|
||||||
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%"]
|
willChange: "border-radius",
|
||||||
}}
|
}}
|
||||||
transition={{ duration: 8, repeat: Infinity, ease: "easeInOut" }}
|
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
|
<Image
|
||||||
src="/images/me.jpg"
|
src="/images/me.jpg"
|
||||||
alt="Dennis Konkol"
|
alt="Dennis Konkol"
|
||||||
fill
|
fill
|
||||||
className="object-cover scale-105 hover:scale-110 transition-transform duration-700"
|
className="object-cover scale-105 hover:scale-[1.08] transition-transform duration-1000 ease-out"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Glossy Overlay for Liquid Feel */}
|
{/* 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" />
|
<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 */}
|
{/* Inner Border/Highlight */}
|
||||||
<div className="absolute inset-0 border-[3px] border-white/20 rounded-[inherit] pointer-events-none z-20" />
|
<div className="absolute inset-0 border-[2px] border-white/30 rounded-[inherit] pointer-events-none z-20" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Floating Badges */}
|
{/* Domain Badge - repositioned below image */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ scale: 1 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 1.5, type: "spring" }}
|
transition={{ duration: 1, delay: 0.8, ease: "easeOut" }}
|
||||||
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"
|
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} />
|
<Code size={24} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0 }}
|
initial={{ scale: 0, opacity: 0 }}
|
||||||
animate={{ scale: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
transition={{ delay: 1.7, type: "spring" }}
|
transition={{ delay: 1.4, duration: 0.8, ease: "easeOut" }}
|
||||||
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"
|
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} />
|
<Zap size={24} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -112,64 +150,84 @@ const Hero = () => {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Main Title */}
|
{/* Main Title */}
|
||||||
<div className="mb-6 flex flex-col items-center justify-center relative">
|
<motion.div
|
||||||
<LiquidHeading
|
initial={{ opacity: 0, y: 20 }}
|
||||||
text="Dennis Konkol"
|
animate={{ opacity: 1, y: 0 }}
|
||||||
level={1}
|
transition={{ duration: 1, delay: 0.6, ease: [0.25, 0.1, 0.25, 1] }}
|
||||||
className="text-5xl md:text-8xl font-bold tracking-tighter text-stone-800"
|
className="mb-8 flex flex-col items-center justify-center relative"
|
||||||
/>
|
>
|
||||||
<LiquidHeading
|
<h1 className="text-5xl md:text-8xl font-bold tracking-tighter text-stone-900 mb-2">
|
||||||
text="Software Engineer"
|
Dennis Konkol
|
||||||
level={2}
|
</h1>
|
||||||
className="text-2xl md:text-4xl font-light tracking-wide text-stone-500 mt-2"
|
<h2 className="text-2xl md:text-4xl font-light tracking-wide text-stone-600 mt-2">
|
||||||
/>
|
Software Engineer
|
||||||
</div>
|
</h2>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<motion.p
|
<motion.p
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.8, delay: 1.2 }}
|
transition={{ duration: 1, delay: 0.9, ease: [0.25, 0.1, 0.25, 1] }}
|
||||||
className="text-lg md:text-xl text-stone-600 mb-12 max-w-2xl mx-auto leading-relaxed"
|
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>
|
</motion.p>
|
||||||
|
|
||||||
{/* Features */}
|
{/* Features */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
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"
|
className="flex flex-wrap justify-center gap-4 mb-12"
|
||||||
>
|
>
|
||||||
{features.map((feature, index) => (
|
{features.map((feature, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={feature.text}
|
key={feature.text}
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.5, delay: 1.6 + index * 0.1 }}
|
transition={{
|
||||||
whileHover={{ scale: 1.05, y: -2 }}
|
duration: 0.8,
|
||||||
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"
|
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" />
|
<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>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* CTA Buttons */}
|
{/* CTA Buttons */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
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"
|
className="flex flex-col sm:flex-row gap-5 justify-center items-center"
|
||||||
>
|
>
|
||||||
<motion.a
|
<motion.a
|
||||||
href="#projects"
|
href="#projects"
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.03, y: -2 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
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"
|
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>
|
<span>View My Work</span>
|
||||||
<ArrowDown size={18} />
|
<ArrowDown size={18} />
|
||||||
@@ -177,9 +235,10 @@ const Hero = () => {
|
|||||||
|
|
||||||
<motion.a
|
<motion.a
|
||||||
href="#contact"
|
href="#contact"
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.03, y: -2 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
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"
|
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>
|
<span>Contact Me</span>
|
||||||
</motion.a>
|
</motion.a>
|
||||||
|
|||||||
@@ -1,11 +1,42 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from "framer-motion";
|
||||||
import { ExternalLink, Github, Calendar, Layers, ArrowRight } from 'lucide-react';
|
import {
|
||||||
import Link from 'next/link';
|
ExternalLink,
|
||||||
import { LiquidHeading } from '@/components/LiquidHeading';
|
Github,
|
||||||
import Image from 'next/image';
|
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 {
|
interface Project {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -29,13 +60,15 @@ const Projects = () => {
|
|||||||
setMounted(true);
|
setMounted(true);
|
||||||
const loadProjects = async () => {
|
const loadProjects = async () => {
|
||||||
try {
|
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) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setProjects(data.projects || []);
|
setProjects(data.projects || []);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading projects:', error);
|
console.error("Error loading projects:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadProjects();
|
loadProjects();
|
||||||
@@ -44,67 +77,93 @@ const Projects = () => {
|
|||||||
if (!mounted) return null;
|
if (!mounted) return null;
|
||||||
|
|
||||||
return (
|
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="max-w-7xl mx-auto">
|
||||||
<div className="text-center mb-20">
|
<motion.div
|
||||||
<LiquidHeading
|
initial="hidden"
|
||||||
text="Selected Works"
|
whileInView="visible"
|
||||||
level={2}
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
className="text-4xl md:text-6xl font-bold mb-6 text-stone-900"
|
variants={fadeInUp}
|
||||||
/>
|
className="text-center mb-20"
|
||||||
<p className="text-lg text-stone-500 max-w-2xl mx-auto mt-4 font-light">
|
>
|
||||||
A collection of projects I've worked on, ranging from web applications to experiments.
|
<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've worked on, ranging from web
|
||||||
|
applications to experiments.
|
||||||
</p>
|
</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) => (
|
{projects.map((project, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={project.id}
|
key={project.id}
|
||||||
initial={{ opacity: 0, y: 20 }}
|
variants={fadeInUp}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileHover={{
|
||||||
viewport={{ once: true }}
|
y: -12,
|
||||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
transition: { duration: 0.5, ease: "easeOut" },
|
||||||
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"
|
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 */}
|
{/* 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 ? (
|
{project.imageUrl ? (
|
||||||
<Image
|
<Image
|
||||||
src={project.imageUrl}
|
src={project.imageUrl}
|
||||||
alt={project.title}
|
alt={project.title}
|
||||||
fill
|
fill
|
||||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
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="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">
|
<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" />
|
<Layers className="text-stone-300 w-12 h-12" />
|
||||||
</div>
|
</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-tr from-liquid-mint/10 via-transparent to-liquid-rose/10 opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Overlay Links */}
|
{/* 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]">
|
<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 && (
|
{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">
|
<a
|
||||||
<Github size={20} />
|
href={project.github}
|
||||||
</a>
|
target="_blank"
|
||||||
)}
|
rel="noopener noreferrer"
|
||||||
{project.live && (
|
className="p-3 bg-white rounded-full text-stone-900 hover:scale-110 transition-all duration-500 ease-out hover:shadow-lg"
|
||||||
<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">
|
aria-label="GitHub"
|
||||||
<ExternalLink size={20} />
|
>
|
||||||
</a>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex flex-col flex-1 p-6">
|
<div className="flex flex-col flex-1 p-6">
|
||||||
<div className="flex justify-between items-start mb-3">
|
<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}
|
{project.title}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="text-xs font-mono text-stone-400 bg-stone-100 px-2 py-1 rounded">
|
<span className="text-xs font-mono text-stone-400 bg-stone-100 px-2 py-1 rounded">
|
||||||
@@ -112,42 +171,57 @@ const Projects = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</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}
|
{project.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-4 mt-auto">
|
<div className="space-y-4 mt-auto">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{project.tags.slice(0, 3).map(tag => (
|
{project.tags.slice(0, 3).map((tag, tIdx) => (
|
||||||
<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">
|
<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}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{project.tags.length > 3 && (
|
{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>
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={`/projects/${project.title.toLowerCase().replace(/\s+/g, '-')}`}
|
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"
|
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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.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
|
<Link
|
||||||
href="/projects"
|
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>
|
</Link>
|
||||||
</div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
240
app/components/admin/AIImageGenerator.tsx
Normal file
240
app/components/admin/AIImageGenerator.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
189
app/globals.css
189
app/globals.css
@@ -2,152 +2,181 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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 {
|
:root {
|
||||||
/* Organic Modern Palette */
|
/* Organic Modern Palette */
|
||||||
--background: #FDFCF8; /* Cream */
|
--background: #fdfcf8; /* Cream */
|
||||||
--foreground: #292524; /* Warm Grey */
|
--foreground: #292524; /* Warm Grey */
|
||||||
--card: rgba(255, 255, 255, 0.6);
|
--card: rgba(255, 255, 255, 0.6);
|
||||||
--card-foreground: #292524;
|
--card-foreground: #292524;
|
||||||
--popover: #FFFFFF;
|
--popover: #ffffff;
|
||||||
--popover-foreground: #292524;
|
--popover-foreground: #292524;
|
||||||
--primary: #292524;
|
--primary: #292524;
|
||||||
--primary-foreground: #FDFCF8;
|
--primary-foreground: #fdfcf8;
|
||||||
--secondary: #E7E5E4;
|
--secondary: #e7e5e4;
|
||||||
--secondary-foreground: #292524;
|
--secondary-foreground: #292524;
|
||||||
--muted: #F5F5F4;
|
--muted: #f5f5f4;
|
||||||
--muted-foreground: #78716C;
|
--muted-foreground: #78716c;
|
||||||
--accent: #F3F1E7; /* Sand */
|
--accent: #f3f1e7; /* Sand */
|
||||||
--accent-foreground: #292524;
|
--accent-foreground: #292524;
|
||||||
--destructive: #EF4444;
|
--destructive: #ef4444;
|
||||||
--destructive-foreground: #FDFCF8;
|
--destructive-foreground: #fdfcf8;
|
||||||
--border: #E7E5E4;
|
--border: #e7e5e4;
|
||||||
--input: #E7E5E4;
|
--input: #e7e5e4;
|
||||||
--ring: #A7F3D0; /* Mint ring */
|
--ring: #a7f3d0; /* Mint ring */
|
||||||
--radius: 1rem;
|
--radius: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom Selection */
|
/* Custom Selection */
|
||||||
::selection {
|
::selection {
|
||||||
background: #A7F3D0; /* Mint */
|
background: #a7f3d0; /* Mint */
|
||||||
color: #292524;
|
color: #292524;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smooth Scrolling */
|
/* Smooth Scrolling */
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Liquid Glass Effects */
|
/* Liquid Glass Effects */
|
||||||
.glass-panel {
|
.glass-panel {
|
||||||
background: rgba(255, 255, 255, 0.3);
|
background: rgba(255, 255, 255, 0.4);
|
||||||
backdrop-filter: blur(12px) saturate(120%);
|
backdrop-filter: blur(12px) saturate(120%);
|
||||||
-webkit-backdrop-filter: blur(12px) saturate(120%);
|
-webkit-backdrop-filter: blur(12px) saturate(120%);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||||||
|
will-change: backdrop-filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-card {
|
.glass-card {
|
||||||
background: rgba(255, 255, 255, 0.65);
|
background: rgba(255, 255, 255, 0.7);
|
||||||
backdrop-filter: blur(24px) saturate(180%);
|
backdrop-filter: blur(24px) saturate(180%);
|
||||||
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
border: 1px solid rgba(255, 255, 255, 0.85);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 4px 6px -1px rgba(0, 0, 0, 0.02),
|
0 4px 6px -1px rgba(0, 0, 0, 0.03),
|
||||||
0 2px 4px -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);
|
inset 0 0 20px rgba(255, 255, 255, 0.5);
|
||||||
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
transition: all 0.6s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||||
|
will-change: transform, box-shadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-card:hover {
|
.glass-card:hover {
|
||||||
background: rgba(255, 255, 255, 0.75);
|
background: rgba(255, 255, 255, 0.8);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 20px 25px -5px rgba(0, 0, 0, 0.05),
|
0 20px 25px -5px rgba(0, 0, 0, 0.08),
|
||||||
0 10px 10px -5px rgba(0, 0, 0, 0.01),
|
0 10px 10px -5px rgba(0, 0, 0, 0.02),
|
||||||
inset 0 0 20px rgba(255, 255, 255, 0.8);
|
inset 0 0 20px rgba(255, 255, 255, 0.8);
|
||||||
transform: translateY(-4px) scale(1.005);
|
transform: translateY(-4px) scale(1.005);
|
||||||
border-color: #ffffff;
|
border-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Typography & Headings */
|
/* Typography & Headings */
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1,
|
||||||
letter-spacing: -0.02em;
|
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 */
|
/* Utility for the liquid melt effect container */
|
||||||
.liquid-container {
|
/* Liquid container removed - no filters applied */
|
||||||
filter: url('#goo');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide scrollbar but keep functionality */
|
/* Hide scrollbar but keep functionality */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #D6D3D1;
|
background: #d6d3d1;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #A8A29E;
|
background: #a8a29e;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
0%, 100% { transform: translateY(0px); }
|
0%,
|
||||||
50% { transform: translateY(-10px); }
|
100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-float {
|
.animate-float {
|
||||||
animation: float 5s ease-in-out infinite;
|
animation: float 8s ease-in-out infinite;
|
||||||
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes liquid-pulse {
|
@keyframes liquid-pulse {
|
||||||
0% { transform: scale(1); }
|
0% {
|
||||||
50% { transform: scale(1.05); }
|
transform: scale(1);
|
||||||
100% { transform: scale(1); }
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Liquid Blobs Background */
|
/* Liquid Blobs Background */
|
||||||
.liquid-bg-blob {
|
.liquid-bg-blob {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
filter: blur(80px);
|
filter: blur(80px);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
animation: float 10s ease-in-out infinite;
|
animation: float 10s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Markdown Specifics for Blog/Projects */
|
/* Markdown Specifics for Blog/Projects */
|
||||||
.markdown h1 {
|
.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 {
|
.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 {
|
.markdown p {
|
||||||
@apply mb-4 leading-relaxed text-stone-600;
|
@apply mb-4 leading-relaxed text-stone-700;
|
||||||
}
|
}
|
||||||
.markdown a {
|
.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 {
|
.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 {
|
.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 {
|
.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;
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,6 @@ import React from "react";
|
|||||||
import { ToastProvider } from "@/components/Toast";
|
import { ToastProvider } from "@/components/Toast";
|
||||||
import { AnalyticsProvider } from "@/components/AnalyticsProvider";
|
import { AnalyticsProvider } from "@/components/AnalyticsProvider";
|
||||||
import { PerformanceDashboard } from "@/components/PerformanceDashboard";
|
import { PerformanceDashboard } from "@/components/PerformanceDashboard";
|
||||||
import { GooFilter } from "@/components/GooFilter";
|
|
||||||
import { BackgroundBlobs } from "@/components/BackgroundBlobs";
|
import { BackgroundBlobs } from "@/components/BackgroundBlobs";
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
@@ -21,18 +20,19 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<script defer src="https://analytics.dk0.dev/script.js" data-website-id="b3665829-927a-4ada-b9bb-fcf24171061e"></script>
|
<script
|
||||||
<meta charSet="utf-8"/>
|
defer
|
||||||
|
src="https://analytics.dk0.dev/script.js"
|
||||||
|
data-website-id="b3665829-927a-4ada-b9bb-fcf24171061e"
|
||||||
|
></script>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
<title>Dennis Konkol's Portfolio</title>
|
<title>Dennis Konkol's Portfolio</title>
|
||||||
</head>
|
</head>
|
||||||
<body className={inter.variable}>
|
<body className={inter.variable}>
|
||||||
<AnalyticsProvider>
|
<AnalyticsProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<GooFilter />
|
|
||||||
<BackgroundBlobs />
|
<BackgroundBlobs />
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
<PerformanceDashboard />
|
<PerformanceDashboard />
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</AnalyticsProvider>
|
</AnalyticsProvider>
|
||||||
@@ -43,12 +43,14 @@ export default function RootLayout({
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Dennis Konkol | Portfolio",
|
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"],
|
keywords: ["Dennis Konkol", "Software Engineer", "Portfolio", "Student"],
|
||||||
authors: [{name: "Dennis Konkol", url: "https://dk0.dev"}],
|
authors: [{ name: "Dennis Konkol", url: "https://dk0.dev" }],
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: "Dennis Konkol | Portfolio",
|
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",
|
url: "https://dk0.dev",
|
||||||
siteName: "Dennis Konkol Portfolio",
|
siteName: "Dennis Konkol Portfolio",
|
||||||
images: [
|
images: [
|
||||||
|
|||||||
105
app/page.tsx
105
app/page.tsx
@@ -8,6 +8,7 @@ import Contact from "./components/Contact";
|
|||||||
import Footer from "./components/Footer";
|
import Footer from "./components/Footer";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import { ActivityFeed } from "./components/ActivityFeed";
|
import { ActivityFeed } from "./components/ActivityFeed";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
@@ -36,10 +37,114 @@ export default function Home() {
|
|||||||
/>
|
/>
|
||||||
<ActivityFeed />
|
<ActivityFeed />
|
||||||
<Header />
|
<Header />
|
||||||
|
{/* Spacer to prevent navbar overlap */}
|
||||||
|
<div className="h-24 md:h-32" aria-hidden="true"></div>
|
||||||
<main className="relative">
|
<main className="relative">
|
||||||
<Hero />
|
<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 />
|
<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 />
|
<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 />
|
<Contact />
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -1,42 +1,47 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { motion, useMotionValue, useTransform, useSpring } from 'framer-motion';
|
import { motion, useMotionValue, useTransform, useSpring } from "framer-motion";
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const BackgroundBlobs = () => {
|
export const BackgroundBlobs = () => {
|
||||||
const mouseX = useMotionValue(0);
|
const mouseX = useMotionValue(0);
|
||||||
const mouseY = useMotionValue(0);
|
const mouseY = useMotionValue(0);
|
||||||
|
|
||||||
const springConfig = { damping: 50, stiffness: 200 };
|
const springConfig = { damping: 50, stiffness: 50, mass: 2 };
|
||||||
const springX = useSpring(mouseX, springConfig);
|
const springX = useSpring(mouseX, springConfig);
|
||||||
const springY = useSpring(mouseY, springConfig);
|
const springY = useSpring(mouseY, springConfig);
|
||||||
|
|
||||||
// Parallax offsets
|
// Very subtle parallax offsets
|
||||||
const x1 = useTransform(springX, (value) => value / 20);
|
const x1 = useTransform(springX, (value) => value / 30);
|
||||||
const y1 = useTransform(springY, (value) => value / 20);
|
const y1 = useTransform(springY, (value) => value / 30);
|
||||||
|
|
||||||
const x2 = useTransform(springX, (value) => value / -15);
|
const x2 = useTransform(springX, (value) => value / -25);
|
||||||
const y2 = useTransform(springY, (value) => value / -15);
|
const y2 = useTransform(springY, (value) => value / -25);
|
||||||
|
|
||||||
const x3 = useTransform(springX, (value) => value / 10);
|
const x3 = useTransform(springX, (value) => value / 20);
|
||||||
const y3 = useTransform(springY, (value) => value / 10);
|
const y3 = useTransform(springY, (value) => value / 20);
|
||||||
|
|
||||||
|
const x4 = useTransform(springX, (value) => value / -35);
|
||||||
|
const y4 = useTransform(springY, (value) => value / -35);
|
||||||
|
|
||||||
|
const x5 = useTransform(springX, (value) => value / 15);
|
||||||
|
const y5 = useTransform(springY, (value) => value / 15);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
// Center the coordinate system
|
const x = e.clientX - window.innerWidth / 2;
|
||||||
const { innerWidth, innerHeight } = window;
|
const y = e.clientY - window.innerHeight / 2;
|
||||||
const x = e.clientX - innerWidth / 2;
|
|
||||||
const y = e.clientY - innerHeight / 2;
|
|
||||||
mouseX.set(x);
|
mouseX.set(x);
|
||||||
mouseY.set(y);
|
mouseY.set(y);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('mousemove', handleMouseMove);
|
window.addEventListener("mousemove", handleMouseMove);
|
||||||
return () => window.removeEventListener('mousemove', handleMouseMove);
|
return () => window.removeEventListener("mousemove", handleMouseMove);
|
||||||
}, [mouseX, mouseY]);
|
}, [mouseX, mouseY]);
|
||||||
|
|
||||||
// Prevent hydration mismatch
|
// Prevent hydration mismatch
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -44,34 +49,120 @@ export const BackgroundBlobs = () => {
|
|||||||
if (!mounted) return null;
|
if (!mounted) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 overflow-hidden pointer-events-none z-0 liquid-container">
|
<div className="fixed inset-0 overflow-hidden pointer-events-none z-0">
|
||||||
<motion.div
|
{/* Mint blob - top left */}
|
||||||
className="absolute top-[-10%] left-[-10%] w-[40vw] h-[40vw] bg-liquid-mint/40 rounded-full blur-[80px] mix-blend-multiply opacity-70"
|
<motion.div
|
||||||
style={{ x: x1, y: y1 }}
|
className="absolute top-[-10%] left-[-10%] w-[40vw] h-[40vw] bg-liquid-mint/40 rounded-full blur-[100px] mix-blend-multiply"
|
||||||
animate={{
|
style={{ x: x1, y: y1 }}
|
||||||
scale: [1, 1.2, 1],
|
animate={{
|
||||||
rotate: [0, 90, 0],
|
scale: [1, 1.15, 1],
|
||||||
}}
|
rotate: [0, 45, 0],
|
||||||
transition={{ duration: 25, repeat: Infinity, ease: "linear" }}
|
}}
|
||||||
/>
|
transition={{
|
||||||
<motion.div
|
duration: 40,
|
||||||
className="absolute top-[20%] right-[-10%] w-[35vw] h-[35vw] bg-liquid-lavender/40 rounded-full blur-[80px] mix-blend-multiply opacity-70"
|
repeat: Infinity,
|
||||||
style={{ x: x2, y: y2 }}
|
ease: "easeInOut",
|
||||||
animate={{
|
repeatType: "reverse",
|
||||||
scale: [1, 1.1, 1],
|
}}
|
||||||
rotate: [0, -60, 0],
|
/>
|
||||||
}}
|
|
||||||
transition={{ duration: 30, repeat: Infinity, ease: "linear" }}
|
{/* Lavender blob - top right */}
|
||||||
/>
|
<motion.div
|
||||||
<motion.div
|
className="absolute top-[10%] right-[-5%] w-[35vw] h-[35vw] bg-liquid-lavender/35 rounded-full blur-[100px] mix-blend-multiply"
|
||||||
className="absolute bottom-[-10%] left-[20%] w-[45vw] h-[45vw] bg-liquid-rose/30 rounded-full blur-[80px] mix-blend-multiply opacity-70"
|
style={{ x: x2, y: y2 }}
|
||||||
style={{ x: x3, y: y3 }}
|
animate={{
|
||||||
animate={{
|
scale: [1, 1.1, 1],
|
||||||
scale: [1, 1.3, 1],
|
rotate: [0, -30, 0],
|
||||||
rotate: [0, 45, 0]
|
}}
|
||||||
}}
|
transition={{
|
||||||
transition={{ duration: 35, repeat: Infinity, ease: "linear" }}
|
duration: 45,
|
||||||
/>
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut",
|
||||||
|
repeatType: "reverse",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Rose blob - bottom left */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute bottom-[-5%] left-[15%] w-[45vw] h-[45vw] bg-liquid-rose/35 rounded-full blur-[100px] mix-blend-multiply"
|
||||||
|
style={{ x: x3, y: y3 }}
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.2, 1],
|
||||||
|
rotate: [0, 60, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 50,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut",
|
||||||
|
repeatType: "reverse",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Peach blob - middle right */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-[40%] right-[10%] w-[30vw] h-[30vw] bg-orange-200/30 rounded-full blur-[120px] mix-blend-multiply"
|
||||||
|
style={{ x: x4, y: y4 }}
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.25, 1],
|
||||||
|
rotate: [0, -45, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 55,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut",
|
||||||
|
repeatType: "reverse",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Blue blob - center */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-[50%] left-[40%] w-[38vw] h-[38vw] bg-blue-200/30 rounded-full blur-[110px] mix-blend-multiply"
|
||||||
|
style={{ x: x5, y: y5 }}
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.18, 1],
|
||||||
|
rotate: [0, 90, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 48,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut",
|
||||||
|
repeatType: "reverse",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Pink blob - bottom right */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute bottom-[10%] right-[-8%] w-[32vw] h-[32vw] bg-pink-200/35 rounded-full blur-[100px] mix-blend-multiply"
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.12, 1],
|
||||||
|
rotate: [0, -60, 0],
|
||||||
|
x: [0, -20, 0],
|
||||||
|
y: [0, 20, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 43,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut",
|
||||||
|
repeatType: "reverse",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Yellow-green blob - top center */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-[5%] left-[45%] w-[28vw] h-[28vw] bg-lime-200/30 rounded-full blur-[115px] mix-blend-multiply"
|
||||||
|
animate={{
|
||||||
|
scale: [1, 1.22, 1],
|
||||||
|
rotate: [0, 75, 0],
|
||||||
|
x: [0, 15, 0],
|
||||||
|
y: [0, -15, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 52,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut",
|
||||||
|
repeatType: "reverse",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
export const GooFilter = () => (
|
|
||||||
<svg
|
|
||||||
style={{ position: 'fixed', top: 0, left: 0, width: 0, height: 0, pointerEvents: 'none', zIndex: -1 }}
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
version="1.1"
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
{/* Global subtle filter */}
|
|
||||||
<filter id="goo">
|
|
||||||
<feGaussianBlur in="SourceGraphic" stdDeviation="2" result="blur" />
|
|
||||||
<feColorMatrix
|
|
||||||
in="blur"
|
|
||||||
mode="matrix"
|
|
||||||
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -9"
|
|
||||||
result="goo"
|
|
||||||
/>
|
|
||||||
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
|
|
||||||
</filter>
|
|
||||||
|
|
||||||
{/* Stronger filter specifically for LiquidHeading interaction */}
|
|
||||||
<filter id="goo-text">
|
|
||||||
<feGaussianBlur in="SourceGraphic" stdDeviation="2" result="blur" />
|
|
||||||
<feColorMatrix
|
|
||||||
in="blur"
|
|
||||||
mode="matrix"
|
|
||||||
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 25 -10"
|
|
||||||
result="goo"
|
|
||||||
/>
|
|
||||||
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
interface LiquidHeadingProps {
|
|
||||||
text: string;
|
|
||||||
className?: string;
|
|
||||||
level?: 1 | 2 | 3 | 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LiquidHeading = ({ text, className, level = 1 }: LiquidHeadingProps) => {
|
|
||||||
const Tag = `h${level}` as keyof JSX.IntrinsicElements;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tag className={clsx("font-bold tracking-tight text-stone-800", className)}>
|
|
||||||
{text}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
460
docs/ACTIVITY_FEATURES.md
Normal file
460
docs/ACTIVITY_FEATURES.md
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
# 🎨 Activity Feed Features & Animations
|
||||||
|
|
||||||
|
## ✨ Implementierte Features
|
||||||
|
|
||||||
|
### 1. **Dynamische Activity Bubbles**
|
||||||
|
Jede Aktivität hat ihre eigene:
|
||||||
|
- 🎨 Einzigartige Pastellfarben
|
||||||
|
- 🎭 Spezifische Animationen
|
||||||
|
- 🔗 Interaktive Links
|
||||||
|
- 💫 Hintergrundeffekte
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎬 Animations-Typen
|
||||||
|
|
||||||
|
### 🔨 Coding Activity
|
||||||
|
**Visueller Effekt:**
|
||||||
|
- **Matrix Rain** im Hintergrund (grüne 0/1 Zahlen fallen)
|
||||||
|
- Rotierendes Terminal-Icon
|
||||||
|
- Grüner Pulsing-Dot
|
||||||
|
|
||||||
|
**Daten:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "coding",
|
||||||
|
"details": "Building Portfolio Website",
|
||||||
|
"project": "portfolio",
|
||||||
|
"language": "TypeScript",
|
||||||
|
"repo": "https://github.com/user/repo",
|
||||||
|
"link": "https://github.com/user/repo/commit/abc123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Animation:**
|
||||||
|
- 15 vertikal fallende Spalten mit 0/1 Zeichen
|
||||||
|
- Unterschiedliche Geschwindigkeiten (2-5s)
|
||||||
|
- Opacity fade in/out
|
||||||
|
- Mint-grüne Farbe (liquid-mint)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎵 Music Activity (Now Playing)
|
||||||
|
**Visueller Effekt:**
|
||||||
|
- **Sound Waves** (5 animierte Balken)
|
||||||
|
- Rotierendes Album Cover (10s pro Rotation)
|
||||||
|
- Pulsierendes Headphone-Icon
|
||||||
|
- Progress Bar
|
||||||
|
|
||||||
|
**Daten:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"isPlaying": true,
|
||||||
|
"track": "Song Title",
|
||||||
|
"artist": "Artist Name",
|
||||||
|
"album": "Album Name",
|
||||||
|
"progress": 45,
|
||||||
|
"albumArt": "https://url-to-image.jpg",
|
||||||
|
"spotifyUrl": "https://open.spotify.com/track/..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- ✅ Link "Listen with me" → Spotify Track
|
||||||
|
- ✅ Live Progress Bar (0-100%)
|
||||||
|
- ✅ Verschwindet automatisch wenn Musik stoppt
|
||||||
|
- ✅ Album Cover rotiert kontinuierlich
|
||||||
|
|
||||||
|
**Animation:**
|
||||||
|
- 5 vertikale Balken bewegen sich wellenförmig (20-80% Höhe)
|
||||||
|
- Jeder Balken 0.1s delay
|
||||||
|
- 0.8s Animationsdauer
|
||||||
|
- Rose/Coral Gradient
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🏃 Running Activity
|
||||||
|
**Visueller Effekt:**
|
||||||
|
- **Animierter Läufer-Emoji** (🏃) bewegt sich von links nach rechts
|
||||||
|
- Horizontale "Laufbahn" als Linie
|
||||||
|
- Lime-grüne Farbpalette
|
||||||
|
|
||||||
|
**Daten:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "running",
|
||||||
|
"details": "Morning run - 5km",
|
||||||
|
"link": "https://strava.com/activities/..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Animation:**
|
||||||
|
- Läufer bewegt sich linear von -10% bis 110% (3s)
|
||||||
|
- Kontinuierliche Wiederholung
|
||||||
|
- Unendlich Loop
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎮 Gaming Activity
|
||||||
|
**Visueller Effekt:**
|
||||||
|
- **Particle System** (10 schwebende Partikel)
|
||||||
|
- Peach/Orange Farbschema
|
||||||
|
- Gamepad-Icon
|
||||||
|
|
||||||
|
**Daten:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"game": "Elden Ring",
|
||||||
|
"platform": "steam",
|
||||||
|
"status": "playing"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Animation:**
|
||||||
|
- 10 Partikel an zufälligen Positionen
|
||||||
|
- Scale animation (0 → 1 → 0)
|
||||||
|
- Opacity fade
|
||||||
|
- Unterschiedliche Delays (0-2s)
|
||||||
|
- 2s Gesamtdauer, unendlich
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📺 Watching Activity
|
||||||
|
**Visueller Effekt:**
|
||||||
|
- **TV Scan Lines** (retro CRT-Effekt)
|
||||||
|
- Lavender/Pink Gradient
|
||||||
|
- TV-Icon
|
||||||
|
|
||||||
|
**Daten:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "Breaking Bad S05E14",
|
||||||
|
"platform": "netflix",
|
||||||
|
"type": "series"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Animation:**
|
||||||
|
- Horizontaler Gradient-Balken (8px hoch)
|
||||||
|
- Bewegt sich von -100% bis 200% vertikal
|
||||||
|
- 3s linear
|
||||||
|
- Weiß/transparent gradient
|
||||||
|
- Simuliert alte TV-Bildschirme
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 😊 Status & Mood
|
||||||
|
**Visueller Effekt:**
|
||||||
|
- **Wackelndes Emoji** (rotate: 0° → 10° → -10° → 0°)
|
||||||
|
- Lavender/Pink Gradient
|
||||||
|
- Custom Message
|
||||||
|
|
||||||
|
**Daten:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mood": "💻",
|
||||||
|
"customMessage": "Deep work mode - Building features"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Animation:**
|
||||||
|
- Emoji schwingt hin und her
|
||||||
|
- 2s Dauer, easeInOut
|
||||||
|
- Unendliche Wiederholung
|
||||||
|
- Subtile Bewegung (-10° bis +10°)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Interaktive Elemente
|
||||||
|
|
||||||
|
### 1. **Spotify "Listen with me"**
|
||||||
|
```tsx
|
||||||
|
<a href={spotifyUrl} target="_blank">
|
||||||
|
<Waves size={10} />
|
||||||
|
Listen with me
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
- Öffnet Spotify Web Player
|
||||||
|
- Direkt zum aktuellen Song
|
||||||
|
- Kleine Wellen-Icon
|
||||||
|
|
||||||
|
### 2. **GitHub "View Repo"**
|
||||||
|
```tsx
|
||||||
|
<a href={repoUrl} target="_blank">
|
||||||
|
View <ExternalLink size={10} />
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
- Link zum Repository
|
||||||
|
- External Link Icon
|
||||||
|
- Hover Underline
|
||||||
|
|
||||||
|
### 3. **Live Progress Bar**
|
||||||
|
- Dynamisch basiert auf Spotify API
|
||||||
|
- Smooth animation (0.5s transition)
|
||||||
|
- Rose → Coral Gradient
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Farbschema pro Activity
|
||||||
|
|
||||||
|
| Activity | Background Gradient | Border | Pulse Dot |
|
||||||
|
|----------|-------------------|--------|-----------|
|
||||||
|
| Coding | `from-liquid-mint/20 to-liquid-sky/20` | `border-liquid-mint/40` | Green |
|
||||||
|
| Music | `from-liquid-rose/20 to-liquid-coral/20` | `border-liquid-rose/40` | Red |
|
||||||
|
| Gaming | `from-liquid-peach/20 to-liquid-yellow/20` | `border-liquid-peach/40` | Orange |
|
||||||
|
| Watching | `from-liquid-lavender/20 to-liquid-pink/20` | `border-liquid-lavender/40` | Purple |
|
||||||
|
| Running | `from-liquid-lime/20 to-liquid-mint/20` | `border-liquid-lime/40` | Lime |
|
||||||
|
| Reading | `from-liquid-teal/20 to-liquid-lime/20` | `border-liquid-teal/40` | Teal |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 AI Chatbot Features
|
||||||
|
|
||||||
|
### Design
|
||||||
|
- **Gradient Header**: Mint → Sky
|
||||||
|
- **Message Bubbles**:
|
||||||
|
- User: Stone-900 gradient, rounded-tr-none
|
||||||
|
- AI: White → Stone-50 gradient, rounded-tl-none
|
||||||
|
- **Sparkles Icon**: Animated AI indicator
|
||||||
|
- **Thinking State**: Rotating Loader2 mit liquid-mint Farbe
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- ✅ Real-time responses via n8n
|
||||||
|
- ✅ Fallback responses bei Offline
|
||||||
|
- ✅ Context über Dennis
|
||||||
|
- ✅ Smooth animations
|
||||||
|
- ✅ Loading states
|
||||||
|
- ✅ Error handling
|
||||||
|
|
||||||
|
### Example Responses
|
||||||
|
```
|
||||||
|
"Great question! Dennis specializes in..."
|
||||||
|
"Dennis loves self-hosting! He manages..."
|
||||||
|
"Check out his projects to see what he's building!"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 Zusätzliche Animation-Ideen
|
||||||
|
|
||||||
|
### Noch nicht implementiert (Ideen):
|
||||||
|
|
||||||
|
#### 1. **Coffee Counter ☕**
|
||||||
|
```tsx
|
||||||
|
{coffeeCount > 0 && (
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
y: [0, -5, 0],
|
||||||
|
rotate: [0, -5, 5, 0]
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
☕ × {coffeeCount}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. **Code Streak 🔥**
|
||||||
|
```tsx
|
||||||
|
<motion.div>
|
||||||
|
🔥 {consecutiveDays} day streak!
|
||||||
|
</motion.div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. **Live Visitor Count 👥**
|
||||||
|
```tsx
|
||||||
|
<motion.div
|
||||||
|
animate={{ scale: [1, 1.05, 1] }}
|
||||||
|
>
|
||||||
|
👥 {liveVisitors} online
|
||||||
|
</motion.div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. **Deployment Status 🚀**
|
||||||
|
```tsx
|
||||||
|
{isDeploying && (
|
||||||
|
<motion.div>
|
||||||
|
<Rocket className="animate-bounce" />
|
||||||
|
Deploying...
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. **Weather Integration 🌤️**
|
||||||
|
```tsx
|
||||||
|
<motion.div>
|
||||||
|
{weatherEmoji} {temperature}°C in Osnabrück
|
||||||
|
</motion.div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. **Pomodoro Timer 🍅**
|
||||||
|
```tsx
|
||||||
|
{pomodoroActive && (
|
||||||
|
<CircularProgress value={timeLeft} />
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Auto-Clear Logic
|
||||||
|
|
||||||
|
### Musik
|
||||||
|
- ✅ Verschwindet automatisch wenn `is_playing = false`
|
||||||
|
- ✅ n8n checkt alle 30s via Spotify API
|
||||||
|
- ✅ Database Update wenn gestoppt
|
||||||
|
|
||||||
|
### Aktivitäten
|
||||||
|
- ✅ Verfallen nach 2 Stunden
|
||||||
|
- ✅ Check in API Route: `hoursSinceUpdate < 2`
|
||||||
|
- ✅ Optionaler n8n Cleanup-Workflow
|
||||||
|
|
||||||
|
### Gaming
|
||||||
|
- ✅ Basiert auf Discord Presence
|
||||||
|
- ✅ Auto-clear wenn Spiel beendet
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 Responsive Design
|
||||||
|
|
||||||
|
### Mobile (< 768px)
|
||||||
|
- Bubbles: `max-w-[calc(100vw-6rem)]`
|
||||||
|
- Stacked vertikal
|
||||||
|
- Chat: Full-width minus padding
|
||||||
|
|
||||||
|
### Desktop (> 768px)
|
||||||
|
- Fixed `bottom-6 right-6`
|
||||||
|
- Bubbles: `max-w-xs` (320px)
|
||||||
|
- Chat: 384px breit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Performance
|
||||||
|
|
||||||
|
### Optimierungen
|
||||||
|
- ✅ `will-change: transform` für Animationen
|
||||||
|
- ✅ `AnimatePresence` für smooth exit
|
||||||
|
- ✅ `overflow: hidden` auf animated containers
|
||||||
|
- ✅ `pointer-events-none` auf Hintergrund-Effekte
|
||||||
|
- ✅ CSS `backdrop-filter` statt JS blur
|
||||||
|
- ✅ Relative Z-Index (10, 20, 9999)
|
||||||
|
|
||||||
|
### Polling
|
||||||
|
- Frontend: Alle 30s (konfigurierbar)
|
||||||
|
- Spotify: Alle 30s (n8n)
|
||||||
|
- GitHub: Echtzeit via Webhooks
|
||||||
|
- Discord: Alle 60s
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Neue Activity hinzufügen
|
||||||
|
|
||||||
|
### 1. **Backend (Database)**
|
||||||
|
```sql
|
||||||
|
ALTER TABLE activity_status
|
||||||
|
ADD COLUMN new_activity_field VARCHAR(255);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **n8n Workflow**
|
||||||
|
```javascript
|
||||||
|
// Update database
|
||||||
|
UPDATE activity_status SET
|
||||||
|
new_activity_field = 'value'
|
||||||
|
WHERE id = 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Frontend (ActivityFeed.tsx)**
|
||||||
|
```tsx
|
||||||
|
// Add to interface
|
||||||
|
interface ActivityData {
|
||||||
|
newActivity: {
|
||||||
|
field: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add color scheme
|
||||||
|
const activityColors = {
|
||||||
|
newActivity: {
|
||||||
|
bg: "from-liquid-purple/20 to-liquid-pink/20",
|
||||||
|
border: "border-liquid-purple/40",
|
||||||
|
text: "text-liquid-purple",
|
||||||
|
pulse: "bg-purple-500",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add animation component
|
||||||
|
const NewActivityAnimation = () => {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
{/* Your custom animation */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render function
|
||||||
|
const renderNewActivity = () => {
|
||||||
|
if (!data?.newActivity) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div className="...">
|
||||||
|
<NewActivityAnimation />
|
||||||
|
{/* Content */}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Analytics Integration
|
||||||
|
|
||||||
|
### Track Activity Views
|
||||||
|
```typescript
|
||||||
|
// In ActivityFeed
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.activity) {
|
||||||
|
fetch('/api/analytics/activity-view', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: data.activity.type
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data?.activity]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Popular Activities Dashboard
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
activity_type,
|
||||||
|
COUNT(*) as views,
|
||||||
|
AVG(duration) as avg_duration
|
||||||
|
FROM activity_history
|
||||||
|
WHERE created_at > NOW() - INTERVAL '7 days'
|
||||||
|
GROUP BY activity_type
|
||||||
|
ORDER BY views DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Nächste Steps
|
||||||
|
|
||||||
|
1. ✅ Datenbank Setup (`setup_activity_status.sql`)
|
||||||
|
2. ✅ n8n Workflows importieren
|
||||||
|
3. ⏳ Spotify OAuth konfigurieren
|
||||||
|
4. ⏳ GitHub Webhooks setup
|
||||||
|
5. ⏳ Activity Dashboard testen
|
||||||
|
6. ⏳ AI Chatbot mit OpenAI verbinden
|
||||||
|
7. ⏳ Auto-Clear Workflows aktivieren
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Design Philosophy
|
||||||
|
|
||||||
|
- **Smooth**: Alle Animationen 0.5-1s, nie schneller
|
||||||
|
- **Subtle**: Opacity 20-40%, nie zu grell
|
||||||
|
- **Consistent**: Gleiche Easing-Function überall
|
||||||
|
- **Performant**: GPU-beschleunigt (transform, opacity)
|
||||||
|
- **Delightful**: Kleine Details machen den Unterschied
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Happy Coding! 🚀**
|
||||||
470
docs/DYNAMIC_ACTIVITY_MANAGEMENT.md
Normal file
470
docs/DYNAMIC_ACTIVITY_MANAGEMENT.md
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
# 🎛️ Dynamic Activity Management - No Rebuild Required!
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Dieses System erlaubt dir, alle Aktivitäten dynamisch zu steuern **ohne die Website neu zu bauen**. Alle Änderungen werden in Echtzeit über die Datenbank und n8n gesteuert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Konzept: Zentrales Management
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ n8n Dashboard │ ← Du steuerst hier alles
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ PostgreSQL │ ← Daten werden hier gespeichert
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ API Route │ ← Website liest alle 30s
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ ActivityFeed UI │ ← Besucher sehen live updates
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vorteile:**
|
||||||
|
- ✅ Keine Website-Rebuild notwendig
|
||||||
|
- ✅ Echtzeit-Updates (30 Sekunden)
|
||||||
|
- ✅ Volle Kontrolle via n8n
|
||||||
|
- ✅ Historische Daten verfügbar
|
||||||
|
- ✅ Multiple Steuerungsmöglichkeiten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎮 Management Optionen
|
||||||
|
|
||||||
|
### Option 1: n8n Dashboard UI ⭐ EMPFOHLEN
|
||||||
|
|
||||||
|
Erstelle ein simples n8n Workflow-Dashboard mit Webhook-Buttons:
|
||||||
|
|
||||||
|
**Workflow: "Activity Manager Dashboard"**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "HTTP Server",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"parameters": {
|
||||||
|
"path": "activity-dashboard",
|
||||||
|
"method": "GET",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "HTML Dashboard",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"parameters": {
|
||||||
|
"responseBody": "=<html>\n<head>\n <title>Activity Manager</title>\n <style>\n body { font-family: system-ui; max-width: 800px; margin: 50px auto; padding: 20px; }\n .activity-section { background: #f5f5f5; padding: 20px; margin: 20px 0; border-radius: 8px; }\n button { background: #333; color: white; padding: 10px 20px; margin: 5px; border: none; border-radius: 5px; cursor: pointer; }\n button:hover { background: #555; }\n input, select, textarea { padding: 8px; margin: 5px 0; border: 1px solid #ddd; border-radius: 4px; width: 100%; }\n .status { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }\n .active { background: #4ade80; }\n .inactive { background: #ef4444; }\n </style>\n</head>\n<body>\n <h1>🎛️ Activity Manager</h1>\n \n <div class=\"activity-section\">\n <h2>🎵 Music Control</h2>\n <p>Status: <span class=\"status active\"></span> Auto-syncing from Spotify</p>\n <button onclick=\"fetch('/webhook/stop-music', {method:'POST'})\">Stop Music Display</button>\n </div>\n\n <div class=\"activity-section\">\n <h2>💻 Coding Activity</h2>\n <input type=\"text\" id=\"project\" placeholder=\"Project name\">\n <input type=\"text\" id=\"language\" placeholder=\"Language (e.g., TypeScript)\">\n <input type=\"text\" id=\"repo\" placeholder=\"GitHub Repo URL\">\n <button onclick=\"updateCoding()\">Update Coding Status</button>\n <button onclick=\"clearCoding()\">Clear</button>\n </div>\n\n <div class=\"activity-section\">\n <h2>🎮 Gaming</h2>\n <input type=\"text\" id=\"game\" placeholder=\"Game name\">\n <select id=\"platform\">\n <option>steam</option>\n <option>playstation</option>\n <option>xbox</option>\n </select>\n <button onclick=\"updateGaming()\">Start Gaming</button>\n <button onclick=\"stopGaming()\">Stop Gaming</button>\n </div>\n\n <div class=\"activity-section\">\n <h2>😊 Mood & Status</h2>\n <input type=\"text\" id=\"mood\" placeholder=\"Emoji (e.g., 😊, 💻, 🎮)\" maxlength=\"2\">\n <textarea id=\"message\" placeholder=\"Custom message\" rows=\"2\"></textarea>\n <button onclick=\"updateStatus()\">Update Status</button>\n </div>\n\n <div class=\"activity-section\">\n <h2>🏃 Manual Activities</h2>\n <select id=\"activity-type\">\n <option value=\"running\">Running</option>\n <option value=\"reading\">Reading</option>\n <option value=\"watching\">Watching</option>\n </select>\n <input type=\"text\" id=\"activity-details\" placeholder=\"Details\">\n <button onclick=\"updateActivity()\">Start Activity</button>\n <button onclick=\"clearActivity()\">Clear</button>\n </div>\n\n <div class=\"activity-section\">\n <h2>🧹 Quick Actions</h2>\n <button onclick=\"clearAll()\">Clear All Activities</button>\n <button onclick=\"setAFK()\">Set AFK</button>\n <button onclick=\"setFocusMode()\">Focus Mode (DND)</button>\n </div>\n\n <script>\n function updateCoding() {\n fetch('/webhook/update-activity', {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({\n type: 'coding',\n project: document.getElementById('project').value,\n language: document.getElementById('language').value,\n repo: document.getElementById('repo').value\n })\n }).then(() => alert('✅ Updated!'));\n }\n\n function updateGaming() {\n fetch('/webhook/update-activity', {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({\n type: 'gaming',\n game: document.getElementById('game').value,\n platform: document.getElementById('platform').value\n })\n }).then(() => alert('✅ Gaming status updated!'));\n }\n\n function updateStatus() {\n fetch('/webhook/update-status', {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({\n mood: document.getElementById('mood').value,\n message: document.getElementById('message').value\n })\n }).then(() => alert('✅ Status updated!'));\n }\n\n function clearAll() {\n if(confirm('Clear all activities?')) {\n fetch('/webhook/clear-all', {method: 'POST'})\n .then(() => alert('✅ All cleared!'));\n }\n }\n\n function setAFK() {\n fetch('/webhook/update-status', {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify({mood: '💤', message: 'AFK - Be right back'})\n }).then(() => alert('✅ AFK mode activated!'));\n }\n </script>\n</body>\n</html>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zugriff:**
|
||||||
|
```
|
||||||
|
https://your-n8n-instance.com/webhook/activity-dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option 2: Discord Bot Commands
|
||||||
|
|
||||||
|
Erstelle einen Discord Bot für schnelle Updates:
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
```
|
||||||
|
!status 💻 Working on new features
|
||||||
|
!coding Portfolio Next.js
|
||||||
|
!music <automatic from spotify>
|
||||||
|
!gaming Elden Ring
|
||||||
|
!clear
|
||||||
|
!afk
|
||||||
|
```
|
||||||
|
|
||||||
|
**n8n Workflow:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Discord Webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"parameters": {
|
||||||
|
"path": "discord-bot"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Parse Command",
|
||||||
|
"type": "n8n-nodes-base.function",
|
||||||
|
"parameters": {
|
||||||
|
"functionCode": "const message = items[0].json.content;\nconst [command, ...args] = message.split(' ');\n\nswitch(command) {\n case '!status':\n return [{\n json: {\n action: 'update_status',\n mood: args[0],\n message: args.slice(1).join(' ')\n }\n }];\n \n case '!coding':\n return [{\n json: {\n action: 'update_activity',\n type: 'coding',\n details: args.join(' ')\n }\n }];\n \n case '!clear':\n return [{\n json: { action: 'clear_all' }\n }];\n}\n\nreturn [{ json: {} }];"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Database",
|
||||||
|
"type": "n8n-nodes-base.postgres"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option 3: Mobile App / Shortcut
|
||||||
|
|
||||||
|
**iOS Shortcuts:**
|
||||||
|
```
|
||||||
|
1. "Start Coding" → POST to n8n webhook
|
||||||
|
2. "Finished Work" → Clear activity
|
||||||
|
3. "Set Mood" → Update status
|
||||||
|
```
|
||||||
|
|
||||||
|
**Android Tasker:**
|
||||||
|
- Similar webhooks
|
||||||
|
- Location-based triggers
|
||||||
|
- Time-based automation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option 4: CLI Tool
|
||||||
|
|
||||||
|
Erstelle ein simples CLI Tool:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# activity.sh
|
||||||
|
|
||||||
|
N8N_URL="https://your-n8n-instance.com"
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
status)
|
||||||
|
curl -X POST "$N8N_URL/webhook/update-status" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"mood\":\"$2\",\"message\":\"$3\"}"
|
||||||
|
;;
|
||||||
|
coding)
|
||||||
|
curl -X POST "$N8N_URL/webhook/update-activity" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"type\":\"coding\",\"project\":\"$2\",\"language\":\"$3\"}"
|
||||||
|
;;
|
||||||
|
clear)
|
||||||
|
curl -X POST "$N8N_URL/webhook/clear-all"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: activity.sh [status|coding|clear] [args]"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
./activity.sh status 💻 "Deep work mode"
|
||||||
|
./activity.sh coding "Portfolio" "TypeScript"
|
||||||
|
./activity.sh clear
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Automatische Sync-Workflows
|
||||||
|
|
||||||
|
### Musik geht weg wenn nicht mehr läuft
|
||||||
|
|
||||||
|
**n8n Workflow: "Spotify Auto-Clear"**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Check Every 30s",
|
||||||
|
"type": "n8n-nodes-base.cron",
|
||||||
|
"parameters": {
|
||||||
|
"cronExpression": "*/30 * * * * *"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Spotify Status",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"parameters": {
|
||||||
|
"url": "https://api.spotify.com/v1/me/player/currently-playing"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Check If Playing",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"boolean": [
|
||||||
|
{
|
||||||
|
"value1": "={{$json.is_playing}}",
|
||||||
|
"value2": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Clear Music from Database",
|
||||||
|
"type": "n8n-nodes-base.postgres",
|
||||||
|
"parameters": {
|
||||||
|
"operation": "executeQuery",
|
||||||
|
"query": "UPDATE activity_status SET music_playing = FALSE, music_track = NULL, music_artist = NULL, music_album = NULL, music_album_art = NULL, music_progress = NULL WHERE id = 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-Clear nach Zeit
|
||||||
|
|
||||||
|
**n8n Workflow: "Activity Timeout"**
|
||||||
|
```javascript
|
||||||
|
// Function Node: Check Activity Age
|
||||||
|
const lastUpdate = new Date(items[0].json.updated_at);
|
||||||
|
const now = new Date();
|
||||||
|
const hoursSinceUpdate = (now - lastUpdate) / (1000 * 60 * 60);
|
||||||
|
|
||||||
|
// Clear activity if older than 2 hours
|
||||||
|
if (hoursSinceUpdate > 2) {
|
||||||
|
return [{
|
||||||
|
json: {
|
||||||
|
should_clear: true,
|
||||||
|
reason: `Activity too old (${hoursSinceUpdate.toFixed(1)} hours)`
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{ json: { should_clear: false } }];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Smart Activity Detection
|
||||||
|
|
||||||
|
**Workflow: "Detect Coding from Git Commits"**
|
||||||
|
```javascript
|
||||||
|
// When you push to GitHub
|
||||||
|
const commit = items[0].json;
|
||||||
|
const repo = commit.repository.name;
|
||||||
|
const message = commit.head_commit.message;
|
||||||
|
|
||||||
|
// Detect language from files
|
||||||
|
const files = commit.head_commit.modified;
|
||||||
|
const language = files[0]?.split('.').pop(); // Get extension
|
||||||
|
|
||||||
|
return [{
|
||||||
|
json: {
|
||||||
|
activity_type: 'coding',
|
||||||
|
activity_details: message,
|
||||||
|
activity_project: repo,
|
||||||
|
activity_language: language,
|
||||||
|
activity_repo: commit.repository.html_url,
|
||||||
|
link: commit.head_commit.url
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Activity Analytics Dashboard
|
||||||
|
|
||||||
|
**Workflow: "Activity History & Stats"**
|
||||||
|
|
||||||
|
Speichere Historie in separater Tabelle:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE activity_history (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
activity_type VARCHAR(50),
|
||||||
|
details TEXT,
|
||||||
|
duration INTEGER, -- in minutes
|
||||||
|
started_at TIMESTAMP,
|
||||||
|
ended_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- View für Statistiken
|
||||||
|
CREATE VIEW activity_stats AS
|
||||||
|
SELECT
|
||||||
|
activity_type,
|
||||||
|
COUNT(*) as count,
|
||||||
|
SUM(duration) as total_minutes,
|
||||||
|
AVG(duration) as avg_duration,
|
||||||
|
DATE(created_at) as date
|
||||||
|
FROM activity_history
|
||||||
|
GROUP BY activity_type, DATE(created_at)
|
||||||
|
ORDER BY date DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dashboard Queries:**
|
||||||
|
```sql
|
||||||
|
-- Heute
|
||||||
|
SELECT * FROM activity_stats WHERE date = CURRENT_DATE;
|
||||||
|
|
||||||
|
-- Diese Woche
|
||||||
|
SELECT activity_type, SUM(total_minutes) as minutes
|
||||||
|
FROM activity_stats
|
||||||
|
WHERE date >= CURRENT_DATE - INTERVAL '7 days'
|
||||||
|
GROUP BY activity_type;
|
||||||
|
|
||||||
|
-- Most Coded Languages
|
||||||
|
SELECT activity_language, COUNT(*)
|
||||||
|
FROM activity_history
|
||||||
|
WHERE activity_type = 'coding'
|
||||||
|
GROUP BY activity_language
|
||||||
|
ORDER BY COUNT(*) DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Custom Activity Types
|
||||||
|
|
||||||
|
Erweitere das System mit eigenen Activity-Types:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Add custom columns
|
||||||
|
ALTER TABLE activity_status
|
||||||
|
ADD COLUMN custom_activity_type VARCHAR(100),
|
||||||
|
ADD COLUMN custom_activity_data JSONB;
|
||||||
|
|
||||||
|
-- Example: Workout tracking
|
||||||
|
UPDATE activity_status SET
|
||||||
|
custom_activity_type = 'workout',
|
||||||
|
custom_activity_data = '{
|
||||||
|
"exercise": "Push-ups",
|
||||||
|
"reps": 50,
|
||||||
|
"icon": "💪",
|
||||||
|
"color": "orange"
|
||||||
|
}'::jsonb
|
||||||
|
WHERE id = 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Frontend Support:**
|
||||||
|
```typescript
|
||||||
|
// In ActivityFeed.tsx
|
||||||
|
interface CustomActivity {
|
||||||
|
type: string;
|
||||||
|
data: {
|
||||||
|
icon: string;
|
||||||
|
color: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render custom activities dynamically
|
||||||
|
if (data.customActivity) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={`bg-${data.customActivity.data.color}/20`}
|
||||||
|
>
|
||||||
|
<span>{data.customActivity.data.icon}</span>
|
||||||
|
<span>{data.customActivity.type}</span>
|
||||||
|
{/* Render data fields dynamically */}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Security & Best Practices
|
||||||
|
|
||||||
|
### 1. Webhook Authentication
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In n8n webhook
|
||||||
|
const secret = $credentials.webhookSecret;
|
||||||
|
const providedSecret = $node["Webhook"].json.headers["x-webhook-secret"];
|
||||||
|
|
||||||
|
if (secret !== providedSecret) {
|
||||||
|
return [{
|
||||||
|
json: { error: "Unauthorized" },
|
||||||
|
statusCode: 401
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Rate Limiting
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Track requests
|
||||||
|
CREATE TABLE webhook_requests (
|
||||||
|
ip_address VARCHAR(45),
|
||||||
|
endpoint VARCHAR(100),
|
||||||
|
requested_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Check rate limit (max 10 requests per minute)
|
||||||
|
SELECT COUNT(*) FROM webhook_requests
|
||||||
|
WHERE ip_address = $1
|
||||||
|
AND requested_at > NOW() - INTERVAL '1 minute';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Input Validation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In n8n Function node
|
||||||
|
const validateInput = (data) => {
|
||||||
|
if (!data.type || typeof data.type !== 'string') {
|
||||||
|
throw new Error('Invalid activity type');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'coding' && !data.project) {
|
||||||
|
throw new Error('Project name required for coding activity');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Deploy Checklist
|
||||||
|
|
||||||
|
- [ ] Datenbank Table erstellt (`setup_activity_status.sql`)
|
||||||
|
- [ ] n8n Workflows importiert
|
||||||
|
- [ ] Spotify OAuth konfiguriert
|
||||||
|
- [ ] GitHub Webhooks eingerichtet
|
||||||
|
- [ ] Dashboard-URL getestet
|
||||||
|
- [ ] API Routes deployed
|
||||||
|
- [ ] Environment Variables gesetzt
|
||||||
|
- [ ] Frontend ActivityFeed getestet
|
||||||
|
- [ ] Auto-Clear Workflows aktiviert
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Pro-Tipps
|
||||||
|
|
||||||
|
1. **Backup System**: Exportiere n8n Workflows regelmäßig
|
||||||
|
2. **Monitoring**: Setup alerts wenn Workflows fehlschlagen
|
||||||
|
3. **Testing**: Nutze n8n's Test-Modus vor Produktion
|
||||||
|
4. **Logging**: Speichere alle Aktivitäten für Analyse
|
||||||
|
5. **Fallbacks**: Zeige Placeholder wenn keine Daten vorhanden
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Quick Support Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check database status
|
||||||
|
psql -d portfolio_dev -c "SELECT * FROM activity_status WHERE id = 1;"
|
||||||
|
|
||||||
|
# Clear all activities
|
||||||
|
psql -d portfolio_dev -c "UPDATE activity_status SET activity_type = NULL, music_playing = FALSE WHERE id = 1;"
|
||||||
|
|
||||||
|
# View recent history
|
||||||
|
psql -d portfolio_dev -c "SELECT * FROM activity_history ORDER BY created_at DESC LIMIT 10;"
|
||||||
|
|
||||||
|
# Test n8n webhook
|
||||||
|
curl -X POST https://your-n8n.com/webhook/update-activity \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"type":"coding","details":"Testing","project":"Portfolio"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Happy automating! 🎉
|
||||||
590
docs/N8N_INTEGRATION.md
Normal file
590
docs/N8N_INTEGRATION.md
Normal file
@@ -0,0 +1,590 @@
|
|||||||
|
# 🚀 n8n Integration Guide - Complete Setup
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Dieses Portfolio nutzt n8n für:
|
||||||
|
- ⚡ **Echtzeit-Aktivitätsanzeige** (Coding, Musik, Gaming, etc.)
|
||||||
|
- 💬 **AI-Chatbot** (mit OpenAI/Anthropic)
|
||||||
|
- 📊 **Aktivitäts-Tracking** (GitHub, Spotify, Netflix, etc.)
|
||||||
|
- 🎮 **Gaming-Status** (Steam, Discord)
|
||||||
|
- 📧 **Automatische Benachrichtigungen**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Coole Ideen für Integrationen
|
||||||
|
|
||||||
|
### 1. **GitHub Activity Feed** 🔨
|
||||||
|
**Was es zeigt:**
|
||||||
|
- "Currently coding: Portfolio Website"
|
||||||
|
- "Last commit: 5 minutes ago"
|
||||||
|
- "Working on: feature/n8n-integration"
|
||||||
|
- Programming language (TypeScript, Python, etc.)
|
||||||
|
|
||||||
|
**n8n Workflow:**
|
||||||
|
```
|
||||||
|
GitHub Webhook → Extract Data → Update Database → Display on Site
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Spotify Now Playing** 🎵
|
||||||
|
**Was es zeigt:**
|
||||||
|
- Aktueller Song + Artist
|
||||||
|
- Album Cover (rotierend animiert!)
|
||||||
|
- Fortschrittsbalken
|
||||||
|
- "Listening to X since Y minutes"
|
||||||
|
|
||||||
|
**n8n Workflow:**
|
||||||
|
```
|
||||||
|
Cron (every 30s) → Spotify API → Parse Track Data → Update Database
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Netflix/YouTube/Twitch Watching** 📺
|
||||||
|
**Was es zeigt:**
|
||||||
|
- "Watching: Breaking Bad S05E14"
|
||||||
|
- "Streaming: Coding Tutorial"
|
||||||
|
- Platform badges (Netflix/YouTube/Twitch)
|
||||||
|
|
||||||
|
**n8n Workflow:**
|
||||||
|
```
|
||||||
|
Trakt.tv API → Get Current Watching → Update Database
|
||||||
|
Discord Rich Presence → Extract Activity → Update Database
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Gaming Activity** 🎮
|
||||||
|
**Was es zeigt:**
|
||||||
|
- "Playing: Elden Ring"
|
||||||
|
- Platform: Steam/PlayStation/Xbox
|
||||||
|
- Play time
|
||||||
|
- Achievement notifications
|
||||||
|
|
||||||
|
**n8n Workflow:**
|
||||||
|
```
|
||||||
|
Steam API → Get Current Game → Update Database
|
||||||
|
Discord Presence → Parse Game → Update Database
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. **Mood & Custom Status** 😊
|
||||||
|
**Was es zeigt:**
|
||||||
|
- Emoji mood (😊, 💻, 🏃, 🎮, 😴)
|
||||||
|
- Custom message: "Focused on DevOps"
|
||||||
|
- Auto-status based on time/activity
|
||||||
|
|
||||||
|
**n8n Workflow:**
|
||||||
|
```
|
||||||
|
Schedule → Determine Status (work hours/break/sleep) → Update Database
|
||||||
|
Manual Webhook → Set Custom Status → Update Database
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. **Smart Notifications** 📬
|
||||||
|
**Was es zeigt:**
|
||||||
|
- "New email from X"
|
||||||
|
- "GitHub PR needs review"
|
||||||
|
- "Calendar event in 15 min"
|
||||||
|
|
||||||
|
**n8n Workflow:**
|
||||||
|
```
|
||||||
|
Email/Calendar/GitHub → Filter Important → Create Notification → Display
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Setup: Datenbank Schema
|
||||||
|
|
||||||
|
### PostgreSQL Table: `activity_status`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE activity_status (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- Activity
|
||||||
|
activity_type VARCHAR(50), -- 'coding', 'listening', 'watching', 'gaming', 'reading'
|
||||||
|
activity_details TEXT,
|
||||||
|
activity_project VARCHAR(255),
|
||||||
|
activity_language VARCHAR(50),
|
||||||
|
activity_repo VARCHAR(255),
|
||||||
|
|
||||||
|
-- Music
|
||||||
|
music_playing BOOLEAN DEFAULT FALSE,
|
||||||
|
music_track VARCHAR(255),
|
||||||
|
music_artist VARCHAR(255),
|
||||||
|
music_album VARCHAR(255),
|
||||||
|
music_platform VARCHAR(50), -- 'spotify', 'apple'
|
||||||
|
music_progress INTEGER, -- 0-100
|
||||||
|
music_album_art TEXT,
|
||||||
|
|
||||||
|
-- Watching
|
||||||
|
watching_title VARCHAR(255),
|
||||||
|
watching_platform VARCHAR(50), -- 'youtube', 'netflix', 'twitch'
|
||||||
|
watching_type VARCHAR(50), -- 'video', 'stream', 'movie', 'series'
|
||||||
|
|
||||||
|
-- Gaming
|
||||||
|
gaming_game VARCHAR(255),
|
||||||
|
gaming_platform VARCHAR(50), -- 'steam', 'playstation', 'xbox'
|
||||||
|
gaming_status VARCHAR(50), -- 'playing', 'idle'
|
||||||
|
|
||||||
|
-- Status
|
||||||
|
status_mood VARCHAR(10), -- emoji
|
||||||
|
status_message TEXT,
|
||||||
|
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 n8n Workflows
|
||||||
|
|
||||||
|
### Workflow 1: GitHub Activity Tracker
|
||||||
|
|
||||||
|
**Trigger:** Webhook bei Push/Commit
|
||||||
|
**Frequenz:** Echtzeit
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "GitHub Webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"parameters": {
|
||||||
|
"path": "github-activity",
|
||||||
|
"method": "POST"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Extract Commit Data",
|
||||||
|
"type": "n8n-nodes-base.function",
|
||||||
|
"parameters": {
|
||||||
|
"functionCode": "const commit = items[0].json;\nreturn [\n {\n json: {\n activity_type: 'coding',\n activity_details: commit.head_commit.message,\n activity_project: commit.repository.name,\n activity_language: 'TypeScript',\n activity_repo: commit.repository.html_url,\n updated_at: new Date().toISOString()\n }\n }\n];"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Database",
|
||||||
|
"type": "n8n-nodes-base.postgres",
|
||||||
|
"parameters": {
|
||||||
|
"operation": "executeQuery",
|
||||||
|
"query": "INSERT INTO activity_status (activity_type, activity_details, activity_project, activity_language, activity_repo, updated_at) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET activity_type = $1, activity_details = $2, activity_project = $3, activity_language = $4, activity_repo = $5, updated_at = $6 WHERE activity_status.id = 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Setup in GitHub:**
|
||||||
|
1. Gehe zu deinem Repository → Settings → Webhooks
|
||||||
|
2. Add webhook: `https://your-n8n-instance.com/webhook/github-activity`
|
||||||
|
3. Content type: `application/json`
|
||||||
|
4. Events: Push events
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Workflow 2: Spotify Now Playing
|
||||||
|
|
||||||
|
**Trigger:** Cron (alle 30 Sekunden)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Schedule",
|
||||||
|
"type": "n8n-nodes-base.cron",
|
||||||
|
"parameters": {
|
||||||
|
"cronExpression": "*/30 * * * * *"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Spotify API",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"parameters": {
|
||||||
|
"url": "https://api.spotify.com/v1/me/player/currently-playing",
|
||||||
|
"method": "GET",
|
||||||
|
"authentication": "oAuth2",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer {{$credentials.spotify.accessToken}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Parse Track Data",
|
||||||
|
"type": "n8n-nodes-base.function",
|
||||||
|
"parameters": {
|
||||||
|
"functionCode": "const track = items[0].json;\nif (!track || !track.is_playing) {\n return [{ json: { music_playing: false } }];\n}\n\nreturn [\n {\n json: {\n music_playing: true,\n music_track: track.item.name,\n music_artist: track.item.artists[0].name,\n music_album: track.item.album.name,\n music_platform: 'spotify',\n music_progress: Math.round((track.progress_ms / track.item.duration_ms) * 100),\n music_album_art: track.item.album.images[0].url,\n updated_at: new Date().toISOString()\n }\n }\n];"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Database",
|
||||||
|
"type": "n8n-nodes-base.postgres",
|
||||||
|
"parameters": {
|
||||||
|
"operation": "executeQuery",
|
||||||
|
"query": "UPDATE activity_status SET music_playing = $1, music_track = $2, music_artist = $3, music_album = $4, music_platform = $5, music_progress = $6, music_album_art = $7, updated_at = $8 WHERE id = 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Spotify API Setup:**
|
||||||
|
1. Gehe zu https://developer.spotify.com/dashboard
|
||||||
|
2. Create App
|
||||||
|
3. Add Redirect URI: `https://your-n8n-instance.com/oauth/callback`
|
||||||
|
4. Kopiere Client ID & Secret in n8n Credentials
|
||||||
|
5. Scopes: `user-read-currently-playing`, `user-read-playback-state`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Workflow 3: AI Chatbot mit OpenAI
|
||||||
|
|
||||||
|
**Trigger:** Webhook bei Chat-Message
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Chat Webhook",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"parameters": {
|
||||||
|
"path": "chat",
|
||||||
|
"method": "POST"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Build Context",
|
||||||
|
"type": "n8n-nodes-base.function",
|
||||||
|
"parameters": {
|
||||||
|
"functionCode": "const userMessage = items[0].json.message;\n\nconst context = `You are Dennis Konkol's AI assistant. Here's information about Dennis:\n\n- Student in Osnabrück, Germany\n- Passionate self-hoster and DevOps enthusiast\n- Skills: Next.js, Flutter, Docker Swarm, Traefik, CI/CD, n8n\n- Runs own infrastructure on IONOS and OVHcloud\n- Projects: Clarity (Flutter dyslexia app), Self-hosted portfolio with Docker Swarm\n- Hobbies: Gaming, Jogging, Experimenting with tech\n- Fun fact: Uses pen & paper for calendar despite automating everything\n\nAnswer questions about Dennis professionally and friendly. Keep answers concise (2-3 sentences).\n\nUser question: ${userMessage}`;\n\nreturn [{ json: { context, userMessage } }];"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OpenAI Chat",
|
||||||
|
"type": "n8n-nodes-base.openAi",
|
||||||
|
"parameters": {
|
||||||
|
"resource": "chat",
|
||||||
|
"operation": "message",
|
||||||
|
"model": "gpt-4",
|
||||||
|
"messages": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "={{$node[\"Build Context\"].json[\"context\"]}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "={{$node[\"Build Context\"].json[\"userMessage\"]}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Return Response",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"parameters": {
|
||||||
|
"responseBody": "={{ { reply: $json.message.content } }}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**OpenAI API Setup:**
|
||||||
|
1. Gehe zu https://platform.openai.com/api-keys
|
||||||
|
2. Create API Key
|
||||||
|
3. Add zu n8n Credentials
|
||||||
|
4. Wähle Model: gpt-4 oder gpt-3.5-turbo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Workflow 4: Discord/Steam Gaming Status
|
||||||
|
|
||||||
|
**Trigger:** Cron (alle 60 Sekunden)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Schedule",
|
||||||
|
"type": "n8n-nodes-base.cron",
|
||||||
|
"parameters": {
|
||||||
|
"cronExpression": "0 * * * * *"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Discord API",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"parameters": {
|
||||||
|
"url": "https://discord.com/api/v10/users/@me",
|
||||||
|
"method": "GET",
|
||||||
|
"authentication": "oAuth2",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bot {{$credentials.discord.token}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Parse Gaming Status",
|
||||||
|
"type": "n8n-nodes-base.function",
|
||||||
|
"parameters": {
|
||||||
|
"functionCode": "const user = items[0].json;\nconst activity = user.activities?.find(a => a.type === 0); // 0 = Playing\n\nif (!activity) {\n return [{ json: { gaming_game: null, gaming_status: 'idle' } }];\n}\n\nreturn [\n {\n json: {\n gaming_game: activity.name,\n gaming_platform: 'discord',\n gaming_status: 'playing',\n updated_at: new Date().toISOString()\n }\n }\n];"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Database",
|
||||||
|
"type": "n8n-nodes-base.postgres",
|
||||||
|
"parameters": {
|
||||||
|
"operation": "executeQuery",
|
||||||
|
"query": "UPDATE activity_status SET gaming_game = $1, gaming_platform = $2, gaming_status = $3, updated_at = $4 WHERE id = 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Workflow 5: Smart Status (Auto-Detect)
|
||||||
|
|
||||||
|
**Trigger:** Cron (alle 5 Minuten)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"name": "Schedule",
|
||||||
|
"type": "n8n-nodes-base.cron",
|
||||||
|
"parameters": {
|
||||||
|
"cronExpression": "*/5 * * * *"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Determine Status",
|
||||||
|
"type": "n8n-nodes-base.function",
|
||||||
|
"parameters": {
|
||||||
|
"functionCode": "const hour = new Date().getHours();\nconst day = new Date().getDay(); // 0 = Sunday, 6 = Saturday\n\nlet mood = '💻';\nlet message = 'Working on projects';\n\n// Sleep time (0-7 Uhr)\nif (hour >= 0 && hour < 7) {\n mood = '😴';\n message = 'Sleeping (probably dreaming of code)';\n}\n// Morning (7-9 Uhr)\nelse if (hour >= 7 && hour < 9) {\n mood = '☕';\n message = 'Morning coffee & catching up';\n}\n// Work time (9-17 Uhr, Mo-Fr)\nelse if (hour >= 9 && hour < 17 && day >= 1 && day <= 5) {\n mood = '💻';\n message = 'Deep work mode - coding & learning';\n}\n// Evening (17-22 Uhr)\nelse if (hour >= 17 && hour < 22) {\n mood = '🎮';\n message = 'Relaxing - gaming or watching shows';\n}\n// Late night (22-24 Uhr)\nelse if (hour >= 22) {\n mood = '🌙';\n message = 'Late night coding session';\n}\n// Weekend\nif (day === 0 || day === 6) {\n mood = '🏃';\n message = 'Weekend vibes - exploring & experimenting';\n}\n\nreturn [\n {\n json: {\n status_mood: mood,\n status_message: message,\n updated_at: new Date().toISOString()\n }\n }\n];"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Update Database",
|
||||||
|
"type": "n8n-nodes-base.postgres",
|
||||||
|
"parameters": {
|
||||||
|
"operation": "executeQuery",
|
||||||
|
"query": "UPDATE activity_status SET status_mood = $1, status_message = $2, updated_at = $3 WHERE id = 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 Frontend API Integration
|
||||||
|
|
||||||
|
### Update `/app/api/n8n/status/route.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// Fetch from your activity_status table
|
||||||
|
const status = await prisma.$queryRaw`
|
||||||
|
SELECT * FROM activity_status WHERE id = 1 LIMIT 1
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (!status || status.length === 0) {
|
||||||
|
return NextResponse.json({
|
||||||
|
activity: null,
|
||||||
|
music: null,
|
||||||
|
watching: null,
|
||||||
|
gaming: null,
|
||||||
|
status: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = status[0];
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
activity: data.activity_type ? {
|
||||||
|
type: data.activity_type,
|
||||||
|
details: data.activity_details,
|
||||||
|
project: data.activity_project,
|
||||||
|
language: data.activity_language,
|
||||||
|
repo: data.activity_repo,
|
||||||
|
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,
|
||||||
|
progress: data.music_progress,
|
||||||
|
albumArt: data.music_album_art,
|
||||||
|
} : null,
|
||||||
|
watching: data.watching_title ? {
|
||||||
|
title: data.watching_title,
|
||||||
|
platform: data.watching_platform,
|
||||||
|
type: data.watching_type,
|
||||||
|
} : null,
|
||||||
|
gaming: data.gaming_game ? {
|
||||||
|
game: data.gaming_game,
|
||||||
|
platform: data.gaming_platform,
|
||||||
|
status: data.gaming_status,
|
||||||
|
} : null,
|
||||||
|
status: data.status_mood ? {
|
||||||
|
mood: data.status_mood,
|
||||||
|
customMessage: data.status_message,
|
||||||
|
} : null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching activity status:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
activity: null,
|
||||||
|
music: null,
|
||||||
|
watching: null,
|
||||||
|
gaming: null,
|
||||||
|
status: null,
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create `/app/api/n8n/chat/route.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const { message } = await request.json();
|
||||||
|
|
||||||
|
// Call your n8n chat webhook
|
||||||
|
const response = await fetch(`${process.env.N8N_WEBHOOK_URL}/webhook/chat`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ message }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('n8n webhook failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return NextResponse.json({ reply: data.reply });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Chat API error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ reply: 'Sorry, I encountered an error. Please try again later.' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌟 Zusätzliche coole Ideen
|
||||||
|
|
||||||
|
### 1. **Live Coding Stats**
|
||||||
|
- Lines of code today
|
||||||
|
- Most used language this week
|
||||||
|
- GitHub contribution graph
|
||||||
|
- Pull requests merged
|
||||||
|
|
||||||
|
### 2. **Coffee Counter** ☕
|
||||||
|
- Button in n8n Dashboard: "I had coffee"
|
||||||
|
- Displays: "3 coffees today"
|
||||||
|
- Funny messages bei > 5 cups
|
||||||
|
|
||||||
|
### 3. **Mood Tracker**
|
||||||
|
- Manual mood updates via Discord Bot
|
||||||
|
- Shows emoji + custom message
|
||||||
|
- Persists über den Tag
|
||||||
|
|
||||||
|
### 4. **Auto-DND Status**
|
||||||
|
- Wenn du in einem Meeting bist (Calendar API)
|
||||||
|
- Wenn du fokussiert arbeitest (Pomodoro Timer)
|
||||||
|
- Custom status: "🔴 In Deep Work - Back at 15:00"
|
||||||
|
|
||||||
|
### 5. **Project Highlights**
|
||||||
|
- "Currently building: X"
|
||||||
|
- "Deployed Y minutes ago"
|
||||||
|
- "Last successful build: Z"
|
||||||
|
|
||||||
|
### 6. **Social Activity**
|
||||||
|
- "New blog post: Title"
|
||||||
|
- "Trending on Twitter: X mentions"
|
||||||
|
- "LinkedIn: Y profile views this week"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Environment Variables
|
||||||
|
|
||||||
|
Add to `.env.local`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# n8n
|
||||||
|
N8N_WEBHOOK_URL=https://your-n8n-instance.com
|
||||||
|
N8N_API_KEY=your_n8n_api_key
|
||||||
|
|
||||||
|
# Spotify
|
||||||
|
SPOTIFY_CLIENT_ID=your_spotify_client_id
|
||||||
|
SPOTIFY_CLIENT_SECRET=your_spotify_client_secret
|
||||||
|
|
||||||
|
# OpenAI
|
||||||
|
OPENAI_API_KEY=your_openai_api_key
|
||||||
|
|
||||||
|
# Discord (optional)
|
||||||
|
DISCORD_BOT_TOKEN=your_discord_bot_token
|
||||||
|
|
||||||
|
# GitHub (optional)
|
||||||
|
GITHUB_WEBHOOK_SECRET=your_github_webhook_secret
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
1. **Setup Database:**
|
||||||
|
```bash
|
||||||
|
psql -U postgres -d portfolio_dev -f setup_activity_status.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create n8n Workflows:**
|
||||||
|
- Import workflows via n8n UI
|
||||||
|
- Configure credentials
|
||||||
|
- Activate workflows
|
||||||
|
|
||||||
|
3. **Update API Routes:**
|
||||||
|
- Add `status/route.ts` and `chat/route.ts`
|
||||||
|
- Set environment variables
|
||||||
|
|
||||||
|
4. **Test:**
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
- Check bottom-right corner for activity bubbles
|
||||||
|
- Click chat button to test AI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Best Practices
|
||||||
|
|
||||||
|
1. **Caching:** Cache API responses für 30s (nicht bei jedem Request neu fetchen)
|
||||||
|
2. **Error Handling:** Graceful fallbacks wenn n8n down ist
|
||||||
|
3. **Rate Limiting:** Limitiere Chat-Requests (max 10/minute)
|
||||||
|
4. **Privacy:** Zeige nur das, was du teilen willst
|
||||||
|
5. **Performance:** Nutze Webhooks statt Polling wo möglich
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Community Ideas
|
||||||
|
|
||||||
|
Teile deine coolen n8n-Integrationen!
|
||||||
|
- Discord: Zeig deinen Setup
|
||||||
|
- GitHub: Share deine Workflows
|
||||||
|
- Blog: Write-up über dein System
|
||||||
|
|
||||||
|
Happy automating! 🎉
|
||||||
311
docs/ai-image-generation/ENVIRONMENT.md
Normal file
311
docs/ai-image-generation/ENVIRONMENT.md
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
# Environment Variables for AI Image Generation
|
||||||
|
|
||||||
|
This document lists all environment variables needed for the AI image generation system.
|
||||||
|
|
||||||
|
## Required Variables
|
||||||
|
|
||||||
|
Add these to your `.env.local` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# =============================================================================
|
||||||
|
# AI IMAGE GENERATION CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# n8n Webhook Configuration
|
||||||
|
# The base URL where your n8n instance is running
|
||||||
|
N8N_WEBHOOK_URL=http://localhost:5678/webhook
|
||||||
|
|
||||||
|
# Secret token for authenticating webhook requests
|
||||||
|
# Generate a secure random token: openssl rand -hex 32
|
||||||
|
N8N_SECRET_TOKEN=your-secure-random-token-here
|
||||||
|
|
||||||
|
# Stable Diffusion API Configuration
|
||||||
|
# The URL where your Stable Diffusion WebUI is running
|
||||||
|
SD_API_URL=http://localhost:7860
|
||||||
|
|
||||||
|
# Optional: API key if your SD instance requires authentication
|
||||||
|
# SD_API_KEY=your-sd-api-key-here
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# IMAGE GENERATION SETTINGS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Automatically generate images when new projects are created
|
||||||
|
# Set to 'true' to enable, 'false' to disable
|
||||||
|
AUTO_GENERATE_IMAGES=true
|
||||||
|
|
||||||
|
# Directory where generated images will be saved
|
||||||
|
# Should be inside your public directory for web access
|
||||||
|
GENERATED_IMAGES_DIR=/app/public/generated-images
|
||||||
|
|
||||||
|
# Maximum time to wait for image generation (in milliseconds)
|
||||||
|
# Default: 180000 (3 minutes)
|
||||||
|
IMAGE_GENERATION_TIMEOUT=180000
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# STABLE DIFFUSION SETTINGS (Optional - Overrides n8n workflow defaults)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Default image dimensions
|
||||||
|
SD_DEFAULT_WIDTH=1024
|
||||||
|
SD_DEFAULT_HEIGHT=768
|
||||||
|
|
||||||
|
# Generation quality settings
|
||||||
|
SD_DEFAULT_STEPS=30
|
||||||
|
SD_DEFAULT_CFG_SCALE=7
|
||||||
|
|
||||||
|
# Sampler algorithm
|
||||||
|
# Options: "Euler a", "DPM++ 2M Karras", "DDIM", etc.
|
||||||
|
SD_DEFAULT_SAMPLER=DPM++ 2M Karras
|
||||||
|
|
||||||
|
# Default model checkpoint
|
||||||
|
# SD_DEFAULT_MODEL=sd_xl_base_1.0.safetensors
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FEATURE FLAGS (Optional)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Enable/disable specific features
|
||||||
|
ENABLE_IMAGE_REGENERATION=true
|
||||||
|
ENABLE_BATCH_GENERATION=false
|
||||||
|
ENABLE_IMAGE_OPTIMIZATION=true
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# LOGGING & MONITORING (Optional)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Log all image generation requests
|
||||||
|
LOG_IMAGE_GENERATION=true
|
||||||
|
|
||||||
|
# Send notifications on generation success/failure
|
||||||
|
# DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
|
||||||
|
# SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ADVANCED SETTINGS (Optional)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Custom prompt prefix for all generations
|
||||||
|
# SD_CUSTOM_PROMPT_PREFIX=professional tech illustration, modern design,
|
||||||
|
|
||||||
|
# Custom negative prompt suffix for all generations
|
||||||
|
# SD_CUSTOM_NEGATIVE_SUFFIX=low quality, blurry, pixelated, text, watermark
|
||||||
|
|
||||||
|
# Image file naming pattern
|
||||||
|
# Available variables: {projectId}, {timestamp}, {title}
|
||||||
|
IMAGE_FILENAME_PATTERN=project-{projectId}-{timestamp}.png
|
||||||
|
|
||||||
|
# Maximum concurrent image generation requests
|
||||||
|
MAX_CONCURRENT_GENERATIONS=2
|
||||||
|
|
||||||
|
# Retry failed generations
|
||||||
|
AUTO_RETRY_ON_FAILURE=true
|
||||||
|
MAX_RETRY_ATTEMPTS=3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Environment
|
||||||
|
|
||||||
|
For production deployments, adjust these settings:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Production n8n (if using cloud/dedicated instance)
|
||||||
|
N8N_WEBHOOK_URL=https://n8n.yourdomain.com/webhook
|
||||||
|
|
||||||
|
# Production Stable Diffusion (if using dedicated GPU server)
|
||||||
|
SD_API_URL=https://sd-api.yourdomain.com
|
||||||
|
|
||||||
|
# Production image storage (use absolute path)
|
||||||
|
GENERATED_IMAGES_DIR=/var/www/portfolio/public/generated-images
|
||||||
|
|
||||||
|
# Disable auto-generation in production (manual only)
|
||||||
|
AUTO_GENERATE_IMAGES=false
|
||||||
|
|
||||||
|
# Enable logging
|
||||||
|
LOG_IMAGE_GENERATION=true
|
||||||
|
|
||||||
|
# Set timeouts appropriately
|
||||||
|
IMAGE_GENERATION_TIMEOUT=300000
|
||||||
|
|
||||||
|
# Limit concurrent generations
|
||||||
|
MAX_CONCURRENT_GENERATIONS=1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Environment
|
||||||
|
|
||||||
|
If running in Docker, use these paths:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker-specific paths
|
||||||
|
N8N_WEBHOOK_URL=http://n8n:5678/webhook
|
||||||
|
SD_API_URL=http://stable-diffusion:7860
|
||||||
|
GENERATED_IMAGES_DIR=/app/public/generated-images
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
portfolio:
|
||||||
|
environment:
|
||||||
|
- N8N_WEBHOOK_URL=http://n8n:5678/webhook
|
||||||
|
- N8N_SECRET_TOKEN=${N8N_SECRET_TOKEN}
|
||||||
|
- SD_API_URL=http://stable-diffusion:7860
|
||||||
|
- AUTO_GENERATE_IMAGES=true
|
||||||
|
- GENERATED_IMAGES_DIR=/app/public/generated-images
|
||||||
|
volumes:
|
||||||
|
- ./public/generated-images:/app/public/generated-images
|
||||||
|
|
||||||
|
n8n:
|
||||||
|
image: n8nio/n8n
|
||||||
|
ports:
|
||||||
|
- "5678:5678"
|
||||||
|
environment:
|
||||||
|
- N8N_BASIC_AUTH_ACTIVE=true
|
||||||
|
- N8N_BASIC_AUTH_USER=admin
|
||||||
|
- N8N_BASIC_AUTH_PASSWORD=${N8N_PASSWORD}
|
||||||
|
|
||||||
|
stable-diffusion:
|
||||||
|
image: your-sd-webui-image
|
||||||
|
ports:
|
||||||
|
- "7860:7860"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: 1
|
||||||
|
capabilities: [gpu]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cloud GPU Configuration
|
||||||
|
|
||||||
|
If using cloud GPU services (RunPod, vast.ai, etc.):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remote GPU URL with authentication
|
||||||
|
SD_API_URL=https://your-runpod-instance.com:7860
|
||||||
|
SD_API_KEY=your-api-key-here
|
||||||
|
|
||||||
|
# Longer timeout for network latency
|
||||||
|
IMAGE_GENERATION_TIMEOUT=300000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Never commit `.env.local` to version control**
|
||||||
|
```bash
|
||||||
|
# Add to .gitignore
|
||||||
|
echo ".env.local" >> .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Generate secure tokens**
|
||||||
|
```bash
|
||||||
|
# Generate N8N_SECRET_TOKEN
|
||||||
|
openssl rand -hex 32
|
||||||
|
|
||||||
|
# Or using Node.js
|
||||||
|
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Restrict API access**
|
||||||
|
- Use firewall rules to limit SD API access
|
||||||
|
- Enable authentication on n8n webhooks
|
||||||
|
- Use HTTPS in production
|
||||||
|
|
||||||
|
4. **Environment-specific files**
|
||||||
|
- `.env.local` - local development
|
||||||
|
- `.env.production` - production (server-side only)
|
||||||
|
- `.env.test` - testing environment
|
||||||
|
|
||||||
|
## Verifying Configuration
|
||||||
|
|
||||||
|
Test your environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if variables are loaded
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# In another terminal
|
||||||
|
node -e "
|
||||||
|
const envFile = require('fs').readFileSync('.env.local', 'utf8');
|
||||||
|
console.log('✓ .env.local exists');
|
||||||
|
console.log('✓ Variables found:', envFile.split('\\n').filter(l => l && !l.startsWith('#')).length);
|
||||||
|
"
|
||||||
|
|
||||||
|
# Test n8n connection
|
||||||
|
curl -f $N8N_WEBHOOK_URL/health || echo "n8n not reachable"
|
||||||
|
|
||||||
|
# Test SD API connection
|
||||||
|
curl -f $SD_API_URL/sdapi/v1/sd-models || echo "SD API not reachable"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Variables not loading
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ensure .env.local is in the project root
|
||||||
|
ls -la .env.local
|
||||||
|
|
||||||
|
# Restart Next.js dev server
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wrong paths in Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check volume mounts
|
||||||
|
docker-compose exec portfolio ls -la /app/public/generated-images
|
||||||
|
|
||||||
|
# Fix permissions
|
||||||
|
docker-compose exec portfolio chmod 755 /app/public/generated-images
|
||||||
|
```
|
||||||
|
|
||||||
|
### n8n webhook unreachable
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check n8n is running
|
||||||
|
docker ps | grep n8n
|
||||||
|
|
||||||
|
# Check network connectivity
|
||||||
|
docker-compose exec portfolio ping n8n
|
||||||
|
|
||||||
|
# Verify webhook URL in n8n UI
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Complete Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env.local - Complete working example
|
||||||
|
|
||||||
|
# Database (required for project data)
|
||||||
|
DATABASE_URL=postgresql://user:password@localhost:5432/portfolio
|
||||||
|
|
||||||
|
# NextAuth (if using authentication)
|
||||||
|
NEXTAUTH_URL=http://localhost:3000
|
||||||
|
NEXTAUTH_SECRET=your-nextauth-secret
|
||||||
|
|
||||||
|
# AI Image Generation
|
||||||
|
N8N_WEBHOOK_URL=http://localhost:5678/webhook
|
||||||
|
N8N_SECRET_TOKEN=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6
|
||||||
|
SD_API_URL=http://localhost:7860
|
||||||
|
AUTO_GENERATE_IMAGES=true
|
||||||
|
GENERATED_IMAGES_DIR=/Users/dennis/code/gitea/portfolio/public/generated-images
|
||||||
|
|
||||||
|
# Image settings
|
||||||
|
SD_DEFAULT_WIDTH=1024
|
||||||
|
SD_DEFAULT_HEIGHT=768
|
||||||
|
SD_DEFAULT_STEPS=30
|
||||||
|
SD_DEFAULT_CFG_SCALE=7
|
||||||
|
SD_DEFAULT_SAMPLER=DPM++ 2M Karras
|
||||||
|
|
||||||
|
# Optional features
|
||||||
|
ENABLE_IMAGE_REGENERATION=true
|
||||||
|
LOG_IMAGE_GENERATION=true
|
||||||
|
IMAGE_GENERATION_TIMEOUT=180000
|
||||||
|
MAX_CONCURRENT_GENERATIONS=2
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note**: Always keep your `.env.local` file secure and never share tokens publicly!
|
||||||
612
docs/ai-image-generation/PROMPT_TEMPLATES.md
Normal file
612
docs/ai-image-generation/PROMPT_TEMPLATES.md
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
# AI Image Generation Prompt Templates
|
||||||
|
|
||||||
|
This document contains optimized prompt templates for different project categories to ensure consistent, high-quality AI-generated images.
|
||||||
|
|
||||||
|
## Template Structure
|
||||||
|
|
||||||
|
Each template follows this structure:
|
||||||
|
- **Base Prompt**: Core visual elements and style
|
||||||
|
- **Technical Keywords**: Category-specific terminology
|
||||||
|
- **Color Palette**: Recommended colors for the category
|
||||||
|
- **Negative Prompt**: Elements to avoid
|
||||||
|
- **Recommended Model**: Best SD model for this category
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Web Application Projects
|
||||||
|
|
||||||
|
### Base Prompt
|
||||||
|
```
|
||||||
|
modern web application interface, clean dashboard UI, sleek web design,
|
||||||
|
gradient backgrounds, glass morphism effect, floating panels,
|
||||||
|
data visualization charts, modern typography,
|
||||||
|
soft shadows, depth layers, isometric perspective,
|
||||||
|
professional tech aesthetic, vibrant interface elements,
|
||||||
|
smooth gradients, minimalist composition,
|
||||||
|
4k resolution, high quality digital art
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Keywords
|
||||||
|
- SaaS dashboard, web portal, admin panel
|
||||||
|
- Interactive UI elements, responsive design
|
||||||
|
- Navigation bars, sidebars, cards
|
||||||
|
- Progress indicators, status badges
|
||||||
|
|
||||||
|
### Color Palette
|
||||||
|
- Primary: `#3B82F6` (Blue), `#8B5CF6` (Purple)
|
||||||
|
- Secondary: `#06B6D4` (Cyan), `#EC4899` (Pink)
|
||||||
|
- Accent: `#10B981` (Green), `#F59E0B` (Amber)
|
||||||
|
|
||||||
|
### Negative Prompt
|
||||||
|
```
|
||||||
|
mobile phone, smartphone, app mockup, tablet,
|
||||||
|
realistic photo, stock photo, people, faces,
|
||||||
|
cluttered, messy, dark, gloomy, text, watermark
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Model
|
||||||
|
- SDXL Base 1.0
|
||||||
|
- DreamShaper 8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mobile Application Projects
|
||||||
|
|
||||||
|
### Base Prompt
|
||||||
|
```
|
||||||
|
modern mobile app interface mockup, sleek smartphone design,
|
||||||
|
iOS or Android app screen, mobile UI elements,
|
||||||
|
app icons grid, notification badges, bottom navigation,
|
||||||
|
touch gestures indicators, smooth animations preview,
|
||||||
|
gradient app background, modern mobile design trends,
|
||||||
|
floating action button, card-based layout,
|
||||||
|
professional mobile photography, studio lighting,
|
||||||
|
4k quality, trending on dribbble
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Keywords
|
||||||
|
- Native app, cross-platform, Flutter, React Native
|
||||||
|
- Mobile-first design, touch interface
|
||||||
|
- Swipe gestures, pull-to-refresh
|
||||||
|
- Push notifications, app widgets
|
||||||
|
|
||||||
|
### Color Palette
|
||||||
|
- Primary: `#6366F1` (Indigo), `#EC4899` (Pink)
|
||||||
|
- Secondary: `#8B5CF6` (Purple), `#06B6D4` (Cyan)
|
||||||
|
- Accent: `#F59E0B` (Amber), `#EF4444` (Red)
|
||||||
|
|
||||||
|
### Negative Prompt
|
||||||
|
```
|
||||||
|
desktop interface, web browser, laptop, monitor,
|
||||||
|
desktop computer, keyboard, mouse,
|
||||||
|
old phone, cracked screen, low resolution,
|
||||||
|
text, watermark, people holding phones
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Model
|
||||||
|
- Realistic Vision V5.1
|
||||||
|
- Juggernaut XL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DevOps & Infrastructure Projects
|
||||||
|
|
||||||
|
### Base Prompt
|
||||||
|
```
|
||||||
|
cloud infrastructure visualization, modern server architecture diagram,
|
||||||
|
Docker containers network, Kubernetes cluster illustration,
|
||||||
|
CI/CD pipeline flowchart, automated deployment system,
|
||||||
|
interconnected server nodes, data flow arrows,
|
||||||
|
cloud services icons, microservices architecture,
|
||||||
|
network topology, distributed systems,
|
||||||
|
glowing connections, tech blueprint style,
|
||||||
|
isometric technical illustration, cyberpunk aesthetic,
|
||||||
|
blue and orange tech colors, professional diagram
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Keywords
|
||||||
|
- Docker Swarm, Kubernetes, container orchestration
|
||||||
|
- CI/CD pipeline, Jenkins, GitHub Actions
|
||||||
|
- Cloud architecture, AWS, Azure, GCP
|
||||||
|
- Monitoring dashboard, Grafana, Prometheus
|
||||||
|
|
||||||
|
### Color Palette
|
||||||
|
- Primary: `#0EA5E9` (Sky Blue), `#F97316` (Orange)
|
||||||
|
- Secondary: `#06B6D4` (Cyan), `#8B5CF6` (Purple)
|
||||||
|
- Accent: `#10B981` (Green), `#EF4444` (Red)
|
||||||
|
|
||||||
|
### Negative Prompt
|
||||||
|
```
|
||||||
|
realistic datacenter photo, physical servers,
|
||||||
|
people, technicians, hands, cables mess,
|
||||||
|
dark server room, blurry, low quality,
|
||||||
|
text labels, company logos, watermark
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Model
|
||||||
|
- SDXL Base 1.0
|
||||||
|
- DreamShaper 8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend & API Projects
|
||||||
|
|
||||||
|
### Base Prompt
|
||||||
|
```
|
||||||
|
API architecture visualization, RESTful endpoints diagram,
|
||||||
|
database schema illustration, data flow architecture,
|
||||||
|
server-side processing, microservices connections,
|
||||||
|
API gateway, request-response flow,
|
||||||
|
JSON data structures, GraphQL schema visualization,
|
||||||
|
modern backend architecture, technical blueprint,
|
||||||
|
glowing data streams, interconnected services,
|
||||||
|
professional tech diagram, isometric view,
|
||||||
|
clean composition, high quality illustration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Keywords
|
||||||
|
- REST API, GraphQL, WebSocket
|
||||||
|
- Microservices, serverless functions
|
||||||
|
- Database architecture, SQL, NoSQL
|
||||||
|
- Authentication, JWT, OAuth
|
||||||
|
|
||||||
|
### Color Palette
|
||||||
|
- Primary: `#8B5CF6` (Purple), `#06B6D4` (Cyan)
|
||||||
|
- Secondary: `#3B82F6` (Blue), `#10B981` (Green)
|
||||||
|
- Accent: `#F59E0B` (Amber), `#EC4899` (Pink)
|
||||||
|
|
||||||
|
### Negative Prompt
|
||||||
|
```
|
||||||
|
frontend UI, user interface, buttons, forms,
|
||||||
|
people, faces, hands, realistic photo,
|
||||||
|
messy cables, physical hardware,
|
||||||
|
text, code snippets, watermark
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Model
|
||||||
|
- SDXL Base 1.0
|
||||||
|
- DreamShaper 8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI & Machine Learning Projects
|
||||||
|
|
||||||
|
### Base Prompt
|
||||||
|
```
|
||||||
|
artificial intelligence concept art, neural network visualization,
|
||||||
|
glowing AI nodes and connections, machine learning algorithm,
|
||||||
|
data science visualization, deep learning architecture,
|
||||||
|
brain-inspired computing, futuristic AI interface,
|
||||||
|
holographic data displays, floating neural pathways,
|
||||||
|
AI chip design, quantum computing aesthetic,
|
||||||
|
particle systems, energy flows, digital consciousness,
|
||||||
|
sci-fi technology, purple and cyan neon lighting,
|
||||||
|
high-tech laboratory, 4k quality, cinematic lighting
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Keywords
|
||||||
|
- Neural networks, deep learning, TensorFlow
|
||||||
|
- Computer vision, NLP, transformers
|
||||||
|
- Model training, GPU acceleration
|
||||||
|
- AI agents, reinforcement learning
|
||||||
|
|
||||||
|
### Color Palette
|
||||||
|
- Primary: `#8B5CF6` (Purple), `#EC4899` (Pink)
|
||||||
|
- Secondary: `#06B6D4` (Cyan), `#3B82F6` (Blue)
|
||||||
|
- Accent: `#A855F7` (Fuchsia), `#14B8A6` (Teal)
|
||||||
|
|
||||||
|
### Negative Prompt
|
||||||
|
```
|
||||||
|
realistic lab photo, scientists, people, faces,
|
||||||
|
physical robots, mechanical parts,
|
||||||
|
cluttered, messy, text, formulas, equations,
|
||||||
|
low quality, dark, gloomy, stock photo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Model
|
||||||
|
- SDXL Base 1.0
|
||||||
|
- Juggernaut XL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Game Development Projects
|
||||||
|
|
||||||
|
### Base Prompt
|
||||||
|
```
|
||||||
|
game environment scene, 3D rendered game world,
|
||||||
|
video game interface, game UI overlay, HUD elements,
|
||||||
|
fantasy game landscape, sci-fi game setting,
|
||||||
|
character perspective view, gaming atmosphere,
|
||||||
|
dynamic lighting, particle effects, atmospheric fog,
|
||||||
|
game asset showcase, level design preview,
|
||||||
|
cinematic game screenshot, unreal engine quality,
|
||||||
|
vibrant game colors, epic composition,
|
||||||
|
4k game graphics, trending on artstation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Keywords
|
||||||
|
- Unity, Unreal Engine, game engine
|
||||||
|
- 3D modeling, texture mapping, shaders
|
||||||
|
- Game mechanics, physics engine
|
||||||
|
- Multiplayer, networking, matchmaking
|
||||||
|
|
||||||
|
### Color Palette
|
||||||
|
- Primary: `#EF4444` (Red), `#F59E0B` (Amber)
|
||||||
|
- Secondary: `#8B5CF6` (Purple), `#06B6D4` (Cyan)
|
||||||
|
- Accent: `#10B981` (Green), `#EC4899` (Pink)
|
||||||
|
|
||||||
|
### Negative Prompt
|
||||||
|
```
|
||||||
|
real photo, realistic photography, real people,
|
||||||
|
mobile game screenshot, casual game,
|
||||||
|
low poly, pixelated, retro graphics,
|
||||||
|
text, game title, logos, watermark
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Model
|
||||||
|
- Juggernaut XL
|
||||||
|
- DreamShaper 8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blockchain & Crypto Projects
|
||||||
|
|
||||||
|
### Base Prompt
|
||||||
|
```
|
||||||
|
blockchain network visualization, cryptocurrency concept art,
|
||||||
|
distributed ledger technology, decentralized network nodes,
|
||||||
|
crypto mining visualization, digital currency symbols,
|
||||||
|
smart contracts interface, DeFi platform design,
|
||||||
|
glowing blockchain connections, cryptographic security,
|
||||||
|
web3 technology aesthetic, neon blockchain grid,
|
||||||
|
futuristic finance, holographic crypto data,
|
||||||
|
clean modern composition, professional tech illustration,
|
||||||
|
blue and gold color scheme, high quality render
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Keywords
|
||||||
|
- Smart contracts, Solidity, Ethereum
|
||||||
|
- DeFi, NFT, token economics
|
||||||
|
- Consensus mechanisms, proof of stake
|
||||||
|
- Web3, dApp, wallet integration
|
||||||
|
|
||||||
|
### Color Palette
|
||||||
|
- Primary: `#F59E0B` (Gold), `#3B82F6` (Blue)
|
||||||
|
- Secondary: `#8B5CF6` (Purple), `#10B981` (Green)
|
||||||
|
- Accent: `#06B6D4` (Cyan), `#EC4899` (Pink)
|
||||||
|
|
||||||
|
### Negative Prompt
|
||||||
|
```
|
||||||
|
real coins, physical money, paper currency,
|
||||||
|
people, traders, faces, hands,
|
||||||
|
stock market photo, trading floor,
|
||||||
|
text, ticker symbols, logos, watermark
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Model
|
||||||
|
- SDXL Base 1.0
|
||||||
|
- Juggernaut XL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IoT & Hardware Projects
|
||||||
|
|
||||||
|
### Base Prompt
|
||||||
|
```
|
||||||
|
Internet of Things network, smart home devices connected,
|
||||||
|
IoT sensor network, embedded systems visualization,
|
||||||
|
smart device ecosystem, wireless communication,
|
||||||
|
connected hardware illustration, automation network,
|
||||||
|
sensor data visualization, edge computing nodes,
|
||||||
|
modern tech devices, clean product design,
|
||||||
|
isometric hardware illustration, minimalist tech aesthetic,
|
||||||
|
glowing connection lines, mesh network topology,
|
||||||
|
professional product photography, studio lighting
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Keywords
|
||||||
|
- Raspberry Pi, Arduino, ESP32
|
||||||
|
- Sensor networks, MQTT, edge computing
|
||||||
|
- Smart home, automation, wireless protocols
|
||||||
|
- Embedded systems, firmware, microcontrollers
|
||||||
|
|
||||||
|
### Color Palette
|
||||||
|
- Primary: `#10B981` (Green), `#06B6D4` (Cyan)
|
||||||
|
- Secondary: `#3B82F6` (Blue), `#8B5CF6` (Purple)
|
||||||
|
- Accent: `#F59E0B` (Amber), `#EC4899` (Pink)
|
||||||
|
|
||||||
|
### Negative Prompt
|
||||||
|
```
|
||||||
|
messy wiring, cluttered breadboard, realistic lab photo,
|
||||||
|
people, hands holding devices, technicians,
|
||||||
|
old electronics, broken hardware,
|
||||||
|
text, labels, brand names, watermark
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Model
|
||||||
|
- Realistic Vision V5.1
|
||||||
|
- Juggernaut XL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security & Cybersecurity Projects
|
||||||
|
|
||||||
|
### Base Prompt
|
||||||
|
```
|
||||||
|
cybersecurity concept art, digital security shield,
|
||||||
|
encrypted data streams, firewall visualization,
|
||||||
|
network security diagram, threat detection system,
|
||||||
|
secure connection network, cryptography illustration,
|
||||||
|
cyber defense interface, security monitoring dashboard,
|
||||||
|
glowing security barriers, protected data vault,
|
||||||
|
ethical hacking interface, penetration testing tools,
|
||||||
|
dark mode tech aesthetic, green matrix-style code,
|
||||||
|
professional security illustration, high-tech composition
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Keywords
|
||||||
|
- Penetration testing, vulnerability scanning
|
||||||
|
- Firewall, IDS/IPS, SIEM
|
||||||
|
- Encryption, SSL/TLS, zero trust
|
||||||
|
- Security monitoring, threat intelligence
|
||||||
|
|
||||||
|
### Color Palette
|
||||||
|
- Primary: `#10B981` (Green), `#0EA5E9` (Sky Blue)
|
||||||
|
- Secondary: `#8B5CF6` (Purple), `#EF4444` (Red)
|
||||||
|
- Accent: `#F59E0B` (Amber), `#06B6D4` (Cyan)
|
||||||
|
|
||||||
|
### Negative Prompt
|
||||||
|
```
|
||||||
|
realistic office photo, security guards, people,
|
||||||
|
physical locks, keys, cameras,
|
||||||
|
dark, scary, threatening, ominous,
|
||||||
|
text, code snippets, terminal text, watermark
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Model
|
||||||
|
- SDXL Base 1.0
|
||||||
|
- DreamShaper 8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Science & Analytics Projects
|
||||||
|
|
||||||
|
### Base Prompt
|
||||||
|
```
|
||||||
|
data visualization dashboard, analytics interface,
|
||||||
|
big data processing, statistical charts and graphs,
|
||||||
|
machine learning insights, predictive analytics,
|
||||||
|
data pipeline illustration, ETL process visualization,
|
||||||
|
interactive data dashboard, business intelligence,
|
||||||
|
colorful data charts, infographic elements,
|
||||||
|
modern analytics design, clean data presentation,
|
||||||
|
professional data visualization, gradient backgrounds,
|
||||||
|
isometric data center, flowing information streams
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Keywords
|
||||||
|
- Data pipeline, ETL, data warehouse
|
||||||
|
- BI dashboard, Tableau, Power BI
|
||||||
|
- Statistical analysis, data mining
|
||||||
|
- Pandas, NumPy, data processing
|
||||||
|
|
||||||
|
### Color Palette
|
||||||
|
- Primary: `#3B82F6` (Blue), `#8B5CF6` (Purple)
|
||||||
|
- Secondary: `#06B6D4` (Cyan), `#10B981` (Green)
|
||||||
|
- Accent: `#EC4899` (Pink), `#F59E0B` (Amber)
|
||||||
|
|
||||||
|
### Negative Prompt
|
||||||
|
```
|
||||||
|
spreadsheet screenshot, Excel interface,
|
||||||
|
people analyzing data, hands, faces,
|
||||||
|
cluttered charts, messy graphs, confusing layout,
|
||||||
|
text labels, numbers, watermark, low quality
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Model
|
||||||
|
- SDXL Base 1.0
|
||||||
|
- DreamShaper 8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E-commerce & Marketplace Projects
|
||||||
|
|
||||||
|
### Base Prompt
|
||||||
|
```
|
||||||
|
modern e-commerce platform interface, online shopping design,
|
||||||
|
product showcase grid, shopping cart visualization,
|
||||||
|
payment system interface, marketplace dashboard,
|
||||||
|
product cards layout, checkout flow design,
|
||||||
|
clean storefront design, modern retail aesthetic,
|
||||||
|
shopping bag icons, product imagery, price tags design,
|
||||||
|
conversion-optimized layout, mobile commerce,
|
||||||
|
professional e-commerce photography, studio product shots,
|
||||||
|
vibrant shopping experience, user-friendly interface
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Keywords
|
||||||
|
- Online store, payment gateway, Stripe
|
||||||
|
- Product catalog, inventory management
|
||||||
|
- Shopping cart, checkout flow, conversion
|
||||||
|
- Marketplace platform, vendor management
|
||||||
|
|
||||||
|
### Color Palette
|
||||||
|
- Primary: `#EC4899` (Pink), `#F59E0B` (Amber)
|
||||||
|
- Secondary: `#8B5CF6` (Purple), `#10B981` (Green)
|
||||||
|
- Accent: `#3B82F6` (Blue), `#EF4444` (Red)
|
||||||
|
|
||||||
|
### Negative Prompt
|
||||||
|
```
|
||||||
|
realistic store photo, physical shop, retail store,
|
||||||
|
people shopping, customers, cashiers, hands,
|
||||||
|
cluttered shelves, messy products,
|
||||||
|
text prices, brand logos, watermark
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Model
|
||||||
|
- Realistic Vision V5.1
|
||||||
|
- Juggernaut XL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Automation & Workflow Projects
|
||||||
|
|
||||||
|
### Base Prompt
|
||||||
|
```
|
||||||
|
workflow automation visualization, process flow diagram,
|
||||||
|
automated pipeline illustration, task orchestration,
|
||||||
|
business process automation, workflow nodes connected,
|
||||||
|
integration platform design, automation dashboard,
|
||||||
|
robotic process automation, efficiency visualization,
|
||||||
|
streamlined processes, gear mechanisms, conveyor systems,
|
||||||
|
modern workflow interface, productivity tools,
|
||||||
|
clean automation design, professional illustration,
|
||||||
|
isometric process view, smooth gradient backgrounds
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technical Keywords
|
||||||
|
- n8n, Zapier, workflow automation
|
||||||
|
- Integration platform, API orchestration
|
||||||
|
- Task scheduling, cron jobs, triggers
|
||||||
|
- Business process automation, RPA
|
||||||
|
|
||||||
|
### Color Palette
|
||||||
|
- Primary: `#8B5CF6` (Purple), `#06B6D4` (Cyan)
|
||||||
|
- Secondary: `#10B981` (Green), `#3B82F6` (Blue)
|
||||||
|
- Accent: `#F59E0B` (Amber), `#EC4899` (Pink)
|
||||||
|
|
||||||
|
### Negative Prompt
|
||||||
|
```
|
||||||
|
realistic factory photo, physical machinery,
|
||||||
|
people working, hands, faces, workers,
|
||||||
|
cluttered, messy, industrial setting,
|
||||||
|
text, labels, watermark, low quality
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Model
|
||||||
|
- SDXL Base 1.0
|
||||||
|
- DreamShaper 8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Universal Negative Prompt
|
||||||
|
|
||||||
|
Use this as a base for all generations:
|
||||||
|
|
||||||
|
```
|
||||||
|
low quality, blurry, pixelated, grainy, jpeg artifacts, compression artifacts,
|
||||||
|
text, letters, words, numbers, watermark, signature, copyright, logo, brand name,
|
||||||
|
people, person, human, face, faces, hands, fingers, arms, body parts,
|
||||||
|
portrait, selfie, crowd, group of people,
|
||||||
|
cluttered, messy, chaotic, disorganized, busy, overwhelming,
|
||||||
|
dark, gloomy, depressing, scary, ominous, threatening,
|
||||||
|
ugly, distorted, deformed, mutation, extra limbs, bad anatomy,
|
||||||
|
realistic photo, stock photo, photograph, camera phone,
|
||||||
|
duplicate, duplication, repetitive, copied elements,
|
||||||
|
old, outdated, vintage, retro (unless specifically wanted),
|
||||||
|
screenshot, UI screenshot, browser window
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt Engineering Best Practices
|
||||||
|
|
||||||
|
### 1. Specificity Matters
|
||||||
|
- Be specific about visual elements you want
|
||||||
|
- Include style keywords: "isometric", "minimalist", "modern"
|
||||||
|
- Specify quality: "4k resolution", "high quality", "professional"
|
||||||
|
|
||||||
|
### 2. Weight Distribution
|
||||||
|
- Most important elements should be early in the prompt
|
||||||
|
- Use emphasis syntax if your tool supports it: `(keyword:1.2)` or `((keyword))`
|
||||||
|
|
||||||
|
### 3. Category Mixing
|
||||||
|
- Combine multiple category templates for hybrid projects
|
||||||
|
- Example: AI + Web App = neural network + modern dashboard UI
|
||||||
|
|
||||||
|
### 4. Color Psychology
|
||||||
|
- **Blue**: Trust, technology, corporate
|
||||||
|
- **Purple**: Innovation, creativity, luxury
|
||||||
|
- **Green**: Growth, success, eco-friendly
|
||||||
|
- **Orange**: Energy, action, excitement
|
||||||
|
- **Pink**: Modern, playful, creative
|
||||||
|
|
||||||
|
### 5. Consistency
|
||||||
|
- Use the same negative prompt across all generations
|
||||||
|
- Maintain consistent aspect ratios (4:3 for project cards)
|
||||||
|
- Stick to similar quality settings
|
||||||
|
|
||||||
|
### 6. A/B Testing
|
||||||
|
- Generate 2-3 variants with slightly different prompts
|
||||||
|
- Test which style resonates better with your audience
|
||||||
|
- Refine prompts based on results
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Techniques
|
||||||
|
|
||||||
|
### ControlNet Integration
|
||||||
|
If using ControlNet, you can guide composition:
|
||||||
|
- Use Canny edge detection for layout control
|
||||||
|
- Use Depth maps for 3D perspective
|
||||||
|
- Use OpenPose for element positioning
|
||||||
|
|
||||||
|
### Multi-Stage Generation
|
||||||
|
1. Generate base composition at lower resolution (512x512)
|
||||||
|
2. Upscale using img2img with same prompt
|
||||||
|
3. Apply post-processing (sharpening, color grading)
|
||||||
|
|
||||||
|
### Style Consistency
|
||||||
|
To maintain consistent style across all project images:
|
||||||
|
```
|
||||||
|
Add to every prompt:
|
||||||
|
"in the style of modern tech illustration, consistent design language,
|
||||||
|
professional portfolio aesthetic, cohesive visual identity"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting Common Issues
|
||||||
|
|
||||||
|
### Issue: Too Abstract / Not Related to Project
|
||||||
|
**Solution**: Add more specific technical keywords from project description
|
||||||
|
|
||||||
|
### Issue: Text Appearing in Images
|
||||||
|
**Solution**: Add multiple text-related terms to negative prompt:
|
||||||
|
`text, letters, words, typography, font, writing, characters`
|
||||||
|
|
||||||
|
### Issue: Dark or Poorly Lit
|
||||||
|
**Solution**: Add lighting keywords:
|
||||||
|
`studio lighting, bright, well-lit, soft lighting, professional lighting`
|
||||||
|
|
||||||
|
### Issue: Cluttered Composition
|
||||||
|
**Solution**: Add composition keywords:
|
||||||
|
`clean composition, minimalist, negative space, centered, balanced, organized`
|
||||||
|
|
||||||
|
### Issue: Wrong Aspect Ratio
|
||||||
|
**Solution**: Specify dimensions explicitly in generation settings:
|
||||||
|
- Cards: 1024x768 (4:3)
|
||||||
|
- Hero: 1920x1080 (16:9)
|
||||||
|
- Square: 1024x1024 (1:1)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference Card
|
||||||
|
|
||||||
|
| Category | Primary Colors | Key Style | Model |
|
||||||
|
|----------|---------------|-----------|-------|
|
||||||
|
| Web | Blue, Purple | Glass UI | SDXL |
|
||||||
|
| Mobile | Indigo, Pink | Mockup | Realistic Vision |
|
||||||
|
| DevOps | Cyan, Orange | Diagram | SDXL |
|
||||||
|
| AI/ML | Purple, Cyan | Futuristic | SDXL |
|
||||||
|
| Game | Red, Amber | Cinematic | Juggernaut |
|
||||||
|
| Blockchain | Gold, Blue | Neon | SDXL |
|
||||||
|
| IoT | Green, Cyan | Product | Realistic Vision |
|
||||||
|
| Security | Green, Blue | Dark Tech | SDXL |
|
||||||
|
| Data | Blue, Purple | Charts | SDXL |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2024
|
||||||
|
**Version**: 1.0
|
||||||
|
**Maintained by**: Portfolio AI Image Generation System
|
||||||
366
docs/ai-image-generation/QUICKSTART.md
Normal file
366
docs/ai-image-generation/QUICKSTART.md
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
# Quick Start Guide: AI Image Generation
|
||||||
|
|
||||||
|
Get AI-powered project images up and running in 15 minutes.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker installed
|
||||||
|
- 8GB+ RAM
|
||||||
|
- GPU recommended (NVIDIA with CUDA support)
|
||||||
|
- Node.js 18+ for portfolio app
|
||||||
|
|
||||||
|
## Step 1: Install Stable Diffusion WebUI (5 min)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git
|
||||||
|
cd stable-diffusion-webui
|
||||||
|
|
||||||
|
# Run with API enabled
|
||||||
|
./webui.sh --api --listen
|
||||||
|
|
||||||
|
# For low VRAM GPUs (< 8GB)
|
||||||
|
./webui.sh --api --listen --medvram
|
||||||
|
|
||||||
|
# Wait for model download and startup
|
||||||
|
# Access WebUI at: http://localhost:7860
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Download a Model (3 min)
|
||||||
|
|
||||||
|
Open WebUI at `http://localhost:7860` and download a model:
|
||||||
|
|
||||||
|
**Option A: Via WebUI**
|
||||||
|
1. Go to **Checkpoint Merger** tab
|
||||||
|
2. Click **Model Download**
|
||||||
|
3. Enter: `stabilityai/stable-diffusion-xl-base-1.0`
|
||||||
|
4. Wait for download (6.94 GB)
|
||||||
|
|
||||||
|
**Option B: Manual Download**
|
||||||
|
```bash
|
||||||
|
cd models/Stable-diffusion/
|
||||||
|
wget https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Test Stable Diffusion API (1 min)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:7860/sdapi/v1/txt2img \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"prompt": "modern tech dashboard, blue gradient, minimalist design",
|
||||||
|
"steps": 20,
|
||||||
|
"width": 512,
|
||||||
|
"height": 512
|
||||||
|
}' | jq '.images[0]' | base64 -d > test.png
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `test.png` - if you see an image, API is working! ✅
|
||||||
|
|
||||||
|
## Step 4: Setup n8n (2 min)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker Compose method
|
||||||
|
docker run -d \
|
||||||
|
--name n8n \
|
||||||
|
-p 5678:5678 \
|
||||||
|
-v ~/.n8n:/home/node/.n8n \
|
||||||
|
n8nio/n8n
|
||||||
|
|
||||||
|
# Wait 30 seconds for startup
|
||||||
|
# Access n8n at: http://localhost:5678
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5: Import Workflow (1 min)
|
||||||
|
|
||||||
|
1. Open n8n at `http://localhost:5678`
|
||||||
|
2. Create account (first time only)
|
||||||
|
3. Click **+ New Workflow**
|
||||||
|
4. Click **⋮** (three dots) → **Import from File**
|
||||||
|
5. Select `docs/ai-image-generation/n8n-workflow-ai-image-generator.json`
|
||||||
|
6. Click **Save**
|
||||||
|
|
||||||
|
## Step 6: Configure Workflow (2 min)
|
||||||
|
|
||||||
|
### A. Add PostgreSQL Credentials
|
||||||
|
1. Click **Get Project Data** node
|
||||||
|
2. Click **Credential to connect with**
|
||||||
|
3. Enter your database credentials:
|
||||||
|
- Host: `localhost` (or your DB host)
|
||||||
|
- Database: `portfolio`
|
||||||
|
- User: `your_username`
|
||||||
|
- Password: `your_password`
|
||||||
|
4. Click **Save**
|
||||||
|
|
||||||
|
### B. Configure Stable Diffusion URL
|
||||||
|
1. Click **Generate Image (Stable Diffusion)** node
|
||||||
|
2. Update URL to: `http://localhost:7860/sdapi/v1/txt2img`
|
||||||
|
3. If SD is on different machine: `http://YOUR_SD_IP:7860/sdapi/v1/txt2img`
|
||||||
|
|
||||||
|
### C. Set Webhook Authentication
|
||||||
|
1. Click **Webhook Trigger** node
|
||||||
|
2. Click **Add Credential**
|
||||||
|
3. Set header: `Authorization`
|
||||||
|
4. Set value: `Bearer your-secret-token-here`
|
||||||
|
5. Save this token - you'll need it!
|
||||||
|
|
||||||
|
### D. Update Image Save Path
|
||||||
|
1. Click **Save Image to File** node
|
||||||
|
2. Update `uploadDir` path to your portfolio's public folder:
|
||||||
|
```javascript
|
||||||
|
const uploadDir = '/path/to/portfolio/public/generated-images';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 7: Create Directory for Images (1 min)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/portfolio
|
||||||
|
mkdir -p public/generated-images
|
||||||
|
chmod 755 public/generated-images
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 8: Add Environment Variables (1 min)
|
||||||
|
|
||||||
|
Add to `portfolio/.env.local`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# n8n Webhook Configuration
|
||||||
|
N8N_WEBHOOK_URL=http://localhost:5678/webhook
|
||||||
|
N8N_SECRET_TOKEN=your-secret-token-here
|
||||||
|
|
||||||
|
# Stable Diffusion API
|
||||||
|
SD_API_URL=http://localhost:7860
|
||||||
|
|
||||||
|
# Auto-generate images for new projects
|
||||||
|
AUTO_GENERATE_IMAGES=true
|
||||||
|
|
||||||
|
# Image storage
|
||||||
|
GENERATED_IMAGES_DIR=/path/to/portfolio/public/generated-images
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 9: Test the Full Pipeline (2 min)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start your portfolio app
|
||||||
|
cd portfolio
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# In another terminal, trigger image generation
|
||||||
|
curl -X POST http://localhost:5678/webhook/ai-image-generation \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer your-secret-token-here" \
|
||||||
|
-d '{
|
||||||
|
"projectId": 1
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Check response (should take 15-30 seconds)
|
||||||
|
# Response example:
|
||||||
|
# {
|
||||||
|
# "success": true,
|
||||||
|
# "projectId": 1,
|
||||||
|
# "imageUrl": "/generated-images/project-1-1234567890.png",
|
||||||
|
# "generatedAt": "2024-01-15T10:30:00Z"
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 10: Verify Image (1 min)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if image was created
|
||||||
|
ls -lh public/generated-images/
|
||||||
|
|
||||||
|
# Open in browser
|
||||||
|
open http://localhost:3000/generated-images/project-1-*.png
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see a generated image! 🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Using the Admin UI
|
||||||
|
|
||||||
|
If you created the admin component:
|
||||||
|
|
||||||
|
1. Navigate to your admin page (create one if needed)
|
||||||
|
2. Add the AI Image Generator component:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import AIImageGenerator from '@/app/components/admin/AIImageGenerator';
|
||||||
|
|
||||||
|
<AIImageGenerator
|
||||||
|
projectId={projectId}
|
||||||
|
projectTitle="My Awesome Project"
|
||||||
|
currentImageUrl={project.imageUrl}
|
||||||
|
onImageGenerated={(url) => console.log('Generated:', url)}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Click **Generate Image** button
|
||||||
|
4. Wait 15-30 seconds
|
||||||
|
5. Image appears automatically!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Automatic Generation on New Projects
|
||||||
|
|
||||||
|
Add this to your project creation API:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In portfolio/app/api/projects/route.ts (or similar)
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
// ... your project creation code ...
|
||||||
|
|
||||||
|
const newProject = await createProject(data);
|
||||||
|
|
||||||
|
// Trigger AI image generation
|
||||||
|
if (process.env.AUTO_GENERATE_IMAGES === 'true') {
|
||||||
|
fetch(`${process.env.N8N_WEBHOOK_URL}/ai-image-generation`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${process.env.N8N_SECRET_TOKEN}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ projectId: newProject.id })
|
||||||
|
}).catch(err => console.error('AI generation failed:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(newProject);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Connection refused to localhost:7860"
|
||||||
|
```bash
|
||||||
|
# Check if SD WebUI is running
|
||||||
|
ps aux | grep webui
|
||||||
|
|
||||||
|
# Restart with API flag
|
||||||
|
cd stable-diffusion-webui
|
||||||
|
./webui.sh --api --listen
|
||||||
|
```
|
||||||
|
|
||||||
|
### "CUDA out of memory"
|
||||||
|
```bash
|
||||||
|
# Restart with lower VRAM usage
|
||||||
|
./webui.sh --api --listen --medvram
|
||||||
|
|
||||||
|
# Or even lower
|
||||||
|
./webui.sh --api --listen --lowvram
|
||||||
|
```
|
||||||
|
|
||||||
|
### "n8n workflow fails at database step"
|
||||||
|
- Check PostgreSQL is running: `pg_isready`
|
||||||
|
- Verify credentials in n8n node
|
||||||
|
- Check database connection from terminal:
|
||||||
|
```bash
|
||||||
|
psql -h localhost -U your_username -d portfolio
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Image saves but doesn't appear on website"
|
||||||
|
- Check directory permissions: `chmod 755 public/generated-images`
|
||||||
|
- Verify path in n8n workflow matches portfolio structure
|
||||||
|
- Check Next.js static files config in `next.config.js`
|
||||||
|
|
||||||
|
### "Generated images are low quality"
|
||||||
|
Edit n8n workflow's SD node, increase:
|
||||||
|
- `steps`: 20 → 40
|
||||||
|
- `cfg_scale`: 7 → 9
|
||||||
|
- `width/height`: 512 → 1024
|
||||||
|
|
||||||
|
### "Images don't match project theme"
|
||||||
|
Edit **Build AI Prompt** node in n8n:
|
||||||
|
- Add more specific technical keywords
|
||||||
|
- Include project category in style description
|
||||||
|
- Adjust color palette keywords
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
✅ **You're done!** Images now generate automatically.
|
||||||
|
|
||||||
|
**Optional Enhancements:**
|
||||||
|
|
||||||
|
1. **Batch Generate**: Generate images for all existing projects
|
||||||
|
```bash
|
||||||
|
# Create a script: scripts/batch-generate-images.ts
|
||||||
|
for projectId in $(psql -t -c "SELECT id FROM projects WHERE image_url IS NULL"); do
|
||||||
|
curl -X POST http://localhost:5678/webhook/ai-image-generation \
|
||||||
|
-H "Authorization: Bearer $N8N_SECRET_TOKEN" \
|
||||||
|
-d "{\"projectId\": $projectId}"
|
||||||
|
sleep 30 # Wait for generation
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Custom Models**: Download specialized models for better results
|
||||||
|
- `dreamshaper_8.safetensors` for web/UI projects
|
||||||
|
- `realisticVision_v51.safetensors` for product shots
|
||||||
|
- `juggernautXL_v8.safetensors` for modern tech aesthetics
|
||||||
|
|
||||||
|
3. **Prompt Refinement**: Edit prompt templates in n8n workflow
|
||||||
|
- Check `docs/ai-image-generation/PROMPT_TEMPLATES.md`
|
||||||
|
- Test different styles for your brand
|
||||||
|
|
||||||
|
4. **Monitoring**: Set up logging and alerts
|
||||||
|
- Add Discord/Slack notifications to n8n workflow
|
||||||
|
- Log generation stats to analytics
|
||||||
|
|
||||||
|
5. **Optimization**: Compress images after generation
|
||||||
|
```bash
|
||||||
|
npm install sharp
|
||||||
|
# Add post-processing step to n8n workflow
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Benchmarks
|
||||||
|
|
||||||
|
| Hardware | Generation Time | Image Quality |
|
||||||
|
|----------|----------------|---------------|
|
||||||
|
| RTX 4090 | ~8 seconds | Excellent |
|
||||||
|
| RTX 3080 | ~15 seconds | Excellent |
|
||||||
|
| RTX 3060 | ~25 seconds | Good |
|
||||||
|
| GTX 1660 | ~45 seconds | Good |
|
||||||
|
| CPU only | ~5 minutes | Fair |
|
||||||
|
|
||||||
|
**Recommended**: RTX 3060 or better for production use.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cost Analysis
|
||||||
|
|
||||||
|
**Local Setup (One-time):**
|
||||||
|
- GPU (RTX 3060): ~$300-400
|
||||||
|
- OR Cloud GPU (RunPod, vast.ai): $0.20-0.50/hour
|
||||||
|
|
||||||
|
**Per Image Cost:**
|
||||||
|
- Local: $0.00 (electricity ~$0.001)
|
||||||
|
- Cloud GPU: ~$0.01-0.02 per image
|
||||||
|
|
||||||
|
**vs. Commercial APIs:**
|
||||||
|
- DALL-E 3: $0.04 per image
|
||||||
|
- Midjourney: ~$0.06 per image (with subscription)
|
||||||
|
- Stable Diffusion API: $0.02 per image
|
||||||
|
|
||||||
|
💡 **Break-even**: After ~500 images, local setup pays for itself!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support & Resources
|
||||||
|
|
||||||
|
- **Documentation**: `docs/ai-image-generation/SETUP.md`
|
||||||
|
- **Prompt Templates**: `docs/ai-image-generation/PROMPT_TEMPLATES.md`
|
||||||
|
- **SD WebUI Wiki**: https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki
|
||||||
|
- **n8n Documentation**: https://docs.n8n.io
|
||||||
|
- **Community Discord**: [Your Discord link]
|
||||||
|
|
||||||
|
**Need Help?** Open an issue or reach out!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total Setup Time**: ~15 minutes
|
||||||
|
**Result**: Automatic AI-generated project images 🎨✨
|
||||||
423
docs/ai-image-generation/README.md
Normal file
423
docs/ai-image-generation/README.md
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
# AI Image Generation System
|
||||||
|
|
||||||
|
Automatically generate stunning project cover images using local AI models.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## 🎨 What is this?
|
||||||
|
|
||||||
|
This system automatically creates professional, tech-themed cover images for your portfolio projects using AI. No more stock photos, no design skills needed.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
✨ **Fully Automatic** - Generate images when creating new projects
|
||||||
|
🎯 **Context-Aware** - Uses project title, description, category, and tech stack
|
||||||
|
🖼️ **High Quality** - 1024x768 optimized for web display
|
||||||
|
🔒 **Privacy-First** - Runs 100% locally, no data sent to external APIs
|
||||||
|
⚡ **Fast** - 15-30 seconds per image with GPU
|
||||||
|
💰 **Free** - No per-image costs after initial setup
|
||||||
|
🎨 **Customizable** - Full control over style, colors, and aesthetics
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
**Want to get started in 15 minutes?** → Check out [QUICKSTART.md](./QUICKSTART.md)
|
||||||
|
|
||||||
|
**For detailed setup and configuration** → See [SETUP.md](./SETUP.md)
|
||||||
|
|
||||||
|
## 📋 Table of Contents
|
||||||
|
|
||||||
|
- [How It Works](#how-it-works)
|
||||||
|
- [System Architecture](#system-architecture)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Usage](#usage)
|
||||||
|
- [Prompt Engineering](#prompt-engineering)
|
||||||
|
- [Examples](#examples)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [FAQ](#faq)
|
||||||
|
|
||||||
|
## 🔧 How It Works
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[Create Project] --> B[Trigger n8n Webhook]
|
||||||
|
B --> C[Fetch Project Data]
|
||||||
|
C --> D[Build AI Prompt]
|
||||||
|
D --> E[Stable Diffusion]
|
||||||
|
E --> F[Save Image]
|
||||||
|
F --> G[Update Database]
|
||||||
|
G --> H[Display on Site]
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Project Creation**: You create or update a project
|
||||||
|
2. **Data Extraction**: System reads project metadata (title, description, tags, category)
|
||||||
|
3. **Prompt Generation**: AI-optimized prompt is created based on project type
|
||||||
|
4. **Image Generation**: Stable Diffusion generates a unique image
|
||||||
|
5. **Storage**: Image is saved and optimized
|
||||||
|
6. **Database Update**: Project's `imageUrl` is updated
|
||||||
|
7. **Display**: Image appears automatically on your portfolio
|
||||||
|
|
||||||
|
## 🏗️ System Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Portfolio App │
|
||||||
|
│ (Next.js) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ n8n Workflow │─────▶│ PostgreSQL DB │
|
||||||
|
│ (Automation) │◀─────│ (Projects) │
|
||||||
|
└────────┬────────┘ └─────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Stable Diffusion│
|
||||||
|
│ WebUI │
|
||||||
|
│ (Image Gen) │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
- **Next.js App**: Frontend and API endpoints
|
||||||
|
- **n8n**: Workflow automation and orchestration
|
||||||
|
- **Stable Diffusion**: Local AI image generation
|
||||||
|
- **PostgreSQL**: Project data storage
|
||||||
|
- **File System**: Generated image storage
|
||||||
|
|
||||||
|
## 📦 Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- **Node.js** 18+
|
||||||
|
- **Docker** (recommended) or Python 3.10+
|
||||||
|
- **PostgreSQL** database
|
||||||
|
- **8GB+ RAM** minimum
|
||||||
|
- **GPU recommended** (NVIDIA with CUDA support)
|
||||||
|
- Minimum: GTX 1060 6GB
|
||||||
|
- Recommended: RTX 3060 12GB or better
|
||||||
|
- Also works on CPU (slower)
|
||||||
|
|
||||||
|
### Step-by-Step Setup
|
||||||
|
|
||||||
|
#### 1. Install Stable Diffusion WebUI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git
|
||||||
|
cd stable-diffusion-webui
|
||||||
|
./webui.sh --api --listen
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for model download (~7GB). Access at: `http://localhost:7860`
|
||||||
|
|
||||||
|
#### 2. Install n8n
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker (recommended)
|
||||||
|
docker run -d --name n8n -p 5678:5678 -v ~/.n8n:/home/node/.n8n n8nio/n8n
|
||||||
|
|
||||||
|
# Or npm
|
||||||
|
npm install -g n8n
|
||||||
|
n8n start
|
||||||
|
```
|
||||||
|
|
||||||
|
Access at: `http://localhost:5678`
|
||||||
|
|
||||||
|
#### 3. Import Workflow
|
||||||
|
|
||||||
|
1. Open n8n at `http://localhost:5678`
|
||||||
|
2. Import `n8n-workflow-ai-image-generator.json`
|
||||||
|
3. Configure database credentials
|
||||||
|
4. Update Stable Diffusion API URL
|
||||||
|
5. Set webhook authentication token
|
||||||
|
|
||||||
|
#### 4. Configure Portfolio App
|
||||||
|
|
||||||
|
Add to `.env.local`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
N8N_WEBHOOK_URL=http://localhost:5678/webhook
|
||||||
|
N8N_SECRET_TOKEN=your-secure-token-here
|
||||||
|
SD_API_URL=http://localhost:7860
|
||||||
|
AUTO_GENERATE_IMAGES=true
|
||||||
|
GENERATED_IMAGES_DIR=/path/to/portfolio/public/generated-images
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Create Image Directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p public/generated-images
|
||||||
|
chmod 755 public/generated-images
|
||||||
|
```
|
||||||
|
|
||||||
|
**That's it!** 🎉 You're ready to generate images.
|
||||||
|
|
||||||
|
## 💻 Usage
|
||||||
|
|
||||||
|
### Automatic Generation
|
||||||
|
|
||||||
|
When you create a new project, an image is automatically generated:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In your project creation API
|
||||||
|
const newProject = await createProject(data);
|
||||||
|
|
||||||
|
if (process.env.AUTO_GENERATE_IMAGES === 'true') {
|
||||||
|
await fetch(`${process.env.N8N_WEBHOOK_URL}/ai-image-generation`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${process.env.N8N_SECRET_TOKEN}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ projectId: newProject.id })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Generation via API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/n8n/generate-image \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-d '{"projectId": 123}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Admin UI Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import AIImageGenerator from '@/app/components/admin/AIImageGenerator';
|
||||||
|
|
||||||
|
<AIImageGenerator
|
||||||
|
projectId={project.id}
|
||||||
|
projectTitle={project.title}
|
||||||
|
currentImageUrl={project.imageUrl}
|
||||||
|
onImageGenerated={(url) => {
|
||||||
|
console.log('New image:', url);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Generation
|
||||||
|
|
||||||
|
Generate images for all existing projects:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get all projects without images
|
||||||
|
psql -d portfolio -t -c "SELECT id FROM projects WHERE image_url IS NULL" | while read id; do
|
||||||
|
curl -X POST http://localhost:3000/api/n8n/generate-image \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"projectId\": $id}"
|
||||||
|
sleep 30 # Wait for generation
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Prompt Engineering
|
||||||
|
|
||||||
|
The system automatically generates optimized prompts based on project category:
|
||||||
|
|
||||||
|
### Web Application Example
|
||||||
|
|
||||||
|
**Input Project:**
|
||||||
|
- Title: "Real-Time Analytics Dashboard"
|
||||||
|
- Category: "web"
|
||||||
|
- Tags: ["React", "Next.js", "TypeScript"]
|
||||||
|
|
||||||
|
**Generated Prompt:**
|
||||||
|
```
|
||||||
|
Professional tech project cover image, modern web interface,
|
||||||
|
clean dashboard UI, gradient backgrounds, glass morphism effect,
|
||||||
|
representing "Real-Time Analytics Dashboard", React, Next.js, TypeScript,
|
||||||
|
modern minimalist design, vibrant gradient colors, high quality digital art,
|
||||||
|
isometric perspective, color palette: cyan, purple, pink, blue accents,
|
||||||
|
4k resolution, no text, no watermarks, futuristic, professional
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result:** Clean, modern dashboard visualization in your brand colors
|
||||||
|
|
||||||
|
### Customize Prompts
|
||||||
|
|
||||||
|
Edit the `Build AI Prompt` node in n8n workflow to customize:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Add your brand colors
|
||||||
|
const brandColors = 'navy blue, gold accents, white backgrounds';
|
||||||
|
|
||||||
|
// Add style preferences
|
||||||
|
const stylePreference = 'minimalist, clean, corporate, professional';
|
||||||
|
|
||||||
|
// Modify prompt template
|
||||||
|
const prompt = `
|
||||||
|
${categoryStyle},
|
||||||
|
${projectTitle},
|
||||||
|
${brandColors},
|
||||||
|
${stylePreference},
|
||||||
|
4k quality, trending on artstation
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
See [PROMPT_TEMPLATES.md](./PROMPT_TEMPLATES.md) for category-specific templates.
|
||||||
|
|
||||||
|
## 🖼️ Examples
|
||||||
|
|
||||||
|
### Before & After
|
||||||
|
|
||||||
|
| Category | Without AI Image | With AI Image |
|
||||||
|
|----------|------------------|---------------|
|
||||||
|
| Web App | Generic stock photo | Custom dashboard visualization |
|
||||||
|
| Mobile App | App store screenshot | Professional phone mockup |
|
||||||
|
| DevOps | Server rack photo | Cloud architecture diagram |
|
||||||
|
| AI/ML | Brain illustration | Neural network visualization |
|
||||||
|
|
||||||
|
### Quality Comparison
|
||||||
|
|
||||||
|
**Settings:**
|
||||||
|
- Resolution: 1024x768
|
||||||
|
- Steps: 30
|
||||||
|
- CFG Scale: 7
|
||||||
|
- Sampler: DPM++ 2M Karras
|
||||||
|
- Model: SDXL Base 1.0
|
||||||
|
|
||||||
|
**Generation Time:**
|
||||||
|
- RTX 4090: ~8 seconds
|
||||||
|
- RTX 3080: ~15 seconds
|
||||||
|
- RTX 3060: ~25 seconds
|
||||||
|
- CPU: ~5 minutes
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### "Connection refused to SD API"
|
||||||
|
```bash
|
||||||
|
# Check if SD WebUI is running
|
||||||
|
ps aux | grep webui
|
||||||
|
|
||||||
|
# Restart with API enabled
|
||||||
|
cd stable-diffusion-webui
|
||||||
|
./webui.sh --api --listen
|
||||||
|
```
|
||||||
|
|
||||||
|
#### "CUDA out of memory"
|
||||||
|
```bash
|
||||||
|
# Use lower VRAM mode
|
||||||
|
./webui.sh --api --listen --medvram
|
||||||
|
```
|
||||||
|
|
||||||
|
#### "Images are low quality"
|
||||||
|
In n8n workflow, increase:
|
||||||
|
- Steps: 30 → 40
|
||||||
|
- CFG Scale: 7 → 9
|
||||||
|
- Resolution: 512 → 1024
|
||||||
|
|
||||||
|
#### "Images don't match project"
|
||||||
|
- Add more specific keywords to prompt
|
||||||
|
- Use category-specific templates
|
||||||
|
- Refine negative prompts
|
||||||
|
|
||||||
|
See [SETUP.md](./SETUP.md#troubleshooting) for more solutions.
|
||||||
|
|
||||||
|
## ❓ FAQ
|
||||||
|
|
||||||
|
### How much does it cost?
|
||||||
|
|
||||||
|
**Initial Setup:** $300-400 for GPU (or $0 with cloud GPU rental)
|
||||||
|
**Per Image:** $0.00 (local electricity ~$0.001)
|
||||||
|
**Break-even:** ~500 images vs. commercial APIs
|
||||||
|
|
||||||
|
### Can I use this without a GPU?
|
||||||
|
|
||||||
|
Yes, but it's slower (~5 minutes per image on CPU). Consider cloud GPU services:
|
||||||
|
- RunPod: ~$0.20/hour
|
||||||
|
- vast.ai: ~$0.15/hour
|
||||||
|
- Google Colab: Free with limitations
|
||||||
|
|
||||||
|
### Is the data sent anywhere?
|
||||||
|
|
||||||
|
No! Everything runs locally. Your project data never leaves your server.
|
||||||
|
|
||||||
|
### Can I customize the style?
|
||||||
|
|
||||||
|
Absolutely! Edit prompts in the n8n workflow or use the template system.
|
||||||
|
|
||||||
|
### What models should I use?
|
||||||
|
|
||||||
|
- **SDXL Base 1.0**: Best all-around quality
|
||||||
|
- **DreamShaper 8**: Artistic, modern tech style
|
||||||
|
- **Realistic Vision V5**: Photorealistic results
|
||||||
|
- **Juggernaut XL**: Clean, professional aesthetics
|
||||||
|
|
||||||
|
### Can I generate images on-demand?
|
||||||
|
|
||||||
|
Yes! Use the admin UI component or API endpoint to regenerate anytime.
|
||||||
|
|
||||||
|
### How do I change image dimensions?
|
||||||
|
|
||||||
|
Edit the n8n workflow's SD node:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"width": 1920, // Change this
|
||||||
|
"height": 1080 // And this
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Can I use a different AI model?
|
||||||
|
|
||||||
|
Yes! The system works with:
|
||||||
|
- Stable Diffusion WebUI (default)
|
||||||
|
- ComfyUI (more advanced)
|
||||||
|
- Any API that accepts txt2img requests
|
||||||
|
|
||||||
|
## 📚 Additional Resources
|
||||||
|
|
||||||
|
- **[SETUP.md](./SETUP.md)** - Detailed installation guide
|
||||||
|
- **[QUICKSTART.md](./QUICKSTART.md)** - 15-minute setup guide
|
||||||
|
- **[PROMPT_TEMPLATES.md](./PROMPT_TEMPLATES.md)** - Category-specific prompts
|
||||||
|
- **[n8n-workflow-ai-image-generator.json](./n8n-workflow-ai-image-generator.json)** - Workflow file
|
||||||
|
|
||||||
|
### External Documentation
|
||||||
|
|
||||||
|
- [Stable Diffusion WebUI Wiki](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki)
|
||||||
|
- [n8n Documentation](https://docs.n8n.io)
|
||||||
|
- [Stable Diffusion Prompt Guide](https://prompthero.com/stable-diffusion-prompt-guide)
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Have improvements or new prompt templates? Contributions welcome!
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Test your changes
|
||||||
|
4. Submit a pull request
|
||||||
|
|
||||||
|
## 📝 License
|
||||||
|
|
||||||
|
This system is part of your portfolio project. AI-generated images are yours to use freely.
|
||||||
|
|
||||||
|
**Model Licenses:**
|
||||||
|
- SDXL Base 1.0: CreativeML Open RAIL++-M License
|
||||||
|
- Other models: Check individual model licenses
|
||||||
|
|
||||||
|
## 🙏 Credits
|
||||||
|
|
||||||
|
- **Stable Diffusion**: Stability AI & AUTOMATIC1111
|
||||||
|
- **n8n**: n8n GmbH
|
||||||
|
- **Prompt Engineering**: Community templates and best practices
|
||||||
|
|
||||||
|
## 💬 Support
|
||||||
|
|
||||||
|
Need help? Found a bug?
|
||||||
|
|
||||||
|
- Open an issue on GitHub
|
||||||
|
- Check existing documentation
|
||||||
|
- Join the community Discord
|
||||||
|
- Email: contact@dk0.dev
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Built with ❤️ for automatic, beautiful project images**
|
||||||
|
|
||||||
|
*Last Updated: 2024*
|
||||||
486
docs/ai-image-generation/SETUP.md
Normal file
486
docs/ai-image-generation/SETUP.md
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
# AI Image Generation Setup
|
||||||
|
|
||||||
|
This guide explains how to set up automatic AI-powered image generation for your portfolio projects using local AI models.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The system automatically generates project cover images by:
|
||||||
|
1. Reading project metadata (title, description, tags, tech stack)
|
||||||
|
2. Creating an optimized prompt for image generation
|
||||||
|
3. Sending the prompt to a local AI image generator
|
||||||
|
4. Saving the generated image
|
||||||
|
5. Updating the project's `imageUrl` in the database
|
||||||
|
|
||||||
|
## Supported Local AI Tools
|
||||||
|
|
||||||
|
### Option 1: Stable Diffusion WebUI (AUTOMATIC1111) - Recommended
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Most mature and widely used
|
||||||
|
- Excellent API support
|
||||||
|
- Large model ecosystem
|
||||||
|
- Easy to use
|
||||||
|
|
||||||
|
**Installation:**
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git
|
||||||
|
cd stable-diffusion-webui
|
||||||
|
|
||||||
|
# Install and run (will download models automatically)
|
||||||
|
./webui.sh --api --listen
|
||||||
|
```
|
||||||
|
|
||||||
|
**API Endpoint:** `http://localhost:7860`
|
||||||
|
|
||||||
|
**Recommended Models:**
|
||||||
|
- **SDXL Base 1.0** - High quality, versatile
|
||||||
|
- **Realistic Vision V5.1** - Photorealistic images
|
||||||
|
- **DreamShaper 8** - Artistic, tech-focused imagery
|
||||||
|
- **Juggernaut XL** - Modern, clean aesthetics
|
||||||
|
|
||||||
|
**Download Models:**
|
||||||
|
```bash
|
||||||
|
cd models/Stable-diffusion/
|
||||||
|
|
||||||
|
# SDXL Base (6.94 GB)
|
||||||
|
wget https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors
|
||||||
|
|
||||||
|
# Or use the WebUI's model downloader
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: ComfyUI
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Node-based workflow system
|
||||||
|
- More control over generation pipeline
|
||||||
|
- Better for complex compositions
|
||||||
|
|
||||||
|
**Installation:**
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/comfyanonymous/ComfyUI.git
|
||||||
|
cd ComfyUI
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python main.py --listen 0.0.0.0 --port 8188
|
||||||
|
```
|
||||||
|
|
||||||
|
**API Endpoint:** `http://localhost:8188`
|
||||||
|
|
||||||
|
### Option 3: Ollama + Stable Diffusion
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Lightweight
|
||||||
|
- Easy model management
|
||||||
|
- Can combine with LLM for better prompts
|
||||||
|
|
||||||
|
**Installation:**
|
||||||
|
```bash
|
||||||
|
# Install Ollama
|
||||||
|
curl -fsSL https://ollama.com/install.sh | sh
|
||||||
|
|
||||||
|
# Install a vision-capable model
|
||||||
|
ollama pull llava
|
||||||
|
|
||||||
|
# For image generation, you'll still need SD WebUI or ComfyUI
|
||||||
|
```
|
||||||
|
|
||||||
|
## n8n Workflow Setup
|
||||||
|
|
||||||
|
### 1. Install n8n (if not already installed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker Compose (recommended)
|
||||||
|
docker-compose up -d n8n
|
||||||
|
|
||||||
|
# Or npm
|
||||||
|
npm install -g n8n
|
||||||
|
n8n start
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Import Workflow
|
||||||
|
|
||||||
|
1. Open n8n at `http://localhost:5678`
|
||||||
|
2. Go to **Workflows** → **Import from File**
|
||||||
|
3. Import `n8n-workflows/ai-image-generator.json`
|
||||||
|
|
||||||
|
### 3. Configure Workflow Nodes
|
||||||
|
|
||||||
|
#### Node 1: Webhook Trigger
|
||||||
|
- **Method:** POST
|
||||||
|
- **Path:** `ai-image-generation`
|
||||||
|
- **Authentication:** Header Auth (use secret token)
|
||||||
|
|
||||||
|
#### Node 2: Postgres - Get Project Data
|
||||||
|
```sql
|
||||||
|
SELECT id, title, description, tags, category, content
|
||||||
|
FROM projects
|
||||||
|
WHERE id = $json.projectId
|
||||||
|
LIMIT 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Node 3: Code - Build AI Prompt
|
||||||
|
```javascript
|
||||||
|
// Extract project data
|
||||||
|
const project = $input.first().json;
|
||||||
|
|
||||||
|
// Build sophisticated prompt
|
||||||
|
const styleKeywords = {
|
||||||
|
'web': 'modern web interface, clean UI, gradient backgrounds, glass morphism',
|
||||||
|
'mobile': 'mobile app mockup, sleek design, app icons, smartphone screen',
|
||||||
|
'devops': 'server infrastructure, network diagram, cloud architecture, terminal windows',
|
||||||
|
'game': 'game scene, 3D environment, gaming interface, player HUD',
|
||||||
|
'ai': 'neural network visualization, AI chip, data flow, futuristic tech',
|
||||||
|
'automation': 'workflow diagram, automated processes, gears and circuits'
|
||||||
|
};
|
||||||
|
|
||||||
|
const categoryStyle = styleKeywords[project.category?.toLowerCase()] || 'technology concept';
|
||||||
|
|
||||||
|
const prompt = `
|
||||||
|
Professional tech project cover image, ${categoryStyle},
|
||||||
|
representing "${project.title}",
|
||||||
|
modern design, vibrant colors, high quality,
|
||||||
|
isometric view, minimalist, clean composition,
|
||||||
|
4k resolution, trending on artstation,
|
||||||
|
color palette: blue, purple, teal accents,
|
||||||
|
no text, no people, no logos
|
||||||
|
`.trim().replace(/\s+/g, ' ');
|
||||||
|
|
||||||
|
const negativePrompt = `
|
||||||
|
low quality, blurry, pixelated, text, watermark,
|
||||||
|
signature, logo, people, faces, hands,
|
||||||
|
cluttered, messy, dark, gloomy
|
||||||
|
`.trim().replace(/\s+/g, ' ');
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: {
|
||||||
|
projectId: project.id,
|
||||||
|
prompt: prompt,
|
||||||
|
negativePrompt: negativePrompt,
|
||||||
|
title: project.title,
|
||||||
|
category: project.category
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Node 4: HTTP Request - Generate Image (Stable Diffusion)
|
||||||
|
- **Method:** POST
|
||||||
|
- **URL:** `http://your-sd-server:7860/sdapi/v1/txt2img`
|
||||||
|
- **Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"prompt": "={{ $json.prompt }}",
|
||||||
|
"negative_prompt": "={{ $json.negativePrompt }}",
|
||||||
|
"steps": 30,
|
||||||
|
"cfg_scale": 7,
|
||||||
|
"width": 1024,
|
||||||
|
"height": 768,
|
||||||
|
"sampler_name": "DPM++ 2M Karras",
|
||||||
|
"seed": -1,
|
||||||
|
"batch_size": 1,
|
||||||
|
"n_iter": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Node 5: Code - Save Image to File
|
||||||
|
```javascript
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const imageData = $input.first().json.images[0]; // Base64 image
|
||||||
|
const projectId = $json.projectId;
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
// Create directory if doesn't exist
|
||||||
|
const uploadDir = '/app/public/generated-images';
|
||||||
|
if (!fs.existsSync(uploadDir)) {
|
||||||
|
fs.mkdirSync(uploadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save image
|
||||||
|
const filename = `project-${projectId}-${timestamp}.png`;
|
||||||
|
const filepath = path.join(uploadDir, filename);
|
||||||
|
|
||||||
|
fs.writeFileSync(filepath, Buffer.from(imageData, 'base64'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: {
|
||||||
|
projectId: projectId,
|
||||||
|
imageUrl: `/generated-images/${filename}`,
|
||||||
|
filepath: filepath
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Node 6: Postgres - Update Project
|
||||||
|
```sql
|
||||||
|
UPDATE projects
|
||||||
|
SET image_url = $json.imageUrl,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $json.projectId;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Node 7: Webhook Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"projectId": "={{ $json.projectId }}",
|
||||||
|
"imageUrl": "={{ $json.imageUrl }}",
|
||||||
|
"message": "Image generated successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Integration
|
||||||
|
|
||||||
|
### Generate Image for Project
|
||||||
|
|
||||||
|
**Endpoint:** `POST /api/n8n/generate-image`
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"projectId": 123,
|
||||||
|
"regenerate": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"projectId": 123,
|
||||||
|
"imageUrl": "/generated-images/project-123-1234567890.png",
|
||||||
|
"generatedAt": "2024-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automatic Generation on Project Creation
|
||||||
|
|
||||||
|
Add this to your project creation API:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// After creating project in database
|
||||||
|
if (process.env.AUTO_GENERATE_IMAGES === 'true') {
|
||||||
|
await fetch(`${process.env.N8N_WEBHOOK_URL}/ai-image-generation`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${process.env.N8N_SECRET_TOKEN}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
projectId: newProject.id
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Add to `.env.local`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# AI Image Generation
|
||||||
|
N8N_WEBHOOK_URL=http://localhost:5678/webhook
|
||||||
|
N8N_SECRET_TOKEN=your-secure-token-here
|
||||||
|
AUTO_GENERATE_IMAGES=true
|
||||||
|
|
||||||
|
# Stable Diffusion API
|
||||||
|
SD_API_URL=http://localhost:7860
|
||||||
|
SD_API_KEY=optional-if-protected
|
||||||
|
|
||||||
|
# Image Storage
|
||||||
|
GENERATED_IMAGES_DIR=/app/public/generated-images
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prompt Engineering Tips
|
||||||
|
|
||||||
|
### Good Prompts for Tech Projects
|
||||||
|
|
||||||
|
**Web Application:**
|
||||||
|
```
|
||||||
|
modern web dashboard interface, clean UI design, gradient background,
|
||||||
|
glass morphism, floating panels, data visualization, charts and graphs,
|
||||||
|
vibrant blue and purple color scheme, isometric view, 4k quality
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mobile App:**
|
||||||
|
```
|
||||||
|
sleek mobile app interface mockup, smartphone screen, modern app design,
|
||||||
|
minimalist UI, smooth gradients, app icons, notification badges,
|
||||||
|
floating elements, teal and pink accents, professional photography
|
||||||
|
```
|
||||||
|
|
||||||
|
**DevOps/Infrastructure:**
|
||||||
|
```
|
||||||
|
cloud infrastructure diagram, server network visualization,
|
||||||
|
interconnected nodes, data flow arrows, container icons,
|
||||||
|
modern tech illustration, isometric perspective, cyan and orange colors
|
||||||
|
```
|
||||||
|
|
||||||
|
**AI/ML Project:**
|
||||||
|
```
|
||||||
|
artificial intelligence concept, neural network visualization,
|
||||||
|
glowing nodes and connections, data streams, futuristic interface,
|
||||||
|
holographic elements, purple and blue neon lighting, high tech
|
||||||
|
```
|
||||||
|
|
||||||
|
### Negative Prompts (What to Avoid)
|
||||||
|
|
||||||
|
```
|
||||||
|
text, watermark, signature, logo, brand name, letters, numbers,
|
||||||
|
people, faces, hands, fingers, human figures,
|
||||||
|
low quality, blurry, pixelated, jpeg artifacts,
|
||||||
|
dark, gloomy, depressing, messy, cluttered,
|
||||||
|
realistic photo, stock photo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Image Specifications
|
||||||
|
|
||||||
|
**Recommended Settings:**
|
||||||
|
- **Resolution:** 1024x768 (4:3 aspect ratio for cards)
|
||||||
|
- **Format:** PNG (with transparency support)
|
||||||
|
- **Size:** < 500KB (optimize after generation)
|
||||||
|
- **Color Profile:** sRGB
|
||||||
|
- **Sampling Steps:** 25-35 (balance quality vs speed)
|
||||||
|
- **CFG Scale:** 6-8 (how closely to follow prompt)
|
||||||
|
|
||||||
|
## Optimization
|
||||||
|
|
||||||
|
### Post-Processing Pipeline
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install image optimization tools
|
||||||
|
npm install sharp tinypng-cli
|
||||||
|
|
||||||
|
# Optimize generated images
|
||||||
|
sharp input.png -o optimized.png --webp --quality 85
|
||||||
|
|
||||||
|
# Or use TinyPNG
|
||||||
|
tinypng input.png --key YOUR_API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caching Strategy
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Cache generated images in Redis
|
||||||
|
await redis.set(
|
||||||
|
`project:${projectId}:image`,
|
||||||
|
imageUrl,
|
||||||
|
'EX',
|
||||||
|
60 * 60 * 24 * 30 // 30 days
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring & Debugging
|
||||||
|
|
||||||
|
### Check Stable Diffusion Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:7860/sdapi/v1/sd-models
|
||||||
|
```
|
||||||
|
|
||||||
|
### View n8n Execution Logs
|
||||||
|
|
||||||
|
1. Open n8n UI → Executions
|
||||||
|
2. Filter by workflow "AI Image Generator"
|
||||||
|
3. Check error logs and execution time
|
||||||
|
|
||||||
|
### Test Image Generation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:7860/sdapi/v1/txt2img \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"prompt": "modern tech interface, blue gradient",
|
||||||
|
"steps": 20,
|
||||||
|
"width": 512,
|
||||||
|
"height": 512
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "CUDA out of memory"
|
||||||
|
- Reduce image resolution (768x576 instead of 1024x768)
|
||||||
|
- Lower batch size to 1
|
||||||
|
- Use `--lowvram` or `--medvram` flags when starting SD
|
||||||
|
|
||||||
|
### "Connection refused to SD API"
|
||||||
|
- Check if SD WebUI is running: `ps aux | grep webui`
|
||||||
|
- Verify API is enabled: `--api` flag in startup
|
||||||
|
- Check firewall: `sudo ufw allow 7860`
|
||||||
|
|
||||||
|
### "Poor image quality"
|
||||||
|
- Increase sampling steps (30-40)
|
||||||
|
- Try different samplers (Euler a, DPM++ 2M Karras)
|
||||||
|
- Adjust CFG scale (7-9)
|
||||||
|
- Use better checkpoint model (SDXL, Realistic Vision)
|
||||||
|
|
||||||
|
### "Images don't match project theme"
|
||||||
|
- Refine prompts with more specific keywords
|
||||||
|
- Use category-specific style templates
|
||||||
|
- Add technical keywords from project tags
|
||||||
|
- Experiment with different negative prompts
|
||||||
|
|
||||||
|
## Advanced: Multi-Model Strategy
|
||||||
|
|
||||||
|
Use different models for different project types:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const modelMap = {
|
||||||
|
'web': 'dreamshaper_8.safetensors',
|
||||||
|
'mobile': 'realisticVision_v51.safetensors',
|
||||||
|
'devops': 'juggernautXL_v8.safetensors',
|
||||||
|
'ai': 'sdxl_base_1.0.safetensors'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Switch model before generation
|
||||||
|
await fetch('http://localhost:7860/sdapi/v1/options', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
sd_model_checkpoint: modelMap[project.category]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Isolate SD WebUI:** Run in Docker container, not exposed to internet
|
||||||
|
2. **Authentication:** Protect n8n webhooks with tokens
|
||||||
|
3. **Rate Limiting:** Limit image generation requests
|
||||||
|
4. **Content Filtering:** Validate prompts to prevent abuse
|
||||||
|
5. **Resource Limits:** Set GPU memory limits in Docker
|
||||||
|
|
||||||
|
## Cost & Performance
|
||||||
|
|
||||||
|
**Hardware Requirements:**
|
||||||
|
- **Minimum:** 8GB RAM, GTX 1060 6GB
|
||||||
|
- **Recommended:** 16GB RAM, RTX 3060 12GB
|
||||||
|
- **Optimal:** 32GB RAM, RTX 4090 24GB
|
||||||
|
|
||||||
|
**Generation Time:**
|
||||||
|
- **512x512:** ~5-10 seconds
|
||||||
|
- **1024x768:** ~15-30 seconds
|
||||||
|
- **1024x1024 (SDXL):** ~30-60 seconds
|
||||||
|
|
||||||
|
**Storage:**
|
||||||
|
- ~500KB per optimized image
|
||||||
|
- ~50MB for 100 projects
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- [ ] Style transfer from existing brand assets
|
||||||
|
- [ ] A/B testing different image variants
|
||||||
|
- [ ] User feedback loop for prompt refinement
|
||||||
|
- [ ] Batch generation for multiple projects
|
||||||
|
- [ ] Integration with DALL-E 3 / Midjourney as fallback
|
||||||
|
- [ ] Automatic alt text generation for accessibility
|
||||||
|
- [ ] Version history for generated images
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Next Steps:**
|
||||||
|
1. Set up Stable Diffusion WebUI locally
|
||||||
|
2. Import n8n workflow
|
||||||
|
3. Test with sample project
|
||||||
|
4. Refine prompts based on results
|
||||||
|
5. Enable auto-generation for new projects
|
||||||
340
docs/ai-image-generation/n8n-workflow-ai-image-generator.json
Normal file
340
docs/ai-image-generation/n8n-workflow-ai-image-generator.json
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
{
|
||||||
|
"name": "AI Project Image Generator",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"httpMethod": "POST",
|
||||||
|
"path": "ai-image-generation",
|
||||||
|
"responseMode": "responseNode",
|
||||||
|
"options": {
|
||||||
|
"authType": "headerAuth"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "webhook-trigger",
|
||||||
|
"name": "Webhook Trigger",
|
||||||
|
"type": "n8n-nodes-base.webhook",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [250, 300],
|
||||||
|
"webhookId": "ai-image-gen-webhook",
|
||||||
|
"credentials": {
|
||||||
|
"httpHeaderAuth": {
|
||||||
|
"id": "1",
|
||||||
|
"name": "Header Auth"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "executeQuery",
|
||||||
|
"query": "SELECT id, title, description, tags, category, content, tech_stack FROM projects WHERE id = $1 LIMIT 1",
|
||||||
|
"additionalFields": {
|
||||||
|
"queryParameters": "={{ $json.body.projectId }}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "get-project-data",
|
||||||
|
"name": "Get Project Data",
|
||||||
|
"type": "n8n-nodes-base.postgres",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [450, 300],
|
||||||
|
"credentials": {
|
||||||
|
"postgres": {
|
||||||
|
"id": "2",
|
||||||
|
"name": "PostgreSQL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "// Extract project data\nconst project = $input.first().json;\n\n// Style keywords by category\nconst styleKeywords = {\n 'web': 'modern web interface, clean UI dashboard, gradient backgrounds, glass morphism effect, floating panels',\n 'mobile': 'mobile app mockup, sleek smartphone design, app icons, modern UI elements, notification badges',\n 'devops': 'server infrastructure, cloud network diagram, container orchestration, CI/CD pipeline visualization',\n 'backend': 'API architecture, database systems, microservices diagram, server endpoints, data flow',\n 'game': 'game environment scene, 3D rendered world, gaming interface, player HUD elements',\n 'ai': 'neural network visualization, AI chip design, machine learning data flow, futuristic technology',\n 'automation': 'workflow automation diagram, process flows, interconnected systems, automated pipeline',\n 'security': 'cybersecurity shields, encrypted data streams, security locks, firewall visualization',\n 'iot': 'Internet of Things devices, sensor networks, smart home technology, connected devices',\n 'blockchain': 'blockchain network, crypto technology, distributed ledger, decentralized nodes'\n};\n\nconst categoryStyle = styleKeywords[project.category?.toLowerCase()] || 'modern technology concept visualization';\n\n// Extract tech-specific keywords from tags and tech_stack\nconst techKeywords = [];\nif (project.tags) {\n const tags = Array.isArray(project.tags) ? project.tags : JSON.parse(project.tags || '[]');\n techKeywords.push(...tags.slice(0, 3));\n}\nif (project.tech_stack) {\n const stack = Array.isArray(project.tech_stack) ? project.tech_stack : JSON.parse(project.tech_stack || '[]');\n techKeywords.push(...stack.slice(0, 2));\n}\n\nconst techContext = techKeywords.length > 0 ? techKeywords.join(', ') + ' technology,' : '';\n\n// Build sophisticated prompt\nconst prompt = `\nProfessional tech project cover image, ${categoryStyle},\nrepresenting the concept of \"${project.title}\",\n${techContext}\nmodern minimalist design, vibrant gradient colors,\nhigh quality digital art, isometric perspective,\nclean composition, soft lighting,\ncolor palette: cyan, purple, pink, blue accents,\n4k resolution, trending on artstation,\nno text, no watermarks, no people, no logos,\nfuturistic, professional, tech-focused\n`.trim().replace(/\\s+/g, ' ');\n\n// Comprehensive negative prompt\nconst negativePrompt = `\nlow quality, blurry, pixelated, grainy, jpeg artifacts,\ntext, letters, words, watermark, signature, logo, brand name,\npeople, faces, hands, fingers, human figures, person,\ncluttered, messy, chaotic, disorganized,\ndark, gloomy, depressing, ugly, distorted,\nrealistic photo, stock photo, photograph,\nbad anatomy, deformed, mutation, extra limbs,\nduplication, duplicate elements, repetitive patterns\n`.trim().replace(/\\s+/g, ' ');\n\nreturn {\n json: {\n projectId: project.id,\n prompt: prompt,\n negativePrompt: negativePrompt,\n title: project.title,\n category: project.category,\n timestamp: Date.now()\n }\n};"
|
||||||
|
},
|
||||||
|
"id": "build-ai-prompt",
|
||||||
|
"name": "Build AI Prompt",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [650, 300]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "={{ $env.SD_API_URL || 'http://localhost:7860' }}/sdapi/v1/txt2img",
|
||||||
|
"authentication": "genericCredentialType",
|
||||||
|
"genericAuthType": "httpHeaderAuth",
|
||||||
|
"sendBody": true,
|
||||||
|
"bodyParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "prompt",
|
||||||
|
"value": "={{ $json.prompt }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "negative_prompt",
|
||||||
|
"value": "={{ $json.negativePrompt }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "steps",
|
||||||
|
"value": "30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cfg_scale",
|
||||||
|
"value": "7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "width",
|
||||||
|
"value": "1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "height",
|
||||||
|
"value": "768"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sampler_name",
|
||||||
|
"value": "DPM++ 2M Karras"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "seed",
|
||||||
|
"value": "-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "batch_size",
|
||||||
|
"value": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "n_iter",
|
||||||
|
"value": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "save_images",
|
||||||
|
"value": "false"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"timeout": 180000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "generate-image-sd",
|
||||||
|
"name": "Generate Image (Stable Diffusion)",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4,
|
||||||
|
"position": [850, 300],
|
||||||
|
"credentials": {
|
||||||
|
"httpHeaderAuth": {
|
||||||
|
"id": "3",
|
||||||
|
"name": "SD API Auth"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "const fs = require('fs');\nconst path = require('path');\n\n// Get the base64 image data from Stable Diffusion response\nconst response = $input.first().json;\nconst imageData = response.images[0]; // Base64 encoded PNG\n\nconst projectId = $('Build AI Prompt').first().json.projectId;\nconst timestamp = Date.now();\n\n// Define upload directory (adjust path based on your setup)\nconst uploadDir = process.env.GENERATED_IMAGES_DIR || '/app/public/generated-images';\n\n// Create directory if it doesn't exist\nif (!fs.existsSync(uploadDir)) {\n fs.mkdirSync(uploadDir, { recursive: true });\n}\n\n// Generate filename\nconst filename = `project-${projectId}-${timestamp}.png`;\nconst filepath = path.join(uploadDir, filename);\n\n// Convert base64 to buffer and save\ntry {\n const imageBuffer = Buffer.from(imageData, 'base64');\n fs.writeFileSync(filepath, imageBuffer);\n \n // Get file size for logging\n const stats = fs.statSync(filepath);\n const fileSizeKB = (stats.size / 1024).toFixed(2);\n \n return {\n json: {\n projectId: projectId,\n imageUrl: `/generated-images/${filename}`,\n filepath: filepath,\n filename: filename,\n fileSize: fileSizeKB + ' KB',\n generatedAt: new Date().toISOString(),\n success: true\n }\n };\n} catch (error) {\n return {\n json: {\n projectId: projectId,\n error: error.message,\n success: false\n }\n };\n}"
|
||||||
|
},
|
||||||
|
"id": "save-image-file",
|
||||||
|
"name": "Save Image to File",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [1050, 300]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "executeQuery",
|
||||||
|
"query": "UPDATE projects SET image_url = $1, updated_at = NOW() WHERE id = $2 RETURNING id, title, image_url",
|
||||||
|
"additionalFields": {
|
||||||
|
"queryParameters": "={{ $json.imageUrl }},={{ $json.projectId }}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "update-project-image",
|
||||||
|
"name": "Update Project Image URL",
|
||||||
|
"type": "n8n-nodes-base.postgres",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [1250, 300],
|
||||||
|
"credentials": {
|
||||||
|
"postgres": {
|
||||||
|
"id": "2",
|
||||||
|
"name": "PostgreSQL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={\n \"success\": true,\n \"projectId\": {{ $json.id }},\n \"title\": \"{{ $json.title }}\",\n \"imageUrl\": \"{{ $json.image_url }}\",\n \"generatedAt\": \"{{ $('Save Image to File').first().json.generatedAt }}\",\n \"fileSize\": \"{{ $('Save Image to File').first().json.fileSize }}\",\n \"message\": \"Project image generated successfully\"\n}",
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "webhook-response",
|
||||||
|
"name": "Webhook Response",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [1450, 300]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"boolean": [
|
||||||
|
{
|
||||||
|
"value1": "={{ $json.success }}",
|
||||||
|
"value2": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "check-save-success",
|
||||||
|
"name": "Check Save Success",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [1050, 450]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"respondWith": "json",
|
||||||
|
"responseBody": "={\n \"success\": false,\n \"error\": \"{{ $json.error || 'Failed to save image' }}\",\n \"projectId\": {{ $json.projectId }},\n \"message\": \"Image generation failed\"\n}",
|
||||||
|
"options": {
|
||||||
|
"responseCode": 500
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "error-response",
|
||||||
|
"name": "Error Response",
|
||||||
|
"type": "n8n-nodes-base.respondToWebhook",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [1250, 500]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"operation": "executeQuery",
|
||||||
|
"query": "INSERT INTO activity_logs (type, action, details, created_at) VALUES ('ai_generation', 'image_generated', $1, NOW())",
|
||||||
|
"additionalFields": {
|
||||||
|
"queryParameters": "={{ JSON.stringify({ projectId: $json.id, imageUrl: $json.image_url, timestamp: new Date().toISOString() }) }}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "log-activity",
|
||||||
|
"name": "Log Generation Activity",
|
||||||
|
"type": "n8n-nodes-base.postgres",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [1250, 150],
|
||||||
|
"credentials": {
|
||||||
|
"postgres": {
|
||||||
|
"id": "2",
|
||||||
|
"name": "PostgreSQL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Webhook Trigger": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Get Project Data",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Get Project Data": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Build AI Prompt",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Build AI Prompt": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Generate Image (Stable Diffusion)",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Generate Image (Stable Diffusion)": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Save Image to File",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Save Image to File": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Check Save Success",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Check Save Success": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Update Project Image URL",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Error Response",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Update Project Image URL": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Log Generation Activity",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "Webhook Response",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1",
|
||||||
|
"saveManualExecutions": true,
|
||||||
|
"callerPolicy": "workflowsFromSameOwner",
|
||||||
|
"errorWorkflow": ""
|
||||||
|
},
|
||||||
|
"staticData": null,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "AI",
|
||||||
|
"id": "ai-tag"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Automation",
|
||||||
|
"id": "automation-tag"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Image Generation",
|
||||||
|
"id": "image-gen-tag"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "your-instance-id"
|
||||||
|
},
|
||||||
|
"id": "ai-image-generator-workflow",
|
||||||
|
"versionId": "1",
|
||||||
|
"triggerCount": 1,
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
91
docs/setup_activity_status.sql
Normal file
91
docs/setup_activity_status.sql
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
-- Activity Status Table Setup for n8n Integration
|
||||||
|
-- This table stores real-time activity data from various sources
|
||||||
|
|
||||||
|
-- Drop existing table if it exists
|
||||||
|
DROP TABLE IF EXISTS activity_status CASCADE;
|
||||||
|
|
||||||
|
-- Create the activity_status table
|
||||||
|
CREATE TABLE activity_status (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- Activity (Coding, Reading, etc.)
|
||||||
|
activity_type VARCHAR(50), -- 'coding', 'listening', 'watching', 'gaming', 'reading'
|
||||||
|
activity_details TEXT,
|
||||||
|
activity_project VARCHAR(255),
|
||||||
|
activity_language VARCHAR(50),
|
||||||
|
activity_repo VARCHAR(255),
|
||||||
|
|
||||||
|
-- Music (Spotify, Apple Music)
|
||||||
|
music_playing BOOLEAN DEFAULT FALSE,
|
||||||
|
music_track VARCHAR(255),
|
||||||
|
music_artist VARCHAR(255),
|
||||||
|
music_album VARCHAR(255),
|
||||||
|
music_platform VARCHAR(50), -- 'spotify', 'apple'
|
||||||
|
music_progress INTEGER, -- 0-100 (percentage)
|
||||||
|
music_album_art TEXT, -- URL to album art
|
||||||
|
|
||||||
|
-- Watching (YouTube, Netflix, Twitch)
|
||||||
|
watching_title VARCHAR(255),
|
||||||
|
watching_platform VARCHAR(50), -- 'youtube', 'netflix', 'twitch'
|
||||||
|
watching_type VARCHAR(50), -- 'video', 'stream', 'movie', 'series'
|
||||||
|
|
||||||
|
-- Gaming (Steam, PlayStation, Xbox, Discord)
|
||||||
|
gaming_game VARCHAR(255),
|
||||||
|
gaming_platform VARCHAR(50), -- 'steam', 'playstation', 'xbox', 'discord'
|
||||||
|
gaming_status VARCHAR(50), -- 'playing', 'idle'
|
||||||
|
|
||||||
|
-- Status (Mood & Custom Message)
|
||||||
|
status_mood VARCHAR(10), -- emoji like '😊', '💻', '🎮', '😴'
|
||||||
|
status_message TEXT,
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create index for faster queries
|
||||||
|
CREATE INDEX idx_activity_status_updated_at ON activity_status(updated_at DESC);
|
||||||
|
|
||||||
|
-- Insert default row (will be updated by n8n workflows)
|
||||||
|
INSERT INTO activity_status (
|
||||||
|
id,
|
||||||
|
activity_type,
|
||||||
|
activity_details,
|
||||||
|
music_playing,
|
||||||
|
status_mood,
|
||||||
|
status_message
|
||||||
|
) VALUES (
|
||||||
|
1,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
FALSE,
|
||||||
|
'💻',
|
||||||
|
'Getting started...'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create function to automatically update updated_at timestamp
|
||||||
|
CREATE OR REPLACE FUNCTION update_activity_status_timestamp()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Create trigger to call the function on UPDATE
|
||||||
|
CREATE TRIGGER trigger_update_activity_status_timestamp
|
||||||
|
BEFORE UPDATE ON activity_status
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_activity_status_timestamp();
|
||||||
|
|
||||||
|
-- Grant permissions (adjust as needed)
|
||||||
|
-- GRANT SELECT, INSERT, UPDATE ON activity_status TO your_app_user;
|
||||||
|
-- GRANT USAGE, SELECT ON SEQUENCE activity_status_id_seq TO your_app_user;
|
||||||
|
|
||||||
|
-- Display success message
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE '✅ Activity Status table created successfully!';
|
||||||
|
RAISE NOTICE '📝 You can now configure your n8n workflows to update this table.';
|
||||||
|
RAISE NOTICE '🔗 See docs/N8N_INTEGRATION.md for setup instructions.';
|
||||||
|
END $$;
|
||||||
@@ -1,35 +1,29 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from "next/server";
|
||||||
import type { NextRequest } from 'next/server';
|
import type { NextRequest } from "next/server";
|
||||||
import { verifySessionAuth } from '@/lib/auth';
|
import { verifySessionAuth } from "@/lib/auth";
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
export function middleware(request: NextRequest) {
|
||||||
// For /manage and /editor routes, require authentication
|
// For /manage and /editor routes, the pages handle their own authentication
|
||||||
if (request.nextUrl.pathname.startsWith('/manage') ||
|
// No middleware redirect needed - let the pages show login forms
|
||||||
request.nextUrl.pathname.startsWith('/editor')) {
|
|
||||||
// Check for session authentication
|
|
||||||
if (!verifySessionAuth(request)) {
|
|
||||||
// Redirect to home page if not authenticated
|
|
||||||
const url = request.nextUrl.clone();
|
|
||||||
url.pathname = '/';
|
|
||||||
return NextResponse.redirect(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add security headers to all responses
|
// Add security headers to all responses
|
||||||
const response = NextResponse.next();
|
const response = NextResponse.next();
|
||||||
|
|
||||||
// Security headers (complementing next.config.ts headers)
|
// Security headers (complementing next.config.ts headers)
|
||||||
response.headers.set('X-DNS-Prefetch-Control', 'on');
|
response.headers.set("X-DNS-Prefetch-Control", "on");
|
||||||
response.headers.set('X-Frame-Options', 'DENY');
|
response.headers.set("X-Frame-Options", "DENY");
|
||||||
response.headers.set('X-Content-Type-Options', 'nosniff');
|
response.headers.set("X-Content-Type-Options", "nosniff");
|
||||||
response.headers.set('X-XSS-Protection', '1; mode=block');
|
response.headers.set("X-XSS-Protection", "1; mode=block");
|
||||||
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||||
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
response.headers.set(
|
||||||
|
"Permissions-Policy",
|
||||||
|
"camera=(), microphone=(), geolocation=()",
|
||||||
|
);
|
||||||
|
|
||||||
// Rate limiting headers for API routes
|
// Rate limiting headers for API routes
|
||||||
if (request.nextUrl.pathname.startsWith('/api/')) {
|
if (request.nextUrl.pathname.startsWith("/api/")) {
|
||||||
response.headers.set('X-RateLimit-Limit', '100');
|
response.headers.set("X-RateLimit-Limit", "100");
|
||||||
response.headers.set('X-RateLimit-Remaining', '99');
|
response.headers.set("X-RateLimit-Remaining", "99");
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -46,6 +40,6 @@ export const config = {
|
|||||||
* - favicon.ico (favicon file)
|
* - favicon.ico (favicon file)
|
||||||
* - api/auth (auth API routes - need to be processed)
|
* - api/auth (auth API routes - need to be processed)
|
||||||
*/
|
*/
|
||||||
'/((?!api/email|api/health|_next/static|_next/image|favicon.ico|api/auth).*)',
|
"/((?!api/email|api/health|_next/static|_next/image|favicon.ico|api/auth).*)",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
"dev:simple": "node scripts/dev-simple.js",
|
"dev:simple": "node scripts/dev-simple.js",
|
||||||
"dev:next": "next dev",
|
"dev:next": "next dev",
|
||||||
"db:setup": "node scripts/setup-database.js",
|
"db:setup": "node scripts/setup-database.js",
|
||||||
"db:seed": "tsx prisma/seed.ts",
|
"db:seed": "tsx -r dotenv/config prisma/seed.ts dotenv_config_path=.env.local",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
|||||||
127
prisma/migrations/README.md
Normal file
127
prisma/migrations/README.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# Database Migrations
|
||||||
|
|
||||||
|
This directory contains SQL migration scripts for manual database updates.
|
||||||
|
|
||||||
|
## Running Migrations
|
||||||
|
|
||||||
|
### Method 1: Using psql (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Connect to your database
|
||||||
|
psql -d portfolio -f prisma/migrations/create_activity_status.sql
|
||||||
|
|
||||||
|
# Or with connection string
|
||||||
|
psql "postgresql://user:password@localhost:5432/portfolio" -f prisma/migrations/create_activity_status.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: Using Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# If your database is in Docker
|
||||||
|
docker exec -i postgres_container psql -U username -d portfolio < prisma/migrations/create_activity_status.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 3: Using pgAdmin or Database GUI
|
||||||
|
|
||||||
|
1. Open pgAdmin or your database GUI
|
||||||
|
2. Connect to your `portfolio` database
|
||||||
|
3. Open Query Tool
|
||||||
|
4. Copy and paste the contents of `create_activity_status.sql`
|
||||||
|
5. Execute the query
|
||||||
|
|
||||||
|
## Verifying Migration
|
||||||
|
|
||||||
|
After running the migration, verify it was successful:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if table exists
|
||||||
|
psql -d portfolio -c "\dt activity_status"
|
||||||
|
|
||||||
|
# View table structure
|
||||||
|
psql -d portfolio -c "\d activity_status"
|
||||||
|
|
||||||
|
# Check if default row was inserted
|
||||||
|
psql -d portfolio -c "SELECT * FROM activity_status;"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
id | activity_type | ... | updated_at
|
||||||
|
----+---------------+-----+---------------------------
|
||||||
|
1 | | ... | 2024-01-15 10:30:00+00
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration: create_activity_status.sql
|
||||||
|
|
||||||
|
**Purpose**: Creates the `activity_status` table for n8n activity feed integration.
|
||||||
|
|
||||||
|
**What it does**:
|
||||||
|
- Creates `activity_status` table with all necessary columns
|
||||||
|
- Inserts a default row with `id = 1`
|
||||||
|
- Sets up automatic `updated_at` timestamp trigger
|
||||||
|
- Adds table comment for documentation
|
||||||
|
|
||||||
|
**Required by**:
|
||||||
|
- `/api/n8n/status` endpoint
|
||||||
|
- `ActivityFeed` component
|
||||||
|
- n8n workflows for status updates
|
||||||
|
|
||||||
|
**Safe to run multiple times**: Yes (uses `IF NOT EXISTS` and `ON CONFLICT`)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "relation already exists"
|
||||||
|
Table already exists - migration is already applied. Safe to ignore.
|
||||||
|
|
||||||
|
### "permission denied"
|
||||||
|
Your database user needs CREATE TABLE permissions:
|
||||||
|
```sql
|
||||||
|
GRANT CREATE ON DATABASE portfolio TO your_user;
|
||||||
|
```
|
||||||
|
|
||||||
|
### "database does not exist"
|
||||||
|
Create the database first:
|
||||||
|
```bash
|
||||||
|
createdb portfolio
|
||||||
|
# Or
|
||||||
|
psql -c "CREATE DATABASE portfolio;"
|
||||||
|
```
|
||||||
|
|
||||||
|
### "connection refused"
|
||||||
|
Ensure PostgreSQL is running:
|
||||||
|
```bash
|
||||||
|
# Check status
|
||||||
|
pg_isready
|
||||||
|
|
||||||
|
# Start PostgreSQL (macOS)
|
||||||
|
brew services start postgresql
|
||||||
|
|
||||||
|
# Start PostgreSQL (Linux)
|
||||||
|
sudo systemctl start postgresql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rolling Back
|
||||||
|
|
||||||
|
To remove the activity_status table:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
DROP TRIGGER IF EXISTS activity_status_updated_at ON activity_status;
|
||||||
|
DROP FUNCTION IF EXISTS update_activity_status_updated_at();
|
||||||
|
DROP TABLE IF EXISTS activity_status;
|
||||||
|
```
|
||||||
|
|
||||||
|
Save this as `rollback_activity_status.sql` and run if needed.
|
||||||
|
|
||||||
|
## Future Migrations
|
||||||
|
|
||||||
|
When adding new migrations:
|
||||||
|
1. Create a new `.sql` file with descriptive name
|
||||||
|
2. Use timestamps in filename: `YYYYMMDD_description.sql`
|
||||||
|
3. Document what it does in this README
|
||||||
|
4. Test on local database first
|
||||||
|
5. Mark as safe/unsafe for production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2024-01-15
|
||||||
|
**Status**: Required for n8n integration
|
||||||
49
prisma/migrations/create_activity_status.sql
Normal file
49
prisma/migrations/create_activity_status.sql
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
-- Create activity_status table for n8n integration
|
||||||
|
CREATE TABLE IF NOT EXISTS activity_status (
|
||||||
|
id INTEGER PRIMARY KEY DEFAULT 1,
|
||||||
|
activity_type VARCHAR(50),
|
||||||
|
activity_details VARCHAR(255),
|
||||||
|
activity_project VARCHAR(255),
|
||||||
|
activity_language VARCHAR(50),
|
||||||
|
activity_repo VARCHAR(500),
|
||||||
|
music_playing BOOLEAN DEFAULT FALSE,
|
||||||
|
music_track VARCHAR(255),
|
||||||
|
music_artist VARCHAR(255),
|
||||||
|
music_album VARCHAR(255),
|
||||||
|
music_platform VARCHAR(50),
|
||||||
|
music_progress INTEGER,
|
||||||
|
music_album_art VARCHAR(500),
|
||||||
|
watching_title VARCHAR(255),
|
||||||
|
watching_platform VARCHAR(50),
|
||||||
|
watching_type VARCHAR(50),
|
||||||
|
gaming_game VARCHAR(255),
|
||||||
|
gaming_platform VARCHAR(50),
|
||||||
|
gaming_status VARCHAR(50),
|
||||||
|
status_mood VARCHAR(50),
|
||||||
|
status_message VARCHAR(500),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert default row
|
||||||
|
INSERT INTO activity_status (id, updated_at)
|
||||||
|
VALUES (1, NOW())
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Create function to automatically update updated_at
|
||||||
|
CREATE OR REPLACE FUNCTION update_activity_status_updated_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Create trigger for automatic timestamp updates
|
||||||
|
DROP TRIGGER IF EXISTS activity_status_updated_at ON activity_status;
|
||||||
|
CREATE TRIGGER activity_status_updated_at
|
||||||
|
BEFORE UPDATE ON activity_status
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_activity_status_updated_at();
|
||||||
|
|
||||||
|
-- Add helpful comment
|
||||||
|
COMMENT ON TABLE activity_status IS 'Stores real-time activity status from n8n workflows (coding, music, gaming, etc.)';
|
||||||
73
prisma/migrations/quick-fix.sh
Executable file
73
prisma/migrations/quick-fix.sh
Executable file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Quick Fix Script for Portfolio Database
|
||||||
|
# This script creates the activity_status table needed for n8n integration
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔧 Portfolio Database Quick Fix"
|
||||||
|
echo "================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Check if .env.local exists
|
||||||
|
if [ ! -f .env.local ]; then
|
||||||
|
echo -e "${RED}❌ Error: .env.local not found${NC}"
|
||||||
|
echo "Please create .env.local with DATABASE_URL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Load DATABASE_URL from .env.local
|
||||||
|
export $(grep -v '^#' .env.local | xargs)
|
||||||
|
|
||||||
|
if [ -z "$DATABASE_URL" ]; then
|
||||||
|
echo -e "${RED}❌ Error: DATABASE_URL not found in .env.local${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Found DATABASE_URL${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Extract database name from DATABASE_URL
|
||||||
|
DB_NAME=$(echo $DATABASE_URL | sed -n 's/.*\/\([^?]*\).*/\1/p')
|
||||||
|
echo "📦 Database: $DB_NAME"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run the migration
|
||||||
|
echo "🚀 Creating activity_status table..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
psql "$DATABASE_URL" -f prisma/migrations/create_activity_status.sql
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✅ SUCCESS! Migration completed${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Verifying table..."
|
||||||
|
psql "$DATABASE_URL" -c "\d activity_status" | head -20
|
||||||
|
echo ""
|
||||||
|
echo "Checking default row..."
|
||||||
|
psql "$DATABASE_URL" -c "SELECT id, updated_at FROM activity_status LIMIT 1;"
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}🎉 All done! Your database is ready.${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Restart your Next.js dev server: npm run dev"
|
||||||
|
echo " 2. Visit http://localhost:3000"
|
||||||
|
echo " 3. The activity feed should now work without errors"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}❌ Migration failed${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Troubleshooting:"
|
||||||
|
echo " 1. Ensure PostgreSQL is running: pg_isready"
|
||||||
|
echo " 2. Check your DATABASE_URL in .env.local"
|
||||||
|
echo " 3. Verify database exists: psql -l | grep $DB_NAME"
|
||||||
|
echo " 4. Try manual migration: psql $DB_NAME -f prisma/migrations/create_activity_status.sql"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -103,3 +103,30 @@ enum InteractionType {
|
|||||||
BOOKMARK
|
BOOKMARK
|
||||||
COMMENT
|
COMMENT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ActivityStatus {
|
||||||
|
id Int @id @default(1)
|
||||||
|
activityType String? @map("activity_type") @db.VarChar(50)
|
||||||
|
activityDetails String? @map("activity_details") @db.VarChar(255)
|
||||||
|
activityProject String? @map("activity_project") @db.VarChar(255)
|
||||||
|
activityLanguage String? @map("activity_language") @db.VarChar(50)
|
||||||
|
activityRepo String? @map("activity_repo") @db.VarChar(500)
|
||||||
|
musicPlaying Boolean @default(false) @map("music_playing")
|
||||||
|
musicTrack String? @map("music_track") @db.VarChar(255)
|
||||||
|
musicArtist String? @map("music_artist") @db.VarChar(255)
|
||||||
|
musicAlbum String? @map("music_album") @db.VarChar(255)
|
||||||
|
musicPlatform String? @map("music_platform") @db.VarChar(50)
|
||||||
|
musicProgress Int? @map("music_progress")
|
||||||
|
musicAlbumArt String? @map("music_album_art") @db.VarChar(500)
|
||||||
|
watchingTitle String? @map("watching_title") @db.VarChar(255)
|
||||||
|
watchingPlatform String? @map("watching_platform") @db.VarChar(50)
|
||||||
|
watchingType String? @map("watching_type") @db.VarChar(50)
|
||||||
|
gamingGame String? @map("gaming_game") @db.VarChar(255)
|
||||||
|
gamingPlatform String? @map("gaming_platform") @db.VarChar(50)
|
||||||
|
gamingStatus String? @map("gaming_status") @db.VarChar(50)
|
||||||
|
statusMood String? @map("status_mood") @db.VarChar(50)
|
||||||
|
statusMessage String? @map("status_message") @db.VarChar(500)
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
@@map("activity_status")
|
||||||
|
}
|
||||||
|
|||||||
450
prisma/seed.ts
450
prisma/seed.ts
@@ -1,296 +1,236 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log('🌱 Seeding database...');
|
console.log("🌱 Seeding database...");
|
||||||
|
|
||||||
// Clear existing data
|
// Clear existing data
|
||||||
await prisma.userInteraction.deleteMany();
|
await prisma.userInteraction.deleteMany();
|
||||||
await prisma.pageView.deleteMany();
|
await prisma.pageView.deleteMany();
|
||||||
await prisma.project.deleteMany();
|
await prisma.project.deleteMany();
|
||||||
|
|
||||||
// Create sample projects
|
// Create real projects
|
||||||
const projects = [
|
const projects = [
|
||||||
{
|
{
|
||||||
title: "Portfolio Website 2.0",
|
title: "Clarity",
|
||||||
description: "A cutting-edge portfolio website showcasing modern web development techniques with advanced features and stunning design.",
|
description:
|
||||||
content: `# Portfolio Website 2.0
|
"A Flutter mobile app supporting people with dyslexia by displaying text in OpenDyslexic font and simplifying content using AI.",
|
||||||
|
content: `# Clarity - Dyslexia Support App
|
||||||
|
|
||||||
This is my personal portfolio website built with cutting-edge web technologies. The site features a dark theme with glassmorphism effects, smooth animations, and advanced interactive elements.
|
Clarity is a mobile application built with Flutter to help people with dyslexia read and understand text more easily.
|
||||||
|
|
||||||
|
## 🎯 Purpose
|
||||||
|
|
||||||
|
The app was designed to make reading more accessible by using the OpenDyslexic font, which is specifically designed to make letters more distinguishable and reduce reading errors.
|
||||||
|
|
||||||
## 🚀 Features
|
## 🚀 Features
|
||||||
|
|
||||||
- **Responsive Design**: Works perfectly on all devices
|
- **OpenDyslexic Font**: All text is displayed in the OpenDyslexic typeface
|
||||||
- **Dark Theme**: Modern dark mode with glassmorphism effects
|
- **AI Text Simplification**: Complex texts are simplified using AI integration
|
||||||
- **Animations**: Smooth animations powered by Framer Motion
|
- **Clean Interface**: Simple, distraction-free reading experience
|
||||||
- **Markdown Support**: Projects are written in Markdown for easy editing
|
- **Mobile-First**: Optimized for smartphones and tablets
|
||||||
- **Performance**: Optimized for speed and SEO
|
- **Accessibility**: Built with accessibility in mind from the ground up
|
||||||
- **Interactive Elements**: Advanced UI components and micro-interactions
|
|
||||||
- **Accessibility**: WCAG 2.1 AA compliant
|
|
||||||
- **Analytics**: Built-in performance and user analytics
|
|
||||||
|
|
||||||
## 🛠️ Technologies Used
|
## 🛠️ Technologies Used
|
||||||
|
|
||||||
- Next.js 15
|
- Flutter
|
||||||
- TypeScript
|
- Dart
|
||||||
- Tailwind CSS
|
- AI Integration for text simplification
|
||||||
- Framer Motion
|
- OpenDyslexic Font
|
||||||
- React Markdown
|
|
||||||
- Advanced CSS (Grid, Flexbox, Custom Properties)
|
|
||||||
- Performance optimization techniques
|
|
||||||
|
|
||||||
## 📈 Development Process
|
## 📱 Platform Support
|
||||||
|
|
||||||
The website was designed with a focus on user experience, performance, and accessibility. I used modern CSS techniques and best practices to create a responsive, fast, and beautiful layout.
|
- iOS
|
||||||
|
- Android
|
||||||
|
|
||||||
## 🔮 Future Improvements
|
## 💡 What I Learned
|
||||||
|
|
||||||
- AI-powered content suggestions
|
Building Clarity taught me a lot about accessibility, mobile UI/UX design, and how to integrate AI services into mobile applications. It was rewarding to create something that could genuinely help people in their daily lives.
|
||||||
- Advanced project filtering and search
|
|
||||||
- Interactive project demos
|
|
||||||
- Real-time collaboration features
|
|
||||||
- Advanced analytics dashboard
|
|
||||||
|
|
||||||
## 🔗 Links
|
## 🔮 Future Plans
|
||||||
|
|
||||||
- [Live Demo](https://dki.one)
|
- Add more font options
|
||||||
- [GitHub Repository](https://github.com/Denshooter/portfolio)`,
|
- Implement text-to-speech
|
||||||
tags: ["Next.js", "TypeScript", "Tailwind CSS", "Framer Motion", "Advanced CSS", "Performance"],
|
- Support for more languages
|
||||||
|
- PDF and document scanning`,
|
||||||
|
tags: ["Flutter", "Mobile", "AI", "Accessibility", "Dart"],
|
||||||
featured: true,
|
featured: true,
|
||||||
category: "Web Development",
|
category: "Mobile Development",
|
||||||
date: "2024",
|
|
||||||
published: true,
|
|
||||||
difficulty: "ADVANCED",
|
|
||||||
timeToComplete: "3-4 weeks",
|
|
||||||
technologies: ["Next.js 15", "TypeScript", "Tailwind CSS", "Framer Motion", "React Markdown"],
|
|
||||||
challenges: ["Complex state management", "Performance optimization", "Responsive design across devices"],
|
|
||||||
lessonsLearned: ["Advanced CSS techniques", "Performance optimization", "User experience design"],
|
|
||||||
futureImprovements: ["AI integration", "Advanced analytics", "Real-time features"],
|
|
||||||
demoVideo: "",
|
|
||||||
screenshots: [],
|
|
||||||
colorScheme: "Dark with glassmorphism",
|
|
||||||
accessibility: true,
|
|
||||||
performance: {
|
|
||||||
lighthouse: 0,
|
|
||||||
bundleSize: "0KB",
|
|
||||||
loadTime: "0s"
|
|
||||||
},
|
|
||||||
analytics: {
|
|
||||||
views: 1250,
|
|
||||||
likes: 89,
|
|
||||||
shares: 23
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "E-Commerce Platform",
|
|
||||||
description: "A full-stack e-commerce solution with advanced features like real-time inventory, payment processing, and admin dashboard.",
|
|
||||||
content: `# E-Commerce Platform
|
|
||||||
|
|
||||||
A comprehensive e-commerce solution built with modern web technologies, featuring a robust backend, secure payment processing, and an intuitive user interface.
|
|
||||||
|
|
||||||
## 🚀 Features
|
|
||||||
|
|
||||||
- **User Authentication**: Secure login and registration
|
|
||||||
- **Product Management**: Add, edit, and delete products
|
|
||||||
- **Shopping Cart**: Persistent cart with real-time updates
|
|
||||||
- **Payment Processing**: Stripe integration for secure payments
|
|
||||||
- **Order Management**: Complete order lifecycle tracking
|
|
||||||
- **Admin Dashboard**: Comprehensive admin interface
|
|
||||||
- **Inventory Management**: Real-time stock tracking
|
|
||||||
- **Responsive Design**: Mobile-first approach
|
|
||||||
|
|
||||||
## 🛠️ Technologies Used
|
|
||||||
|
|
||||||
- Frontend: React, TypeScript, Tailwind CSS
|
|
||||||
- Backend: Node.js, Express, Prisma
|
|
||||||
- Database: PostgreSQL
|
|
||||||
- Payment: Stripe API
|
|
||||||
- Authentication: JWT, bcrypt
|
|
||||||
- Deployment: Docker, AWS
|
|
||||||
|
|
||||||
## 📈 Development Process
|
|
||||||
|
|
||||||
Built with a focus on scalability and user experience. Implemented proper error handling, input validation, and security measures throughout the development process.
|
|
||||||
|
|
||||||
## 🔮 Future Improvements
|
|
||||||
|
|
||||||
- Multi-language support
|
|
||||||
- Advanced analytics dashboard
|
|
||||||
- AI-powered product recommendations
|
|
||||||
- Mobile app development
|
|
||||||
- Advanced search and filtering`,
|
|
||||||
tags: ["React", "Node.js", "PostgreSQL", "Stripe", "E-commerce", "Full-Stack"],
|
|
||||||
featured: true,
|
|
||||||
category: "Full-Stack",
|
|
||||||
date: "2024",
|
|
||||||
published: true,
|
|
||||||
difficulty: "EXPERT",
|
|
||||||
timeToComplete: "8-10 weeks",
|
|
||||||
technologies: ["React", "Node.js", "PostgreSQL", "Stripe", "Docker", "AWS"],
|
|
||||||
challenges: ["Payment integration", "Real-time updates", "Scalability", "Security"],
|
|
||||||
lessonsLearned: ["Payment processing", "Real-time systems", "Security best practices", "Scalable architecture"],
|
|
||||||
futureImprovements: ["AI recommendations", "Mobile app", "Multi-language", "Advanced analytics"],
|
|
||||||
demoVideo: "",
|
|
||||||
screenshots: [],
|
|
||||||
colorScheme: "Professional and clean",
|
|
||||||
accessibility: true,
|
|
||||||
performance: {
|
|
||||||
lighthouse: 0,
|
|
||||||
bundleSize: "0KB",
|
|
||||||
loadTime: "0s"
|
|
||||||
},
|
|
||||||
analytics: {
|
|
||||||
views: 890,
|
|
||||||
likes: 67,
|
|
||||||
shares: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Task Management App",
|
|
||||||
description: "A collaborative task management application with real-time updates, team collaboration, and progress tracking.",
|
|
||||||
content: `# Task Management App
|
|
||||||
|
|
||||||
A collaborative task management application designed for teams to organize, track, and complete projects efficiently.
|
|
||||||
|
|
||||||
## 🚀 Features
|
|
||||||
|
|
||||||
- **Task Creation**: Easy task creation with descriptions and deadlines
|
|
||||||
- **Team Collaboration**: Assign tasks to team members
|
|
||||||
- **Real-time Updates**: Live updates across all connected clients
|
|
||||||
- **Progress Tracking**: Visual progress indicators and analytics
|
|
||||||
- **File Attachments**: Support for documents and images
|
|
||||||
- **Notifications**: Email and push notifications for updates
|
|
||||||
- **Mobile Responsive**: Works perfectly on all devices
|
|
||||||
- **Dark/Light Theme**: User preference support
|
|
||||||
|
|
||||||
## 🛠️ Technologies Used
|
|
||||||
|
|
||||||
- Frontend: React, TypeScript, Tailwind CSS
|
|
||||||
- Backend: Node.js, Express, Socket.io
|
|
||||||
- Database: MongoDB
|
|
||||||
- Real-time: WebSockets
|
|
||||||
- Authentication: JWT
|
|
||||||
- File Storage: AWS S3
|
|
||||||
- Deployment: Heroku
|
|
||||||
|
|
||||||
## 📈 Development Process
|
|
||||||
|
|
||||||
Focused on creating an intuitive user interface and seamless real-time collaboration. Implemented proper error handling and user feedback throughout the development.
|
|
||||||
|
|
||||||
## 🔮 Future Improvements
|
|
||||||
|
|
||||||
- Advanced reporting and analytics
|
|
||||||
- Integration with external tools
|
|
||||||
- Mobile app development
|
|
||||||
- AI-powered task suggestions
|
|
||||||
- Advanced automation features`,
|
|
||||||
tags: ["React", "Node.js", "MongoDB", "WebSockets", "Collaboration", "Real-time"],
|
|
||||||
featured: false,
|
|
||||||
category: "Web Application",
|
|
||||||
date: "2024",
|
date: "2024",
|
||||||
published: true,
|
published: true,
|
||||||
difficulty: "INTERMEDIATE",
|
difficulty: "INTERMEDIATE",
|
||||||
timeToComplete: "6-8 weeks",
|
timeToComplete: "4-6 weeks",
|
||||||
technologies: ["React", "Node.js", "MongoDB", "Socket.io", "AWS S3", "Heroku"],
|
technologies: ["Flutter", "Dart", "AI Integration", "OpenDyslexic Font"],
|
||||||
challenges: ["Real-time synchronization", "Team collaboration", "File management", "Mobile responsiveness"],
|
challenges: [
|
||||||
lessonsLearned: ["WebSocket implementation", "Real-time systems", "File upload handling", "Team collaboration features"],
|
"Implementing AI text simplification",
|
||||||
futureImprovements: ["Advanced analytics", "Mobile app", "AI integration", "Automation"],
|
"Font rendering optimization",
|
||||||
|
"Mobile accessibility standards",
|
||||||
|
],
|
||||||
|
lessonsLearned: [
|
||||||
|
"Mobile development with Flutter",
|
||||||
|
"Accessibility best practices",
|
||||||
|
"AI API integration",
|
||||||
|
],
|
||||||
|
futureImprovements: [
|
||||||
|
"Text-to-speech",
|
||||||
|
"Multi-language support",
|
||||||
|
"Document scanning",
|
||||||
|
],
|
||||||
demoVideo: "",
|
demoVideo: "",
|
||||||
screenshots: [],
|
screenshots: [],
|
||||||
colorScheme: "Modern and clean",
|
colorScheme: "Clean and minimal with high contrast",
|
||||||
accessibility: true,
|
|
||||||
performance: {
|
|
||||||
lighthouse: 88,
|
|
||||||
bundleSize: "65KB",
|
|
||||||
loadTime: "1.5s"
|
|
||||||
},
|
|
||||||
analytics: {
|
|
||||||
views: 567,
|
|
||||||
likes: 34,
|
|
||||||
shares: 12
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Weather Dashboard",
|
|
||||||
description: "A beautiful weather application with real-time data, forecasts, and interactive maps.",
|
|
||||||
content: `# Weather Dashboard
|
|
||||||
|
|
||||||
A beautiful and functional weather application that provides real-time weather data, forecasts, and interactive maps.
|
|
||||||
|
|
||||||
## 🚀 Features
|
|
||||||
|
|
||||||
- **Current Weather**: Real-time weather conditions
|
|
||||||
- **Forecast**: 7-day weather predictions
|
|
||||||
- **Interactive Maps**: Visual weather maps with overlays
|
|
||||||
- **Location Search**: Find weather for any location
|
|
||||||
- **Weather Alerts**: Severe weather notifications
|
|
||||||
- **Historical Data**: Past weather information
|
|
||||||
- **Responsive Design**: Works on all devices
|
|
||||||
- **Offline Support**: Basic functionality without internet
|
|
||||||
|
|
||||||
## 🛠️ Technologies Used
|
|
||||||
|
|
||||||
- Frontend: React, TypeScript, Tailwind CSS
|
|
||||||
- Maps: Mapbox GL JS
|
|
||||||
- Weather API: OpenWeatherMap
|
|
||||||
- State Management: Zustand
|
|
||||||
- Charts: Chart.js
|
|
||||||
- Icons: Weather Icons
|
|
||||||
- Deployment: Vercel
|
|
||||||
|
|
||||||
## 📈 Development Process
|
|
||||||
|
|
||||||
Built with a focus on user experience and visual appeal. Implemented proper error handling for API failures and created an intuitive interface for weather information.
|
|
||||||
|
|
||||||
## 🔮 Future Improvements
|
|
||||||
|
|
||||||
- Weather widgets for other websites
|
|
||||||
- Advanced forecasting algorithms
|
|
||||||
- Weather-based recommendations
|
|
||||||
- Social sharing features
|
|
||||||
- Weather photography integration`,
|
|
||||||
tags: ["React", "TypeScript", "Weather API", "Maps", "Real-time", "UI/UX"],
|
|
||||||
featured: false,
|
|
||||||
category: "Web Application",
|
|
||||||
date: "2024",
|
|
||||||
published: true,
|
|
||||||
difficulty: "BEGINNER",
|
|
||||||
timeToComplete: "3-4 weeks",
|
|
||||||
technologies: ["React", "TypeScript", "Tailwind CSS", "Mapbox", "OpenWeatherMap", "Chart.js"],
|
|
||||||
challenges: ["API integration", "Map implementation", "Responsive design", "Error handling"],
|
|
||||||
lessonsLearned: ["External API integration", "Map libraries", "Responsive design", "Error handling"],
|
|
||||||
futureImprovements: ["Advanced forecasting", "Weather widgets", "Social features", "Mobile app"],
|
|
||||||
demoVideo: "",
|
|
||||||
screenshots: [],
|
|
||||||
colorScheme: "Light and colorful",
|
|
||||||
accessibility: true,
|
accessibility: true,
|
||||||
performance: {
|
performance: {
|
||||||
lighthouse: 0,
|
lighthouse: 0,
|
||||||
bundleSize: "0KB",
|
bundleSize: "0KB",
|
||||||
loadTime: "0s"
|
loadTime: "0s",
|
||||||
},
|
},
|
||||||
analytics: {
|
analytics: {
|
||||||
views: 423,
|
views: 850,
|
||||||
likes: 28,
|
likes: 67,
|
||||||
shares: 8
|
shares: 34,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
title: "Self-Hosted Infrastructure & Portfolio",
|
||||||
|
description:
|
||||||
|
"A complete DevOps setup running in Docker Swarm. My Next.js projects are deployed via automated CI/CD pipelines with custom runners.",
|
||||||
|
content: `# Self-Hosted Infrastructure & Portfolio
|
||||||
|
|
||||||
|
Not just a website – this is a complete self-hosted infrastructure project showcasing my DevOps skills and passion for self-hosting.
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
All my projects run on a Docker Swarm cluster hosted on IONOS and OVHcloud servers. Everything is self-managed, from the networking layer to the application deployments.
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
- **Docker Swarm Cluster**: Multi-node orchestration for high availability
|
||||||
|
- **Traefik Reverse Proxy**: Automatic SSL certificates and routing
|
||||||
|
- **Automated CI/CD**: Custom GitLab/Gitea runners for continuous deployment
|
||||||
|
- **Zero-Downtime Deployments**: Rolling updates without service interruption
|
||||||
|
- **Redis Caching**: Performance optimization with Redis
|
||||||
|
- **Nginx Proxy Manager**: Additional layer for complex routing scenarios
|
||||||
|
|
||||||
|
## 🛠️ Tech Stack
|
||||||
|
|
||||||
|
- **Frontend**: Next.js, Tailwind CSS
|
||||||
|
- **Infrastructure**: Docker Swarm, Traefik, Nginx Proxy Manager
|
||||||
|
- **CI/CD**: Custom Git runners with automated pipelines
|
||||||
|
- **Monitoring**: Self-hosted monitoring stack
|
||||||
|
- **Security**: CrowdSec, Suricata, Mailcow
|
||||||
|
- **Caching**: Redis
|
||||||
|
|
||||||
|
## 🔐 Security
|
||||||
|
|
||||||
|
Security is a top priority. I use CrowdSec for intrusion prevention, Suricata for network monitoring, and Mailcow for secure email communications.
|
||||||
|
|
||||||
|
## 📈 DevOps Process
|
||||||
|
|
||||||
|
1. Code push triggers CI/CD pipeline
|
||||||
|
2. Automated tests run on custom runners
|
||||||
|
3. Docker images are built and tagged
|
||||||
|
4. Rolling deployment to Swarm cluster
|
||||||
|
5. Traefik automatically routes traffic
|
||||||
|
6. Zero downtime for users
|
||||||
|
|
||||||
|
## 💡 What I Learned
|
||||||
|
|
||||||
|
This project taught me everything about production-grade DevOps, from container orchestration to security hardening. Managing my own infrastructure has given me deep insights into networking, load balancing, and system administration.
|
||||||
|
|
||||||
|
## 🎯 Other Projects
|
||||||
|
|
||||||
|
Besides this portfolio, I host:
|
||||||
|
- Interactive photo galleries
|
||||||
|
- Quiz applications
|
||||||
|
- Game servers
|
||||||
|
- n8n automation workflows
|
||||||
|
- Various experimental Next.js apps
|
||||||
|
|
||||||
|
## 🔮 Future Improvements
|
||||||
|
|
||||||
|
- Kubernetes migration for more advanced orchestration
|
||||||
|
- Automated backup and disaster recovery
|
||||||
|
- Advanced monitoring with Prometheus and Grafana
|
||||||
|
- Multi-region deployment`,
|
||||||
|
tags: [
|
||||||
|
"Docker",
|
||||||
|
"Swarm",
|
||||||
|
"DevOps",
|
||||||
|
"CI/CD",
|
||||||
|
"Next.js",
|
||||||
|
"Traefik",
|
||||||
|
"Self-Hosting",
|
||||||
|
],
|
||||||
|
featured: true,
|
||||||
|
category: "DevOps",
|
||||||
|
date: "2024",
|
||||||
|
published: true,
|
||||||
|
difficulty: "ADVANCED",
|
||||||
|
timeToComplete: "Ongoing project",
|
||||||
|
technologies: [
|
||||||
|
"Docker Swarm",
|
||||||
|
"Traefik",
|
||||||
|
"Next.js",
|
||||||
|
"Redis",
|
||||||
|
"CI/CD",
|
||||||
|
"Nginx",
|
||||||
|
"CrowdSec",
|
||||||
|
"Suricata",
|
||||||
|
],
|
||||||
|
challenges: [
|
||||||
|
"Zero-downtime deployments",
|
||||||
|
"Network configuration",
|
||||||
|
"Security hardening",
|
||||||
|
"Performance optimization",
|
||||||
|
],
|
||||||
|
lessonsLearned: [
|
||||||
|
"Container orchestration",
|
||||||
|
"DevOps practices",
|
||||||
|
"Infrastructure as Code",
|
||||||
|
"Security best practices",
|
||||||
|
],
|
||||||
|
futureImprovements: [
|
||||||
|
"Kubernetes migration",
|
||||||
|
"Multi-region setup",
|
||||||
|
"Advanced monitoring",
|
||||||
|
"Automated backups",
|
||||||
|
],
|
||||||
|
demoVideo: "",
|
||||||
|
screenshots: [],
|
||||||
|
colorScheme: "Modern and professional",
|
||||||
|
accessibility: true,
|
||||||
|
performance: {
|
||||||
|
lighthouse: 0,
|
||||||
|
bundleSize: "0KB",
|
||||||
|
loadTime: "0s",
|
||||||
|
},
|
||||||
|
analytics: {
|
||||||
|
views: 1420,
|
||||||
|
likes: 112,
|
||||||
|
shares: 45,
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
await prisma.project.create({
|
await prisma.project.create({
|
||||||
data: {
|
data: {
|
||||||
...project,
|
...project,
|
||||||
difficulty: project.difficulty as 'BEGINNER' | 'INTERMEDIATE' | 'ADVANCED' | 'EXPERT',
|
difficulty: project.difficulty as
|
||||||
}
|
| "BEGINNER"
|
||||||
|
| "INTERMEDIATE"
|
||||||
|
| "ADVANCED"
|
||||||
|
| "EXPERT",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✅ Created ${projects.length} sample projects`);
|
console.log(`✅ Created ${projects.length} projects`);
|
||||||
|
|
||||||
// Create some sample analytics data
|
// Create some sample analytics data
|
||||||
for (let i = 1; i <= 4; i++) {
|
for (let i = 1; i <= projects.length; i++) {
|
||||||
// Create page views
|
// Create page views
|
||||||
for (let j = 0; j < Math.floor(Math.random() * 100) + 50; j++) {
|
for (let j = 0; j < Math.floor(Math.random() * 100) + 50; j++) {
|
||||||
await prisma.pageView.create({
|
await prisma.pageView.create({
|
||||||
@@ -298,9 +238,10 @@ Built with a focus on user experience and visual appeal. Implemented proper erro
|
|||||||
projectId: i,
|
projectId: i,
|
||||||
page: `/projects/${i}`,
|
page: `/projects/${i}`,
|
||||||
ip: `192.168.1.${Math.floor(Math.random() * 255)}`,
|
ip: `192.168.1.${Math.floor(Math.random() * 255)}`,
|
||||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
userAgent:
|
||||||
referrer: 'https://google.com'
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||||
}
|
referrer: "https://google.com",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,22 +250,23 @@ Built with a focus on user experience and visual appeal. Implemented proper erro
|
|||||||
await prisma.userInteraction.create({
|
await prisma.userInteraction.create({
|
||||||
data: {
|
data: {
|
||||||
projectId: i,
|
projectId: i,
|
||||||
type: Math.random() > 0.5 ? 'LIKE' : 'SHARE',
|
type: Math.random() > 0.5 ? "LIKE" : "SHARE",
|
||||||
ip: `192.168.1.${Math.floor(Math.random() * 255)}`,
|
ip: `192.168.1.${Math.floor(Math.random() * 255)}`,
|
||||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
userAgent:
|
||||||
}
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ Created sample analytics data');
|
console.log("✅ Created sample analytics data");
|
||||||
|
|
||||||
console.log('🎉 Database seeding completed!');
|
console.log("🎉 Database seeding completed!");
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error('❌ Error seeding database:', e);
|
console.error("❌ Error seeding database:", e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
})
|
})
|
||||||
.finally(async () => {
|
.finally(async () => {
|
||||||
|
|||||||
@@ -65,11 +65,19 @@ export default {
|
|||||||
blue: "#BFDBFE",
|
blue: "#BFDBFE",
|
||||||
rose: "#FECACA",
|
rose: "#FECACA",
|
||||||
yellow: "#FDE68A",
|
yellow: "#FDE68A",
|
||||||
}
|
peach: "#FED7AA",
|
||||||
|
pink: "#FBCFE8",
|
||||||
|
sky: "#BAE6FD",
|
||||||
|
lime: "#D9F99D",
|
||||||
|
coral: "#FCA5A5",
|
||||||
|
purple: "#E9D5FF",
|
||||||
|
teal: "#99F6E4",
|
||||||
|
amber: "#FDE047",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['var(--font-inter)', 'sans-serif'],
|
sans: ["var(--font-inter)", "sans-serif"],
|
||||||
mono: ['var(--font-roboto-mono)', 'monospace'],
|
mono: ["var(--font-roboto-mono)", "monospace"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user