From 2f40fc67535e31b9820f603d740ded77222cd665 Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 10 Sep 2025 10:59:14 +0200 Subject: [PATCH] huge update --- app/__tests__/components/Toast.test.tsx | 34 ++ app/api/projects/[id]/route.ts | 30 +- app/api/projects/route.ts | 34 +- app/components/Contact.tsx | 8 +- app/editor/page.tsx | 540 +++++++++++++++++------- app/globals.css | 126 ++++++ app/projects/[slug]/page.tsx | 58 +-- app/projects/page.tsx | 54 +-- components/AnalyticsDashboard.tsx | 14 +- components/EmailManager.tsx | 9 +- components/GhostEditor.tsx | 26 +- components/ModernAdminDashboard.tsx | 5 +- components/ProjectManager.tsx | 20 +- components/ResizableGhostEditor.tsx | 25 +- lib/cache.ts | 47 ++- 15 files changed, 729 insertions(+), 301 deletions(-) create mode 100644 app/__tests__/components/Toast.test.tsx diff --git a/app/__tests__/components/Toast.test.tsx b/app/__tests__/components/Toast.test.tsx new file mode 100644 index 0000000..2084bd3 --- /dev/null +++ b/app/__tests__/components/Toast.test.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { ToastProvider } from '@/components/Toast'; + +// Simple test component +const TestComponent = () => { + return ( +
+

Toast Test

+
+ ); +}; + +const renderWithToast = (component: React.ReactElement) => { + return render( + + {component} + + ); +}; + +describe('Toast Component', () => { + it('renders ToastProvider without crashing', () => { + renderWithToast(); + expect(screen.getByText('Toast Test')).toBeInTheDocument(); + }); + + it('provides toast context', () => { + // Simple test to ensure the provider works + const { container } = renderWithToast(); + expect(container).toBeInTheDocument(); + }); +}); diff --git a/app/api/projects/[id]/route.ts b/app/api/projects/[id]/route.ts index ca71642..9134235 100644 --- a/app/api/projects/[id]/route.ts +++ b/app/api/projects/[id]/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/prisma'; +import { apiCache } from '@/lib/cache'; export async function GET( request: NextRequest, @@ -35,20 +36,41 @@ export async function PUT( { params }: { params: Promise<{ id: string }> } ) { try { + // Check if this is an admin request + const isAdminRequest = request.headers.get('x-admin-request') === 'true'; + if (!isAdminRequest) { + return NextResponse.json( + { error: 'Admin access required' }, + { status: 403 } + ); + } + const { id: idParam } = await params; const id = parseInt(idParam); const data = await request.json(); + // Remove difficulty field if it exists (since we're removing it) + const { difficulty, ...projectData } = data; + const project = await prisma.project.update({ where: { id }, - data: { ...data, updatedAt: new Date() } + data: { + ...projectData, + updatedAt: new Date(), + // Keep existing difficulty if not provided + ...(difficulty ? { difficulty } : {}) + } }); + // Invalidate cache after successful update + await apiCache.invalidateProject(id); + await apiCache.invalidateAll(); + return NextResponse.json(project); } catch (error) { console.error('Error updating project:', error); return NextResponse.json( - { error: 'Failed to update project' }, + { error: 'Failed to update project', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 } ); } @@ -66,6 +88,10 @@ export async function DELETE( where: { id } }); + // Invalidate cache after successful deletion + await apiCache.invalidateProject(id); + await apiCache.invalidateAll(); + return NextResponse.json({ success: true }); } catch (error) { console.error('Error deleting project:', error); diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts index 11c70d8..3ab4c8b 100644 --- a/app/api/projects/route.ts +++ b/app/api/projects/route.ts @@ -37,8 +37,19 @@ export async function GET(request: NextRequest) { const difficulty = searchParams.get('difficulty'); const search = searchParams.get('search'); + // Create cache parameters object + const cacheParams = { + page: page.toString(), + limit: limit.toString(), + category, + featured, + published, + difficulty, + search + }; + // Check cache first - const cached = await apiCache.getProjects(); + const cached = await apiCache.getProjects(cacheParams); if (cached && !search) { // Don't cache search results return NextResponse.json(cached); } @@ -80,7 +91,7 @@ export async function GET(request: NextRequest) { // Cache the result (only for non-search queries) if (!search) { - await apiCache.setProjects(result); + await apiCache.setProjects(cacheParams, result); } return NextResponse.json(result); @@ -95,11 +106,26 @@ export async function GET(request: NextRequest) { export async function POST(request: NextRequest) { try { + // Check if this is an admin request + const isAdminRequest = request.headers.get('x-admin-request') === 'true'; + if (!isAdminRequest) { + return NextResponse.json( + { error: 'Admin access required' }, + { status: 403 } + ); + } + const data = await request.json(); + // Remove difficulty field if it exists (since we're removing it) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { difficulty, ...projectData } = data; + const project = await prisma.project.create({ data: { - ...data, + ...projectData, + // Set default difficulty since it's required in schema + difficulty: 'INTERMEDIATE', performance: data.performance || { lighthouse: 0, bundleSize: '0KB', loadTime: '0s' }, analytics: data.analytics || { views: 0, likes: 0, shares: 0 } } @@ -112,7 +138,7 @@ export async function POST(request: NextRequest) { } catch (error) { console.error('Error creating project:', error); return NextResponse.json( - { error: 'Failed to create project' }, + { error: 'Failed to create project', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 } ); } diff --git a/app/components/Contact.tsx b/app/components/Contact.tsx index 06b0284..d968927 100644 --- a/app/components/Contact.tsx +++ b/app/components/Contact.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; -import { Mail, Phone, MapPin, Send } from 'lucide-react'; +import { Mail, MapPin, Send } from 'lucide-react'; import { useToast } from '@/components/Toast'; const Contact = () => { @@ -69,12 +69,6 @@ const Contact = () => { value: 'contact@dk0.dev', href: 'mailto:contact@dk0.dev' }, - { - icon: Phone, - title: 'Phone', - value: '+49 176 12669990', - href: 'tel:+4917612669990' - }, { icon: MapPin, title: 'Location', diff --git a/app/editor/page.tsx b/app/editor/page.tsx index 9196c0c..f0a39ed 100644 --- a/app/editor/page.tsx +++ b/app/editor/page.tsx @@ -1,27 +1,26 @@ 'use client'; -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useCallback, Suspense } from 'react'; import { useSearchParams } from 'next/navigation'; import { motion, AnimatePresence } from 'framer-motion'; import { ArrowLeft, Save, Eye, - Plus, X, Bold, Italic, Code, Image, Link, - Type, List, ListOrdered, Quote, Hash, Loader2, - Upload, - Check + ExternalLink, + Github, + Tag } from 'lucide-react'; interface Project { @@ -30,7 +29,6 @@ interface Project { description: string; content?: string; category: string; - difficulty?: string; tags?: string[]; featured: boolean; published: boolean; @@ -41,17 +39,18 @@ interface Project { updatedAt: string; } -export default function EditorPage() { +function EditorPageContent() { const searchParams = useSearchParams(); const projectId = searchParams.get('id'); const contentRef = useRef(null); - const [project, setProject] = useState(null); + const [, setProject] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [isCreating, setIsCreating] = useState(!projectId); const [showPreview, setShowPreview] = useState(false); + const [isTyping, setIsTyping] = useState(false); // Form state const [formData, setFormData] = useState({ @@ -59,7 +58,6 @@ export default function EditorPage() { description: '', content: '', category: 'web', - difficulty: 'beginner', tags: [] as string[], featured: false, published: false, @@ -68,6 +66,44 @@ export default function EditorPage() { image: '' }); + const loadProject = useCallback(async (id: string) => { + try { + console.log('Fetching projects...'); + const response = await fetch('/api/projects'); + + if (response.ok) { + const data = await response.json(); + console.log('Projects loaded:', data); + + const foundProject = data.projects.find((p: Project) => p.id.toString() === id); + console.log('Found project:', foundProject); + + if (foundProject) { + setProject(foundProject); + setFormData({ + title: foundProject.title || '', + description: foundProject.description || '', + content: foundProject.content || '', + category: foundProject.category || 'web', + tags: foundProject.tags || [], + featured: foundProject.featured || false, + published: foundProject.published || false, + github: foundProject.github || '', + live: foundProject.live || '', + image: foundProject.image || '' + }); + console.log('Form data set for project:', foundProject.title); + } else { + console.log('Project not found with ID:', id); + } + } else { + console.error('Failed to fetch projects:', response.status); + } + } catch (error) { + console.error('Error loading project:', error); + } + }, []); + // Check authentication and load project useEffect(() => { const init = async () => { @@ -103,85 +139,92 @@ export default function EditorPage() { }; init(); - }, [projectId]); - - const loadProject = async (id: string) => { - try { - console.log('Fetching projects...'); - const response = await fetch('/api/projects'); - - if (response.ok) { - const data = await response.json(); - console.log('Projects loaded:', data); - - const foundProject = data.projects.find((p: Project) => p.id.toString() === id); - console.log('Found project:', foundProject); - - if (foundProject) { - setProject(foundProject); - setFormData({ - title: foundProject.title || '', - description: foundProject.description || '', - content: foundProject.content || '', - category: foundProject.category || 'web', - difficulty: foundProject.difficulty || 'beginner', - tags: foundProject.tags || [], - featured: foundProject.featured || false, - published: foundProject.published || false, - github: foundProject.github || '', - live: foundProject.live || '', - image: foundProject.image || '' - }); - console.log('Form data set:', formData); - } else { - console.log('Project not found with ID:', id); - } - } else { - console.error('Failed to fetch projects:', response.status); - } - } catch (error) { - console.error('Error loading project:', error); - } - }; + }, [projectId, loadProject]); const handleSave = async () => { try { setIsSaving(true); + // Validate required fields + if (!formData.title.trim()) { + alert('Please enter a project title'); + return; + } + + if (!formData.description.trim()) { + alert('Please enter a project description'); + return; + } + const url = projectId ? `/api/projects/${projectId}` : '/api/projects'; const method = projectId ? 'PUT' : 'POST'; - console.log('Saving project:', { url, method, formData }); + // Prepare data for saving - only include fields that exist in the database schema + const saveData = { + title: formData.title.trim(), + description: formData.description.trim(), + content: formData.content.trim(), + category: formData.category, + tags: formData.tags, + github: formData.github.trim() || null, + live: formData.live.trim() || null, + imageUrl: formData.image.trim() || null, + published: formData.published, + featured: formData.featured, + // Add required fields that might be missing + date: new Date().toISOString().split('T')[0] // Current date in YYYY-MM-DD format + }; + + console.log('Saving project:', { url, method, saveData }); const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json', + 'x-admin-request': 'true' }, - body: JSON.stringify(formData) + body: JSON.stringify(saveData) }); if (response.ok) { const savedProject = await response.json(); - console.log('Project saved:', savedProject); + console.log('Project saved successfully:', savedProject); + + // Update local state with the saved project data + setProject(savedProject); + setFormData(prev => ({ + ...prev, + title: savedProject.title || '', + description: savedProject.description || '', + content: savedProject.content || '', + category: savedProject.category || 'web', + tags: savedProject.tags || [], + featured: savedProject.featured || false, + published: savedProject.published || false, + github: savedProject.github || '', + live: savedProject.live || '', + image: savedProject.imageUrl || '' + })); // Show success and redirect + alert('Project saved successfully!'); setTimeout(() => { window.location.href = '/manage'; }, 1000); } else { - console.error('Error saving project:', response.status); - alert('Error saving project'); + const errorData = await response.json(); + console.error('Error saving project:', response.status, errorData); + alert(`Error saving project: ${errorData.error || 'Unknown error'}`); } } catch (error) { console.error('Error saving project:', error); - alert('Error saving project'); + alert(`Error saving project: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { setIsSaving(false); } }; - const handleInputChange = (field: string, value: any) => { + const handleInputChange = (field: string, value: string | boolean | string[]) => { setFormData(prev => ({ ...prev, [field]: value @@ -196,6 +239,42 @@ export default function EditorPage() { })); }; + // Simple markdown to HTML converter + const parseMarkdown = (text: string) => { + if (!text) return ''; + + return text + // Headers + .replace(/^### (.*$)/gim, '

$1

') + .replace(/^## (.*$)/gim, '

$1

') + .replace(/^# (.*$)/gim, '

$1

') + // Bold + .replace(/\*\*(.*?)\*\*/g, '$1') + // Italic + .replace(/\*(.*?)\*/g, '$1') + // Code blocks + .replace(/```([\s\S]*?)```/g, '
$1
') + // Inline code + .replace(/`(.*?)`/g, '$1') + // Links + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') + // Images + .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1') + // Ensure all images have alt attributes + .replace(/]*?)(?:\s+alt\s*=\s*["'][^"']*["'])?([^>]*?)>/g, (match, before, after) => { + if (match.includes('alt=')) return match; + return ``; + }) + // Lists + .replace(/^\* (.*$)/gim, '
  • $1
  • ') + .replace(/^- (.*$)/gim, '
  • $1
  • ') + .replace(/^(\d+)\. (.*$)/gim, '
  • $2
  • ') + // Blockquotes + .replace(/^> (.*$)/gim, '
    $1
    ') + // Line breaks + .replace(/\n/g, '
    '); + }; + // Rich text editor functions const insertFormatting = (format: string) => { const content = contentRef.current; @@ -263,14 +342,21 @@ export default function EditorPage() { if (isLoading) { return ( -
    +
    -

    Loading editor...

    + initial={{ opacity: 0, scale: 0.8 }} + animate={{ opacity: 1, scale: 1 }} + className="glass-card p-8 rounded-2xl" + > + +

    Loading Editor

    +

    Preparing your workspace...

    +
    ); @@ -278,7 +364,7 @@ export default function EditorPage() { if (!isAuthenticated) { return ( -
    +
    +
    {/* Header */} -
    -
    -
    -
    +
    +
    +
    +
    -
    -

    +
    +

    {isCreating ? 'Create New Project' : `Edit: ${formData.title || 'Untitled'}`}

    -
    +
    {/* Editor Content */} -
    -
    +
    +
    + {/* Floating particles background */} +
    + {[...Array(20)].map((_, i) => ( +
    + ))} +
    {/* Main Editor */}
    {/* Project Title */} handleInputChange('title', e.target.value)} - className="w-full text-3xl font-bold bg-white/10 text-white placeholder-white/50 focus:outline-none p-4 rounded-lg border border-white/20 focus:ring-2 focus:ring-blue-500" + className="w-full text-3xl font-bold form-input-enhanced focus:outline-none p-4 rounded-lg" placeholder="Enter project title..." /> @@ -378,95 +479,95 @@ export default function EditorPage() { initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }} - className="admin-glass-card p-4 rounded-xl" + className="glass-card p-4 rounded-2xl" > -
    -
    +
    +
    -
    +
    -
    +
    @@ -477,24 +578,29 @@ export default function EditorPage() { initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }} - className="admin-glass-card p-6 rounded-xl" + className="glass-card p-6 rounded-2xl" > -

    Content

    +

    Content

    { const target = e.target as HTMLDivElement; + setIsTyping(true); setFormData(prev => ({ ...prev, content: target.textContent || '' })); }} + onBlur={() => { + setIsTyping(false); + }} suppressContentEditableWarning={true} + data-placeholder="Start writing your project content..." > - {formData.content || 'Start writing your project content...'} + {!isTyping ? formData.content : undefined}

    Supports Markdown formatting. Use the toolbar above or type directly. @@ -506,14 +612,14 @@ export default function EditorPage() { initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.3 }} - className="admin-glass-card p-6 rounded-xl" + className="glass-card p-6 rounded-2xl" > -

    Description

    +

    Description