huge update

This commit is contained in:
2025-09-10 10:59:14 +02:00
parent be01ee2adb
commit 2f40fc6753
15 changed files with 729 additions and 301 deletions

View File

@@ -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 (
<div>
<h1>Toast Test</h1>
</div>
);
};
const renderWithToast = (component: React.ReactElement) => {
return render(
<ToastProvider>
{component}
</ToastProvider>
);
};
describe('Toast Component', () => {
it('renders ToastProvider without crashing', () => {
renderWithToast(<TestComponent />);
expect(screen.getByText('Toast Test')).toBeInTheDocument();
});
it('provides toast context', () => {
// Simple test to ensure the provider works
const { container } = renderWithToast(<TestComponent />);
expect(container).toBeInTheDocument();
});
});

View File

@@ -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);

View File

@@ -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 }
);
}

View File

@@ -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',

View File

@@ -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<HTMLDivElement>(null);
const [project, setProject] = useState<Project | null>(null);
const [, setProject] = useState<Project | null>(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, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
// Bold
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
// Italic
.replace(/\*(.*?)\*/g, '<em>$1</em>')
// Code blocks
.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
// Inline code
.replace(/`(.*?)`/g, '<code>$1</code>')
// Links
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
// Images
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" />')
// Ensure all images have alt attributes
.replace(/<img([^>]*?)(?:\s+alt\s*=\s*["'][^"']*["'])?([^>]*?)>/g, (match, before, after) => {
if (match.includes('alt=')) return match;
return `<img${before} alt=""${after}>`;
})
// Lists
.replace(/^\* (.*$)/gim, '<li>$1</li>')
.replace(/^- (.*$)/gim, '<li>$1</li>')
.replace(/^(\d+)\. (.*$)/gim, '<li>$2</li>')
// Blockquotes
.replace(/^> (.*$)/gim, '<blockquote>$1</blockquote>')
// Line breaks
.replace(/\n/g, '<br>');
};
// Rich text editor functions
const insertFormatting = (format: string) => {
const content = contentRef.current;
@@ -263,14 +342,21 @@ export default function EditorPage() {
if (isLoading) {
return (
<div className="min-h-screen admin-gradient flex items-center justify-center">
<div className="min-h-screen animated-bg flex items-center justify-center">
<div className="text-center">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full mx-auto mb-4"
/>
<p className="text-white">Loading editor...</p>
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
className="glass-card p-8 rounded-2xl"
>
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="w-12 h-12 border-3 border-blue-500 border-t-transparent rounded-full mx-auto mb-6"
/>
<h2 className="text-xl font-semibold gradient-text mb-2">Loading Editor</h2>
<p className="text-gray-400">Preparing your workspace...</p>
</motion.div>
</div>
</div>
);
@@ -278,7 +364,7 @@ export default function EditorPage() {
if (!isAuthenticated) {
return (
<div className="min-h-screen admin-gradient flex items-center justify-center">
<div className="min-h-screen animated-bg flex items-center justify-center">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
@@ -304,42 +390,43 @@ export default function EditorPage() {
}
return (
<div className="min-h-screen admin-gradient">
<div className="min-h-screen animated-bg">
{/* Header */}
<div className="admin-glass-header border-b border-white/10">
<div className="max-w-7xl mx-auto px-6">
<div className="flex items-center justify-between h-16">
<div className="flex items-center space-x-4">
<div className="glass-card border-b border-white/10 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between h-auto sm:h-16 py-4 sm:py-0 gap-4 sm:gap-0">
<div className="flex flex-col sm:flex-row items-start sm:items-center space-y-2 sm:space-y-0 sm:space-x-4">
<button
onClick={() => window.location.href = '/manage'}
className="flex items-center space-x-2 text-white/70 hover:text-white transition-colors"
className="inline-flex items-center space-x-2 text-blue-400 hover:text-blue-300 transition-colors"
>
<ArrowLeft className="w-5 h-5" />
<span>Back to Dashboard</span>
<span className="hidden sm:inline">Back to Dashboard</span>
<span className="sm:hidden">Back</span>
</button>
<div className="h-6 w-px bg-white/20" />
<h1 className="text-xl font-semibold text-white">
<div className="hidden sm:block h-6 w-px bg-white/20" />
<h1 className="text-lg sm:text-xl font-semibold gradient-text truncate max-w-xs sm:max-w-none">
{isCreating ? 'Create New Project' : `Edit: ${formData.title || 'Untitled'}`}
</h1>
</div>
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-2 sm:space-x-3 w-full sm:w-auto">
<button
onClick={() => setShowPreview(!showPreview)}
className={`flex items-center space-x-2 px-4 py-2 rounded-xl transition-all ${
className={`flex items-center space-x-2 px-4 py-2 rounded-lg font-medium transition-all duration-200 text-sm ${
showPreview
? 'bg-purple-500 text-white'
: 'bg-white/10 text-white/70 hover:bg-white/20'
? 'bg-blue-600 text-white shadow-lg'
: 'bg-gray-800/50 text-gray-300 hover:bg-gray-700/50 hover:text-white'
}`}
>
<Eye className="w-4 h-4" />
<span>Preview</span>
<span className="hidden sm:inline">Preview</span>
</button>
<button
onClick={handleSave}
disabled={isSaving}
className="flex items-center space-x-2 px-6 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-xl hover:scale-105 transition-all font-medium disabled:opacity-50"
className="btn-primary flex items-center space-x-2 px-6 py-2 text-sm sm:text-base flex-1 sm:flex-none disabled:opacity-50"
>
{isSaving ? (
<Loader2 className="w-4 h-4 animate-spin" />
@@ -354,21 +441,35 @@ export default function EditorPage() {
</div>
{/* Editor Content */}
<div className="max-w-7xl mx-auto px-6 py-8">
<div className="grid grid-cols-1 xl:grid-cols-4 gap-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-6 sm:py-8">
<div className="grid grid-cols-1 xl:grid-cols-4 gap-6 lg:gap-8">
{/* Floating particles background */}
<div className="particles">
{[...Array(20)].map((_, i) => (
<div
key={i}
className="particle"
style={{
left: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 20}s`,
animationDuration: `${20 + Math.random() * 10}s`
}}
/>
))}
</div>
{/* Main Editor */}
<div className="xl:col-span-3 space-y-6">
{/* Project Title */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="admin-glass-card p-6 rounded-xl"
className="glass-card p-6 rounded-2xl"
>
<input
type="text"
value={formData.title}
onChange={(e) => 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..."
/>
</motion.div>
@@ -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"
>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center space-x-1 border-r border-white/20 pr-3">
<div className="flex flex-wrap items-center gap-1 sm:gap-2">
<div className="flex items-center space-x-1 border-r border-white/20 pr-2 sm:pr-3">
<button
onClick={() => insertFormatting('bold')}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
className="p-2 rounded-lg text-gray-300"
title="Bold"
>
<Bold className="w-4 h-4 text-white/70" />
<Bold className="w-4 h-4" />
</button>
<button
onClick={() => insertFormatting('italic')}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
className="p-2 rounded-lg text-gray-300"
title="Italic"
>
<Italic className="w-4 h-4 text-white/70" />
<Italic className="w-4 h-4" />
</button>
<button
onClick={() => insertFormatting('code')}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
className="p-2 rounded-lg text-gray-300"
title="Code"
>
<Code className="w-4 h-4 text-white/70" />
<Code className="w-4 h-4" />
</button>
</div>
<div className="flex items-center space-x-1 border-r border-white/20 pr-3">
<div className="flex items-center space-x-1 border-r border-white/20 pr-2 sm:pr-3">
<button
onClick={() => insertFormatting('h1')}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
className="p-2 rounded-lg text-gray-300"
title="Heading 1"
>
<Hash className="w-4 h-4 text-white/70" />
<Hash className="w-4 h-4" />
</button>
<button
onClick={() => insertFormatting('h2')}
className="p-2 hover:bg-white/10 rounded-lg transition-colors text-sm"
className="p-2 hover:bg-gray-800/50 rounded-lg transition-all duration-200 text-xs sm:text-sm text-gray-300 hover:text-white hover:scale-105"
title="Heading 2"
>
H2
</button>
<button
onClick={() => insertFormatting('h3')}
className="p-2 hover:bg-white/10 rounded-lg transition-colors text-sm"
className="p-2 hover:bg-gray-800/50 rounded-lg transition-all duration-200 text-xs sm:text-sm text-gray-300 hover:text-white hover:scale-105"
title="Heading 3"
>
H3
</button>
</div>
<div className="flex items-center space-x-1 border-r border-white/20 pr-3">
<div className="flex items-center space-x-1 border-r border-white/20 pr-2 sm:pr-3">
<button
onClick={() => insertFormatting('list')}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
className="p-2 rounded-lg text-gray-300"
title="Bullet List"
>
<List className="w-4 h-4 text-white/70" />
<List className="w-4 h-4" />
</button>
<button
onClick={() => insertFormatting('orderedList')}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
className="p-2 rounded-lg text-gray-300"
title="Numbered List"
>
<ListOrdered className="w-4 h-4 text-white/70" />
<ListOrdered className="w-4 h-4" />
</button>
<button
onClick={() => insertFormatting('quote')}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
className="p-2 rounded-lg text-gray-300"
title="Quote"
>
<Quote className="w-4 h-4 text-white/70" />
<Quote className="w-4 h-4" />
</button>
</div>
<div className="flex items-center space-x-1">
<button
onClick={() => insertFormatting('link')}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
className="p-2 rounded-lg text-gray-300"
title="Link"
>
<Link className="w-4 h-4 text-white/70" />
<Link className="w-4 h-4" />
</button>
<button
onClick={() => insertFormatting('image')}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
className="p-2 rounded-lg text-gray-300"
title="Image"
>
<Image className="w-4 h-4 text-white/70" />
<Image className="w-4 h-4" />
</button>
</div>
</div>
@@ -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"
>
<h3 className="text-lg font-semibold text-white mb-4">Content</h3>
<h3 className="text-lg font-semibold gradient-text mb-4">Content</h3>
<div
ref={contentRef}
contentEditable
className="w-full min-h-[400px] p-6 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500 leading-relaxed"
className="editor-content-editable w-full min-h-[400px] p-6 form-input-enhanced rounded-lg focus:outline-none leading-relaxed"
style={{ whiteSpace: 'pre-wrap' }}
onInput={(e) => {
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}
</div>
<p className="text-xs text-white/50 mt-2">
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"
>
<h3 className="text-lg font-semibold text-white mb-4">Description</h3>
<h3 className="text-lg font-semibold gradient-text mb-4">Description</h3>
<textarea
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
rows={4}
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
className="w-full px-4 py-3 form-input-enhanced rounded-lg focus:outline-none resize-none"
placeholder="Brief description of your project..."
/>
</motion.div>
@@ -526,43 +632,30 @@ export default function EditorPage() {
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4 }}
className="admin-glass-card p-6 rounded-xl"
className="glass-card p-6 rounded-2xl"
>
<h3 className="text-lg font-semibold text-white mb-4">Settings</h3>
<h3 className="text-lg font-semibold gradient-text mb-4">Settings</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-white/70 mb-2">
Category
</label>
<select
value={formData.category}
onChange={(e) => handleInputChange('category', e.target.value)}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="web">Web Development</option>
<option value="mobile">Mobile Development</option>
<option value="desktop">Desktop Application</option>
<option value="game">Game Development</option>
<option value="ai">AI/ML</option>
<option value="other">Other</option>
</select>
<div className="custom-select">
<select
value={formData.category}
onChange={(e) => handleInputChange('category', e.target.value)}
>
<option value="web">Web Development</option>
<option value="mobile">Mobile Development</option>
<option value="desktop">Desktop Application</option>
<option value="game">Game Development</option>
<option value="ai">AI/ML</option>
<option value="other">Other</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-white/70 mb-2">
Difficulty
</label>
<select
value={formData.difficulty}
onChange={(e) => handleInputChange('difficulty', e.target.value)}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-white/70 mb-2">
@@ -572,7 +665,7 @@ export default function EditorPage() {
type="text"
value={formData.tags.join(', ')}
onChange={(e) => handleTagsChange(e.target.value)}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-3 py-2 form-input-enhanced rounded-lg focus:outline-none"
placeholder="React, TypeScript, Next.js"
/>
</div>
@@ -584,9 +677,9 @@ export default function EditorPage() {
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
className="admin-glass-card p-6 rounded-xl"
className="glass-card p-6 rounded-2xl"
>
<h3 className="text-lg font-semibold text-white mb-4">Links</h3>
<h3 className="text-lg font-semibold gradient-text mb-4">Links</h3>
<div className="space-y-4">
<div>
@@ -597,7 +690,7 @@ export default function EditorPage() {
type="url"
value={formData.github}
onChange={(e) => handleInputChange('github', e.target.value)}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-3 py-2 form-input-enhanced rounded-lg focus:outline-none"
placeholder="https://github.com/username/repo"
/>
</div>
@@ -610,7 +703,7 @@ export default function EditorPage() {
type="url"
value={formData.live}
onChange={(e) => handleInputChange('live', e.target.value)}
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500"
className="w-full px-3 py-2 form-input-enhanced rounded-lg focus:outline-none"
placeholder="https://example.com"
/>
</div>
@@ -622,9 +715,9 @@ export default function EditorPage() {
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.6 }}
className="admin-glass-card p-6 rounded-xl"
className="glass-card p-6 rounded-2xl"
>
<h3 className="text-lg font-semibold text-white mb-4">Publish</h3>
<h3 className="text-lg font-semibold gradient-text mb-4">Publish</h3>
<div className="space-y-4">
<label className="flex items-center space-x-3">
@@ -632,7 +725,7 @@ export default function EditorPage() {
type="checkbox"
checked={formData.featured}
onChange={(e) => handleInputChange('featured', e.target.checked)}
className="w-4 h-4 text-blue-500 bg-white/10 border-white/20 rounded focus:ring-blue-500"
className="w-4 h-4 text-blue-500 bg-gray-900/80 border-gray-600/50 rounded focus:ring-blue-500 focus:ring-2"
/>
<span className="text-white">Featured Project</span>
</label>
@@ -642,7 +735,7 @@ export default function EditorPage() {
type="checkbox"
checked={formData.published}
onChange={(e) => handleInputChange('published', e.target.checked)}
className="w-4 h-4 text-blue-500 bg-white/10 border-white/20 rounded focus:ring-blue-500"
className="w-4 h-4 text-blue-500 bg-gray-900/80 border-gray-600/50 rounded focus:ring-blue-500 focus:ring-2"
/>
<span className="text-white">Published</span>
</label>
@@ -661,6 +754,151 @@ export default function EditorPage() {
</div>
</div>
</div>
{/* Preview Modal */}
<AnimatePresence>
{showPreview && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={() => setShowPreview(false)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="glass-card rounded-2xl p-8 max-w-4xl w-full max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold gradient-text">Project Preview</h2>
<button
onClick={() => setShowPreview(false)}
className="p-2 rounded-lg"
>
<X className="w-5 h-5 text-white/70" />
</button>
</div>
{/* Preview Content */}
<div className="space-y-6">
{/* Project Header */}
<div className="text-center">
<h1 className="text-4xl font-bold gradient-text mb-4">
{formData.title || 'Untitled Project'}
</h1>
<p className="text-xl text-gray-400 mb-6">
{formData.description || 'No description provided'}
</p>
{/* Project Meta */}
<div className="flex flex-wrap justify-center gap-4 mb-6">
<div className="flex items-center space-x-2 text-gray-300">
<Tag className="w-4 h-4" />
<span className="capitalize">{formData.category}</span>
</div>
{formData.featured && (
<div className="flex items-center space-x-2 text-blue-400">
<span className="text-sm font-semibold"> Featured</span>
</div>
)}
</div>
{/* Tags */}
{formData.tags.length > 0 && (
<div className="flex flex-wrap justify-center gap-2 mb-6">
{formData.tags.map((tag, index) => (
<span
key={index}
className="px-3 py-1 bg-gray-800/50 text-gray-300 text-sm rounded-full border border-gray-700"
>
{tag}
</span>
))}
</div>
)}
{/* Links */}
{((formData.github && formData.github.trim()) || (formData.live && formData.live.trim())) && (
<div className="flex justify-center space-x-4 mb-8">
{formData.github && formData.github.trim() && (
<a
href={formData.github}
target="_blank"
rel="noopener noreferrer"
className="flex items-center space-x-2 px-4 py-2 bg-gray-800/50 text-gray-300 rounded-lg"
>
<Github className="w-4 h-4" />
<span>GitHub</span>
</a>
)}
{formData.live && formData.live.trim() && (
<a
href={formData.live}
target="_blank"
rel="noopener noreferrer"
className="flex items-center space-x-2 px-4 py-2 bg-blue-600/80 text-white rounded-lg"
>
<ExternalLink className="w-4 h-4" />
<span>Live Demo</span>
</a>
)}
</div>
)}
</div>
{/* Content Preview */}
{formData.content && (
<div className="border-t border-white/10 pt-6">
<h3 className="text-xl font-semibold gradient-text mb-4">Content</h3>
<div className="prose prose-invert max-w-none">
<div
className="markdown text-gray-300 leading-relaxed"
dangerouslySetInnerHTML={{ __html: parseMarkdown(formData.content) }}
/>
</div>
</div>
)}
{/* Status */}
<div className="border-t border-white/10 pt-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
formData.published
? 'bg-green-500/20 text-green-400'
: 'bg-yellow-500/20 text-yellow-400'
}`}>
{formData.published ? 'Published' : 'Draft'}
</span>
{formData.featured && (
<span className="px-3 py-1 bg-blue-500/20 text-blue-400 rounded-full text-sm font-medium">
Featured
</span>
)}
</div>
<div className="text-sm text-gray-400">
Last updated: {new Date().toLocaleDateString()}
</div>
</div>
</div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
export default function EditorPage() {
return (
<Suspense fallback={<div className="min-h-screen bg-gray-900 flex items-center justify-center">
<div className="text-white">Loading editor...</div>
</div>}>
<EditorPageContent />
</Suspense>
);
}

View File

@@ -114,6 +114,132 @@ body {
transition: all 0.2s ease !important;
}
/* Admin Gradient Background */
.admin-gradient {
background:
radial-gradient(circle at 20% 80%, rgba(59, 130, 246, 0.15) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(139, 92, 246, 0.15) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(236, 72, 153, 0.08) 0%, transparent 50%),
linear-gradient(-45deg, #0a0a0a, #111111, #0d0d0d, #151515);
background-size: 400% 400%, 400% 400%, 400% 400%, 400% 400%;
animation: gradientShift 25s ease infinite;
min-height: 100vh;
}
/* Admin Glass Header */
.admin-glass-header {
background: rgba(255, 255, 255, 0.08) !important;
backdrop-filter: blur(20px) !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.15) !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3) !important;
}
/* Editor-specific styles */
.editor-content-editable:empty:before {
content: attr(data-placeholder);
color: #9ca3af;
pointer-events: none;
font-style: italic;
opacity: 0.7;
}
.editor-content-editable:focus:before {
content: none;
}
.editor-content-editable:empty {
min-height: 400px;
display: flex;
align-items: flex-start;
padding-top: 1.5rem;
}
.editor-content-editable:not(:empty) {
min-height: 400px;
}
/* Enhanced form styling */
.form-input-enhanced {
background: rgba(17, 24, 39, 0.8) !important;
border: 1px solid rgba(75, 85, 99, 0.5) !important;
color: #ffffff !important;
transition: all 0.3s ease !important;
backdrop-filter: blur(10px) !important;
}
.form-input-enhanced:focus {
border-color: #3b82f6 !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2), 0 0 20px rgba(59, 130, 246, 0.1) !important;
background: rgba(17, 24, 39, 0.9) !important;
transform: translateY(-1px) !important;
}
.form-input-enhanced::placeholder {
color: #9ca3af !important;
}
/* Select styling */
select.form-input-enhanced {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
appearance: none;
cursor: pointer;
}
select.form-input-enhanced:focus {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%233b82f6' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
}
/* Custom dropdown styling */
.custom-select {
position: relative;
display: inline-block;
width: 100%;
}
.custom-select select {
width: 100%;
padding: 0.75rem 2.5rem 0.75rem 0.75rem;
background: rgba(17, 24, 39, 0.8);
border: 1px solid rgba(75, 85, 99, 0.5);
border-radius: 0.5rem;
color: #ffffff;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.75rem center;
background-repeat: no-repeat;
background-size: 1.25em 1.25em;
}
.custom-select select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2), 0 0 20px rgba(59, 130, 246, 0.1);
background: rgba(17, 24, 39, 0.9);
transform: translateY(-1px);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%233b82f6' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
}
/* Ensure no default browser arrows show */
.custom-select select::-ms-expand {
display: none;
}
.custom-select select::-webkit-appearance {
-webkit-appearance: none;
}
/* Gradient Text */
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);

View File

@@ -115,33 +115,37 @@ const ProjectDetail = () => {
</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>
{((project.github && project.github.trim() && project.github !== "#") || (project.live && project.live.trim() && project.live !== "#")) && (
<div className="flex flex-wrap gap-4">
{project.github && project.github.trim() && project.github !== "#" && (
<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 && project.live.trim() && 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 */}

View File

@@ -145,32 +145,34 @@ const ProjectsPage = () => {
</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>
{((project.github && project.github.trim() && project.github !== "#") || (project.live && project.live.trim() && project.live !== "#")) && (
<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 && project.github.trim() && 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.trim() && 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">

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { motion } from 'framer-motion';
import {
BarChart3,
@@ -8,16 +8,12 @@ import {
Eye,
Heart,
Zap,
Users,
Clock,
Globe,
Activity,
Target,
Award,
RefreshCw,
Calendar,
MousePointer,
Monitor,
RotateCcw,
Trash2,
AlertTriangle
@@ -76,7 +72,7 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
const [resetType, setResetType] = useState<'analytics' | 'pageviews' | 'interactions' | 'performance' | 'all'>('analytics');
const [resetting, setResetting] = useState(false);
const fetchAnalyticsData = async () => {
const fetchAnalyticsData = useCallback(async () => {
if (!isAuthenticated) return;
try {
@@ -132,7 +128,7 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
} finally {
setLoading(false);
}
};
}, [isAuthenticated]);
const resetAnalytics = async () => {
if (!isAuthenticated || resetting) return;
@@ -167,7 +163,7 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
if (isAuthenticated) {
fetchAnalyticsData();
}
}, [isAuthenticated, timeRange]);
}, [isAuthenticated, fetchAnalyticsData]);
const StatCard = ({ title, value, icon: Icon, color, trend, trendValue, description }: {
title: string;
@@ -530,7 +526,7 @@ export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps)
<label className="block text-white/80 text-sm mb-2">Reset Type</label>
<select
value={resetType}
onChange={(e) => setResetType(e.target.value as any)}
onChange={(e) => setResetType(e.target.value as 'all' | 'performance' | 'analytics')}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-red-500"
>
<option value="analytics">Analytics Only (views, likes, shares)</option>

View File

@@ -5,11 +5,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import {
Mail,
Search,
Filter,
Reply,
Archive,
Trash2,
Clock,
User,
CheckCircle,
Circle,
@@ -54,7 +50,7 @@ export const EmailManager: React.FC = () => {
if (response.ok) {
const data = await response.json();
const formattedMessages = data.contacts.map((contact: any) => ({
const formattedMessages = data.contacts.map((contact: ContactMessage) => ({
id: contact.id.toString(),
name: contact.name,
email: contact.email,
@@ -141,6 +137,7 @@ export const EmailManager: React.FC = () => {
});
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'text-red-400';
@@ -195,7 +192,7 @@ export const EmailManager: React.FC = () => {
{['all', 'unread', 'responded'].map((filterType) => (
<button
key={filterType}
onClick={() => setFilter(filterType as any)}
onClick={() => setFilter(filterType as 'all' | 'unread' | 'responded')}
className={`px-4 py-2 rounded-lg transition-colors ${
filter === filterType
? 'bg-blue-500 text-white'

View File

@@ -6,37 +6,21 @@ import {
Save,
X,
Eye,
EyeOff,
Settings,
Link as LinkIcon,
Tag,
Calendar,
Globe,
Github,
Image as ImageIcon,
Bold,
Italic,
List,
Hash,
Quote,
Code,
Zap,
Type,
Columns,
PanelLeft,
PanelRight,
Monitor,
Smartphone,
Tablet,
Undo,
Redo,
AlignLeft,
AlignCenter,
AlignRight,
Link2,
ListOrdered,
Underline,
Strikethrough
Strikethrough,
Type,
Columns
} from 'lucide-react';
interface Project {
@@ -60,7 +44,7 @@ interface GhostEditorProps {
isOpen: boolean;
onClose: () => void;
project?: Project | null;
onSave: (projectData: any) => void;
onSave: (projectData: Partial<Project>) => void;
isCreating: boolean;
}
@@ -251,7 +235,7 @@ export const GhostEditor: React.FC<GhostEditorProps> = ({
// Render markdown preview
const renderMarkdownPreview = (markdown: string) => {
// Simple markdown renderer for preview
let html = markdown
const html = markdown
// Headers
.replace(/^### (.*$)/gim, '<h3 class="text-xl font-semibold text-white mb-3 mt-6">$1</h3>')
.replace(/^## (.*$)/gim, '<h2 class="text-2xl font-bold text-white mb-4 mt-8">$1</h2>')

View File

@@ -7,8 +7,6 @@ import {
Settings,
TrendingUp,
Plus,
Edit,
Trash2,
Shield,
Users,
Activity,
@@ -56,6 +54,7 @@ interface ModernAdminDashboardProps {
const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthenticated = true }) => {
const [activeTab, setActiveTab] = useState<'overview' | 'projects' | 'emails' | 'analytics' | 'settings'>('overview');
const [projects, setProjects] = useState<Project[]>([]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isLoading, setIsLoading] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [analytics, setAnalytics] = useState<Record<string, unknown> | null>(null);
@@ -540,7 +539,7 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
</div>
</div>
<ProjectManager isAuthenticated={isAuthenticated} projects={projects} onProjectsChange={loadProjects} />
<ProjectManager projects={projects} onProjectsChange={loadProjects} />
</div>
)}

View File

@@ -1,29 +1,16 @@
'use client';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import {
Plus,
Edit,
Trash2,
Eye,
Search,
Filter,
Grid,
List,
Save,
X,
Upload,
Image as ImageIcon,
Link as LinkIcon,
Globe,
Github,
Calendar,
Tag,
Star,
TrendingUp,
Settings,
MoreVertical,
RefreshCw
} from 'lucide-react';
// Editor is now a separate page at /editor
@@ -54,13 +41,11 @@ interface Project {
}
interface ProjectManagerProps {
isAuthenticated: boolean;
projects: Project[];
onProjectsChange: () => void;
}
export const ProjectManager: React.FC<ProjectManagerProps> = ({
isAuthenticated,
projects,
onProjectsChange
}) => {
@@ -70,7 +55,6 @@ export const ProjectManager: React.FC<ProjectManagerProps> = ({
// Editor is now a separate page - no modal state needed
const categories = ['all', 'Web Development', 'Full-Stack', 'Web Application', 'Mobile App', 'Design'];
const difficulties = ['Beginner', 'Intermediate', 'Advanced', 'Expert'];
// Filter projects
const filteredProjects = projects.filter((project) => {

View File

@@ -8,36 +8,19 @@ import {
Eye,
EyeOff,
Settings,
Link as LinkIcon,
Tag,
Calendar,
Globe,
Github,
Image as ImageIcon,
Bold,
Italic,
List,
Hash,
Quote,
Code,
Zap,
Type,
Columns,
PanelLeft,
PanelRight,
Monitor,
Smartphone,
Tablet,
Undo,
Redo,
AlignLeft,
AlignCenter,
AlignRight,
Link2,
ListOrdered,
Underline,
Strikethrough,
GripVertical
GripVertical,
Image as ImageIcon
} from 'lucide-react';
interface Project {
@@ -59,7 +42,7 @@ interface Project {
interface ResizableGhostEditorProps {
project?: Project | null;
onSave: (projectData: any) => void;
onSave: (projectData: Partial<Project>) => void;
onClose: () => void;
isCreating: boolean;
}
@@ -277,7 +260,7 @@ export const ResizableGhostEditor: React.FC<ResizableGhostEditorProps> = ({
// Enhanced markdown renderer with proper white text
const renderMarkdownPreview = (markdown: string) => {
let html = markdown
const html = markdown
// Headers - WHITE TEXT
.replace(/^### (.*$)/gim, '<h3 class="text-xl font-semibold text-white mb-3 mt-6">$1</h3>')
.replace(/^## (.*$)/gim, '<h2 class="text-2xl font-bold text-white mb-4 mt-8">$1</h2>')

View File

@@ -2,12 +2,30 @@ import { cache } from './redis';
// API Response caching
export const apiCache = {
async getProjects() {
return await cache.get('api:projects');
// Generate cache key based on query parameters
generateProjectsKey(params: Record<string, string | null> = {}) {
const { page = '1', limit = '50', category, featured, published, difficulty, search } = params;
const keyParts = ['api:projects'];
if (page !== '1') keyParts.push(`page:${page}`);
if (limit !== '50') keyParts.push(`limit:${limit}`);
if (category) keyParts.push(`cat:${category}`);
if (featured !== null) keyParts.push(`feat:${featured}`);
if (published !== null) keyParts.push(`pub:${published}`);
if (difficulty) keyParts.push(`diff:${difficulty}`);
if (search) keyParts.push(`search:${search}`);
return keyParts.join(':');
},
async setProjects(projects: unknown, ttlSeconds = 300) {
return await cache.set('api:projects', projects, ttlSeconds);
async getProjects(params: Record<string, string | null> = {}) {
const key = this.generateProjectsKey(params);
return await cache.get(key);
},
async setProjects(params: Record<string, string | null> = {}, projects: unknown, ttlSeconds = 300) {
const key = this.generateProjectsKey(params);
return await cache.set(key, projects, ttlSeconds);
},
async getProject(id: number) {
@@ -20,11 +38,28 @@ export const apiCache = {
async invalidateProject(id: number) {
await cache.del(`api:project:${id}`);
await cache.del('api:projects');
// Invalidate all project list caches
await this.invalidateAllProjectLists();
},
async invalidateAllProjectLists() {
// Clear all project list caches by pattern
// This is a simplified approach - in production you'd use Redis SCAN
const commonKeys = [
'api:projects',
'api:projects:pub:true',
'api:projects:feat:true:pub:true:limit:6',
'api:projects:page:1:limit:50',
'api:projects:pub:true:page:1:limit:50'
];
for (const key of commonKeys) {
await cache.del(key);
}
},
async invalidateAll() {
await cache.del('api:projects');
await this.invalidateAllProjectLists();
// Clear all project caches
const keys = await this.getAllProjectKeys();
for (const key of keys) {