full upgrade to dev
This commit is contained in:
@@ -1,18 +1,24 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
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,
|
||||
Eye,
|
||||
X,
|
||||
Bold,
|
||||
Italic,
|
||||
Code,
|
||||
Image,
|
||||
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,
|
||||
Eye,
|
||||
X,
|
||||
Bold,
|
||||
Italic,
|
||||
Code,
|
||||
Image,
|
||||
Link,
|
||||
List,
|
||||
ListOrdered,
|
||||
@@ -21,8 +27,8 @@ import {
|
||||
Loader2,
|
||||
ExternalLink,
|
||||
Github,
|
||||
Tag
|
||||
} from 'lucide-react';
|
||||
Tag,
|
||||
} from "lucide-react";
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
@@ -42,9 +48,9 @@ interface Project {
|
||||
|
||||
function EditorPageContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const projectId = searchParams.get('id');
|
||||
const projectId = searchParams.get("id");
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
const [, setProject] = useState<Project | null>(null);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -52,52 +58,54 @@ function EditorPageContent() {
|
||||
const [isCreating, setIsCreating] = useState(!projectId);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
content: '',
|
||||
category: 'web',
|
||||
title: "",
|
||||
description: "",
|
||||
content: "",
|
||||
category: "web",
|
||||
tags: [] as string[],
|
||||
featured: false,
|
||||
published: false,
|
||||
github: '',
|
||||
live: '',
|
||||
image: ''
|
||||
github: "",
|
||||
live: "",
|
||||
image: "",
|
||||
});
|
||||
|
||||
const loadProject = useCallback(async (id: string) => {
|
||||
try {
|
||||
const response = await fetch('/api/projects');
|
||||
|
||||
const response = await fetch("/api/projects");
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const foundProject = data.projects.find((p: Project) => p.id.toString() === id);
|
||||
|
||||
const foundProject = data.projects.find(
|
||||
(p: Project) => p.id.toString() === id,
|
||||
);
|
||||
|
||||
if (foundProject) {
|
||||
setProject(foundProject);
|
||||
setFormData({
|
||||
title: foundProject.title || '',
|
||||
description: foundProject.description || '',
|
||||
content: foundProject.content || '',
|
||||
category: foundProject.category || 'web',
|
||||
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 || ''
|
||||
github: foundProject.github || "",
|
||||
live: foundProject.live || "",
|
||||
image: foundProject.image || "",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Failed to fetch projects:', response.status);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("Failed to fetch projects:", response.status);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error loading project:', error);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("Error loading project:", error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
@@ -107,12 +115,12 @@ function EditorPageContent() {
|
||||
const init = async () => {
|
||||
try {
|
||||
// Check auth
|
||||
const authStatus = sessionStorage.getItem('admin_authenticated');
|
||||
const sessionToken = sessionStorage.getItem('admin_session_token');
|
||||
|
||||
if (authStatus === 'true' && sessionToken) {
|
||||
const authStatus = sessionStorage.getItem("admin_authenticated");
|
||||
const sessionToken = sessionStorage.getItem("admin_session_token");
|
||||
|
||||
if (authStatus === "true" && sessionToken) {
|
||||
setIsAuthenticated(true);
|
||||
|
||||
|
||||
// Load project if editing
|
||||
if (projectId) {
|
||||
await loadProject(projectId);
|
||||
@@ -123,8 +131,8 @@ function EditorPageContent() {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error in init:', error);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.error("Error in init:", error);
|
||||
}
|
||||
setIsAuthenticated(false);
|
||||
} finally {
|
||||
@@ -138,21 +146,21 @@ function EditorPageContent() {
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setIsSaving(true);
|
||||
|
||||
|
||||
// Validate required fields
|
||||
if (!formData.title.trim()) {
|
||||
alert('Please enter a project title');
|
||||
alert("Please enter a project title");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!formData.description.trim()) {
|
||||
alert('Please enter a project description');
|
||||
alert("Please enter a project description");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = projectId ? `/api/projects/${projectId}` : '/api/projects';
|
||||
const method = projectId ? 'PUT' : 'POST';
|
||||
|
||||
|
||||
const url = projectId ? `/api/projects/${projectId}` : "/api/projects";
|
||||
const method = projectId ? "PUT" : "POST";
|
||||
|
||||
// Prepare data for saving - only include fields that exist in the database schema
|
||||
const saveData = {
|
||||
title: formData.title.trim(),
|
||||
@@ -166,94 +174,123 @@ function EditorPageContent() {
|
||||
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
|
||||
date: new Date().toISOString().split("T")[0], // Current date in YYYY-MM-DD format
|
||||
};
|
||||
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-admin-request': 'true'
|
||||
"Content-Type": "application/json",
|
||||
"x-admin-request": "true",
|
||||
},
|
||||
body: JSON.stringify(saveData)
|
||||
body: JSON.stringify(saveData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const savedProject = await response.json();
|
||||
|
||||
|
||||
// Update local state with the saved project data
|
||||
setProject(savedProject);
|
||||
setFormData(prev => ({
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
title: savedProject.title || '',
|
||||
description: savedProject.description || '',
|
||||
content: savedProject.content || '',
|
||||
category: savedProject.category || 'web',
|
||||
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 || ''
|
||||
github: savedProject.github || "",
|
||||
live: savedProject.live || "",
|
||||
image: savedProject.imageUrl || "",
|
||||
}));
|
||||
|
||||
|
||||
// Show success and redirect
|
||||
alert('Project saved successfully!');
|
||||
alert("Project saved successfully!");
|
||||
setTimeout(() => {
|
||||
window.location.href = '/manage';
|
||||
window.location.href = "/manage";
|
||||
}, 1000);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
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'}`);
|
||||
alert(`Error saving project: ${errorData.error || "Unknown error"}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
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'}`);
|
||||
alert(
|
||||
`Error saving project: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (field: string, value: string | boolean | string[]) => {
|
||||
setFormData(prev => ({
|
||||
const handleInputChange = (
|
||||
field: string,
|
||||
value: string | boolean | string[],
|
||||
) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleTagsChange = (tagsString: string) => {
|
||||
const tags = tagsString.split(',').map(tag => tag.trim()).filter(tag => tag);
|
||||
setFormData(prev => ({
|
||||
const tags = tagsString
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter((tag) => tag);
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
tags
|
||||
tags,
|
||||
}));
|
||||
};
|
||||
|
||||
// Markdown components for react-markdown with security
|
||||
const markdownComponents = {
|
||||
a: ({ node, ...props }: { node?: unknown; href?: string; children?: React.ReactNode }) => {
|
||||
a: ({
|
||||
node: _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:');
|
||||
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}
|
||||
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 }) => {
|
||||
img: ({
|
||||
node: _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;
|
||||
const src = props.src || "";
|
||||
const isSafe =
|
||||
src && !src.startsWith("javascript:") && !src.startsWith("data:");
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
return isSafe ? <img {...props} src={src} alt={props.alt || ""} /> : null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -266,46 +303,46 @@ function EditorPageContent() {
|
||||
if (!selection || selection.rangeCount === 0) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
let newText = '';
|
||||
|
||||
let newText = "";
|
||||
|
||||
switch (format) {
|
||||
case 'bold':
|
||||
newText = `**${selection.toString() || 'bold text'}**`;
|
||||
case "bold":
|
||||
newText = `**${selection.toString() || "bold text"}**`;
|
||||
break;
|
||||
case 'italic':
|
||||
newText = `*${selection.toString() || 'italic text'}*`;
|
||||
case "italic":
|
||||
newText = `*${selection.toString() || "italic text"}*`;
|
||||
break;
|
||||
case 'code':
|
||||
newText = `\`${selection.toString() || 'code'}\``;
|
||||
case "code":
|
||||
newText = `\`${selection.toString() || "code"}\``;
|
||||
break;
|
||||
case 'h1':
|
||||
newText = `# ${selection.toString() || 'Heading 1'}`;
|
||||
case "h1":
|
||||
newText = `# ${selection.toString() || "Heading 1"}`;
|
||||
break;
|
||||
case 'h2':
|
||||
newText = `## ${selection.toString() || 'Heading 2'}`;
|
||||
case "h2":
|
||||
newText = `## ${selection.toString() || "Heading 2"}`;
|
||||
break;
|
||||
case 'h3':
|
||||
newText = `### ${selection.toString() || 'Heading 3'}`;
|
||||
case "h3":
|
||||
newText = `### ${selection.toString() || "Heading 3"}`;
|
||||
break;
|
||||
case 'list':
|
||||
newText = `- ${selection.toString() || 'List item'}`;
|
||||
case "list":
|
||||
newText = `- ${selection.toString() || "List item"}`;
|
||||
break;
|
||||
case 'orderedList':
|
||||
newText = `1. ${selection.toString() || 'List item'}`;
|
||||
case "orderedList":
|
||||
newText = `1. ${selection.toString() || "List item"}`;
|
||||
break;
|
||||
case 'quote':
|
||||
newText = `> ${selection.toString() || 'Quote'}`;
|
||||
case "quote":
|
||||
newText = `> ${selection.toString() || "Quote"}`;
|
||||
break;
|
||||
case 'link':
|
||||
const url = prompt('Enter URL:');
|
||||
case "link":
|
||||
const url = prompt("Enter URL:");
|
||||
if (url) {
|
||||
newText = `[${selection.toString() || 'link text'}](${url})`;
|
||||
newText = `[${selection.toString() || "link text"}](${url})`;
|
||||
}
|
||||
break;
|
||||
case 'image':
|
||||
const imageUrl = prompt('Enter image URL:');
|
||||
case "image":
|
||||
const imageUrl = prompt("Enter image URL:");
|
||||
if (imageUrl) {
|
||||
newText = ``;
|
||||
newText = ``;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -313,11 +350,11 @@ function EditorPageContent() {
|
||||
if (newText) {
|
||||
range.deleteContents();
|
||||
range.insertNode(document.createTextNode(newText));
|
||||
|
||||
|
||||
// Update form data
|
||||
setFormData(prev => ({
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
content: content.textContent || ''
|
||||
content: content.textContent || "",
|
||||
}));
|
||||
}
|
||||
};
|
||||
@@ -336,7 +373,9 @@ function EditorPageContent() {
|
||||
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>
|
||||
<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>
|
||||
@@ -347,7 +386,7 @@ function EditorPageContent() {
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="min-h-screen animated-bg flex items-center justify-center">
|
||||
<motion.div
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="text-center text-white max-w-md mx-auto p-8 admin-glass-card rounded-2xl"
|
||||
@@ -357,11 +396,13 @@ function EditorPageContent() {
|
||||
<X className="w-8 h-8 text-red-400" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold mb-2">Access Denied</h1>
|
||||
<p className="text-white/70 mb-6">You need to be logged in to access the editor.</p>
|
||||
<p className="text-white/70 mb-6">
|
||||
You need to be logged in to access the editor.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => window.location.href = '/manage'}
|
||||
onClick={() => (window.location.href = "/manage")}
|
||||
className="w-full px-6 py-3 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-xl hover:scale-105 transition-all font-medium"
|
||||
>
|
||||
Go to Admin Login
|
||||
@@ -379,7 +420,7 @@ function EditorPageContent() {
|
||||
<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'}
|
||||
onClick={() => (window.location.href = "/manage")}
|
||||
className="inline-flex items-center space-x-2 text-blue-400 hover:text-blue-300 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
@@ -388,23 +429,25 @@ function EditorPageContent() {
|
||||
</button>
|
||||
<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'}`}
|
||||
{isCreating
|
||||
? "Create New Project"
|
||||
: `Edit: ${formData.title || "Untitled"}`}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<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-lg font-medium transition-all duration-200 text-sm ${
|
||||
showPreview
|
||||
? 'bg-blue-600 text-white shadow-lg'
|
||||
: 'bg-gray-800/50 text-gray-300 hover:bg-gray-700/50 hover:text-white'
|
||||
showPreview
|
||||
? "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 className="hidden sm:inline">Preview</span>
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
@@ -415,7 +458,7 @@ function EditorPageContent() {
|
||||
) : (
|
||||
<Save className="w-4 h-4" />
|
||||
)}
|
||||
<span>{isSaving ? 'Saving...' : 'Save Project'}</span>
|
||||
<span>{isSaving ? "Saving..." : "Save Project"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -434,7 +477,7 @@ function EditorPageContent() {
|
||||
style={{
|
||||
left: `${Math.random() * 100}%`,
|
||||
animationDelay: `${Math.random() * 20}s`,
|
||||
animationDuration: `${20 + Math.random() * 10}s`
|
||||
animationDuration: `${20 + Math.random() * 10}s`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -450,7 +493,7 @@ function EditorPageContent() {
|
||||
<input
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => handleInputChange('title', e.target.value)}
|
||||
onChange={(e) => handleInputChange("title", e.target.value)}
|
||||
className="w-full text-3xl font-bold form-input-enhanced focus:outline-none p-4 rounded-lg"
|
||||
placeholder="Enter project title..."
|
||||
/>
|
||||
@@ -466,21 +509,21 @@ function EditorPageContent() {
|
||||
<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')}
|
||||
onClick={() => insertFormatting("bold")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Bold"
|
||||
>
|
||||
<Bold className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertFormatting('italic')}
|
||||
onClick={() => insertFormatting("italic")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Italic"
|
||||
>
|
||||
<Italic className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertFormatting('code')}
|
||||
onClick={() => insertFormatting("code")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Code"
|
||||
>
|
||||
@@ -490,21 +533,21 @@ function EditorPageContent() {
|
||||
|
||||
<div className="flex items-center space-x-1 border-r border-white/20 pr-2 sm:pr-3">
|
||||
<button
|
||||
onClick={() => insertFormatting('h1')}
|
||||
onClick={() => insertFormatting("h1")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Heading 1"
|
||||
>
|
||||
<Hash className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertFormatting('h2')}
|
||||
onClick={() => insertFormatting("h2")}
|
||||
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')}
|
||||
onClick={() => insertFormatting("h3")}
|
||||
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"
|
||||
>
|
||||
@@ -514,21 +557,21 @@ function EditorPageContent() {
|
||||
|
||||
<div className="flex items-center space-x-1 border-r border-white/20 pr-2 sm:pr-3">
|
||||
<button
|
||||
onClick={() => insertFormatting('list')}
|
||||
onClick={() => insertFormatting("list")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Bullet List"
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertFormatting('orderedList')}
|
||||
onClick={() => insertFormatting("orderedList")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Numbered List"
|
||||
>
|
||||
<ListOrdered className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertFormatting('quote')}
|
||||
onClick={() => insertFormatting("quote")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Quote"
|
||||
>
|
||||
@@ -538,14 +581,14 @@ function EditorPageContent() {
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
<button
|
||||
onClick={() => insertFormatting('link')}
|
||||
onClick={() => insertFormatting("link")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Link"
|
||||
>
|
||||
<Link className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertFormatting('image')}
|
||||
onClick={() => insertFormatting("image")}
|
||||
className="p-2 rounded-lg text-gray-300"
|
||||
title="Image"
|
||||
>
|
||||
@@ -563,18 +606,20 @@ function EditorPageContent() {
|
||||
transition={{ delay: 0.2 }}
|
||||
className="glass-card p-6 rounded-2xl"
|
||||
>
|
||||
<h3 className="text-lg font-semibold gradient-text mb-4">Content</h3>
|
||||
<h3 className="text-lg font-semibold gradient-text mb-4">
|
||||
Content
|
||||
</h3>
|
||||
<div
|
||||
ref={contentRef}
|
||||
contentEditable
|
||||
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' }}
|
||||
style={{ whiteSpace: "pre-wrap" }}
|
||||
onInput={(e) => {
|
||||
const target = e.target as HTMLDivElement;
|
||||
setIsTyping(true);
|
||||
setFormData(prev => ({
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
content: target.textContent || ''
|
||||
content: target.textContent || "",
|
||||
}));
|
||||
}}
|
||||
onBlur={() => {
|
||||
@@ -586,7 +631,8 @@ function EditorPageContent() {
|
||||
{!isTyping ? formData.content : undefined}
|
||||
</div>
|
||||
<p className="text-xs text-white/50 mt-2">
|
||||
Supports Markdown formatting. Use the toolbar above or type directly.
|
||||
Supports Markdown formatting. Use the toolbar above or type
|
||||
directly.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
@@ -597,10 +643,14 @@ function EditorPageContent() {
|
||||
transition={{ delay: 0.3 }}
|
||||
className="glass-card p-6 rounded-2xl"
|
||||
>
|
||||
<h3 className="text-lg font-semibold gradient-text 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)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("description", e.target.value)
|
||||
}
|
||||
rows={4}
|
||||
className="w-full px-4 py-3 form-input-enhanced rounded-lg focus:outline-none resize-none"
|
||||
placeholder="Brief description of your project..."
|
||||
@@ -617,8 +667,10 @@ function EditorPageContent() {
|
||||
transition={{ delay: 0.4 }}
|
||||
className="glass-card p-6 rounded-2xl"
|
||||
>
|
||||
<h3 className="text-lg font-semibold gradient-text 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">
|
||||
@@ -627,7 +679,9 @@ function EditorPageContent() {
|
||||
<div className="custom-select">
|
||||
<select
|
||||
value={formData.category}
|
||||
onChange={(e) => handleInputChange('category', e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("category", e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="web">Web Development</option>
|
||||
<option value="mobile">Mobile Development</option>
|
||||
@@ -639,14 +693,13 @@ function EditorPageContent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-2">
|
||||
Tags
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.tags.join(', ')}
|
||||
value={formData.tags.join(", ")}
|
||||
onChange={(e) => handleTagsChange(e.target.value)}
|
||||
className="w-full px-3 py-2 form-input-enhanced rounded-lg focus:outline-none"
|
||||
placeholder="React, TypeScript, Next.js"
|
||||
@@ -662,8 +715,10 @@ function EditorPageContent() {
|
||||
transition={{ delay: 0.5 }}
|
||||
className="glass-card p-6 rounded-2xl"
|
||||
>
|
||||
<h3 className="text-lg font-semibold gradient-text mb-4">Links</h3>
|
||||
|
||||
<h3 className="text-lg font-semibold gradient-text mb-4">
|
||||
Links
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-2">
|
||||
@@ -672,7 +727,9 @@ function EditorPageContent() {
|
||||
<input
|
||||
type="url"
|
||||
value={formData.github}
|
||||
onChange={(e) => handleInputChange('github', e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("github", e.target.value)
|
||||
}
|
||||
className="w-full px-3 py-2 form-input-enhanced rounded-lg focus:outline-none"
|
||||
placeholder="https://github.com/username/repo"
|
||||
/>
|
||||
@@ -685,7 +742,7 @@ function EditorPageContent() {
|
||||
<input
|
||||
type="url"
|
||||
value={formData.live}
|
||||
onChange={(e) => handleInputChange('live', e.target.value)}
|
||||
onChange={(e) => handleInputChange("live", e.target.value)}
|
||||
className="w-full px-3 py-2 form-input-enhanced rounded-lg focus:outline-none"
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
@@ -700,14 +757,18 @@ function EditorPageContent() {
|
||||
transition={{ delay: 0.6 }}
|
||||
className="glass-card p-6 rounded-2xl"
|
||||
>
|
||||
<h3 className="text-lg font-semibold gradient-text 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">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.featured}
|
||||
onChange={(e) => handleInputChange('featured', e.target.checked)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("featured", e.target.checked)
|
||||
}
|
||||
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>
|
||||
@@ -717,7 +778,9 @@ function EditorPageContent() {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.published}
|
||||
onChange={(e) => handleInputChange('published', e.target.checked)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("published", e.target.checked)
|
||||
}
|
||||
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>
|
||||
@@ -725,10 +788,14 @@ function EditorPageContent() {
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-4 border-t border-white/20">
|
||||
<h4 className="text-sm font-medium text-white/70 mb-2">Preview</h4>
|
||||
<h4 className="text-sm font-medium text-white/70 mb-2">
|
||||
Preview
|
||||
</h4>
|
||||
<div className="text-xs text-white/50 space-y-1">
|
||||
<p>Status: {formData.published ? 'Published' : 'Draft'}</p>
|
||||
{formData.featured && <p className="text-blue-400">⭐ Featured</p>}
|
||||
<p>Status: {formData.published ? "Published" : "Draft"}</p>
|
||||
{formData.featured && (
|
||||
<p className="text-blue-400">⭐ Featured</p>
|
||||
)}
|
||||
<p>Category: {formData.category}</p>
|
||||
<p>Tags: {formData.tags.length} tags</p>
|
||||
</div>
|
||||
@@ -756,7 +823,9 @@ function EditorPageContent() {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold gradient-text">Project Preview</h2>
|
||||
<h2 className="text-2xl font-bold gradient-text">
|
||||
Project Preview
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowPreview(false)}
|
||||
className="p-2 rounded-lg"
|
||||
@@ -770,12 +839,12 @@ function EditorPageContent() {
|
||||
{/* Project Header */}
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold gradient-text mb-4">
|
||||
{formData.title || 'Untitled Project'}
|
||||
{formData.title || "Untitled Project"}
|
||||
</h1>
|
||||
<p className="text-xl text-gray-400 mb-6">
|
||||
{formData.description || 'No description provided'}
|
||||
{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">
|
||||
@@ -784,7 +853,9 @@ function EditorPageContent() {
|
||||
</div>
|
||||
{formData.featured && (
|
||||
<div className="flex items-center space-x-2 text-blue-400">
|
||||
<span className="text-sm font-semibold">⭐ Featured</span>
|
||||
<span className="text-sm font-semibold">
|
||||
⭐ Featured
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -804,7 +875,8 @@ function EditorPageContent() {
|
||||
)}
|
||||
|
||||
{/* Links */}
|
||||
{((formData.github && formData.github.trim()) || (formData.live && formData.live.trim())) && (
|
||||
{((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
|
||||
@@ -835,7 +907,9 @@ function EditorPageContent() {
|
||||
{/* 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>
|
||||
<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">
|
||||
<ReactMarkdown components={markdownComponents}>
|
||||
@@ -850,12 +924,14 @@ function EditorPageContent() {
|
||||
<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
|
||||
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">
|
||||
@@ -879,10 +955,14 @@ function EditorPageContent() {
|
||||
|
||||
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>}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user