diff --git a/README.md b/README.md index 9370f93..4732bc0 100644 --- a/README.md +++ b/README.md @@ -1,248 +1,30 @@ -# Dennis Konkol's Portfolio Website +# Dennis Konkol Portfolio - Modern Dark Theme -This is a [Next.js](https://nextjs.org) project bootstrapped with [ -`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, clone the repository: - -```bash -git clone https://github.com/Denshooter/my_portfolio.git -cd my_portfolio -``` - -Then, install the dependencies: - -```bash -npm install -# or -yarn install -# or -pnpm install -# or -bun install -``` - -### Development - -To run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -### Build - -To build the application for production: - -```bash -npm run build -# or -yarn build -# or -pnpm build -# or -bun build -``` - -### Start - -To start the application in production mode: - -```bash -npm run start -# or -yarn start -# or -pnpm start -# or -bun start -``` - -### Lint - -To run the linter: - -```bash -npm run lint -# or -yarn lint -# or -pnpm lint -# or -bun lint -``` - -## Project Structure - -```plaintext -my_portfolio/ -├── .github/ -│ └── workflows/ -│ └── main.yml -├── app/ -│ ├── api/ -│ │ ├── email/ -│ │ │ └── route.tsx -│ │ ├── fetchAllProjects/ -│ │ │ └── route.tsx -│ │ ├── fetchImage/ -│ │ │ └── route.tsx -│ │ ├── fetchProject/ -│ │ │ └── route.tsx -│ │ - -i mean to give more examples how it was built what it features and how it works not how to run it, but more explanations about the project and how its deployed etc. - -Got it! Here's an updated README.md that provides more detailed explanations about the project, its features, how it was built, and how it is deployed: - -```md -# Dennis Konkol's Portfolio - -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). It showcases my projects, provides a way to contact me, and includes various other features. +Ein modernes, responsives Portfolio mit dunklem Design, coolen Animationen und einem integrierten Markdown-Editor. ## Features -- **Responsive Design**: The portfolio is fully responsive and works on all devices. -- **Project Showcase**: Displays a list of projects fetched from a Ghost CMS. -- **Contact Form**: Allows visitors to send me messages via email. -- **SEO Optimized**: Includes metadata and Open Graph tags for better SEO. -- **Dynamic Sitemap**: Automatically generates a sitemap for better search engine indexing. -- **Cookie Consent**: Includes a cookie consent banner to comply with GDPR. -- **Zero-Downtime Deployment**: Uses Docker and GitHub Actions for seamless deployments to a Raspberry Pi. +- **Dunkles Theme** mit Glassmorphism-Effekten +- **Responsive Design** für alle Geräte +- **Smooth Animationen** mit Framer Motion +- **Markdown-Editor** für Projekte +- **Admin Dashboard** für Content-Management -## Project Structure +## Technologien -```plaintext -my_portfolio/ -├── .github/ -│ └── workflows/ -│ └── main.yml -├── app/ -│ ├── api/ -│ │ ├── email/ -│ │ │ └── route.tsx -│ │ ├── fetchAllProjects/ -│ │ │ └── route.tsx -│ │ ├── fetchImage/ -│ │ │ └── route.tsx -│ │ ├── fetchProject/ -│ │ │ └── route.tsx -│ │ ├── og/ -│ │ │ └── route.tsx -│ │ ├── projects/ -│ │ │ └── route.tsx -│ │ ├── sitemap/ -│ │ │ └── route.tsx -│ ├── components/ -│ │ ├── ClientCookieConsentBanner.tsx -│ │ ├── Contact.tsx -│ │ ├── CookieConsentBanner.tsx -│ │ ├── Footer.tsx -│ │ ├── Footer_Back.tsx -│ │ ├── Header.tsx -│ │ ├── Hero.tsx -│ │ ├── Projects.tsx -│ ├── styles/ -│ │ └── ghostContent.css -│ ├── globals.css -│ ├── layout.tsx -│ ├── metadata.tsx -│ ├── not-found.tsx -│ ├── page.tsx -│ ├── privacy-policy/ -│ │ └── page.tsx -│ ├── legal-notice/ -│ │ └── page.tsx -│ ├── projects/ -│ │ └── [slug]/ -│ │ └── page.tsx -│ ├── sitemap.xml/ -│ │ └── route.tsx -│ ├── utils/ -│ │ └── send-email.tsx -├── public/ -│ ├── icons/ -│ │ ├── github.svg -│ │ ├── linkedin.svg -│ ├── images/ -│ ├── robots.txt -├── Dockerfile -├── README.md -├── next.config.ts -├── package.json -├── tailwind.config.ts -├── tsconfig.json -└── eslint.config.mjs -``` +- Next.js 15 mit App Router +- TypeScript für Type Safety +- Tailwind CSS für Styling +- Framer Motion für Animationen +- React Markdown für Content -## How It Works +## Installation -### Project Showcase +npm install +npm run dev -Projects are fetched from a Ghost CMS using the Ghost Content API. The API routes in the `app/api` directory handle -fetching all projects, fetching a single project by slug, and fetching images. +## Verwendung -### Contact Form - -The contact form allows visitors to send me messages via email. It uses the `nodemailer` package to send emails through -an SMTP server. The API route `app/api/email/route.tsx` handles the email sending logic. - -### SEO and Open Graph - -The project includes metadata and Open Graph tags to improve SEO. The `app/metadata.tsx` file defines the metadata for -the site. The `app/api/og/route.tsx` file generates dynamic Open Graph images. - -### Dynamic Sitemap - -A dynamic sitemap is generated to help search engines index the site. The `app/api/sitemap/route.tsx` file generates the -sitemap, and the `app/sitemap.xml/route.tsx` file serves it. - -### Cookie Consent - -A cookie consent banner is included to comply with GDPR. The `app/components/CookieConsentBanner.tsx` and -`app/components/ClientCookieConsentBanner.tsx` components handle the display and logic of the cookie consent banner. - -### Zero-Downtime Deployment - -The project uses Docker and GitHub Actions for zero-downtime deployments to a Raspberry Pi. The -`.github/workflows/main.yml` file defines the GitHub Actions workflow for deploying the project. The `Dockerfile` -defines the Docker image for the project. - -## Deployment - -The project is deployed using Docker and GitHub Actions. The GitHub Actions workflow is defined in -`.github/workflows/main.yml`. It builds the Docker image and deploys it to a Raspberry Pi with zero downtime. - -### Steps to Deploy - -1. **Set Up Raspberry Pi**: Ensure Docker is installed on your Raspberry Pi. -2. **Configure GitHub Secrets**: Add the necessary secrets (e.g., `GHOST_API_KEY`, `MY_EMAIL`, `MY_PASSWORD`) to your - GitHub repository. -3. **Push to GitHub**: Push your changes to the `production`, `dev`, or `preview` branches to trigger the deployment - workflow. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions -are welcome! - -## Author - -- **Dennis Konkol** - [GitHub](https://github.com/Denshooter) | [LinkedIn](https://linkedin.com/in/dkonkol) +- `/` - Homepage +- `/projects` - Alle Projekte +- `/admin` - Admin Dashboard mit Markdown-Editor diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..944e7d1 --- /dev/null +++ b/app/admin/page.tsx @@ -0,0 +1,858 @@ +"use client"; + +import { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import { + Save, + Eye, + Plus, + Edit, + Trash2, + Upload, + Bold, + Italic, + List, + Link as LinkIcon, + Image as ImageIcon, + Code, + Quote, + ArrowLeft, + ChevronDown, + ChevronRight, + Palette, + Smile, + FileText, + Download, + Upload as UploadIcon, + Settings, + Smartphone +} from 'lucide-react'; +import Link from 'next/link'; +import ReactMarkdown from 'react-markdown'; + +interface Project { + id: number; + title: string; + description: string; + content: string; + tags: string[]; + featured: boolean; + category: string; + date: string; + github?: string; + live?: string; + published: boolean; + imageUrl?: string; + metaDescription?: string; + keywords?: string; + ogImage?: string; + schema?: any; +} + +const AdminPage = () => { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + const [projects, setProjects] = useState([]); + + // Load projects from localStorage on mount + useEffect(() => { + const savedProjects = localStorage.getItem('portfolio-projects'); + if (savedProjects) { + setProjects(JSON.parse(savedProjects)); + } else { + // Default projects if none exist + const defaultProjects: Project[] = [ + { + id: 1, + title: "Portfolio Website", + description: "A modern, responsive portfolio website built with Next.js, TypeScript, and Tailwind CSS.", + content: "# Portfolio Website\n\nThis is my personal portfolio website built with modern web technologies. The site features a dark theme with glassmorphism effects and smooth animations.\n\n## Features\n\n- **Responsive Design**: Works perfectly on all devices\n- **Dark Theme**: Modern dark mode with glassmorphism effects\n- **Animations**: Smooth animations powered by Framer Motion\n- **Markdown Support**: Projects are written in Markdown for easy editing\n- **Performance**: Optimized for speed and SEO\n\n## Technologies Used\n\n- Next.js 15\n- TypeScript\n- Tailwind CSS\n- Framer Motion\n- React Markdown\n\n## Development Process\n\nThe website was designed with a focus on user experience and performance. I used modern CSS techniques like CSS Grid, Flexbox, and custom properties to create a responsive layout.\n\n## Future Improvements\n\n- Add blog functionality\n- Implement project filtering\n- Add more interactive elements\n- Optimize for Core Web Vitals\n\n## Links\n\n- [Live Demo](https://dki.one)\n- [GitHub Repository](https://github.com/Denshooter/portfolio)", + tags: ["Next.js", "TypeScript", "Tailwind CSS", "Framer Motion"], + featured: true, + category: "Web Development", + date: "2024" + } + ]; + setProjects(defaultProjects); + localStorage.setItem('portfolio-projects', JSON.stringify(defaultProjects)); + } + }, []); + + const [selectedProject, setSelectedProject] = useState(null); + + const [isPreview, setIsPreview] = useState(false); + const [isProjectsCollapsed, setIsProjectsCollapsed] = useState(false); + const [formData, setFormData] = useState({ + title: '', + description: '', + content: '', + tags: '', + category: '', + featured: false, + github: '', + live: '', + published: true, + imageUrl: '' + }); + + const [markdownContent, setMarkdownContent] = useState(''); + + const categories = [ + "Web Development", + "Full-Stack", + "Web Application", + "Mobile App", + "Desktop App", + "API Development", + "Database Design", + "DevOps", + "UI/UX Design", + "Game Development", + "Machine Learning", + "Data Science", + "Blockchain", + "IoT", + "Cybersecurity" + ]; + + if (!mounted) { + return null; + } + + const handleSave = () => { + if (!formData.title || !formData.description || !markdownContent || !formData.category) { + alert('Please fill in all required fields!'); + return; + } + + try { + if (selectedProject) { + // Update existing project + const updatedProjects = projects.map(p => + p.id === selectedProject.id + ? { + ...p, + title: formData.title, + description: formData.description, + content: markdownContent, + tags: formData.tags ? formData.tags.split(',').map(tag => tag.trim()).filter(tag => tag) : [], + category: formData.category, + featured: formData.featured, + github: formData.github || undefined, + live: formData.live || undefined, + published: formData.published, + imageUrl: formData.imageUrl || undefined + } + : p + ); + setProjects(updatedProjects); + localStorage.setItem('portfolio-projects', JSON.stringify(updatedProjects)); + console.log('Project updated successfully:', selectedProject.id); + } else { + // Create new project + const newProject: Project = { + id: Math.floor(Math.random() * 1000000), + title: formData.title, + description: formData.description, + content: markdownContent, + tags: formData.tags ? formData.tags.split(',').map(tag => tag.trim()).filter(tag => tag) : [], + category: formData.category, + featured: formData.featured, + github: formData.github || undefined, + live: formData.live || undefined, + published: formData.published, + imageUrl: formData.imageUrl || undefined, + date: new Date().getFullYear().toString() + }; + const updatedProjects = [...projects, newProject]; + setProjects(updatedProjects); + localStorage.setItem('portfolio-projects', JSON.stringify(updatedProjects)); + console.log('New project created successfully:', newProject.id); + } + + resetForm(); + alert('Project saved successfully!'); + } catch (error) { + console.error('Error saving project:', error); + alert('Error saving project. Please try again.'); + } + }; + + const handleEdit = (project: Project) => { + console.log('Editing project:', project); + setSelectedProject(project); + setFormData({ + title: project.title, + description: project.description, + content: project.content, + tags: project.tags.join(', '), + category: project.category, + featured: project.featured, + github: project.github || '', + live: project.live || '', + published: project.published !== undefined ? project.published : true, + imageUrl: project.imageUrl || '' + }); + setMarkdownContent(project.content); + setIsPreview(false); + }; + + const handleDelete = (projectId: number) => { + if (confirm('Are you sure you want to delete this project?')) { + const updatedProjects = projects.filter(p => p.id !== projectId); + setProjects(updatedProjects); + localStorage.setItem('portfolio-projects', JSON.stringify(updatedProjects)); + } + }; + + const resetForm = () => { + console.log('Resetting form'); + setSelectedProject(null); + setFormData({ + title: '', + description: '', + content: '', + tags: '', + category: '', + featured: false, + github: '', + live: '', + published: true, + imageUrl: '' + }); + setMarkdownContent(''); + setIsPreview(false); + }; + + const insertMarkdown = (type: string) => { + const textarea = document.getElementById('markdown-editor') as HTMLTextAreaElement; + if (!textarea) return; + + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const text = textarea.value; + + let insertion = ''; + let cursorOffset = 0; + + switch (type) { + case 'h1': + insertion = `# ${text.substring(start, end) || 'Heading'}`; + cursorOffset = 2; + break; + case 'h2': + insertion = `## ${text.substring(start, end) || 'Heading'}`; + cursorOffset = 3; + break; + case 'bold': + insertion = `**${text.substring(start, end) || 'bold text'}**`; + cursorOffset = 2; + break; + case 'italic': + insertion = `*${text.substring(start, end) || 'italic text'}*`; + cursorOffset = 1; + break; + case 'list': + insertion = `- ${text.substring(start, end) || 'list item'}`; + cursorOffset = 2; + break; + case 'link': + insertion = `[${text.substring(start, end) || 'link text'}](url)`; + cursorOffset = 3; + break; + case 'image': + insertion = `![alt text](image-url)`; + cursorOffset = 9; + break; + case 'code': + insertion = `\`${text.substring(start, end) || 'code'}\``; + cursorOffset = 1; + break; + case 'quote': + insertion = `> ${text.substring(start, end) || 'quote text'}`; + cursorOffset = 2; + break; + case 'table': + insertion = `| Header 1 | Header 2 | Header 3 |\n|----------|----------|----------|\n| Cell 1 | Cell 2 | Cell 3 |\n| Cell 4 | Cell 5 | Cell 6 |`; + cursorOffset = 0; + break; + } + + const newText = text.substring(0, start) + insertion + text.substring(end); + setMarkdownContent(newText); + + // Set cursor position and select the placeholder text for easy editing + setTimeout(() => { + textarea.focus(); + if (type === 'h1' || type === 'h2') { + // For headings, select the placeholder text so user can type directly + const placeholderStart = start + (type === 'h1' ? 2 : 3); + const placeholderEnd = start + insertion.length; + textarea.setSelectionRange(placeholderStart, placeholderEnd); + } else { + // For other elements, position cursor appropriately + textarea.setSelectionRange(start + insertion.length - cursorOffset, start + insertion.length - cursorOffset); + } + }, 0); + }; + + const handleProjectImageUpload = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files || files.length === 0) return; + + const file = files[0]; + if (file) { + // Simulate image upload - in production you'd upload to a real service + const imageUrl = URL.createObjectURL(file); + setFormData(prev => ({ ...prev, imageUrl })); + } + }; + + const handleImageUpload = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files || files.length === 0) return; + + const file = files[0]; + if (file) { + // Create a more descriptive image URL for better organization + const imageName = file.name.replace(/\.[^/.]+$/, ""); // Remove file extension + const imageUrl = URL.createObjectURL(file); + + const textarea = document.getElementById('markdown-editor') as HTMLTextAreaElement; + if (!textarea) return; + + const start = textarea.selectionStart; + const text = textarea.value; + + // Insert image with better alt text and a newline for spacing + const insertion = `\n![${imageName}](${imageUrl})\n`; + + const newText = text.substring(0, start) + insertion + text.substring(start); + setMarkdownContent(newText); + + // Focus back to textarea and position cursor after the image + setTimeout(() => { + textarea.focus(); + const newCursorPos = start + insertion.length; + textarea.setSelectionRange(newCursorPos, newCursorPos); + }, 0); + } + }; + + return ( +
+
+ {/* Header */} + + + + Back to Home + + +

+ Admin Dashboard +

+

+ Manage your projects with the built-in Markdown editor. Create, edit, and preview your content easily. +

+
+ + {/* Projects Toggle Button - Always Visible */} +
+ setIsProjectsCollapsed(!isProjectsCollapsed)} + className="flex items-center space-x-2 px-6 py-3 bg-gradient-to-r from-gray-700 to-gray-800 hover:from-gray-600 hover:to-gray-700 rounded-xl text-white transition-all duration-200 hover:scale-105 border border-gray-600/50 shadow-lg" + title={isProjectsCollapsed ? "Show Projects" : "Hide Projects"} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + {isProjectsCollapsed ? ( + <> + + Show Projects + + ) : ( + <> + + Hide Projects + + )} + +
+ +
+ {/* Projects List */} +
+ +
+

Projects

+ +
+ +
+ {projects.map((project) => ( +
handleEdit(project)} + > +

{project.title}

+

{project.description}

+
+ {project.category} +
+ + +
+
+
+ ))} +
+
+
+ + {/* Editor */} +
+ +
+

+ {selectedProject ? 'Edit Project' : 'New Project'} +

+
+ + + {selectedProject && ( + + )} +
+
+ + {!isPreview ? ( +
+ {/* Basic Info */} +
+
+ + setFormData({...formData, title: e.target.value})} + className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Project title" + /> +
+ +
+ + +
+
+ + {/* Links */} +
+
+ + setFormData({...formData, github: e.target.value})} + className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="https://github.com/username/repo" + /> +
+ +
+ + setFormData({...formData, live: e.target.value})} + className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="https://demo.example.com" + /> +
+
+ + {/* Project Image */} +
+ +
+
+ {formData.imageUrl ? ( + Project preview + ) : ( +
+ + {formData.title ? formData.title.split(' ').map(word => word[0]).join('').toUpperCase() : 'P'} + +
+ )} +
+
+ + +

Upload a project image or use auto-generated initials

+
+
+
+ +
+ + -
- -
- - -
- - - +

Send Message

+ +
+
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ +