full upgrade

This commit is contained in:
2026-01-07 23:13:25 +01:00
parent 4cd3f60c98
commit c5efd28383
23 changed files with 693 additions and 226 deletions

View File

@@ -3,6 +3,7 @@
import React, { useState, useEffect, useRef, useCallback, Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';
import ReactMarkdown from 'react-markdown';
import {
ArrowLeft,
Save,
@@ -68,15 +69,11 @@ function EditorPageContent() {
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);
@@ -92,15 +89,16 @@ function EditorPageContent() {
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);
if (process.env.NODE_ENV === 'development') {
console.error('Failed to fetch projects:', response.status);
}
}
} catch (error) {
console.error('Error loading project:', error);
if (process.env.NODE_ENV === 'development') {
console.error('Error loading project:', error);
}
}
}, []);
@@ -112,26 +110,22 @@ function EditorPageContent() {
const authStatus = sessionStorage.getItem('admin_authenticated');
const sessionToken = sessionStorage.getItem('admin_session_token');
console.log('Editor Auth check:', { authStatus, hasSessionToken: !!sessionToken, projectId });
if (authStatus === 'true' && sessionToken) {
console.log('User is authenticated');
setIsAuthenticated(true);
// Load project if editing
if (projectId) {
console.log('Loading project with ID:', projectId);
await loadProject(projectId);
} else {
console.log('Creating new project');
setIsCreating(true);
}
} else {
console.log('User not authenticated');
setIsAuthenticated(false);
}
} catch (error) {
console.error('Error in init:', error);
if (process.env.NODE_ENV === 'development') {
console.error('Error in init:', error);
}
setIsAuthenticated(false);
} finally {
setIsLoading(false);
@@ -175,8 +169,6 @@ function EditorPageContent() {
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: {
@@ -188,7 +180,6 @@ function EditorPageContent() {
if (response.ok) {
const savedProject = await response.json();
console.log('Project saved successfully:', savedProject);
// Update local state with the saved project data
setProject(savedProject);
@@ -213,11 +204,15 @@ function EditorPageContent() {
}, 1000);
} else {
const errorData = await response.json();
console.error('Error saving project:', response.status, errorData);
if (process.env.NODE_ENV === 'development') {
console.error('Error saving project:', response.status, errorData);
}
alert(`Error saving project: ${errorData.error || 'Unknown error'}`);
}
} catch (error) {
console.error('Error saving project:', error);
if (process.env.NODE_ENV === 'development') {
console.error('Error saving project:', error);
}
alert(`Error saving project: ${error instanceof Error ? error.message : 'Unknown error'}`);
} finally {
setIsSaving(false);
@@ -239,40 +234,27 @@ function EditorPageContent() {
}));
};
// 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>');
// Markdown components for react-markdown with security
const markdownComponents = {
a: ({ node, ...props }: { node?: unknown; href?: string; children?: React.ReactNode }) => {
// Validate URLs to prevent javascript: and data: protocols
const href = props.href || '';
const isSafe = href && !href.startsWith('javascript:') && !href.startsWith('data:');
return (
<a
{...props}
href={isSafe ? href : '#'}
target={isSafe && href.startsWith('http') ? '_blank' : undefined}
rel={isSafe && href.startsWith('http') ? 'noopener noreferrer' : undefined}
/>
);
},
img: ({ node, ...props }: { node?: unknown; src?: string; alt?: string }) => {
// Validate image URLs
const src = props.src || '';
const isSafe = src && !src.startsWith('javascript:') && !src.startsWith('data:');
return isSafe ? <img {...props} src={src} alt={props.alt || ''} /> : null;
},
};
// Rich text editor functions
@@ -855,10 +837,11 @@ function EditorPageContent() {
<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 className="markdown text-gray-300 leading-relaxed">
<ReactMarkdown components={markdownComponents}>
{formData.content}
</ReactMarkdown>
</div>
</div>
</div>
)}