🎨 Complete Portfolio Redesign: Modern Dark Theme + Admin Dashboard + Enhanced Markdown Editor

 New Features:
- Complete dark theme redesign with glassmorphism effects
- Responsive admin dashboard with collapsible projects list
- Enhanced markdown editor with live preview
- Project image upload functionality
- Improved project management (create, edit, delete, publish/unpublish)
- Slug-based project URLs
- Legal pages (Impressum, Privacy Policy)
- Modern animations with Framer Motion

🔧 Improvements:
- Fixed hydration errors with mounted state
- Enhanced UI/UX with better spacing and proportions
- Improved markdown rendering with custom components
- Better project image placeholders with initials
- Conditional rendering for GitHub/Live Demo links
- Enhanced toolbar with categorized quick actions
- Responsive grid layout for admin dashboard

📱 Technical:
- Next.js 15 + TypeScript + Tailwind CSS
- Local storage for project persistence
- Optimized performance and responsive design
This commit is contained in:
Dennis Konkol
2025-09-01 23:29:58 +00:00
parent eab0b88f59
commit ded873e6b4
16 changed files with 4050 additions and 1248 deletions

View File

@@ -1,171 +1,178 @@
"use client";
import {
useRouter,
useSearchParams,
useParams,
usePathname,
} from "next/navigation";
import { useEffect, useState } from "react";
import Link from "next/link";
import Footer_Back from "@/app/components/Footer_Back";
import Header from "@/app/components/Header";
import Image from "next/image";
import "@/app/styles/ghostContent.css"; // Import the global styles
import { motion } from 'framer-motion';
import { ExternalLink, Calendar, Tag, ArrowLeft, Github as GithubIcon } from 'lucide-react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
interface Project {
slug: string;
id: string;
id: number;
title: string;
feature_image: string;
visibility: string;
published_at: string;
updated_at: string;
html: string;
reading_time: number;
meta_description: string;
description: string;
content: string;
tags: string[];
featured: boolean;
category: string;
date: string;
github?: string;
live?: string;
}
const ProjectDetails = () => {
const router = useRouter();
const searchParams = useSearchParams();
const ProjectDetail = () => {
const params = useParams();
const pathname = usePathname();
const slug = params.slug as string;
const [project, setProject] = useState<Project | null>(null);
const [isVisible, setIsVisible] = useState(false);
const [error, setError] = useState<string | null>(null);
// Load project from localStorage by slug
useEffect(() => {
setTimeout(() => {
setIsVisible(true);
}, 150); // Delay to start the animation
}, []);
useEffect(() => {
const projectData = searchParams.get("project");
if (projectData) {
setProject(JSON.parse(projectData as string));
// Remove the project data from the URL without reloading the page
if (typeof window !== "undefined") {
const url = new URL(window.location.href);
url.searchParams.delete("project");
window.history.replaceState({}, "", url.toString());
}
} else {
// Fetch project data based on slug from URL
const slug = params.slug as string;
try {
fetchProjectData(slug);
} catch (error) {
console.error(error);
setError("Failed to fetch project data");
const savedProjects = localStorage.getItem('portfolio-projects');
if (savedProjects) {
const projects = JSON.parse(savedProjects);
const foundProject = projects.find((p: Project) =>
p.title.toLowerCase().replace(/[^a-z0-9]+/g, '-') === slug
);
if (foundProject) {
setProject(foundProject);
}
}
}, [searchParams, router, params, pathname]);
const fetchProjectData = async (slug: string) => {
try {
const response = await fetch(`/api/fetchProject?slug=${slug}`);
if (!response.ok) {
setError("Failed to fetch project Data");
}
const projectData = (await response.json()) as { posts: Project[] };
if (
!projectData ||
!projectData.posts ||
projectData.posts.length === 0
) {
setError("Project not found");
}
setProject(projectData.posts[0]);
} catch (error) {
console.error("Failed to fetch project data:", error);
setError("Project not found");
}
};
if (error) {
return (
<div className="min-h-screen flex flex-col bg-radiant">
<Header />
<div className="flex-grow flex items-center justify-center">
<div className="text-center p-10 bg-white dark:bg-gray-700 rounded shadow-md">
<h1 className="text-6xl font-bold text-gray-800 dark:text-white">
404
</h1>
<p className="mt-4 text-xl text-gray-600 dark:text-gray-300">
{error}
</p>
<Link
href="/"
className="mt-6 inline-block text-blue-500 hover:underline"
>
Go Back Home
</Link>
</div>
</div>
<Footer_Back />
</div>
);
}
}, [slug]);
if (!project) {
return (
<div className="min-h-screen flex flex-col bg-radiant">
<Header />
<div className="flex-grow flex items-center justify-center">
<div className="loader ease-linear rounded-full border-8 border-t-8 border-gray-200 h-32 w-32"></div>
<div className="min-h-screen animated-bg flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p className="text-gray-400">Loading project...</p>
</div>
<Footer_Back />
</div>
);
}
const featureImageUrl = project.feature_image
? `/api/fetchImage?url=${encodeURIComponent(project.feature_image)}`
: "";
return (
<div
className={`min-h-screen flex flex-col bg-radiant ${isVisible ? "animate-fly-in" : "opacity-0"}`}
>
<Header />
<div className="flex-grow">
<div className="flex justify-center mt-14 md:mt-28 px-4 md:px-0">
{featureImageUrl && (
<div className="relative w-full max-w-4xl h-0 pb-[56.25%] rounded-2xl overflow-hidden">
<Image
src={featureImageUrl}
alt={project.title}
fill
style={{ objectFit: "cover" }}
className="rounded-2xl"
priority={true}
/>
<div className="min-h-screen animated-bg">
<div className="max-w-4xl mx-auto px-4 py-20">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="mb-12"
>
<Link
href="/projects"
className="inline-flex items-center space-x-2 text-blue-400 hover:text-blue-300 transition-colors mb-6"
>
<ArrowLeft size={20} />
<span>Back to Projects</span>
</Link>
<div className="flex items-center justify-between mb-6">
<h1 className="text-4xl md:text-5xl font-bold gradient-text">
{project.title}
</h1>
{project.featured && (
<span className="px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white text-sm font-semibold rounded-full">
Featured
</span>
)}
</div>
<p className="text-xl text-gray-400 mb-6">
{project.description}
</p>
{/* Project Meta */}
<div className="flex flex-wrap items-center gap-6 text-gray-400 mb-8">
<div className="flex items-center space-x-2">
<Calendar size={20} />
<span>{project.date}</span>
</div>
)}
</div>
<div className="flex items-center justify-center mt-4">
<h1 className="text-4xl md:text-6xl font-bold text-gray-600">
{project.title}
</h1>
</div>
<div className="flex items-center space-x-2">
<Tag size={20} />
<span>{project.category}</span>
</div>
</div>
{/* Tags */}
<div className="flex flex-wrap gap-3 mb-8">
{project.tags.map((tag) => (
<span
key={tag}
className="px-4 py-2 bg-gray-800/50 text-gray-300 rounded-full border border-gray-700"
>
{tag}
</span>
))}
</div>
{/* Action Buttons */}
<div className="flex flex-wrap gap-4">
<motion.a
href={project.github}
target="_blank"
rel="noopener noreferrer"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="inline-flex items-center space-x-2 px-6 py-3 bg-gray-800/50 hover:bg-gray-700/50 text-white rounded-lg transition-colors border border-gray-700"
>
<GithubIcon size={20} />
<span>View Code</span>
</motion.a>
{project.live !== "#" && (
<motion.a
href={project.live}
target="_blank"
rel="noopener noreferrer"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="inline-flex items-center space-x-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
>
<ExternalLink size={20} />
<span>Live Demo</span>
</motion.a>
)}
</div>
</motion.div>
{/* Project Content */}
<div className="p-10 pt-12">
<div className="flex flex-col p-8 bg-gradient-to-br from-white/60 to-white/30 backdrop-blur-lg rounded-2xl shadow-xl">
<div
className="content mt-4 text-gray-600 text-lg leading-relaxed"
dangerouslySetInnerHTML={{ __html: project.html }}
></div>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="glass-card p-8 rounded-2xl"
>
<div className="markdown prose prose-invert max-w-none text-white">
<ReactMarkdown
components={{
h1: ({children}) => <h1 className="text-3xl font-bold text-white mb-4">{children}</h1>,
h2: ({children}) => <h2 className="text-2xl font-semibold text-white mb-3">{children}</h2>,
h3: ({children}) => <h3 className="text-xl font-semibold text-white mb-2">{children}</h3>,
p: ({children}) => <p className="text-gray-300 mb-3 leading-relaxed">{children}</p>,
ul: ({children}) => <ul className="list-disc list-inside text-gray-300 mb-3 space-y-1">{children}</ul>,
ol: ({children}) => <ol className="list-decimal list-inside text-gray-300 mb-3 space-y-1">{children}</ol>,
li: ({children}) => <li className="text-gray-300">{children}</li>,
a: ({href, children}) => (
<a href={href} className="text-blue-400 hover:text-blue-300 underline transition-colors" target="_blank" rel="noopener noreferrer">
{children}
</a>
),
code: ({children}) => <code className="bg-gray-800 text-blue-400 px-2 py-1 rounded text-sm">{children}</code>,
pre: ({children}) => <pre className="bg-gray-800 p-4 rounded-lg overflow-x-auto mb-3">{children}</pre>,
blockquote: ({children}) => <blockquote className="border-l-4 border-blue-500 pl-4 italic text-gray-300 mb-3">{children}</blockquote>,
strong: ({children}) => <strong className="font-semibold text-white">{children}</strong>,
em: ({children}) => <em className="italic text-gray-300">{children}</em>
}}
>
{project.content}
</ReactMarkdown>
</div>
</div>
</motion.div>
</div>
<Footer_Back />
</div>
);
};
export default ProjectDetails;
export default ProjectDetail;

207
app/projects/page.tsx Normal file
View File

@@ -0,0 +1,207 @@
"use client";
import { useState, useEffect } from "react";
import { motion } from 'framer-motion';
import { ExternalLink, Github, Calendar, ArrowLeft } from 'lucide-react';
import Link from 'next/link';
interface Project {
id: number;
title: string;
description: string;
content: string;
tags: string[];
featured: boolean;
category: string;
date: string;
github?: string;
live?: string;
}
const ProjectsPage = () => {
const [projects, setProjects] = useState<Project[]>([]);
// Load projects from localStorage
useEffect(() => {
const savedProjects = localStorage.getItem('portfolio-projects');
if (savedProjects) {
setProjects(JSON.parse(savedProjects));
}
}, []);
const categories = ["All", "Web Development", "Full-Stack", "Web Application", "Mobile App"];
const [selectedCategory, setSelectedCategory] = useState("All");
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
const filteredProjects = selectedCategory === "All"
? projects
: projects.filter(project => project.category === selectedCategory);
console.log('Selected category:', selectedCategory);
console.log('Filtered projects:', filteredProjects);
if (!mounted) {
return null;
}
return (
<div className="min-h-screen animated-bg">
<div className="max-w-7xl mx-auto px-4 py-20">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="mb-12"
>
<Link
href="/"
className="inline-flex items-center space-x-2 text-blue-400 hover:text-blue-300 transition-colors mb-6"
>
<ArrowLeft size={20} />
<span>Back to Home</span>
</Link>
<h1 className="text-5xl md:text-6xl font-bold mb-6 gradient-text">
My Projects
</h1>
<p className="text-xl text-gray-400 max-w-3xl">
Explore my portfolio of projects, from web applications to mobile apps.
Each project showcases different skills and technologies.
</p>
</motion.div>
{/* Category Filter */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="mb-12"
>
<div className="flex flex-wrap gap-3">
{categories.map((category) => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={`px-6 py-3 rounded-lg font-medium transition-all duration-200 ${
selectedCategory === category
? 'bg-blue-600 text-white shadow-lg'
: 'bg-gray-800/50 text-gray-300 hover:bg-gray-700/50 hover:text-white'
}`}
>
{category}
</button>
))}
</div>
</motion.div>
{/* Projects Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredProjects.map((project, index) => (
<motion.div
key={project.id}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
whileHover={{ y: -10 }}
className="group relative overflow-hidden rounded-2xl glass-card card-hover"
>
<div className="relative h-48 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/20 to-purple-500/20" />
<div className="absolute inset-0 bg-gray-800/50 flex flex-col items-center justify-center p-4">
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-500 rounded-full flex items-center justify-center mb-2">
<span className="text-2xl font-bold text-white">
{project.title.split(' ').map(word => word[0]).join('').toUpperCase()}
</span>
</div>
<span className="text-sm font-medium text-gray-400 text-center leading-tight">
{project.title}
</span>
</div>
{project.featured && (
<div className="absolute top-4 right-4 px-3 py-1 bg-gradient-to-r from-blue-500 to-purple-500 text-white text-xs font-semibold rounded-full">
Featured
</div>
)}
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center space-x-4">
{project.github && (
<motion.a
href={project.github}
target="_blank"
rel="noopener noreferrer"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
className="p-3 bg-gray-800/80 rounded-lg text-white hover:bg-gray-700/80 transition-colors"
>
<Github size={20} />
</motion.a>
)}
{project.live && project.live !== "#" && (
<motion.a
href={project.live}
target="_blank"
rel="noopener noreferrer"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
className="p-3 bg-blue-600/80 rounded-lg text-white hover:bg-blue-500/80 transition-colors"
>
<ExternalLink size={20} />
</motion.a>
)}
</div>
</div>
<div className="p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-xl font-bold text-white group-hover:text-blue-400 transition-colors">
{project.title}
</h3>
<div className="flex items-center space-x-2 text-gray-400">
<Calendar size={16} />
<span className="text-sm">{project.date}</span>
</div>
</div>
<p className="text-gray-300 mb-4 leading-relaxed">
{project.description}
</p>
<div className="flex flex-wrap gap-2 mb-4">
{project.tags.map((tag) => (
<span
key={tag}
className="px-3 py-1 bg-gray-800/50 text-gray-300 text-sm rounded-full border border-gray-700"
>
{tag}
</span>
))}
</div>
<Link
href={`/projects/${project.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`}
className="inline-flex items-center space-x-2 text-blue-400 hover:text-blue-300 transition-colors font-medium"
>
<span>View Project</span>
<ExternalLink size={16} />
</Link>
</div>
</motion.div>
))}
</div>
</div>
</div>
);
};
export default ProjectsPage;