Files
portfolio/app/components/admin/AIImageGenerator.tsx
denshooter 4cd3f60c98 feat: Fix hydration errors, navbar overlap, and add AI image generation system
## 🎨 UI/UX Fixes

### Fixed React Hydration Errors
- ActivityFeed: Standardized button styling (gradient → solid)
- ActivityFeed: Unified icon sizes and spacing for SSR/CSR consistency
- ActivityFeed: Added timestamps to chat messages for stable React keys
- About: Fixed duplicate keys in tech stack items (added unique key combinations)
- Projects: Fixed duplicate keys in project tags (combined projectId + tag + index)

### Fixed Layout Issues
- Added spacer after Header component (h-24 md:h-32) to prevent navbar overlap
- Hero section now properly visible below fixed navbar

## 🔧 Backend Improvements

### Database Schema
- Added ActivityStatus model for real-time activity tracking
- Supports: coding activity, music playing, watching, gaming, status/mood
- Single-row design (id=1) with auto-updating timestamps

### API Enhancements
- Fixed n8n status endpoint to handle missing table gracefully
- Added TypeScript interfaces (removed ESLint `any` warnings)
- New API: POST /api/n8n/generate-image for AI image generation
- New API: GET /api/n8n/generate-image?projectId=X for status check

## 🔐 Security & Auth

### Middleware Updates
- Removed premature auth redirect for /manage and /editor routes
- Pages now handle their own authentication (show login forms)
- Security headers still applied to all routes

## 🤖 New Feature: AI Image Generation System

### Complete automated project cover image generation using local Stable Diffusion

**Core Components:**
- Admin UI component (AIImageGenerator.tsx) with preview, generate, and regenerate
- n8n workflow integration for automation
- Context-aware prompt generation based on project metadata
- Support for 10+ project categories with optimized prompts

**Documentation (6 new files):**
- README.md - System overview and features
- SETUP.md - Detailed installation guide (486 lines)
- QUICKSTART.md - 15-minute quick start
- PROMPT_TEMPLATES.md - Category-specific templates (612 lines)
- ENVIRONMENT.md - Environment variables reference
- n8n-workflow-ai-image-generator.json - Ready-to-import workflow

**Database Migration:**
- SQL script: create_activity_status.sql
- Auto-setup script: quick-fix.sh
- Migration guide: prisma/migrations/README.md

**Key Features:**
 Automatic generation on project creation
 Manual regeneration via admin UI
 Category-specific prompts (web, mobile, devops, ai, game, etc.)
 Local Stable Diffusion (no API costs, privacy-first)
 n8n workflow orchestration
 Optimized for web (1024x768)

## 📝 Documentation

- CHANGELOG_DEV.md - Complete changelog with migration guide
- PRE_PUSH_CHECKLIST.md - Pre-push verification checklist
- Comprehensive AI image generation docs

## 🐛 Bug Fixes

1. Fixed "Hydration failed" errors in ActivityFeed
2. Fixed "two children with same key" warnings
3. Fixed navbar overlapping hero section
4. Fixed "relation activity_status does not exist" errors
5. Fixed /manage redirect loop (was going to home page)
6. Fixed TypeScript ESLint errors and warnings
7. Fixed duplicate transition prop in Hero component

## ⚠️ Breaking Changes

None - All changes are backward compatible

## 🔄 Migration Required

Database migration needed for new ActivityStatus table:
```bash
./prisma/migrations/quick-fix.sh
# OR
psql -d portfolio -f prisma/migrations/create_activity_status.sql
```

## 📦 Files Changed

**Modified (7):**
- app/page.tsx
- app/components/About.tsx
- app/components/Projects.tsx
- app/components/ActivityFeed.tsx
- app/components/Hero.tsx
- app/api/n8n/status/route.ts
- middleware.ts
- prisma/schema.prisma

**Created (14):**
- app/api/n8n/generate-image/route.ts
- app/components/admin/AIImageGenerator.tsx
- docs/ai-image-generation/* (6 files)
- prisma/migrations/* (3 files)
- CHANGELOG_DEV.md
- PRE_PUSH_CHECKLIST.md
- COMMIT_MESSAGE.txt

##  Testing

- [x] Build successful: npm run build
- [x] Linting passed: npm run lint (0 errors, 8 warnings)
- [x] No hydration errors in console
- [x] No duplicate key warnings
- [x] /manage accessible (shows login form)
- [x] API endpoints responding correctly
- [x] Navbar no longer overlaps content

## 🚀 Next Steps

1. Test AI image generation with Stable Diffusion setup
2. Test n8n workflow integration
3. Create demo screenshots for new features
4. Update main README.md after merge

---
Co-authored-by: AI Assistant (Claude Sonnet 4.5)
2026-01-07 14:38:57 +01:00

243 lines
8.2 KiB
TypeScript

"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Sparkles,
Loader2,
Image as ImageIcon,
RefreshCw,
CheckCircle,
XCircle,
} from "lucide-react";
import Image from "next/image";
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 ? (
<Image
src={generatedImageUrl}
alt={projectTitle}
fill
className="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 || !!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>
);
}