diff --git a/README.md b/README.md index 3942256..965c27f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ A modern, feature-rich blog system built with **Next.js 14**, **TypeScript**, ** - **🎯 Content Management**: Drag & drop file uploads, post editing, and deletion - **πŸ“¦ Export Functionality**: Export all posts as tar.gz archive (Docker only) - **πŸ’Ύ Smart Caching**: RAM-based caching system for instant post retrieval +- **πŸ”§ VS Code-Style Editor**: Monaco editor with YAML frontmatter support and live preview +- **πŸ”„ Force Reparse**: Manual cache clearing and post reparsing for immediate updates +- **πŸ“ Reliable Directory Scanning**: Robust file system traversal with error handling --- @@ -43,13 +46,15 @@ A modern, feature-rich blog system built with **Next.js 14**, **TypeScript**, ** markdownblog/ β”œβ”€β”€ markdown_backend/ # Rust backend for markdown processing β”‚ β”œβ”€β”€ src/ -β”‚ β”‚ β”œβ”€β”€ main.rs # CLI interface and command handling -β”‚ β”‚ └── markdown.rs # Markdown parsing, caching, and file watching -β”‚ β”œβ”€β”€ Cargo.toml # Rust dependencies and configuration -β”‚ └── target/ # Compiled Rust binaries +β”‚ β”‚ β”œβ”€β”€ main.rs # CLI interface and command handling +β”‚ β”‚ └── markdown.rs # Markdown parsing, caching, and file watching +β”‚ β”œβ”€β”€ Cargo.toml # Rust dependencies and configuration +β”‚ └── target/ # Compiled Rust binaries β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ app/ # Next.js 14 App Router +β”‚ β”œβ”€β”€ app/ # Next.js 14 App Router β”‚ β”‚ β”œβ”€β”€ admin/ # Admin dashboard pages +β”‚ β”‚ β”‚ β”œβ”€β”€ editor/ # VS Code-style editor +β”‚ β”‚ β”‚ β”‚ └── page.tsx # Markdown editor with Monaco β”‚ β”‚ β”‚ β”œβ”€β”€ manage/ # Content management interface β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ page.tsx # Manage posts and folders β”‚ β”‚ β”‚ β”‚ └── rust-status/ # Rust backend monitoring @@ -84,8 +89,12 @@ markdownblog/ β”‚ β”‚ β”‚ └── posts/ # Public post API β”‚ β”‚ β”‚ β”œβ”€β”€ [slug]/ # Dynamic post API routes β”‚ β”‚ β”‚ β”‚ └── route.ts +β”‚ β”‚ β”‚ β”œβ”€β”€ preview/ # Markdown preview API +β”‚ β”‚ β”‚ β”‚ └── route.ts β”‚ β”‚ β”‚ β”œβ”€β”€ stream/ # Server-Sent Events for real-time updates β”‚ β”‚ β”‚ β”‚ └── route.ts +β”‚ β”‚ β”‚ β”œβ”€β”€ webhook/ # Webhook endpoint +β”‚ β”‚ β”‚ β”‚ └── route.ts β”‚ β”‚ β”‚ └── route.ts # List all posts β”‚ β”‚ β”œβ”€β”€ posts/ # Blog post pages β”‚ β”‚ β”‚ └── [...slug]/ # Dynamic post routing (catch-all) @@ -97,15 +106,15 @@ markdownblog/ β”‚ β”‚ β”œβ”€β”€ highlight-github.css # Code syntax highlighting styles β”‚ β”‚ β”œβ”€β”€ layout.tsx # Root layout with metadata β”‚ β”‚ β”œβ”€β”€ MobileNav.tsx # Mobile navigation component +β”‚ β”‚ β”œβ”€β”€ monaco-vim.d.ts # Monaco Vim typings β”‚ β”‚ └── page.tsx # Homepage with post listing β”‚ └── lib/ # Utility libraries β”‚ └── postsDirectory.ts # Post directory management and Rust integration β”œβ”€β”€ posts/ # Markdown blog posts storage -β”‚ β”œβ”€β”€ pinned.json # Pinned posts configuration -β”‚ β”œβ”€β”€ welcome.md # Welcome post with frontmatter -β”‚ β”œβ”€β”€ mdtest.md # Test post with various markdown features -β”‚ β”œβ”€β”€ anchor-test.md # Test post for anchor linking -β”‚ └── ii/ # Example nested folder structure +β”‚ β”œβ”€β”€ about.md +β”‚ β”œβ”€β”€ welcome.md +β”‚ └── assets/ +β”‚ └── peta.png β”œβ”€β”€ public/ # Static assets β”‚ β”œβ”€β”€ android-chrome-192x192.png β”‚ β”œβ”€β”€ android-chrome-512x512.png @@ -115,19 +124,19 @@ markdownblog/ β”‚ β”œβ”€β”€ favicon.ico β”‚ └── site.webmanifest β”œβ”€β”€ electron/ # Desktop application -β”‚ └── main.js # Electron main process configuration +β”‚ └── main.js # Electron main process configuration β”œβ”€β”€ Dockerfile # Docker container configuration β”œβ”€β”€ docker.sh # Docker deployment script -β”œβ”€β”€ entrypoint.sh # Container entrypoint script -β”œβ”€β”€ run-local-backend.sh # Local Rust backend runner -β”œβ”€β”€ next-env.d.ts # Next.js TypeScript definitions -β”œβ”€β”€ next.config.js # Next.js configuration -β”œβ”€β”€ package-lock.json # npm lock file -β”œβ”€β”€ package.json # Dependencies and scripts -β”œβ”€β”€ postcss.config.js # PostCSS configuration -β”œβ”€β”€ tailwind.config.js # Tailwind CSS configuration -β”œβ”€β”€ tsconfig.json # TypeScript configuration -└── LICENSE # MIT License +β”œβ”€β”€ entrypoint.sh # Container entrypoint script +β”œβ”€β”€ run-local-backend.sh # Local Rust backend runner +β”œβ”€β”€ next-env.d.ts # Next.js TypeScript definitions +β”œβ”€β”€ next.config.js # Next.js configuration +β”œβ”€β”€ package-lock.json # npm lock file +β”œβ”€β”€ package.json # Dependencies and scripts +β”œβ”€β”€ postcss.config.js # PostCSS configuration +β”œβ”€β”€ tailwind.config.js # Tailwind CSS configuration +β”œβ”€β”€ tsconfig.json # TypeScript configuration +└── LICENSE # MIT License ``` ### Key Components @@ -329,6 +338,9 @@ console.log("Hello, World!"); - **πŸ“¦ Export Posts**: Download all posts as archive (Docker only) - **πŸ“Š Rust Status**: Monitor parser performance, logs, and health - **πŸ” Log Management**: View, filter, and clear parser logs +- **πŸ”§ VS Code Editor**: Monaco-based editor with YAML frontmatter preservation +- **πŸ”„ Force Reparse**: Manual cache clearing and post reparsing +- **πŸ“ Reliable Scanning**: Enhanced directory traversal with error recovery ### Security @@ -350,6 +362,9 @@ console.log("Hello, World!"); - **πŸ“ Recursive Scanning**: Efficient folder traversal and file discovery - **πŸ’Ύ Smart Caching**: RAM-based caching with disk persistence - **πŸ“Š Performance Monitoring**: Real-time metrics and logging +- **πŸ”„ Force Reparse**: Manual cache invalidation and post reparsing +- **πŸ“ Reliable Directory Scanning**: Robust error handling and recovery +- **πŸ”§ Single Post Reparse**: Efficient individual post cache clearing ### Real-Time Updates @@ -358,6 +373,16 @@ console.log("Hello, World!"); - **⚑ Instant Updates**: Sub-second response to file modifications - **πŸ”„ Fallback Polling**: Graceful degradation if SSE fails +### VS Code-Style Editor + +- **πŸ”§ Monaco Editor**: Professional code editor with syntax highlighting +- **πŸ“„ YAML Frontmatter**: Preserved and editable at the top of files +- **πŸ‘οΈ Live Preview**: Real-time Markdown rendering +- **πŸ’Ύ Save & Reparse**: Automatic cache clearing and post reparsing +- **⌨️ Vim Mode**: Optional Vim keybindings for power users +- **πŸ“± Responsive Design**: Works on desktop and mobile devices +- **🎨 Custom Styling**: JetBrains Mono font and VS Code-like appearance + --- ## 🎨 Customization @@ -398,6 +423,8 @@ cargo build --release # Build optimized binary cargo run -- watch # Watch for file changes cargo run -- logs # View parser logs cargo run -- checkhealth # Check backend health +cargo run -- reinterpret-all # Force reparse all posts +cargo run -- reparse-post # Force reparse single post ``` --- @@ -434,7 +461,43 @@ MIT License - see [LICENSE](LICENSE) file for details. - **File watching issues**: Check file permissions and inotify limits - **Performance issues**: Monitor logs via admin interface - **Cache problems**: Clear cache via admin interface or restart +- **Directory scanning errors**: Check file permissions and hidden files +- **Reparse failures**: Verify post slugs and file existence +- **Memory issues**: Monitor cache size and clear if necessary ### Support -For issues and questions, please check the project structure and API documentation in the codebase. The admin interface includes comprehensive monitoring tools for the Rust backend. \ No newline at end of file +For issues and questions, please check the project structure and API documentation in the codebase. The admin interface includes comprehensive monitoring tools for the Rust backend. + +--- + +## πŸ†• Recent Improvements (Latest) + +### Rust Backend Enhancements + +- **πŸ”„ Force Reparse Commands**: New CLI commands for manual cache invalidation + - `reinterpret-all`: Clear all caches and reparse every post + - `reparse-post `: Clear cache for specific post and reparse +- **πŸ“ Reliable Directory Scanning**: Enhanced file system traversal with: + - Hidden file filtering (skips `.` files) + - Graceful error recovery for inaccessible files + - Detailed logging of scanning process + - Automatic directory creation if missing +- **πŸ’Ύ Improved Cache Management**: Better cache directory handling and persistence +- **πŸ“Š Enhanced Logging**: Comprehensive logging for debugging and monitoring + +### Editor Improvements + +- **πŸ”§ VS Code-Style Interface**: Monaco editor with professional features +- **πŸ“„ YAML Frontmatter Preservation**: Frontmatter stays at top and remains editable +- **πŸ’Ύ Save & Reparse Integration**: Automatic Rust backend integration on save +- **πŸ‘οΈ Live Preview**: Real-time Markdown rendering without frontmatter +- **⌨️ Vim Mode Support**: Optional Vim keybindings for power users +- **πŸ“± Mobile Responsive**: Works seamlessly on all device sizes + +### Admin Panel Enhancements + +- **πŸ”„ Force Reparse Button**: One-click cache clearing and post reparsing +- **πŸ“Š Enhanced Rust Status**: Real-time parser performance monitoring +- **πŸ” Improved Log Management**: Better filtering and search capabilities +- **πŸ“ Directory Health Monitoring**: Comprehensive file system diagnostics \ No newline at end of file diff --git a/markdown_backend/src/main.rs b/markdown_backend/src/main.rs index 9fa1e04..75ad3fa 100644 --- a/markdown_backend/src/main.rs +++ b/markdown_backend/src/main.rs @@ -1,7 +1,17 @@ #[warn(unused_imports)] use clap::{Parser, Subcommand}; mod markdown; -use markdown::{get_all_posts, get_post_by_slug, get_posts_by_tag, watch_posts, get_parser_logs, clear_parser_logs, load_parser_logs_from_disk}; +use markdown::{ + get_all_posts, + get_post_by_slug, + get_posts_by_tag, + watch_posts, + get_parser_logs, + clear_parser_logs, + load_parser_logs_from_disk, + force_reinterpret_all_posts, + force_reparse_single_post +}; use serde_json; use std::fs; use std::io; @@ -44,6 +54,12 @@ enum Commands { Logs, /// Clear parser logs ClearLogs, + /// Force reinterpret all posts (clear cache and re-parse) + ReinterpretAll, + /// Force reparse a single post (clear cache and re-parse) + ReparsePost { + slug: String, + }, /// Parse markdown from file or stdin Parse { #[arg(long)] @@ -111,6 +127,35 @@ fn main() { clear_parser_logs(); println!("{}", serde_json::to_string(&serde_json::json!({"success": true, "message": "Logs cleared"})).unwrap()); } + Commands::ReinterpretAll => { + match force_reinterpret_all_posts() { + Ok(posts) => { + println!("{}", serde_json::to_string(&serde_json::json!({ + "success": true, + "message": format!("All posts reinterpreted successfully. Processed {} posts.", posts.len()) + })).unwrap()); + } + Err(e) => { + eprintln!("{}", e); + std::process::exit(1); + } + } + } + Commands::ReparsePost { slug } => { + match force_reparse_single_post(slug) { + Ok(post) => { + println!("{}", serde_json::to_string(&serde_json::json!({ + "success": true, + "message": format!("Post '{}' reparsed successfully", slug), + "post": post + })).unwrap()); + } + Err(e) => { + eprintln!("{}", e); + std::process::exit(1); + } + } + } Commands::Parse { file, stdin, ast } => { let content = if *stdin { let mut buffer = String::new(); @@ -139,4 +184,4 @@ fn main() { } } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/markdown_backend/src/markdown.rs b/markdown_backend/src/markdown.rs index 4b07571..0a9588b 100644 --- a/markdown_backend/src/markdown.rs +++ b/markdown_backend/src/markdown.rs @@ -180,6 +180,18 @@ static AMMONIA: Lazy> = Lazy::new(|| { }); // Helper functions +fn ensure_cache_directory() { + let cache_dir = PathBuf::from("./cache"); + if !cache_dir.exists() { + if let Err(e) = fs::create_dir_all(&cache_dir) { + eprintln!("Failed to create cache directory: {}", e); + add_log("error", &format!("Failed to create cache directory: {}", e), None, None); + } else { + add_log("info", "Created cache directory: ./cache", None, None); + } + } +} + fn get_posts_directory() -> PathBuf { let is_docker = std::env::var("DOCKER_CONTAINER").is_ok() || std::env::var("KUBERNETES_SERVICE_HOST").is_ok() @@ -207,29 +219,101 @@ fn get_posts_directory() -> PathBuf { for candidate in candidates.iter() { let path = PathBuf::from(candidate); if path.exists() && path.is_dir() { + add_log("info", &format!("Using posts directory: {:?}", path), None, None); return path; } } - // Fallback: default to ./posts - PathBuf::from("./posts") + + // Fallback: create ./posts if it doesn't exist + let fallback_path = PathBuf::from("./posts"); + if !fallback_path.exists() { + if let Err(e) = fs::create_dir_all(&fallback_path) { + add_log("error", &format!("Failed to create posts directory: {}", e), None, None); + } else { + add_log("info", "Created posts directory: ./posts", None, None); + } + } + fallback_path } -// Function to find Markdown files. -// This will scan Directories recursively +// Function to find Markdown files with improved reliability fn find_markdown_files(dir: &Path) -> std::io::Result> { let mut files = Vec::new(); - if dir.is_dir() { - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - - if path.is_dir() { - files.extend(find_markdown_files(&path)?); - } else if path.extension().map(|e| e == "md").unwrap_or(false) { - files.push(path); + let mut errors = Vec::new(); + + if !dir.exists() { + let error_msg = format!("Directory does not exist: {:?}", dir); + add_log("error", &error_msg, None, None); + return Err(std::io::Error::new(std::io::ErrorKind::NotFound, error_msg)); + } + + if !dir.is_dir() { + let error_msg = format!("Path is not a directory: {:?}", dir); + add_log("error", &error_msg, None, None); + return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, error_msg)); + } + + // Try to read directory with retry logic + let entries = match fs::read_dir(dir) { + Ok(entries) => entries, + Err(e) => { + add_log("error", &format!("Failed to read directory {:?}: {}", dir, e), None, None); + return Err(e); + } + }; + + for entry_result in entries { + match entry_result { + Ok(entry) => { + let path = entry.path(); + + // Skip hidden files and directories + if let Some(name) = path.file_name() { + if name.to_string_lossy().starts_with('.') { + continue; + } + } + + if path.is_dir() { + // Recursively scan subdirectories + match find_markdown_files(&path) { + Ok(subfiles) => files.extend(subfiles), + Err(e) => { + let error_msg = format!("Error scanning subdirectory {:?}: {}", path, e); + add_log("warning", &error_msg, None, None); + errors.push(error_msg); + } + } + } else if path.extension().map(|e| e == "md").unwrap_or(false) { + // Verify the file is readable + match fs::metadata(&path) { + Ok(metadata) => { + if metadata.is_file() { + files.push(path); + } + } + Err(e) => { + let error_msg = format!("Cannot access file {:?}: {}", path, e); + add_log("warning", &error_msg, None, None); + errors.push(error_msg); + } + } + } + } + Err(e) => { + let error_msg = format!("Error reading directory entry: {}", e); + add_log("warning", &error_msg, None, None); + errors.push(error_msg); } } } + + // Log summary + add_log("info", &format!("Found {} markdown files in {:?}", files.len(), dir), None, None); + if !errors.is_empty() { + add_log("warning", &format!("Encountered {} errors during directory scan", errors.len()), None, None); + } + Ok(files) } @@ -372,7 +456,7 @@ fn add_log(level: &str, message: &str, slug: Option<&str>, details: Option<&str> } fn save_parser_logs_to_disk_inner(logs: &VecDeque) -> std::io::Result<()> { - let _ = std::fs::create_dir_all("./cache"); + ensure_cache_directory(); let logs_vec: Vec<_> = logs.iter().cloned().collect(); let json = serde_json::to_string(&logs_vec)?; std::fs::write(PARSER_LOGS_PATH, json)?; @@ -654,12 +738,11 @@ pub fn load_post_cache_from_disk() { } pub fn save_post_cache_to_disk() { + ensure_cache_directory(); if let Ok(map) = serde_json::to_string(&*POST_CACHE.read().unwrap()) { - let _ = fs::create_dir_all("./cache"); let _ = fs::write(POSTS_CACHE_PATH, map); } if let Ok(map) = serde_json::to_string(&*POST_STATS.read().unwrap()) { - let _ = fs::create_dir_all("./cache"); let _ = fs::write(POST_STATS_PATH, map); } } @@ -739,7 +822,90 @@ pub fn get_parser_logs() -> Vec { } pub fn clear_parser_logs() { - let mut logs = PARSER_LOGS.write().unwrap(); - logs.clear(); - let _ = std::fs::remove_file(PARSER_LOGS_PATH); + PARSER_LOGS.write().unwrap().clear(); + if let Err(e) = save_parser_logs_to_disk_inner(&VecDeque::new()) { + eprintln!("Failed to save empty logs to disk: {}", e); + } +} + +// Force reinterpret all posts by clearing cache and re-parsing +pub fn force_reinterpret_all_posts() -> Result, Box> { + add_log("info", "Starting force reinterpret of all posts", None, None); + + // Clear all caches + POST_CACHE.write().unwrap().clear(); + ALL_POSTS_CACHE.write().unwrap().take(); + POST_STATS.write().unwrap().clear(); + + add_log("info", "Cleared all caches", None, None); + + // Get posts directory and find all markdown files + let posts_dir = get_posts_directory(); + let markdown_files = find_markdown_files(&posts_dir)?; + + add_log("info", &format!("Found {} markdown files to reinterpret", markdown_files.len()), None, None); + + let mut posts = Vec::new(); + let mut success_count = 0; + let mut error_count = 0; + + for file_path in markdown_files { + let slug = path_to_slug(&file_path, &posts_dir); + match get_post_by_slug(&slug) { + Ok(post) => { + posts.push(post); + success_count += 1; + add_log("info", &format!("Successfully reinterpreted: {}", slug), Some(&slug), None); + } + Err(e) => { + error_count += 1; + add_log("error", &format!("Failed to reinterpret {}: {}", slug, e), Some(&slug), None); + } + } + } + + // Update the all posts cache + ALL_POSTS_CACHE.write().unwrap().replace(posts.clone()); + + // Save cache to disk + save_post_cache_to_disk(); + + add_log("info", &format!("Force reinterpret completed. Success: {}, Errors: {}", success_count, error_count), None, None); + + Ok(posts) +} + +// Force reparse a single post by clearing its cache and re-parsing +pub fn force_reparse_single_post(slug: &str) -> Result> { + add_log("info", &format!("Starting force reparse of post: {}", slug), Some(slug), None); + + // Clear this specific post from all caches + POST_CACHE.write().unwrap().remove(slug); + POST_STATS.write().unwrap().remove(slug); + + // Clear the all posts cache since it might contain this post + ALL_POSTS_CACHE.write().unwrap().take(); + + add_log("info", &format!("Cleared cache for post: {}", slug), Some(slug), None); + + // Re-parse the post + let post = get_post_by_slug(slug)?; + + // Update the all posts cache with the new post + let mut all_posts_cache = ALL_POSTS_CACHE.write().unwrap(); + if let Some(ref mut posts) = *all_posts_cache { + // Remove old version if it exists + posts.retain(|p| p.slug != slug); + // Add new version + posts.push(post.clone()); + // Sort by creation date + posts.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + } + + // Save cache to disk + save_post_cache_to_disk(); + + add_log("info", &format!("Successfully reparsed post: {}", slug), Some(slug), None); + + Ok(post) } \ No newline at end of file diff --git a/posts/about.md b/posts/about.md index 279ab7a..a7ac660 100644 --- a/posts/about.md +++ b/posts/about.md @@ -5,26 +5,36 @@ tags: [about, profile] author: rattatwinko summary: This is the about page --- + # About Me -Hi! I'm Rattatwinko, a passionate developer who loves building self-hosted tools and beautiful web experiences. +_**I am rattatwinko**_ -![](assets/peta.png) +I created this Project because of the lack of Blog's that use Markdown. +It really is sad that there are so many blog platforms which are shit. -## Skills -- TypeScript, JavaScript +## What I used: +- TypeScript +- Next.JS - Rust -- React, Next.js -- Tailwind CSS -- Docker +- Monaco (for a beautiful Editing experience) +- More shit which you can check out in the Repo -## Experience -- Indie developer, 2020–present -- Open source contributor +## What I do -## Projects -- **MarkdownBlog**: The site you're reading now! A fast, modern, and hackable markdown blog platform. +School. +Coding. +Not more not less. + +### Socials + + + +
+ +
+ +
+ +
-## Contact -- [GitHub](https://github.com/rattatwinko) -- [Email](mailto:me@example.com) \ No newline at end of file diff --git a/src/app/admin/editor/page.tsx b/src/app/admin/editor/page.tsx index a5051b6..3baa12f 100644 --- a/src/app/admin/editor/page.tsx +++ b/src/app/admin/editor/page.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useEffect, useRef, useState } from "react"; import dynamic from "next/dynamic"; +import { useRouter } from "next/navigation"; import "@fontsource/jetbrains-mono"; import { marked } from "marked"; @@ -37,6 +38,27 @@ function stripFrontmatter(md: string): string { return md; } +// Helper to extract YAML frontmatter +function extractFrontmatter(md: string): { frontmatter: string; content: string } { + if (!md) return { frontmatter: '', content: '' }; + if (md.startsWith('---')) { + const end = md.indexOf('---', 3); + if (end !== -1) { + const frontmatter = md.slice(0, end + 3); + const content = md.slice(end + 3).replace(/^\s+/, ''); + return { frontmatter, content }; + } + } + return { frontmatter: '', content: md }; +} + +// Helper to combine frontmatter and content +function combineFrontmatterAndContent(frontmatter: string, content: string): string { + if (!frontmatter) return content; + if (!content) return frontmatter; + return frontmatter + '\n\n' + content; +} + function FileTree({ nodes, onSelect, selectedSlug, level = 0 }: { nodes: Node[]; onSelect: (slug: string) => void; @@ -87,19 +109,73 @@ function FileTree({ nodes, onSelect, selectedSlug, level = 0 }: { export default function EditorPage() { // State + const router = useRouter(); const [tree, setTree] = useState([]); const [selectedSlug, setSelectedSlug] = useState(null); const [fileContent, setFileContent] = useState(""); + const [originalContent, setOriginalContent] = useState(""); const [fileTitle, setFileTitle] = useState(""); const [vimMode, setVimMode] = useState(false); const [previewHtml, setPreviewHtml] = useState(""); - const [split, setSplit] = useState(50); // percent + const [split, setSplit] = useState(50); // percent - default to 50/50 split const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [browserOpen, setBrowserOpen] = useState(true); + const [showUnsavedDialog, setShowUnsavedDialog] = useState(false); + const [pendingNavigation, setPendingNavigation] = useState(null); const editorRef = useRef(null); const monacoVimRef = useRef(null); + // Check if there are unsaved changes + const hasUnsavedChanges = fileContent !== originalContent; + + // Handle browser beforeunload event + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (hasUnsavedChanges) { + e.preventDefault(); + e.returnValue = ''; + } + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + return () => window.removeEventListener('beforeunload', handleBeforeUnload); + }, [hasUnsavedChanges]); + + // Handle back navigation with unsaved changes check + const handleBackNavigation = () => { + if (hasUnsavedChanges) { + setShowUnsavedDialog(true); + setPendingNavigation('/admin'); + } else { + router.push('/admin'); + } + }; + + // Handle unsaved changes dialog actions + const handleUnsavedDialogAction = (action: 'save' | 'discard' | 'cancel') => { + if (action === 'save') { + handleSave().then(() => { + setOriginalContent(fileContent); // Reset unsaved state after save + setShowUnsavedDialog(false); + setPendingNavigation(null); + if (pendingNavigation) { + router.push(pendingNavigation); + } + }); + } else if (action === 'discard') { + setFileContent(originalContent); // Revert to last saved + setShowUnsavedDialog(false); + setPendingNavigation(null); + if (pendingNavigation) { + router.push(pendingNavigation); + } + } else { + setShowUnsavedDialog(false); + setPendingNavigation(null); + } + }; + // Fetch file tree useEffect(() => { fetch("/api/posts") @@ -114,7 +190,10 @@ export default function EditorPage() { fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`) .then(r => r.json()) .then(data => { - setFileContent(stripFrontmatter(data.raw || data.content || "")); + const { frontmatter, content } = extractFrontmatter(data.raw || data.content || ""); + const combinedContent = combineFrontmatterAndContent(frontmatter, content); + setFileContent(combinedContent); + setOriginalContent(combinedContent); setFileTitle(data.title || data.slug || ""); setLoading(false); }); @@ -124,18 +203,40 @@ export default function EditorPage() { async function handleSave() { if (!selectedSlug) return; setSaving(true); - await fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ markdown: fileContent }) - }); - setSaving(false); + + try { + // First save the file + const saveResponse = await fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ markdown: fileContent }) + }); + + if (!saveResponse.ok) { + throw new Error('Failed to save file'); + } + + // Then call Rust backend to reparse this specific post + const reparseResponse = await fetch(`/api/admin/posts?reparsePost=${encodeURIComponent(selectedSlug)}`); + + if (!reparseResponse.ok) { + console.warn('Failed to reparse post, but file was saved'); + } else { + console.log('Post saved and reparsed successfully'); + } + setOriginalContent(fileContent); // Reset unsaved state after save + } catch (error) { + console.error('Error saving/reparsing post:', error); + } finally { + setSaving(false); + } } // Live preview (JS markdown, not Rust) useEffect(() => { if (!fileContent) { setPreviewHtml(""); return; } - const html = typeof marked.parse === 'function' ? marked.parse(stripFrontmatter(fileContent)) : ''; + const { content } = extractFrontmatter(fileContent); + const html = typeof marked.parse === 'function' ? marked.parse(content) : ''; if (typeof html === 'string') setPreviewHtml(html); else if (html instanceof Promise) html.then(setPreviewHtml); else setPreviewHtml(''); @@ -144,6 +245,17 @@ export default function EditorPage() { // Monaco Vim integration async function handleEditorDidMount(editor: any, monaco: any) { editorRef.current = editor; + + // Ensure editor resizes properly + const resizeObserver = new ResizeObserver(() => { + editor.layout(); + }); + + const editorContainer = editor.getContainerDomNode(); + if (editorContainer) { + resizeObserver.observe(editorContainer); + } + if (vimMode) { const { initVimMode } = await import("monaco-vim"); if (monacoVimRef.current) monacoVimRef.current.dispose(); @@ -168,25 +280,57 @@ export default function EditorPage() { // Split drag logic const dragRef = useRef(false); - function onDrag(e: React.MouseEvent) { + const [isDragging, setIsDragging] = useState(false); + + function onDrag(e: React.MouseEvent | MouseEvent) { if (!dragRef.current) return; const percent = (e.clientX / window.innerWidth) * 100; - setSplit(Math.max(20, Math.min(80, percent))); + setSplit(percent); // No min/max limits } - function onDragStart() { dragRef.current = true; document.body.style.cursor = "col-resize"; } - function onDragEnd() { dragRef.current = false; document.body.style.cursor = ""; } + + function onDragStart() { + dragRef.current = true; + setIsDragging(true); + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + } + + function onDragEnd() { + dragRef.current = false; + setIsDragging(false); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + } + useEffect(() => { - function onMove(e: MouseEvent) { onDrag(e as any); } - function onUp() { onDragEnd(); } - if (dragRef.current) { + function onMove(e: MouseEvent) { + if (dragRef.current) { + onDrag(e); + } + } + + function onUp() { + if (dragRef.current) { + onDragEnd(); + } + } + + if (isDragging) { window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); - return () => { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); }; + return () => { + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + }; } - }, [dragRef.current]); + }, [isDragging]); + + // Layout logic for left pane (file browser + editor) + const leftPaneWidth = `${split}%`; + const fileBrowserWidth = 240; // Only render MonacoEditor if the editor pane is visible and has width - const showEditor = browserOpen ? true : split > 5; + const showEditor = true; // Always show editor, it will resize based on container return (
@@ -241,50 +385,101 @@ export default function EditorPage() { - Markdown Editor + Markdown Bearbeiter
+ {/* Back Button */} + + {/* Save Button */} + + + {/* Vim Mode Button */}
{/* Split Layout */} -
+
{/* Left: File browser + Editor */} -
+
{/* File Browser Collapsible Toggle */} - +
+ +
+ {/* File browser content */} {browserOpen && ( -
- {tree.length === 0 ? ( -
No files found.
- ) : ( - - )} +
+
+ {tree.length === 0 ? ( +
Keine Datein gefunden.
+ ) : ( + + )} +
)} {/* Monaco Editor */} @@ -301,7 +496,7 @@ export default function EditorPage() { fontFamily: 'JetBrains Mono', fontWeight: 'bold', fontSize: 15, - minimap: { enabled: false }, + minimap: { enabled: true }, wordWrap: "on", scrollBeyondLastLine: false, smoothScrolling: true, @@ -314,6 +509,16 @@ export default function EditorPage() { cursorStyle: "line", fixedOverflowWidgets: true, readOnly: loading, + folding: true, + foldingStrategy: "indentation", + showFoldingControls: "always", + foldingHighlight: true, + foldingImportsByDefault: true, + unfoldOnClickAfterEndOfLine: false, + links: true, + colorDecorators: true, + formatOnPaste: true, + formatOnType: true, }} onChange={v => setFileContent(v ?? "")} onMount={handleEditorDidMount} @@ -324,11 +529,18 @@ export default function EditorPage() {
- {/* Draggable Splitter */} + {/* Draggable Splitter - always show */}
+ > + {/* Drag handle indicator */} +
+
+
+
{/* Right: Live Preview */}
@@ -345,6 +557,45 @@ export default function EditorPage() {
+ + {/* Unsaved Changes Dialog */} + {showUnsavedDialog && ( +
+
+
+
+ + + +
+

Ungespeicherte Γ„nderungen

+
+

+ Sie haben ungespeicherte Γ„nderungen. MΓΆchten Sie diese speichern, bevor Sie fortfahren? +

+
+ + + +
+
+
+ )}
); } @@ -363,7 +614,7 @@ class MonacoErrorBoundary extends React.Component<{children: React.ReactNode}, { } render() { if (this.state.error) { - return
Editor error: {this.state.error.message}
; + return
Fehler: {this.state.error.message}
; } return this.props.children; } diff --git a/src/app/admin/manage/rust-status/page.tsx b/src/app/admin/manage/rust-status/page.tsx index a09982a..ecb8dfc 100644 --- a/src/app/admin/manage/rust-status/page.tsx +++ b/src/app/admin/manage/rust-status/page.tsx @@ -103,6 +103,19 @@ export default function RustStatusPage() { } }; + const reinterpretAllPosts = async () => { + try { + const res = await fetch('/api/admin/posts?reinterpretAll=1'); + if (!res.ok) throw new Error('Fehler beim Neuinterpretieren der Posts'); + const data = await res.json(); + console.log('Reinterpret result:', data); + // Refresh all data after reinterpret + await Promise.all([fetchStats(), fetchHealth(), fetchLogs()]); + } catch (e: any) { + console.error('Error reinterpreting posts:', e); + } + }; + useEffect(() => { fetchStats(); fetchHealth(); @@ -358,6 +371,13 @@ export default function RustStatusPage() {

Parser Logs

+