Enhance blog features and improve backend functionality
- Added a VS Code-style editor with YAML frontmatter support and live preview. - Implemented force reparse functionality for immediate updates of posts. - Improved directory scanning with error handling and automatic directory creation. - Introduced new CLI commands for cache management: `reinterpret-all` and `reparse-post`. - Enhanced logging for better debugging and monitoring of the Rust backend. - Updated README to reflect new features and improvements.
This commit is contained in:
107
README.md
107
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
|
- **🎯 Content Management**: Drag & drop file uploads, post editing, and deletion
|
||||||
- **📦 Export Functionality**: Export all posts as tar.gz archive (Docker only)
|
- **📦 Export Functionality**: Export all posts as tar.gz archive (Docker only)
|
||||||
- **💾 Smart Caching**: RAM-based caching system for instant post retrieval
|
- **💾 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/
|
markdownblog/
|
||||||
├── markdown_backend/ # Rust backend for markdown processing
|
├── markdown_backend/ # Rust backend for markdown processing
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── main.rs # CLI interface and command handling
|
│ │ ├── main.rs # CLI interface and command handling
|
||||||
│ │ └── markdown.rs # Markdown parsing, caching, and file watching
|
│ │ └── markdown.rs # Markdown parsing, caching, and file watching
|
||||||
│ ├── Cargo.toml # Rust dependencies and configuration
|
│ ├── Cargo.toml # Rust dependencies and configuration
|
||||||
│ └── target/ # Compiled Rust binaries
|
│ └── target/ # Compiled Rust binaries
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── app/ # Next.js 14 App Router
|
│ ├── app/ # Next.js 14 App Router
|
||||||
│ │ ├── admin/ # Admin dashboard pages
|
│ │ ├── admin/ # Admin dashboard pages
|
||||||
|
│ │ │ ├── editor/ # VS Code-style editor
|
||||||
|
│ │ │ │ └── page.tsx # Markdown editor with Monaco
|
||||||
│ │ │ ├── manage/ # Content management interface
|
│ │ │ ├── manage/ # Content management interface
|
||||||
│ │ │ │ ├── page.tsx # Manage posts and folders
|
│ │ │ │ ├── page.tsx # Manage posts and folders
|
||||||
│ │ │ │ └── rust-status/ # Rust backend monitoring
|
│ │ │ │ └── rust-status/ # Rust backend monitoring
|
||||||
@@ -84,8 +89,12 @@ markdownblog/
|
|||||||
│ │ │ └── posts/ # Public post API
|
│ │ │ └── posts/ # Public post API
|
||||||
│ │ │ ├── [slug]/ # Dynamic post API routes
|
│ │ │ ├── [slug]/ # Dynamic post API routes
|
||||||
│ │ │ │ └── route.ts
|
│ │ │ │ └── route.ts
|
||||||
|
│ │ │ ├── preview/ # Markdown preview API
|
||||||
|
│ │ │ │ └── route.ts
|
||||||
│ │ │ ├── stream/ # Server-Sent Events for real-time updates
|
│ │ │ ├── stream/ # Server-Sent Events for real-time updates
|
||||||
│ │ │ │ └── route.ts
|
│ │ │ │ └── route.ts
|
||||||
|
│ │ │ ├── webhook/ # Webhook endpoint
|
||||||
|
│ │ │ │ └── route.ts
|
||||||
│ │ │ └── route.ts # List all posts
|
│ │ │ └── route.ts # List all posts
|
||||||
│ │ ├── posts/ # Blog post pages
|
│ │ ├── posts/ # Blog post pages
|
||||||
│ │ │ └── [...slug]/ # Dynamic post routing (catch-all)
|
│ │ │ └── [...slug]/ # Dynamic post routing (catch-all)
|
||||||
@@ -97,15 +106,15 @@ markdownblog/
|
|||||||
│ │ ├── highlight-github.css # Code syntax highlighting styles
|
│ │ ├── highlight-github.css # Code syntax highlighting styles
|
||||||
│ │ ├── layout.tsx # Root layout with metadata
|
│ │ ├── layout.tsx # Root layout with metadata
|
||||||
│ │ ├── MobileNav.tsx # Mobile navigation component
|
│ │ ├── MobileNav.tsx # Mobile navigation component
|
||||||
|
│ │ ├── monaco-vim.d.ts # Monaco Vim typings
|
||||||
│ │ └── page.tsx # Homepage with post listing
|
│ │ └── page.tsx # Homepage with post listing
|
||||||
│ └── lib/ # Utility libraries
|
│ └── lib/ # Utility libraries
|
||||||
│ └── postsDirectory.ts # Post directory management and Rust integration
|
│ └── postsDirectory.ts # Post directory management and Rust integration
|
||||||
├── posts/ # Markdown blog posts storage
|
├── posts/ # Markdown blog posts storage
|
||||||
│ ├── pinned.json # Pinned posts configuration
|
│ ├── about.md
|
||||||
│ ├── welcome.md # Welcome post with frontmatter
|
│ ├── welcome.md
|
||||||
│ ├── mdtest.md # Test post with various markdown features
|
│ └── assets/
|
||||||
│ ├── anchor-test.md # Test post for anchor linking
|
│ └── peta.png
|
||||||
│ └── ii/ # Example nested folder structure
|
|
||||||
├── public/ # Static assets
|
├── public/ # Static assets
|
||||||
│ ├── android-chrome-192x192.png
|
│ ├── android-chrome-192x192.png
|
||||||
│ ├── android-chrome-512x512.png
|
│ ├── android-chrome-512x512.png
|
||||||
@@ -115,19 +124,19 @@ markdownblog/
|
|||||||
│ ├── favicon.ico
|
│ ├── favicon.ico
|
||||||
│ └── site.webmanifest
|
│ └── site.webmanifest
|
||||||
├── electron/ # Desktop application
|
├── electron/ # Desktop application
|
||||||
│ └── main.js # Electron main process configuration
|
│ └── main.js # Electron main process configuration
|
||||||
├── Dockerfile # Docker container configuration
|
├── Dockerfile # Docker container configuration
|
||||||
├── docker.sh # Docker deployment script
|
├── docker.sh # Docker deployment script
|
||||||
├── entrypoint.sh # Container entrypoint script
|
├── entrypoint.sh # Container entrypoint script
|
||||||
├── run-local-backend.sh # Local Rust backend runner
|
├── run-local-backend.sh # Local Rust backend runner
|
||||||
├── next-env.d.ts # Next.js TypeScript definitions
|
├── next-env.d.ts # Next.js TypeScript definitions
|
||||||
├── next.config.js # Next.js configuration
|
├── next.config.js # Next.js configuration
|
||||||
├── package-lock.json # npm lock file
|
├── package-lock.json # npm lock file
|
||||||
├── package.json # Dependencies and scripts
|
├── package.json # Dependencies and scripts
|
||||||
├── postcss.config.js # PostCSS configuration
|
├── postcss.config.js # PostCSS configuration
|
||||||
├── tailwind.config.js # Tailwind CSS configuration
|
├── tailwind.config.js # Tailwind CSS configuration
|
||||||
├── tsconfig.json # TypeScript configuration
|
├── tsconfig.json # TypeScript configuration
|
||||||
└── LICENSE # MIT License
|
└── LICENSE # MIT License
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Components
|
### Key Components
|
||||||
@@ -329,6 +338,9 @@ console.log("Hello, World!");
|
|||||||
- **📦 Export Posts**: Download all posts as archive (Docker only)
|
- **📦 Export Posts**: Download all posts as archive (Docker only)
|
||||||
- **📊 Rust Status**: Monitor parser performance, logs, and health
|
- **📊 Rust Status**: Monitor parser performance, logs, and health
|
||||||
- **🔍 Log Management**: View, filter, and clear parser logs
|
- **🔍 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
|
### Security
|
||||||
|
|
||||||
@@ -350,6 +362,9 @@ console.log("Hello, World!");
|
|||||||
- **📁 Recursive Scanning**: Efficient folder traversal and file discovery
|
- **📁 Recursive Scanning**: Efficient folder traversal and file discovery
|
||||||
- **💾 Smart Caching**: RAM-based caching with disk persistence
|
- **💾 Smart Caching**: RAM-based caching with disk persistence
|
||||||
- **📊 Performance Monitoring**: Real-time metrics and logging
|
- **📊 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
|
### Real-Time Updates
|
||||||
|
|
||||||
@@ -358,6 +373,16 @@ console.log("Hello, World!");
|
|||||||
- **⚡ Instant Updates**: Sub-second response to file modifications
|
- **⚡ Instant Updates**: Sub-second response to file modifications
|
||||||
- **🔄 Fallback Polling**: Graceful degradation if SSE fails
|
- **🔄 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
|
## 🎨 Customization
|
||||||
@@ -398,6 +423,8 @@ cargo build --release # Build optimized binary
|
|||||||
cargo run -- watch # Watch for file changes
|
cargo run -- watch # Watch for file changes
|
||||||
cargo run -- logs # View parser logs
|
cargo run -- logs # View parser logs
|
||||||
cargo run -- checkhealth # Check backend health
|
cargo run -- checkhealth # Check backend health
|
||||||
|
cargo run -- reinterpret-all # Force reparse all posts
|
||||||
|
cargo run -- reparse-post <slug> # 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
|
- **File watching issues**: Check file permissions and inotify limits
|
||||||
- **Performance issues**: Monitor logs via admin interface
|
- **Performance issues**: Monitor logs via admin interface
|
||||||
- **Cache problems**: Clear cache via admin interface or restart
|
- **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
|
### 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.
|
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 <slug>`: 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
|
||||||
@@ -1,7 +1,17 @@
|
|||||||
#[warn(unused_imports)]
|
#[warn(unused_imports)]
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
mod markdown;
|
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 serde_json;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
@@ -44,6 +54,12 @@ enum Commands {
|
|||||||
Logs,
|
Logs,
|
||||||
/// Clear parser logs
|
/// Clear parser logs
|
||||||
ClearLogs,
|
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 markdown from file or stdin
|
||||||
Parse {
|
Parse {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -111,6 +127,35 @@ fn main() {
|
|||||||
clear_parser_logs();
|
clear_parser_logs();
|
||||||
println!("{}", serde_json::to_string(&serde_json::json!({"success": true, "message": "Logs cleared"})).unwrap());
|
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 } => {
|
Commands::Parse { file, stdin, ast } => {
|
||||||
let content = if *stdin {
|
let content = if *stdin {
|
||||||
let mut buffer = String::new();
|
let mut buffer = String::new();
|
||||||
@@ -139,4 +184,4 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,6 +180,18 @@ static AMMONIA: Lazy<ammonia::Builder<'static>> = Lazy::new(|| {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Helper functions
|
// 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 {
|
fn get_posts_directory() -> PathBuf {
|
||||||
let is_docker = std::env::var("DOCKER_CONTAINER").is_ok()
|
let is_docker = std::env::var("DOCKER_CONTAINER").is_ok()
|
||||||
|| std::env::var("KUBERNETES_SERVICE_HOST").is_ok()
|
|| std::env::var("KUBERNETES_SERVICE_HOST").is_ok()
|
||||||
@@ -207,29 +219,101 @@ fn get_posts_directory() -> PathBuf {
|
|||||||
for candidate in candidates.iter() {
|
for candidate in candidates.iter() {
|
||||||
let path = PathBuf::from(candidate);
|
let path = PathBuf::from(candidate);
|
||||||
if path.exists() && path.is_dir() {
|
if path.exists() && path.is_dir() {
|
||||||
|
add_log("info", &format!("Using posts directory: {:?}", path), None, None);
|
||||||
return path;
|
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.
|
// Function to find Markdown files with improved reliability
|
||||||
// This will scan Directories recursively
|
|
||||||
fn find_markdown_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
|
fn find_markdown_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
if dir.is_dir() {
|
let mut errors = Vec::new();
|
||||||
for entry in fs::read_dir(dir)? {
|
|
||||||
let entry = entry?;
|
if !dir.exists() {
|
||||||
let path = entry.path();
|
let error_msg = format!("Directory does not exist: {:?}", dir);
|
||||||
|
add_log("error", &error_msg, None, None);
|
||||||
if path.is_dir() {
|
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, error_msg));
|
||||||
files.extend(find_markdown_files(&path)?);
|
}
|
||||||
} else if path.extension().map(|e| e == "md").unwrap_or(false) {
|
|
||||||
files.push(path);
|
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)
|
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<LogEntry>) -> std::io::Result<()> {
|
fn save_parser_logs_to_disk_inner(logs: &VecDeque<LogEntry>) -> std::io::Result<()> {
|
||||||
let _ = std::fs::create_dir_all("./cache");
|
ensure_cache_directory();
|
||||||
let logs_vec: Vec<_> = logs.iter().cloned().collect();
|
let logs_vec: Vec<_> = logs.iter().cloned().collect();
|
||||||
let json = serde_json::to_string(&logs_vec)?;
|
let json = serde_json::to_string(&logs_vec)?;
|
||||||
std::fs::write(PARSER_LOGS_PATH, json)?;
|
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() {
|
pub fn save_post_cache_to_disk() {
|
||||||
|
ensure_cache_directory();
|
||||||
if let Ok(map) = serde_json::to_string(&*POST_CACHE.read().unwrap()) {
|
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);
|
let _ = fs::write(POSTS_CACHE_PATH, map);
|
||||||
}
|
}
|
||||||
if let Ok(map) = serde_json::to_string(&*POST_STATS.read().unwrap()) {
|
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);
|
let _ = fs::write(POST_STATS_PATH, map);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -739,7 +822,90 @@ pub fn get_parser_logs() -> Vec<LogEntry> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_parser_logs() {
|
pub fn clear_parser_logs() {
|
||||||
let mut logs = PARSER_LOGS.write().unwrap();
|
PARSER_LOGS.write().unwrap().clear();
|
||||||
logs.clear();
|
if let Err(e) = save_parser_logs_to_disk_inner(&VecDeque::new()) {
|
||||||
let _ = std::fs::remove_file(PARSER_LOGS_PATH);
|
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<Vec<Post>, Box<dyn std::error::Error>> {
|
||||||
|
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<Post, Box<dyn std::error::Error>> {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@@ -5,26 +5,36 @@ tags: [about, profile]
|
|||||||
author: rattatwinko
|
author: rattatwinko
|
||||||
summary: This is the about page
|
summary: This is the about page
|
||||||
---
|
---
|
||||||
|
|
||||||
# About Me
|
# About Me
|
||||||
|
|
||||||
Hi! I'm Rattatwinko, a passionate developer who loves building self-hosted tools and beautiful web experiences.
|
_**I am rattatwinko**_
|
||||||
|
|
||||||

|
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
|
## What I used:
|
||||||
- TypeScript, JavaScript
|
- TypeScript
|
||||||
|
- Next.JS
|
||||||
- Rust
|
- Rust
|
||||||
- React, Next.js
|
- Monaco (for a beautiful Editing experience)
|
||||||
- Tailwind CSS
|
- More shit which you can check out in the Repo
|
||||||
- Docker
|
|
||||||
|
|
||||||
## Experience
|
## What I do
|
||||||
- Indie developer, 2020–present
|
|
||||||
- Open source contributor
|
|
||||||
|
|
||||||
## Projects
|
School.
|
||||||
- **MarkdownBlog**: The site you're reading now! A fast, modern, and hackable markdown blog platform.
|
Coding.
|
||||||
|
Not more not less.
|
||||||
|
|
||||||
|
### Socials
|
||||||
|
|
||||||
|
<!-- HTML for this cause Markdown does not support this -->
|
||||||
|
|
||||||
|
<form action="https://instagram.com/rattatwinko">
|
||||||
|
<input type="submit" value="Insta" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form action="https://tiktok.com/rattatwinko">
|
||||||
|
<input type="submit" value="TikTok" />
|
||||||
|
</form>
|
||||||
|
|
||||||
## Contact
|
|
||||||
- [GitHub](https://github.com/rattatwinko)
|
|
||||||
- [Email](mailto:me@example.com)
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import "@fontsource/jetbrains-mono";
|
import "@fontsource/jetbrains-mono";
|
||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
|
|
||||||
@@ -37,6 +38,27 @@ function stripFrontmatter(md: string): string {
|
|||||||
return md;
|
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 }: {
|
function FileTree({ nodes, onSelect, selectedSlug, level = 0 }: {
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
onSelect: (slug: string) => void;
|
onSelect: (slug: string) => void;
|
||||||
@@ -87,19 +109,73 @@ function FileTree({ nodes, onSelect, selectedSlug, level = 0 }: {
|
|||||||
|
|
||||||
export default function EditorPage() {
|
export default function EditorPage() {
|
||||||
// State
|
// State
|
||||||
|
const router = useRouter();
|
||||||
const [tree, setTree] = useState<Node[]>([]);
|
const [tree, setTree] = useState<Node[]>([]);
|
||||||
const [selectedSlug, setSelectedSlug] = useState<string | null>(null);
|
const [selectedSlug, setSelectedSlug] = useState<string | null>(null);
|
||||||
const [fileContent, setFileContent] = useState<string>("");
|
const [fileContent, setFileContent] = useState<string>("");
|
||||||
|
const [originalContent, setOriginalContent] = useState<string>("");
|
||||||
const [fileTitle, setFileTitle] = useState<string>("");
|
const [fileTitle, setFileTitle] = useState<string>("");
|
||||||
const [vimMode, setVimMode] = useState(false);
|
const [vimMode, setVimMode] = useState(false);
|
||||||
const [previewHtml, setPreviewHtml] = useState<string>("");
|
const [previewHtml, setPreviewHtml] = useState<string>("");
|
||||||
const [split, setSplit] = useState(50); // percent
|
const [split, setSplit] = useState(50); // percent - default to 50/50 split
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [browserOpen, setBrowserOpen] = useState(true);
|
const [browserOpen, setBrowserOpen] = useState(true);
|
||||||
|
const [showUnsavedDialog, setShowUnsavedDialog] = useState(false);
|
||||||
|
const [pendingNavigation, setPendingNavigation] = useState<string | null>(null);
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
const monacoVimRef = useRef<any>(null);
|
const monacoVimRef = useRef<any>(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
|
// Fetch file tree
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/posts")
|
fetch("/api/posts")
|
||||||
@@ -114,7 +190,10 @@ export default function EditorPage() {
|
|||||||
fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`)
|
fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`)
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.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 || "");
|
setFileTitle(data.title || data.slug || "");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
@@ -124,18 +203,40 @@ export default function EditorPage() {
|
|||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
if (!selectedSlug) return;
|
if (!selectedSlug) return;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
await fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`, {
|
|
||||||
method: "POST",
|
try {
|
||||||
headers: { "Content-Type": "application/json" },
|
// First save the file
|
||||||
body: JSON.stringify({ markdown: fileContent })
|
const saveResponse = await fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`, {
|
||||||
});
|
method: "POST",
|
||||||
setSaving(false);
|
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)
|
// Live preview (JS markdown, not Rust)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!fileContent) { setPreviewHtml(""); return; }
|
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);
|
if (typeof html === 'string') setPreviewHtml(html);
|
||||||
else if (html instanceof Promise) html.then(setPreviewHtml);
|
else if (html instanceof Promise) html.then(setPreviewHtml);
|
||||||
else setPreviewHtml('');
|
else setPreviewHtml('');
|
||||||
@@ -144,6 +245,17 @@ export default function EditorPage() {
|
|||||||
// Monaco Vim integration
|
// Monaco Vim integration
|
||||||
async function handleEditorDidMount(editor: any, monaco: any) {
|
async function handleEditorDidMount(editor: any, monaco: any) {
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
|
|
||||||
|
// Ensure editor resizes properly
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
editor.layout();
|
||||||
|
});
|
||||||
|
|
||||||
|
const editorContainer = editor.getContainerDomNode();
|
||||||
|
if (editorContainer) {
|
||||||
|
resizeObserver.observe(editorContainer);
|
||||||
|
}
|
||||||
|
|
||||||
if (vimMode) {
|
if (vimMode) {
|
||||||
const { initVimMode } = await import("monaco-vim");
|
const { initVimMode } = await import("monaco-vim");
|
||||||
if (monacoVimRef.current) monacoVimRef.current.dispose();
|
if (monacoVimRef.current) monacoVimRef.current.dispose();
|
||||||
@@ -168,25 +280,57 @@ export default function EditorPage() {
|
|||||||
|
|
||||||
// Split drag logic
|
// Split drag logic
|
||||||
const dragRef = useRef(false);
|
const dragRef = useRef(false);
|
||||||
function onDrag(e: React.MouseEvent) {
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
|
function onDrag(e: React.MouseEvent | MouseEvent) {
|
||||||
if (!dragRef.current) return;
|
if (!dragRef.current) return;
|
||||||
const percent = (e.clientX / window.innerWidth) * 100;
|
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(() => {
|
useEffect(() => {
|
||||||
function onMove(e: MouseEvent) { onDrag(e as any); }
|
function onMove(e: MouseEvent) {
|
||||||
function onUp() { onDragEnd(); }
|
if (dragRef.current) {
|
||||||
if (dragRef.current) {
|
onDrag(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUp() {
|
||||||
|
if (dragRef.current) {
|
||||||
|
onDragEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDragging) {
|
||||||
window.addEventListener("mousemove", onMove);
|
window.addEventListener("mousemove", onMove);
|
||||||
window.addEventListener("mouseup", onUp);
|
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
|
// 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 (
|
return (
|
||||||
<div className="h-screen w-screen bg-white flex flex-col font-mono" style={{ fontFamily: 'JetBrains Mono, monospace', fontWeight: 'bold' }}>
|
<div className="h-screen w-screen bg-white flex flex-col font-mono" style={{ fontFamily: 'JetBrains Mono, monospace', fontWeight: 'bold' }}>
|
||||||
@@ -241,50 +385,101 @@ export default function EditorPage() {
|
|||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-black text-lg font-semibold">Markdown Editor</span>
|
<span className="text-black text-lg font-semibold">Markdown Bearbeiter</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
|
{/* Back Button */}
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={() => handleBackNavigation()}
|
||||||
className={`flex items-center gap-2 px-3 py-1 rounded transition-colors border border-blue-400 bg-blue-500 text-white hover:bg-blue-600 font-mono ${saving ? 'opacity-60 cursor-wait' : ''}`}
|
className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1 rounded transition-colors border font-mono text-sm sm:text-base ${
|
||||||
|
saving
|
||||||
|
? 'opacity-60 cursor-wait border-red-400 bg-red-500 text-white'
|
||||||
|
: hasUnsavedChanges
|
||||||
|
? 'border-orange-400 bg-orange-500 text-white hover:bg-orange-600'
|
||||||
|
: 'border-red-400 bg-red-500 text-white hover:bg-red-600'
|
||||||
|
}`}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a2 2 0 01-2 2H7a2 2 0 01-2-2V7a2 2 0 012-2h4a2 2 0 012 2v1" /></svg>
|
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<span>Save</span>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
|
</svg>
|
||||||
|
<span className="hidden sm:inline">
|
||||||
|
{hasUnsavedChanges ? 'Zurück*' : 'Zurück'}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1 rounded transition-colors border font-mono text-sm sm:text-base ${
|
||||||
|
saving
|
||||||
|
? 'opacity-60 cursor-wait border-blue-400 bg-blue-500 text-white'
|
||||||
|
: hasUnsavedChanges
|
||||||
|
? 'border-orange-400 bg-orange-500 text-white hover:bg-orange-600'
|
||||||
|
: 'border-blue-400 bg-blue-500 text-white hover:bg-blue-600'
|
||||||
|
}`}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a2 2 0 01-2 2H7a2 2 0 01-2-2V7a2 2 0 012-2h4a2 2 0 012 2v1" /></svg>
|
||||||
|
<span className="hidden sm:inline">
|
||||||
|
{saving
|
||||||
|
? 'Am Speichern...'
|
||||||
|
: hasUnsavedChanges
|
||||||
|
? 'Speichern*'
|
||||||
|
: 'Speichern'
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Vim Mode Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setVimMode((v) => !v)}
|
onClick={() => setVimMode((v) => !v)}
|
||||||
className={`flex items-center gap-2 px-3 py-1 rounded transition-colors border border-gray-300 ${vimMode ? "bg-green-600 text-white" : "bg-white text-gray-700 hover:bg-gray-100"}`}
|
className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1 rounded transition-colors border border-gray-300 text-sm sm:text-base ${
|
||||||
|
vimMode
|
||||||
|
? "bg-green-600 text-white"
|
||||||
|
: "bg-white text-gray-700 hover:bg-gray-100"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{/* Actual Vim SVG Icon */}
|
{/* Actual Vim SVG Icon */}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" className="w-5 h-5" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" className="w-4 h-4 sm:w-5 sm:h-5" fill="currentColor">
|
||||||
<title>vim</title>
|
<title>vim</title>
|
||||||
<path d="M26.445 22.095l0.592-0.649h1.667l0.386 0.519-1.581 5.132h0.616l-0.1 0.261h-2.228l1.405-4.454h-2.518l-1.346 4.238h0.53l-0.091 0.217h-2.006l1.383-4.434h-2.619l-1.327 4.172h0.545l-0.090 0.261h-2.076l1.892-5.573h-0.732l0.114-0.339h2.062l0.649 0.671h1.132l0.614-0.692h1.326l0.611 0.669zM7.99 27.033h-2.141l-0.327-0.187v-21.979h-1.545l-0.125-0.125v-1.47l0.179-0.192h9.211l0.266 0.267v1.385l-0.177 0.216h-1.348v10.857l11.006-10.857h-2.607l-0.219-0.235v-1.453l0.151-0.139h9.36l0.165 0.166v1.337l-12.615 12.937h-0.466c-0.005-0-0.011-0-0.018-0-0.012 0-0.024 0.001-0.036 0.002l0.002-0-0.025 0.004c-0.058 0.012-0.108 0.039-0.149 0.075l0-0-0.429 0.369-0.005 0.004c-0.040 0.037-0.072 0.084-0.090 0.136l-0.001 0.002-0.37 1.037zM17.916 18.028l0.187 0.189-0.336 1.152-0.281 0.282h-1.211l-0.226-0.226 0.389-1.088 0.36-0.309zM13.298 27.42l1.973-5.635h-0.626l0.371-0.38h2.073l-1.953 5.692h0.779l-0.099 0.322zM30.996 15.982h-0.034l-5.396-5.396 5.377-5.516v-2.24l-0.811-0.81h-10.245l-0.825 0.756v1.306l-3.044-3.044v-0.034l-0.019 0.018-0.018-0.018v0.034l-1.612 1.613-0.672-0.673h-10.151l-0.797 0.865v2.356l0.77 0.77h0.9v6.636l-3.382 3.38h-0.034l0.018 0.016-0.018 0.017h0.034l3.382 3.382v8.081l1.133 0.654h2.902l2.321-2.379 5.206 5.206v0.035l0.019-0.017 0.017 0.017v-0.035l3.136-3.135h0.606c0.144-0.001 0.266-0.093 0.312-0.221l0.001-0.002 0.182-0.532c0.011-0.031 0.017-0.067 0.017-0.105 0-0.073-0.024-0.14-0.064-0.195l0.001 0.001 1.827-1.827-0.765 2.452c-0.009 0.029-0.015 0.063-0.015 0.097 0 0.149 0.098 0.275 0.233 0.317l0.002 0.001c0.029 0.009 0.063 0.015 0.097 0.015 0 0 0 0 0 0h2.279c0.136-0.001 0.252-0.084 0.303-0.201l0.001-0.002 0.206-0.492c0.014-0.036 0.022-0.077 0.022-0.121 0-0.048-0.010-0.094-0.028-0.135l0.001 0.002c-0.035-0.082-0.1-0.145-0.18-0.177l-0.002-0.001c-0.036-0.015-0.077-0.024-0.121-0.025h-0.094l1.050-3.304h1.54l-1.27 4.025c-0.009 0.029-0.015 0.063-0.015 0.097 0 0.149 0.098 0.274 0.232 0.317l0.002 0.001c0.029 0.009 0.063 0.015 0.098 0.015 0 0 0.001 0 0.001 0h2.502c0 0 0.001 0 0.001 0 0.14 0 0.26-0.087 0.308-0.21l0.001-0.002 0.205-0.535c0.013-0.034 0.020-0.073 0.020-0.114 0-0.142-0.090-0.264-0.215-0.311l-0.002-0.001c-0.034-0.013-0.073-0.021-0.114-0.021h-0.181l1.413-4.59c0.011-0.031 0.017-0.066 0.017-0.103 0-0.074-0.025-0.143-0.066-0.198l0.001 0.001-0.469-0.63-0.004-0.006c-0.061-0.078-0.156-0.127-0.261-0.127h-1.795c-0.093 0-0.177 0.039-0.237 0.101l-0 0-0.5 0.549h-0.78l-0.052-0.057 5.555-5.555h0.035l-0.017-0.014z"/>
|
<path d="M26.445 22.095l0.592-0.649h1.667l0.386 0.519-1.581 5.132h0.616l-0.1 0.261h-2.228l1.405-4.454h-2.518l-1.346 4.238h0.53l-0.091 0.217h-2.006l1.383-4.434h-2.619l-1.327 4.172h0.545l-0.090 0.261h-2.076l1.892-5.573h-0.732l0.114-0.339h2.062l0.649 0.671h1.132l0.614-0.692h1.326l0.611 0.669zM7.99 27.033h-2.141l-0.327-0.187v-21.979h-1.545l-0.125-0.125v-1.47l0.179-0.192h9.211l0.266 0.267v1.385l-0.177 0.216h-1.348v10.857l11.006-10.857h-2.607l-0.219-0.235v-1.453l0.151-0.139h9.36l0.165 0.166v1.337l-12.615 12.937h-0.466c-0.005-0-0.011-0-0.018-0-0.012 0-0.024 0.001-0.036 0.002l0.002-0-0.025 0.004c-0.058 0.012-0.108 0.039-0.149 0.075l0-0-0.429 0.369-0.005 0.004c-0.040 0.037-0.072 0.084-0.090 0.136l-0.001 0.002-0.37 1.037zM17.916 18.028l0.187 0.189-0.336 1.152-0.281 0.282h-1.211l-0.226-0.226 0.389-1.088 0.36-0.309zM13.298 27.42l1.973-5.635h-0.626l0.371-0.38h2.073l-1.953 5.692h0.779l-0.099 0.322zM30.996 15.982h-0.034l-5.396-5.396 5.377-5.516v-2.24l-0.811-0.81h-10.245l-0.825 0.756v1.306l-3.044-3.044v-0.034l-0.019 0.018-0.018-0.018v0.034l-1.612 1.613-0.672-0.673h-10.151l-0.797 0.865v2.356l0.77 0.77h0.9v6.636l-3.382 3.38h-0.034l0.018 0.016-0.018 0.017h0.034l3.382 3.382v8.081l1.133 0.654h2.902l2.321-2.379 5.206 5.206v0.035l0.019-0.017 0.017 0.017v-0.035l3.136-3.135h0.606c0.144-0.001 0.266-0.093 0.312-0.221l0.001-0.002 0.182-0.532c0.011-0.031 0.017-0.067 0.017-0.105 0-0.073-0.024-0.14-0.064-0.195l0.001 0.001 1.827-1.827-0.765 2.452c-0.009 0.029-0.015 0.063-0.015 0.097 0 0.149 0.098 0.275 0.233 0.317l0.002 0.001c0.029 0.009 0.063 0.015 0.097 0.015 0 0 0 0 0 0h2.279c0.136-0.001 0.252-0.084 0.303-0.201l0.001-0.002 0.206-0.492c0.014-0.036 0.022-0.077 0.022-0.121 0-0.048-0.010-0.094-0.028-0.135l0.001 0.002c-0.035-0.082-0.1-0.145-0.18-0.177l-0.002-0.001c-0.036-0.015-0.077-0.024-0.121-0.025h-0.094l1.050-3.304h1.54l-1.27 4.025c-0.009 0.029-0.015 0.063-0.015 0.097 0 0.149 0.098 0.274 0.232 0.317l0.002 0.001c0.029 0.009 0.063 0.015 0.098 0.015 0 0 0.001 0 0.001 0h2.502c0 0 0.001 0 0.001 0 0.14 0 0.26-0.087 0.308-0.21l0.001-0.002 0.205-0.535c0.013-0.034 0.020-0.073 0.020-0.114 0-0.142-0.090-0.264-0.215-0.311l-0.002-0.001c-0.034-0.013-0.073-0.021-0.114-0.021h-0.181l1.413-4.59c0.011-0.031 0.017-0.066 0.017-0.103 0-0.074-0.025-0.143-0.066-0.198l0.001 0.001-0.469-0.63-0.004-0.006c-0.061-0.078-0.156-0.127-0.261-0.127h-1.795c-0.093 0-0.177 0.039-0.237 0.101l-0 0-0.5 0.549h-0.78l-0.052-0.057 5.555-5.555h0.035l-0.017-0.014z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="hidden sm:inline">Vim Mode</span>
|
<span className="hidden sm:inline">Vim Modus</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Split Layout */}
|
{/* Split Layout */}
|
||||||
<div className="flex flex-1 min-h-0" style={{ userSelect: dragRef.current ? "none" : undefined }}>
|
<div className="flex flex-1 min-h-0" style={{ userSelect: isDragging ? "none" : undefined }}>
|
||||||
{/* Left: File browser + Editor */}
|
{/* Left: File browser + Editor */}
|
||||||
<div className="flex flex-col" style={{ width: browserOpen ? `${split}%` : '48px', minWidth: browserOpen ? 240 : 48, maxWidth: 900, background: "#fff" }}>
|
<div className="flex flex-row h-full bg-white" style={{ width: leftPaneWidth, minWidth: 0, maxWidth: '100%' }}>
|
||||||
{/* File Browser Collapsible Toggle */}
|
{/* File Browser Collapsible Toggle */}
|
||||||
<button
|
<div style={{ width: 32, minWidth: 32, maxWidth: 32, display: 'flex', flexDirection: 'column' }}>
|
||||||
className="w-full flex items-center gap-2 px-2 py-1 bg-gray-100 border-b border-gray-200 text-gray-700 font-mono hover:bg-gray-200 focus:outline-none"
|
<button
|
||||||
onClick={() => setBrowserOpen(o => !o)}
|
className={`w-full flex items-center justify-center px-2 py-1 font-mono hover:bg-gray-200 focus:outline-none ${
|
||||||
style={{ fontFamily: 'JetBrains Mono, monospace' }}
|
browserOpen
|
||||||
>
|
? 'bg-gray-100 border-b border-gray-200 text-gray-700'
|
||||||
<span className="text-lg">{browserOpen ? '▼' : '▶'}</span>
|
: 'bg-gray-200 border-b border-gray-300 text-gray-600 hover:bg-gray-300'
|
||||||
<span className="font-bold text-sm">Files</span>
|
}`}
|
||||||
</button>
|
onClick={() => setBrowserOpen(o => !o)}
|
||||||
|
style={{ fontFamily: 'JetBrains Mono, monospace', height: 40 }}
|
||||||
|
title={browserOpen ? "Datei-Explorer ausblenden" : "Datei-Explorer anzeigen"}
|
||||||
|
>
|
||||||
|
<span className="text-lg" style={{ transform: browserOpen ? 'rotate(0deg)' : 'rotate(90deg)' }}>
|
||||||
|
▶
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* File browser content */}
|
||||||
{browserOpen && (
|
{browserOpen && (
|
||||||
<div className="h-48 border-b border-gray-200 p-2 overflow-auto bg-gray-50 text-gray-800 font-mono">
|
<div className="border-r border-gray-200 bg-gray-50 text-gray-800 font-mono overflow-auto" style={{ width: fileBrowserWidth, minWidth: fileBrowserWidth, maxWidth: fileBrowserWidth }}>
|
||||||
{tree.length === 0 ? (
|
<div className="h-64 p-2">
|
||||||
<div className="text-xs text-gray-400">No files found.</div>
|
{tree.length === 0 ? (
|
||||||
) : (
|
<div className="text-xs text-gray-400">Keine Datein gefunden.</div>
|
||||||
<FileTree nodes={tree} onSelect={setSelectedSlug} selectedSlug={selectedSlug} />
|
) : (
|
||||||
)}
|
<FileTree nodes={tree} onSelect={setSelectedSlug} selectedSlug={selectedSlug} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Monaco Editor */}
|
{/* Monaco Editor */}
|
||||||
@@ -301,7 +496,7 @@ export default function EditorPage() {
|
|||||||
fontFamily: 'JetBrains Mono',
|
fontFamily: 'JetBrains Mono',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: true },
|
||||||
wordWrap: "on",
|
wordWrap: "on",
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
smoothScrolling: true,
|
smoothScrolling: true,
|
||||||
@@ -314,6 +509,16 @@ export default function EditorPage() {
|
|||||||
cursorStyle: "line",
|
cursorStyle: "line",
|
||||||
fixedOverflowWidgets: true,
|
fixedOverflowWidgets: true,
|
||||||
readOnly: loading,
|
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 ?? "")}
|
onChange={v => setFileContent(v ?? "")}
|
||||||
onMount={handleEditorDidMount}
|
onMount={handleEditorDidMount}
|
||||||
@@ -324,11 +529,18 @@ export default function EditorPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Draggable Splitter */}
|
{/* Draggable Splitter - always show */}
|
||||||
<div
|
<div
|
||||||
className="w-2 cursor-col-resize bg-gray-200 hover:bg-gray-300 transition-colors"
|
className={`w-1 cursor-col-resize transition-colors relative ${
|
||||||
|
isDragging ? 'bg-blue-500' : 'bg-gray-300 hover:bg-gray-400'
|
||||||
|
}`}
|
||||||
onMouseDown={onDragStart}
|
onMouseDown={onDragStart}
|
||||||
/>
|
>
|
||||||
|
{/* Drag handle indicator */}
|
||||||
|
<div className="absolute inset-y-0 left-1/2 transform -translate-x-1/2 w-4 flex items-center justify-center">
|
||||||
|
<div className="w-1 h-8 bg-gray-400 rounded-full opacity-50"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/* Right: Live Preview */}
|
{/* Right: Live Preview */}
|
||||||
<div className="flex-1 bg-gray-50 p-8 overflow-auto border-l border-gray-200">
|
<div className="flex-1 bg-gray-50 p-8 overflow-auto border-l border-gray-200">
|
||||||
<article className="bg-white rounded-lg shadow-sm border p-6 sm:p-8">
|
<article className="bg-white rounded-lg shadow-sm border p-6 sm:p-8">
|
||||||
@@ -345,6 +557,45 @@ export default function EditorPage() {
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Unsaved Changes Dialog */}
|
||||||
|
{showUnsavedDialog && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl p-6 max-w-md w-full mx-4">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Ungespeicherte Änderungen</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
Sie haben ungespeicherte Änderungen. Möchten Sie diese speichern, bevor Sie fortfahren?
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-3 justify-end">
|
||||||
|
<button
|
||||||
|
onClick={() => handleUnsavedDialogAction('cancel')}
|
||||||
|
className="px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleUnsavedDialogAction('discard')}
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors"
|
||||||
|
>
|
||||||
|
Verwerfen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleUnsavedDialogAction('save')}
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -363,7 +614,7 @@ class MonacoErrorBoundary extends React.Component<{children: React.ReactNode}, {
|
|||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
return <div className="text-red-600 p-4">Editor error: {this.state.error.message}</div>;
|
return <div className="text-red-600 p-4">Fehler: {this.state.error.message}</div>;
|
||||||
}
|
}
|
||||||
return this.props.children;
|
return this.props.children;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(() => {
|
useEffect(() => {
|
||||||
fetchStats();
|
fetchStats();
|
||||||
fetchHealth();
|
fetchHealth();
|
||||||
@@ -358,6 +371,13 @@ export default function RustStatusPage() {
|
|||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-3">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-3">
|
||||||
<h2 className="text-base font-semibold">Parser Logs</h2>
|
<h2 className="text-base font-semibold">Parser Logs</h2>
|
||||||
<div className="flex flex-col sm:flex-row gap-2">
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
|
<button
|
||||||
|
onClick={reinterpretAllPosts}
|
||||||
|
className="px-2.5 py-1.5 bg-orange-500 hover:bg-orange-600 text-white rounded text-xs transition-colors"
|
||||||
|
title="Force reinterpret all posts"
|
||||||
|
>
|
||||||
|
Reinterpret All
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={clearLogs}
|
onClick={clearLogs}
|
||||||
className="px-2.5 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded text-xs transition-colors"
|
className="px-2.5 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded text-xs transition-colors"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const dynamic = "force-dynamic";
|
|||||||
*
|
*
|
||||||
* If any Issues about "Window" (For Monaco) pop up. Its not my fucking fault
|
* If any Issues about "Window" (For Monaco) pop up. Its not my fucking fault
|
||||||
*
|
*
|
||||||
* Push later when on local Network. (//5jul25)
|
* Push later when on local Network. (//5jul25) ## Already done
|
||||||
**********************************************/
|
**********************************************/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
@@ -904,7 +904,7 @@ export default function AdminPage() {
|
|||||||
<a
|
<a
|
||||||
href="/admin/editor"
|
href="/admin/editor"
|
||||||
className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-gradient-to-r from-gray-700 to-blue-700 text-white rounded-xl shadow-lg flex items-center justify-center gap-2 text-sm sm:text-base font-semibold hover:from-gray-800 hover:to-blue-800 transition-all focus:outline-none focus:ring-2 focus:ring-blue-400"
|
className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-gradient-to-r from-gray-700 to-blue-700 text-white rounded-xl shadow-lg flex items-center justify-center gap-2 text-sm sm:text-base font-semibold hover:from-gray-800 hover:to-blue-800 transition-all focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||||
title="Markdown Editor (VS Code Style)"
|
title="Markdown Bearbeiter"
|
||||||
style={{ minWidth: '160px' }}
|
style={{ minWidth: '160px' }}
|
||||||
>
|
>
|
||||||
{/* VS Code SVG Icon */}
|
{/* VS Code SVG Icon */}
|
||||||
@@ -950,8 +950,8 @@ export default function AdminPage() {
|
|||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="flex flex-col items-start">
|
<span className="flex flex-col items-start">
|
||||||
<span>Editor</span>
|
<span>Markdown Editor</span>
|
||||||
<span className="text-xs font-normal text-blue-100">VS Code</span>
|
<span className="text-xs font-normal text-blue-100">Visual Studio Code</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
{rememberExportChoice && lastExportChoice && (
|
{rememberExportChoice && lastExportChoice && (
|
||||||
|
|||||||
@@ -110,6 +110,46 @@ export async function GET(request: Request) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const reinterpretAll = searchParams.get('reinterpretAll');
|
||||||
|
if (reinterpretAll === '1') {
|
||||||
|
// Call the Rust backend to force reinterpret all posts
|
||||||
|
const rustResult = spawnSync(
|
||||||
|
process.cwd() + '/markdown_backend/target/release/markdown_backend',
|
||||||
|
['reinterpret-all'],
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
if (rustResult.status === 0 && rustResult.stdout) {
|
||||||
|
return new Response(rustResult.stdout, {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new Response(JSON.stringify({ error: rustResult.stderr || rustResult.error }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const reparsePost = searchParams.get('reparsePost');
|
||||||
|
if (reparsePost) {
|
||||||
|
// Call the Rust backend to reparse a specific post
|
||||||
|
const rustResult = spawnSync(
|
||||||
|
process.cwd() + '/markdown_backend/target/release/markdown_backend',
|
||||||
|
['reparse-post', reparsePost],
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
if (rustResult.status === 0 && rustResult.stdout) {
|
||||||
|
return new Response(rustResult.stdout, {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new Response(JSON.stringify({ error: rustResult.stderr || rustResult.error }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
// Return the current pinned.json object
|
// Return the current pinned.json object
|
||||||
try {
|
try {
|
||||||
const pinnedPath = path.join(process.cwd(), 'posts', 'pinned.json');
|
const pinnedPath = path.join(process.cwd(), 'posts', 'pinned.json');
|
||||||
|
|||||||
Reference in New Issue
Block a user