From 7320a0562d21f8896279b99805df4b00424ce634 Mon Sep 17 00:00:00 2001 From: denshooter Date: Thu, 8 Jan 2026 11:31:57 +0100 Subject: [PATCH] full upgrade to dev --- __mocks__/@prisma/client.ts | 22 +- app/__tests__/api/sitemap.test.tsx | 112 +++--- app/__tests__/sitemap.xml/page.test.tsx | 51 +-- app/api/fetchAllProjects/route.tsx | 11 +- app/api/fetchImage/route.tsx | 26 +- app/api/fetchProject/route.tsx | 34 +- app/api/sitemap/route.tsx | 43 ++- app/components/ChatWidget.tsx | 6 +- app/components/Hero.tsx | 8 - app/components/Projects.tsx | 12 +- app/editor/page.tsx | 460 ++++++++++++++---------- app/sitemap.xml/route.tsx | 105 +++--- components/ErrorBoundary.tsx | 8 +- eslint.config.mjs | 27 +- jest.setup.ts | 25 +- lib/redis.ts | 91 +++-- scripts/test-n8n-connection.js | 30 +- 17 files changed, 629 insertions(+), 442 deletions(-) diff --git a/__mocks__/@prisma/client.ts b/__mocks__/@prisma/client.ts index 042f721..8288e05 100644 --- a/__mocks__/@prisma/client.ts +++ b/__mocks__/@prisma/client.ts @@ -4,30 +4,30 @@ export class PrismaClient { project = { findMany: jest.fn(async () => []), - findUnique: jest.fn(async (args: any) => null), + findUnique: jest.fn(async (_args: unknown) => null), count: jest.fn(async () => 0), - create: jest.fn(async (data: any) => data), - update: jest.fn(async (data: any) => data), - delete: jest.fn(async (data: any) => data), - updateMany: jest.fn(async (data: any) => ({})), + create: jest.fn(async (data: unknown) => data), + update: jest.fn(async (data: unknown) => data), + delete: jest.fn(async (data: unknown) => data), + updateMany: jest.fn(async (_data: unknown) => ({})), }; contact = { - create: jest.fn(async (data: any) => data), + create: jest.fn(async (data: unknown) => data), findMany: jest.fn(async () => []), count: jest.fn(async () => 0), - update: jest.fn(async (data: any) => data), - delete: jest.fn(async (data: any) => data), + update: jest.fn(async (data: unknown) => data), + delete: jest.fn(async (data: unknown) => data), }; pageView = { - create: jest.fn(async (data: any) => data), + create: jest.fn(async (data: unknown) => data), count: jest.fn(async () => 0), deleteMany: jest.fn(async () => ({})), }; userInteraction = { - create: jest.fn(async (data: any) => data), + create: jest.fn(async (data: unknown) => data), groupBy: jest.fn(async () => []), deleteMany: jest.fn(async () => ({})), }; @@ -36,4 +36,4 @@ export class PrismaClient { $disconnect = jest.fn(async () => {}); } -export default PrismaClient; \ No newline at end of file +export default PrismaClient; diff --git a/app/__tests__/api/sitemap.test.tsx b/app/__tests__/api/sitemap.test.tsx index 9ed1939..c1b5343 100644 --- a/app/__tests__/api/sitemap.test.tsx +++ b/app/__tests__/api/sitemap.test.tsx @@ -1,18 +1,17 @@ -jest.mock('next/server', () => ({ +jest.mock("next/server", () => ({ NextResponse: jest.fn().mockImplementation(function (body, init) { // Use function and assign to `this` so `new NextResponse(...)` returns an instance with properties - // eslint-disable-next-line no-invalid-this + this.body = body; - // eslint-disable-next-line no-invalid-this + this.init = init; }), })); -import { GET } from '@/app/api/sitemap/route'; -import { mockFetch } from '@/app/__tests__/__mocks__/mock-fetch'; +import { GET } from "@/app/api/sitemap/route"; // Mock node-fetch so we don't perform real network requests in tests -jest.mock('node-fetch', () => ({ +jest.mock("node-fetch", () => ({ __esModule: true, default: jest.fn(() => Promise.resolve({ @@ -21,60 +20,81 @@ jest.mock('node-fetch', () => ({ Promise.resolve({ posts: [ { - id: '67ac8dfa709c60000117d312', - title: 'Just Doing Some Testing', - meta_description: 'Hello bla bla bla bla', - slug: 'just-doing-some-testing', - updated_at: '2025-02-13T14:25:38.000+00:00', + id: "67ac8dfa709c60000117d312", + title: "Just Doing Some Testing", + meta_description: "Hello bla bla bla bla", + slug: "just-doing-some-testing", + updated_at: "2025-02-13T14:25:38.000+00:00", }, { - id: '67aaffc3709c60000117d2d9', - title: 'Blockchain Based Voting System', - meta_description: 'This project aims to revolutionize voting systems by leveraging blockchain to ensure security, transparency, and immutability.', - slug: 'blockchain-based-voting-system', - updated_at: '2025-02-13T16:54:42.000+00:00', + id: "67aaffc3709c60000117d2d9", + title: "Blockchain Based Voting System", + meta_description: + "This project aims to revolutionize voting systems by leveraging blockchain to ensure security, transparency, and immutability.", + slug: "blockchain-based-voting-system", + updated_at: "2025-02-13T16:54:42.000+00:00", }, ], - meta: { pagination: { limit: 'all', next: null, page: 1, pages: 1, prev: null, total: 2 } }, + meta: { + pagination: { + limit: "all", + next: null, + page: 1, + pages: 1, + prev: null, + total: 2, + }, + }, }), - }) + }), ), })); -describe('GET /api/sitemap', () => { +describe("GET /api/sitemap", () => { beforeAll(() => { - process.env.GHOST_API_URL = 'http://localhost:2368'; - process.env.GHOST_API_KEY = 'test-api-key'; - process.env.NEXT_PUBLIC_BASE_URL = 'https://dki.one'; + process.env.GHOST_API_URL = "http://localhost:2368"; + process.env.GHOST_API_KEY = "test-api-key"; + process.env.NEXT_PUBLIC_BASE_URL = "https://dki.one"; // Provide mock posts via env so route can use them without fetching - process.env.GHOST_MOCK_POSTS = JSON.stringify({ posts: [ - { - id: '67ac8dfa709c60000117d312', - title: 'Just Doing Some Testing', - meta_description: 'Hello bla bla bla bla', - slug: 'just-doing-some-testing', - updated_at: '2025-02-13T14:25:38.000+00:00', - }, - { - id: '67aaffc3709c60000117d2d9', - title: 'Blockchain Based Voting System', - meta_description: 'This project aims to revolutionize voting systems by leveraging blockchain to ensure security, transparency, and immutability.', - slug: 'blockchain-based-voting-system', - updated_at: '2025-02-13T16:54:42.000+00:00', - }, - ] }); + process.env.GHOST_MOCK_POSTS = JSON.stringify({ + posts: [ + { + id: "67ac8dfa709c60000117d312", + title: "Just Doing Some Testing", + meta_description: "Hello bla bla bla bla", + slug: "just-doing-some-testing", + updated_at: "2025-02-13T14:25:38.000+00:00", + }, + { + id: "67aaffc3709c60000117d2d9", + title: "Blockchain Based Voting System", + meta_description: + "This project aims to revolutionize voting systems by leveraging blockchain to ensure security, transparency, and immutability.", + slug: "blockchain-based-voting-system", + updated_at: "2025-02-13T16:54:42.000+00:00", + }, + ], + }); }); - it('should return a sitemap', async () => { + it("should return a sitemap", async () => { const response = await GET(); - expect(response.body).toContain(''); - expect(response.body).toContain('https://dki.one/'); - expect(response.body).toContain('https://dki.one/legal-notice'); - expect(response.body).toContain('https://dki.one/privacy-policy'); - expect(response.body).toContain('https://dki.one/projects/just-doing-some-testing'); - expect(response.body).toContain('https://dki.one/projects/blockchain-based-voting-system'); + expect(response.body).toContain( + '', + ); + expect(response.body).toContain("https://dki.one/"); + expect(response.body).toContain("https://dki.one/legal-notice"); + expect(response.body).toContain( + "https://dki.one/privacy-policy", + ); + expect(response.body).toContain( + "https://dki.one/projects/just-doing-some-testing", + ); + expect(response.body).toContain( + "https://dki.one/projects/blockchain-based-voting-system", + ); // Note: Headers are not available in test environment }); -}); \ No newline at end of file +}); diff --git a/app/__tests__/sitemap.xml/page.test.tsx b/app/__tests__/sitemap.xml/page.test.tsx index 7ab7d10..0e03645 100644 --- a/app/__tests__/sitemap.xml/page.test.tsx +++ b/app/__tests__/sitemap.xml/page.test.tsx @@ -1,12 +1,10 @@ -import '@testing-library/jest-dom'; -import { GET } from '@/app/sitemap.xml/route'; -import { mockFetch } from '@/app/__tests__/__mocks__/mock-fetch-sitemap'; +import "@testing-library/jest-dom"; +import { GET } from "@/app/sitemap.xml/route"; -jest.mock('next/server', () => ({ +jest.mock("next/server", () => ({ NextResponse: jest.fn().mockImplementation(function (body, init) { - // eslint-disable-next-line no-invalid-this this.body = body; - // eslint-disable-next-line no-invalid-this + this.init = init; }), })); @@ -33,36 +31,49 @@ const sitemapXml = ` `; // Mock node-fetch for sitemap endpoint (hoisted by Jest) -jest.mock('node-fetch', () => ({ +jest.mock("node-fetch", () => ({ __esModule: true, - default: jest.fn((url: string) => Promise.resolve({ ok: true, text: () => Promise.resolve(sitemapXml) })), + default: jest.fn((_url: string) => + Promise.resolve({ ok: true, text: () => Promise.resolve(sitemapXml) }), + ), })); -describe('Sitemap Component', () => { +describe("Sitemap Component", () => { beforeAll(() => { - process.env.NEXT_PUBLIC_BASE_URL = 'https://dki.one'; + process.env.NEXT_PUBLIC_BASE_URL = "https://dki.one"; // Provide sitemap XML directly so route uses it without fetching process.env.GHOST_MOCK_SITEMAP = sitemapXml; // Mock global.fetch too, to avoid any network calls global.fetch = jest.fn().mockImplementation((url: string) => { - if (url.includes('/api/sitemap')) { - return Promise.resolve({ ok: true, text: () => Promise.resolve(sitemapXml) }); + if (url.includes("/api/sitemap")) { + return Promise.resolve({ + ok: true, + text: () => Promise.resolve(sitemapXml), + }); } return Promise.reject(new Error(`Unknown URL: ${url}`)); }); }); - it('should render the sitemap XML', async () => { + it("should render the sitemap XML", async () => { const response = await GET(); - expect(response.body).toContain(''); - expect(response.body).toContain('https://dki.one/'); - expect(response.body).toContain('https://dki.one/legal-notice'); - expect(response.body).toContain('https://dki.one/privacy-policy'); - expect(response.body).toContain('https://dki.one/projects/just-doing-some-testing'); - expect(response.body).toContain('https://dki.one/projects/blockchain-based-voting-system'); + expect(response.body).toContain( + '', + ); + expect(response.body).toContain("https://dki.one/"); + expect(response.body).toContain("https://dki.one/legal-notice"); + expect(response.body).toContain( + "https://dki.one/privacy-policy", + ); + expect(response.body).toContain( + "https://dki.one/projects/just-doing-some-testing", + ); + expect(response.body).toContain( + "https://dki.one/projects/blockchain-based-voting-system", + ); // Note: Headers are not available in test environment }); -}); \ No newline at end of file +}); diff --git a/app/api/fetchAllProjects/route.tsx b/app/api/fetchAllProjects/route.tsx index bf5dd9d..8ee99dc 100644 --- a/app/api/fetchAllProjects/route.tsx +++ b/app/api/fetchAllProjects/route.tsx @@ -7,9 +7,9 @@ async function getFetch() { try { const mod = await import("node-fetch"); // support both CJS and ESM interop - return (mod as any).default ?? mod; - } catch (err) { - return (globalThis as any).fetch; + return (mod as { default: unknown }).default ?? mod; + } catch (_err) { + return (globalThis as unknown as { fetch: unknown }).fetch; } } @@ -49,9 +49,10 @@ export async function GET() { const fetchFn = await getFetch(); const response = await fetchFn( `${GHOST_API_URL}/ghost/api/content/posts/?key=${GHOST_API_KEY}&limit=all`, - { agent: agent as unknown as undefined } + { agent: agent as unknown as undefined }, ); - const posts: GhostPostsResponse = await response.json() as GhostPostsResponse; + const posts: GhostPostsResponse = + (await response.json()) as GhostPostsResponse; if (!posts || !posts.posts) { console.error("Invalid posts data"); diff --git a/app/api/fetchImage/route.tsx b/app/api/fetchImage/route.tsx index 017a77b..22f4467 100644 --- a/app/api/fetchImage/route.tsx +++ b/app/api/fetchImage/route.tsx @@ -13,22 +13,28 @@ export async function GET(req: NextRequest) { try { // Try global fetch first, fall back to node-fetch if necessary + // eslint-disable-next-line @typescript-eslint/no-explicit-any let response: any; try { - if (typeof (globalThis as any).fetch === 'function') { - response = await (globalThis as any).fetch(url); + if ( + typeof (globalThis as unknown as { fetch: unknown }).fetch === + "function" + ) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + response = await (globalThis as unknown as { fetch: any }).fetch(url); } - } catch (e) { + } catch (_e) { response = undefined; } - if (!response || typeof response.ok === 'undefined' || !response.ok) { + if (!response || typeof response.ok === "undefined" || !response.ok) { try { - const mod = await import('node-fetch'); - const nodeFetch = (mod as any).default ?? mod; - response = await nodeFetch(url); + const mod = await import("node-fetch"); + const nodeFetch = (mod as { default: unknown }).default ?? mod; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + response = await (nodeFetch as any)(url); } catch (err) { - console.error('Failed to fetch image:', err); + console.error("Failed to fetch image:", err); return NextResponse.json( { error: "Failed to fetch image" }, { status: 500 }, @@ -37,7 +43,9 @@ export async function GET(req: NextRequest) { } if (!response || !response.ok) { - throw new Error(`Failed to fetch image: ${response?.statusText ?? 'no response'}`); + throw new Error( + `Failed to fetch image: ${response?.statusText ?? "no response"}`, + ); } const contentType = response.headers.get("content-type"); diff --git a/app/api/fetchProject/route.tsx b/app/api/fetchProject/route.tsx index e427616..b01a4bd 100644 --- a/app/api/fetchProject/route.tsx +++ b/app/api/fetchProject/route.tsx @@ -15,40 +15,52 @@ export async function GET(request: Request) { try { // Debug: show whether fetch is present/mocked - // eslint-disable-next-line no-console - console.log('DEBUG fetch in fetchProject:', typeof (globalThis as any).fetch, 'globalIsMock:', !!(globalThis as any).fetch?._isMockFunction); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + console.log( + "DEBUG fetch in fetchProject:", + typeof (globalThis as any).fetch, + "globalIsMock:", + !!(globalThis as any).fetch?._isMockFunction, + ); + // Try global fetch first (as tests often mock it). If it fails or returns undefined, // fall back to dynamically importing node-fetch. let response: any; - if (typeof (globalThis as any).fetch === 'function') { + if (typeof (globalThis as any).fetch === "function") { try { response = await (globalThis as any).fetch( `${GHOST_API_URL}/ghost/api/content/posts/slug/${slug}/?key=${GHOST_API_KEY}`, ); - } catch (e) { + } catch (_e) { response = undefined; } } - if (!response || typeof response.ok === 'undefined') { + if (!response || typeof response.ok === "undefined") { try { - const mod = await import('node-fetch'); + const mod = await import("node-fetch"); const nodeFetch = (mod as any).default ?? mod; - response = await nodeFetch( + response = await (nodeFetch as any)( `${GHOST_API_URL}/ghost/api/content/posts/slug/${slug}/?key=${GHOST_API_KEY}`, ); - } catch (err) { + } catch (_err) { response = undefined; } } + /* eslint-enable @typescript-eslint/no-explicit-any */ // Debug: inspect the response returned from the fetch - // eslint-disable-next-line no-console - console.log('DEBUG fetch response:', response); + + // Debug: inspect the response returned from the fetch + + console.log("DEBUG fetch response:", response); if (!response || !response.ok) { - throw new Error(`Failed to fetch post: ${response?.statusText ?? 'no response'}`); + throw new Error( + `Failed to fetch post: ${response?.statusText ?? "no response"}`, + ); } const post = await response.json(); diff --git a/app/api/sitemap/route.tsx b/app/api/sitemap/route.tsx index cd1be01..4d45150 100644 --- a/app/api/sitemap/route.tsx +++ b/app/api/sitemap/route.tsx @@ -14,7 +14,6 @@ export const runtime = "nodejs"; // Force Node runtime // Read Ghost API config at runtime, tests may set env vars in beforeAll - // Funktion, um die XML für die Sitemap zu generieren function generateXml(sitemapRoutes: { url: string; lastModified: string }[]) { const xmlHeader = ''; @@ -63,7 +62,7 @@ export async function GET() { ]; // In test environment we can short-circuit and use a mocked posts payload - if (process.env.NODE_ENV === 'test' && process.env.GHOST_MOCK_POSTS) { + if (process.env.NODE_ENV === "test" && process.env.GHOST_MOCK_POSTS) { const mockData = JSON.parse(process.env.GHOST_MOCK_POSTS); const projects = (mockData as ProjectsData).posts || []; @@ -73,7 +72,7 @@ export async function GET() { url: `${baseUrl}/projects/${project.slug}`, lastModified, priority: 0.8, - changeFreq: 'monthly', + changeFreq: "monthly", }; }); @@ -81,43 +80,46 @@ export async function GET() { const xml = generateXml(allRoutes); // For tests return a plain object so tests can inspect `.body` easily - if (process.env.NODE_ENV === 'test') { - return { body: xml, headers: { 'Content-Type': 'application/xml' } } as any; + if (process.env.NODE_ENV === "test") { + return { + body: xml, + headers: { "Content-Type": "application/xml" }, + }; } return new NextResponse(xml, { - headers: { 'Content-Type': 'application/xml' }, + headers: { "Content-Type": "application/xml" }, }); } try { // Debug: show whether fetch is present/mocked - // eslint-disable-next-line no-console - console.log('DEBUG fetch in sitemap API:', typeof (globalThis as any).fetch, 'globalIsMock:', !!(globalThis as any).fetch?._isMockFunction); + // Try global fetch first (tests may mock global.fetch) - let response: any; + let response: Response | undefined; + try { - if (typeof (globalThis as any).fetch === 'function') { - response = await (globalThis as any).fetch( + if (typeof globalThis.fetch === "function") { + response = await globalThis.fetch( `${process.env.GHOST_API_URL}/ghost/api/content/posts/?key=${process.env.GHOST_API_KEY}&limit=all`, ); // Debug: inspect the result - // eslint-disable-next-line no-console - console.log('DEBUG sitemap global fetch returned:', response); + + console.log("DEBUG sitemap global fetch returned:", response); } - } catch (e) { + } catch (_e) { response = undefined; } - if (!response || typeof response.ok === 'undefined' || !response.ok) { + if (!response || typeof response.ok === "undefined" || !response.ok) { try { - const mod = await import('node-fetch'); - const nodeFetch = (mod as any).default ?? mod; + const mod = await import("node-fetch"); + const nodeFetch = mod.default ?? mod; response = await nodeFetch( `${process.env.GHOST_API_URL}/ghost/api/content/posts/?key=${process.env.GHOST_API_KEY}&limit=all`, ); } catch (err) { - console.log('Failed to fetch posts from Ghost:', err); + console.log("Failed to fetch posts from Ghost:", err); return new NextResponse(generateXml(staticRoutes), { headers: { "Content-Type": "application/xml" }, }); @@ -125,13 +127,16 @@ export async function GET() { } if (!response || !response.ok) { - console.error(`Failed to fetch posts: ${response?.statusText ?? 'no response'}`); + console.error( + `Failed to fetch posts: ${response?.statusText ?? "no response"}`, + ); return new NextResponse(generateXml(staticRoutes), { headers: { "Content-Type": "application/xml" }, }); } const projectsData = (await response.json()) as ProjectsData; + const projects = projectsData.posts; // Dynamische Projekt-Routen generieren diff --git a/app/components/ChatWidget.tsx b/app/components/ChatWidget.tsx index d0de171..2446660 100644 --- a/app/components/ChatWidget.tsx +++ b/app/components/ChatWidget.tsx @@ -226,7 +226,9 @@ export default function ChatWidget() {
-

Dennis's AI Assistant

+

+ Dennis's AI Assistant +

Always online

@@ -358,7 +360,7 @@ export default function ChatWidget() { {/* Quick Actions */}
{[ - "What are Dennis's skills?", + "What are Dennis's skills?", "Tell me about his projects", "How can I contact him?", ].map((suggestion, index) => ( diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index f03b815..457f492 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -18,14 +18,6 @@ const Hero = () => { { icon: Rocket, text: "Self-Hosted Infrastructure" }, ]; - // Smooth scroll configuration - const smoothTransition = { - type: "spring", - damping: 30, - stiffness: 50, - mass: 1, - }; - if (!mounted) { return null; } diff --git a/app/components/Projects.tsx b/app/components/Projects.tsx index cfaa9d9..5600f94 100644 --- a/app/components/Projects.tsx +++ b/app/components/Projects.tsx @@ -2,13 +2,7 @@ import { useState, useEffect } from "react"; import { motion, Variants } from "framer-motion"; -import { - ExternalLink, - Github, - Calendar, - Layers, - ArrowRight, -} from "lucide-react"; +import { ExternalLink, Github, Layers, ArrowRight } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; @@ -65,7 +59,7 @@ const Projects = () => { setProjects(data.projects || []); } } catch (error) { - if (process.env.NODE_ENV === 'development') { + if (process.env.NODE_ENV === "development") { console.error("Error loading projects:", error); } } @@ -104,7 +98,7 @@ const Projects = () => { variants={staggerContainer} className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" > - {projects.map((project, index) => ( + {projects.map((project) => ( (null); - + const [, setProject] = useState(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 ( ); }, - 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 ? {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 ? {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 = `![${selection.toString() || 'alt text'}](${imageUrl})`; + newText = `![${selection.toString() || "alt text"}](${imageUrl})`; } 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" /> -

Loading Editor

+

+ Loading Editor +

Preparing your workspace...

@@ -347,7 +386,7 @@ function EditorPageContent() { if (!isAuthenticated) { return (
-

Access Denied

-

You need to be logged in to access the editor.

+

+ You need to be logged in to access the editor. +

- +

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

- +
- +
@@ -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() { 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() {