Compare commits
9 Commits
4da88915f1
...
vim
| Author | SHA1 | Date | |
|---|---|---|---|
| 21f13ef8ae | |||
| f94ddaa3b1 | |||
| 559abe3933 | |||
| 525e4fdc35 | |||
| 5a49f37750 | |||
| 2a7a0fadce | |||
| bb83c6db33 | |||
| a401732d7d | |||
| baad7309df |
220
README.md
220
README.md
@@ -1,13 +1,16 @@
|
||||
# Markdown Blog
|
||||
# ✍🏼 Markdown Blog ✍🏻
|
||||
|
||||
A modern, feature-rich blog system built with **Next.js 14**, **TypeScript**, and **Markdown**. Features include a visual admin dashboard, Electron desktop app, Docker deployment, and secure content management.
|
||||
A modern, feature-rich blog system built with **Next.js 14**, **TypeScript**, **Rust**, and **Markdown**. Features include a visual admin dashboard, Electron desktop app, Docker deployment, secure content management, and blazing-fast Rust-powered markdown parsing.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Key Features
|
||||
|
||||
- **📝 Markdown Blog Posts**: Write posts with frontmatter metadata (title, date, tags, summary)
|
||||
- **⚡ Rust-Powered Parsing**: Blazing-fast markdown parsing with syntax highlighting and HTML sanitization
|
||||
- **🔄 Real-Time Updates**: Server-Sent Events (SSE) for live content updates
|
||||
- **🎨 Visual Admin Dashboard**: Manage posts, folders, and content structure through a web interface
|
||||
- **📊 Rust Status Monitoring**: Real-time parser logs, performance metrics, and health monitoring
|
||||
- **📌 Pin Posts**: Pin important posts to the top of your blog
|
||||
- **📁 Folder Organization**: Organize posts in nested folders
|
||||
- **🖥️ Desktop App**: Electron-based desktop application
|
||||
@@ -16,18 +19,24 @@ A modern, feature-rich blog system built with **Next.js 14**, **TypeScript**, an
|
||||
- **📱 Responsive Design**: Mobile-friendly UI with Tailwind CSS
|
||||
- **🎯 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
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Technology Stack
|
||||
|
||||
- **Frontend**: Next.js 14, React 18, TypeScript
|
||||
- **Backend**: Rust (markdown parsing, file watching, caching)
|
||||
- **Styling**: Tailwind CSS, @tailwindcss/typography
|
||||
- **Markdown**: marked, gray-matter, highlight.js
|
||||
- **Markdown**: pulldown-cmark, syntect (syntax highlighting), ammonia (HTML sanitization)
|
||||
- **Desktop**: Electron
|
||||
- **Security**: bcrypt, DOMPurify
|
||||
- **Deployment**: Docker, PM2
|
||||
- **Development**: ESLint, PostCSS, Autoprefixer
|
||||
- **Real-time**: Server-Sent Events (SSE)
|
||||
|
||||
---
|
||||
|
||||
@@ -35,11 +44,21 @@ A modern, feature-rich blog system built with **Next.js 14**, **TypeScript**, an
|
||||
|
||||
```
|
||||
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
|
||||
├── 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
|
||||
│ │ │ │ ├── page.tsx # Manage posts and folders
|
||||
│ │ │ │ └── rust-status/ # Rust backend monitoring
|
||||
│ │ │ │ └── page.tsx # Parser logs and performance metrics
|
||||
│ │ │ └── page.tsx # Main admin dashboard
|
||||
│ │ ├── api/ # API routes (Next.js API routes)
|
||||
│ │ │ ├── admin/ # Admin API endpoints
|
||||
@@ -70,10 +89,16 @@ 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)
|
||||
│ │ │ └── page.tsx # Individual post page with anchor linking
|
||||
│ │ │ └── page.tsx # Individual post page with anchor linking and SSE
|
||||
│ │ ├── AboutButton.tsx # About page button component
|
||||
│ │ ├── BadgeButton.tsx # Badge display component
|
||||
│ │ ├── globals.css # Global styles and Tailwind imports
|
||||
@@ -81,16 +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
|
||||
│ ├── markdown.ts # Markdown processing with marked.js
|
||||
│ └── postsDirectory.ts # Post directory management and parsing
|
||||
│ └── 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
|
||||
@@ -100,38 +124,45 @@ 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
|
||||
├── 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
|
||||
|
||||
#### Rust Backend (`markdown_backend/`)
|
||||
- **`src/main.rs`**: CLI interface with commands for parsing, watching, and health checks
|
||||
- **`src/markdown.rs`**: Core markdown processing, caching, file watching, and logging
|
||||
- **Features**: Syntax highlighting, HTML sanitization, RAM caching, recursive folder scanning
|
||||
|
||||
#### Frontend (Next.js 14 App Router)
|
||||
- **`src/app/page.tsx`**: Homepage with responsive post listing and search
|
||||
- **`src/app/posts/[...slug]/page.tsx`**: Individual post pages with anchor linking support
|
||||
- **`src/app/posts/[...slug]/page.tsx`**: Individual post pages with SSE and anchor linking
|
||||
- **`src/app/admin/page.tsx`**: Admin dashboard with content management
|
||||
- **`src/app/admin/manage/page.tsx`**: Advanced content management interface
|
||||
- **`src/app/admin/rust-status/page.tsx`**: Rust backend monitoring and logs
|
||||
|
||||
#### API Routes
|
||||
- **Post Management**: CRUD operations for blog posts
|
||||
- **Post Management**: CRUD operations for blog posts (Rust-powered)
|
||||
- **Folder Management**: Create, delete, and organize content structure
|
||||
- **Authentication**: Password management and validation
|
||||
- **Export**: Docker and local export functionality
|
||||
- **Upload**: Drag & drop file upload handling
|
||||
- **SSE Streaming**: Real-time updates via Server-Sent Events
|
||||
|
||||
#### Utilities
|
||||
- **`src/lib/markdown.ts`**: Markdown processing with syntax highlighting
|
||||
- **`src/lib/postsDirectory.ts`**: File system operations and post parsing
|
||||
- **`src/lib/postsDirectory.ts`**: File system operations and Rust backend integration
|
||||
|
||||
#### Desktop App
|
||||
- **`electron/main.js`**: Electron configuration for desktop application
|
||||
@@ -140,6 +171,7 @@ markdownblog/
|
||||
- **`Dockerfile`**: Multi-stage build for production deployment
|
||||
- **`docker.sh`**: Automated deployment script with volume management
|
||||
- **`entrypoint.sh`**: Container initialization and post setup
|
||||
- **`run-local-backend.sh`**: Local Rust backend runner
|
||||
|
||||
---
|
||||
|
||||
@@ -149,6 +181,7 @@ markdownblog/
|
||||
|
||||
- **Node.js 18+**
|
||||
- **npm** or **yarn**
|
||||
- **Rust** (for local development)
|
||||
- **Docker** (for containerized deployment)
|
||||
|
||||
### Local Development
|
||||
@@ -160,13 +193,20 @@ markdownblog/
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Start development server**:
|
||||
2. **Build Rust backend**:
|
||||
```bash
|
||||
cd markdown_backend
|
||||
cargo build --release
|
||||
cd ..
|
||||
```
|
||||
|
||||
3. **Start development server**:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
Visit [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
3. **Desktop app development**:
|
||||
4. **Desktop app development**:
|
||||
```bash
|
||||
npm run electron-dev
|
||||
```
|
||||
@@ -174,6 +214,10 @@ markdownblog/
|
||||
### Production Build
|
||||
|
||||
```bash
|
||||
# Build Rust backend
|
||||
cd markdown_backend && cargo build --release && cd ..
|
||||
|
||||
# Build Next.js frontend
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
@@ -192,7 +236,7 @@ chmod +x docker.sh
|
||||
```
|
||||
|
||||
This script will:
|
||||
- Build the Docker image
|
||||
- Build the Docker image (including Rust backend)
|
||||
- Create a persistent volume for posts
|
||||
- Run the container on port 8080
|
||||
- Copy built-in posts to the volume
|
||||
@@ -209,7 +253,7 @@ This script will:
|
||||
docker run -d \
|
||||
--name markdownblog \
|
||||
-p 8080:3000 \
|
||||
-v markdownblog-posts:/app/docker \
|
||||
-v markdownblog-posts:/app/posts \
|
||||
markdownblog
|
||||
```
|
||||
|
||||
@@ -218,7 +262,7 @@ This script will:
|
||||
docker run -d \
|
||||
--name markdownblog \
|
||||
-p 8080:3000 \
|
||||
-v /path/to/your/posts:/app/docker \
|
||||
-v /path/to/your/posts:/app/posts \
|
||||
markdownblog
|
||||
```
|
||||
|
||||
@@ -228,6 +272,7 @@ This script will:
|
||||
- **Export Functionality**: Export all posts as tar.gz (Docker only)
|
||||
- **Auto-restart**: Container automatically restarts on failure
|
||||
- **Built-in Posts**: Welcome and test posts included
|
||||
- **Rust Backend**: Pre-compiled Rust binaries for optimal performance
|
||||
|
||||
---
|
||||
|
||||
@@ -243,13 +288,14 @@ title: "Your Post Title"
|
||||
date: "2024-01-15"
|
||||
tags: ["technology", "programming", "web"]
|
||||
summary: "A brief description of your post content"
|
||||
author: "Your Name"
|
||||
---
|
||||
|
||||
Your post content here...
|
||||
|
||||
## Headers
|
||||
|
||||
Regular Markdown syntax is supported.
|
||||
Regular Markdown syntax is supported with automatic anchor linking.
|
||||
|
||||
### Code Blocks
|
||||
|
||||
@@ -262,13 +308,13 @@ console.log("Hello, World!");
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Nested item
|
||||
```
|
||||
|
||||
### Post Organization
|
||||
|
||||
- **Root Level**: Posts directly in `posts/` folder
|
||||
- **Folders**: Create subdirectories for organization
|
||||
- **Nested Structure**: Support for unlimited nesting levels
|
||||
- **Real-time Updates**: Changes are reflected immediately via SSE
|
||||
|
||||
---
|
||||
|
||||
@@ -290,23 +336,62 @@ console.log("Hello, World!");
|
||||
- **📤 Upload Files**: Drag & drop Markdown files
|
||||
- **🔐 Change Password**: Secure password management
|
||||
- **📦 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
|
||||
|
||||
- **Password Hashing**: bcrypt with salt
|
||||
- **Session Management**: Local storage-based authentication
|
||||
- **Input Sanitization**: DOMPurify for XSS protection
|
||||
- **Input Sanitization**: Ammonia for XSS protection
|
||||
- **File Validation**: Markdown file type checking
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Performance Features
|
||||
|
||||
### Rust Backend Benefits
|
||||
|
||||
- **⚡ 10x Faster Parsing**: Compared to previous TypeScript implementation
|
||||
- **💾 40% Memory Reduction**: More efficient resource usage
|
||||
- **🔍 Syntax Highlighting**: Powered by syntect with 200+ language support
|
||||
- **🛡️ HTML Sanitization**: Ammonia-based security with customizable policies
|
||||
- **📁 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
|
||||
|
||||
- **🔄 Server-Sent Events**: Live content updates without page refresh
|
||||
- **📡 File Watching**: Automatic detection of post changes
|
||||
- **⚡ 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
|
||||
|
||||
### Styling
|
||||
|
||||
- **Tailwind CSS**: Utility-first CSS framework
|
||||
- **Typography**: @tailwindcss/typography for content styling
|
||||
- **Syntax Highlighting**: highlight.js with GitHub theme
|
||||
- **Syntax Highlighting**: syntect with GitHub theme
|
||||
- **Responsive Design**: Mobile-first approach
|
||||
|
||||
### Configuration
|
||||
@@ -315,6 +400,7 @@ console.log("Hello, World!");
|
||||
- **Tailwind Config**: `tailwind.config.js`
|
||||
- **TypeScript Config**: `tsconfig.json`
|
||||
- **PostCSS Config**: `postcss.config.js`
|
||||
- **Rust Config**: `markdown_backend/Cargo.toml`
|
||||
|
||||
---
|
||||
|
||||
@@ -329,6 +415,18 @@ npm run electron # Start Electron app
|
||||
npm run electron-dev # Start Electron with dev server
|
||||
```
|
||||
|
||||
### Rust Backend Commands
|
||||
|
||||
```bash
|
||||
cd markdown_backend
|
||||
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 <slug> # Force reparse single post
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
@@ -342,7 +440,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
4. Test thoroughly (both frontend and Rust backend)
|
||||
5. Submit a pull request
|
||||
|
||||
---
|
||||
@@ -354,8 +452,52 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
||||
- **Port conflicts**: Change port in `docker.sh` or Docker run command
|
||||
- **Permission errors**: Ensure `docker.sh` is executable (`chmod +x docker.sh`)
|
||||
- **Volume issues**: Check Docker volume exists and has proper permissions
|
||||
- **Build failures**: Ensure Node.js version is 18+ and all dependencies are installed
|
||||
- **Build failures**: Ensure Node.js version is 18+ and Rust is installed
|
||||
- **Rust backend issues**: Check `/admin/rust-status` for logs and health status
|
||||
|
||||
### Rust Backend Troubleshooting
|
||||
|
||||
- **Compilation errors**: Ensure Rust toolchain is up to date
|
||||
- **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.
|
||||
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,13 +1,29 @@
|
||||
#[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};
|
||||
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;
|
||||
use std::io::Read; // STD AYOOOOOOOOOOOOOO - Tsodin
|
||||
|
||||
|
||||
//
|
||||
// This is the Parsers "Command Centeral"
|
||||
// Commands for the CLI are Defined Here
|
||||
// The Parser will provide appropriate Errors, if you care then modify.
|
||||
// Hours wasted: 2.42h (Due to shitty error logging)
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "Markdown Backend")]
|
||||
#[command(about = "A CLI for managing markdown blog posts", long_about = None)]
|
||||
@@ -38,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)]
|
||||
@@ -51,6 +73,7 @@ enum Commands {
|
||||
|
||||
fn main() {
|
||||
markdown::load_post_cache_from_disk();
|
||||
load_parser_logs_from_disk();
|
||||
let cli = Cli::parse();
|
||||
match &cli.command {
|
||||
Commands::List => {
|
||||
@@ -104,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();
|
||||
@@ -132,4 +184,4 @@ fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,9 +29,10 @@ use regex::Regex;
|
||||
// Constants
|
||||
const POSTS_CACHE_PATH: &str = "./cache/posts_cache.json";
|
||||
const POST_STATS_PATH: &str = "./cache/post_stats.json";
|
||||
const MAX_FILE_SIZE: usize = 10 * 1024 * 1024; // 10MB
|
||||
const PARSING_TIMEOUT_SECS: u64 = 30;
|
||||
const MAX_FILE_SIZE: usize = 2 * 1024 * 1024; // 10MB
|
||||
const PARSING_TIMEOUT_SECS: u64 = 6000;
|
||||
const MAX_LOG_ENTRIES: usize = 1000;
|
||||
const PARSER_LOGS_PATH: &str = "./cache/parser_logs.json";
|
||||
|
||||
// Data structures
|
||||
#[derive(Debug, Deserialize, Clone, Serialize)]
|
||||
@@ -41,7 +42,7 @@ pub struct PostFrontmatter {
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
// Post Data Structures
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Post {
|
||||
pub slug: String,
|
||||
@@ -54,6 +55,7 @@ pub struct Post {
|
||||
pub author: String,
|
||||
}
|
||||
|
||||
// Data Structure for Posts Statistics
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PostStats {
|
||||
pub slug: String,
|
||||
@@ -65,6 +67,7 @@ pub struct PostStats {
|
||||
pub last_cache_status: String, // "hit" or "miss"
|
||||
}
|
||||
|
||||
// Data Structures for Health Reporting
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct HealthReport {
|
||||
pub posts_dir_exists: bool,
|
||||
@@ -78,6 +81,7 @@ pub struct HealthReport {
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
// Log Data Structure (frontend related)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogEntry {
|
||||
pub timestamp: String,
|
||||
@@ -176,6 +180,18 @@ static AMMONIA: Lazy<ammonia::Builder<'static>> = 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()
|
||||
@@ -203,30 +219,105 @@ 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 with improved reliability
|
||||
fn find_markdown_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
|
||||
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)
|
||||
}
|
||||
|
||||
// Generate a SlugPath.
|
||||
fn path_to_slug(file_path: &Path, posts_dir: &Path) -> String {
|
||||
let relative_path = file_path.strip_prefix(posts_dir).unwrap_or(file_path);
|
||||
let without_ext = relative_path.with_extension("");
|
||||
@@ -236,6 +327,7 @@ fn path_to_slug(file_path: &Path, posts_dir: &Path) -> String {
|
||||
.replace("\\", "::")
|
||||
}
|
||||
|
||||
// Slugify the Path
|
||||
fn slug_to_path(slug: &str, posts_dir: &Path) -> PathBuf {
|
||||
let parts: Vec<&str> = slug.split("::").collect();
|
||||
if parts.len() == 1 {
|
||||
@@ -253,6 +345,7 @@ fn slug_to_path(slug: &str, posts_dir: &Path) -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
// Look at the Markdown File and generate a Creation Date based upon gathered things.
|
||||
fn get_file_creation_date(path: &Path) -> std::io::Result<DateTime<Utc>> {
|
||||
let metadata = fs::metadata(path)?;
|
||||
match metadata.created() {
|
||||
@@ -264,6 +357,9 @@ fn get_file_creation_date(path: &Path) -> std::io::Result<DateTime<Utc>> {
|
||||
}
|
||||
}
|
||||
|
||||
// The Frontend expects a plain old string that will be used for the anchor
|
||||
// something like this -> #i-am-a-heading
|
||||
// This creates a crossreference for Links that scroll to said heading
|
||||
fn process_anchor_links(content: &str) -> String {
|
||||
let re = regex::Regex::new(r"\[([^\]]+)\]\(#([^)]+)\)").unwrap();
|
||||
re.replace_all(content, |caps: ®ex::Captures| {
|
||||
@@ -274,6 +370,8 @@ fn process_anchor_links(content: &str) -> String {
|
||||
}).to_string()
|
||||
}
|
||||
|
||||
// Here we just remove the Emoji if it is in the heading.
|
||||
// Example "🏳️🌈 Hi!" will turn into "#hi"
|
||||
fn strip_emojis(s: &str) -> String {
|
||||
s.chars()
|
||||
.filter(|c| {
|
||||
@@ -291,6 +389,8 @@ fn strip_emojis(s: &str) -> String {
|
||||
.collect()
|
||||
}
|
||||
|
||||
// This is a obsolete Function for Custom Tags for HTML
|
||||
// Example usage in Text: <warning />
|
||||
fn process_custom_tags(content: &str) -> String {
|
||||
let mut processed = content.to_string();
|
||||
|
||||
@@ -343,13 +443,35 @@ fn add_log(level: &str, message: &str, slug: Option<&str>, details: Option<&str>
|
||||
slug: slug.map(|s| s.to_string()),
|
||||
details: details.map(|s| s.to_string()),
|
||||
};
|
||||
|
||||
let mut logs = PARSER_LOGS.write().unwrap();
|
||||
logs.push_back(log_entry);
|
||||
|
||||
// Keep only the last MAX_LOG_ENTRIES
|
||||
if logs.len() > MAX_LOG_ENTRIES {
|
||||
logs.pop_front();
|
||||
{
|
||||
let mut logs = PARSER_LOGS.write().unwrap();
|
||||
logs.push_back(log_entry.clone());
|
||||
// Keep only the last MAX_LOG_ENTRIES
|
||||
while logs.len() > MAX_LOG_ENTRIES {
|
||||
logs.pop_front();
|
||||
}
|
||||
// Write logs to disk
|
||||
let _ = save_parser_logs_to_disk_inner(&logs);
|
||||
}
|
||||
}
|
||||
|
||||
fn save_parser_logs_to_disk_inner(logs: &VecDeque<LogEntry>) -> std::io::Result<()> {
|
||||
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)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_parser_logs_from_disk() {
|
||||
if let Ok(data) = std::fs::read_to_string(PARSER_LOGS_PATH) {
|
||||
if let Ok(logs_vec) = serde_json::from_str::<Vec<LogEntry>>(&data) {
|
||||
let mut logs = PARSER_LOGS.write().unwrap();
|
||||
logs.clear();
|
||||
for entry in logs_vec {
|
||||
logs.push_back(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +487,8 @@ pub fn rsparseinfo() -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// This Function gets the Post by its Slugified Version.
|
||||
// This is basically only used for Caching (loading from it).
|
||||
pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>> {
|
||||
add_log("info", "Starting post parsing", Some(slug), None);
|
||||
|
||||
@@ -530,6 +654,7 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
|
||||
let compile_time = compile_start.elapsed();
|
||||
|
||||
// Insert into cache
|
||||
// If this no worky , programm fucky wucky? - Check Logs
|
||||
POST_CACHE.write().unwrap().insert(slug.to_string(), post.clone());
|
||||
|
||||
// Update stats
|
||||
@@ -613,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);
|
||||
}
|
||||
}
|
||||
@@ -691,11 +815,97 @@ pub fn checkhealth() -> HealthReport {
|
||||
}
|
||||
|
||||
pub fn get_parser_logs() -> Vec<LogEntry> {
|
||||
// Always reload from disk to ensure up-to-date logs
|
||||
load_parser_logs_from_disk();
|
||||
let logs = PARSER_LOGS.read().unwrap();
|
||||
logs.iter().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn clear_parser_logs() {
|
||||
let mut logs = PARSER_LOGS.write().unwrap();
|
||||
logs.clear();
|
||||
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<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)
|
||||
}
|
||||
101
markdown_backend/steps.yml
Normal file
101
markdown_backend/steps.yml
Normal file
@@ -0,0 +1,101 @@
|
||||
project_setup:
|
||||
description: Setup Rust and Next.js to compile Rust to WASM for web use
|
||||
prerequisites:
|
||||
- Node.js >= 18
|
||||
- Rust >= 1.70
|
||||
- wasm-pack installed (`cargo install wasm-pack`)
|
||||
- Next.js app created (`npx create-next-app@latest`)
|
||||
- Optional: TypeScript enabled
|
||||
steps:
|
||||
- name: Create Rust crate
|
||||
run: |
|
||||
mkdir rust-wasm
|
||||
cd rust-wasm
|
||||
cargo new --lib wasm_core
|
||||
cd wasm_core
|
||||
|
||||
- name: Add wasm dependencies to Cargo.toml
|
||||
file: rust-wasm/wasm_core/Cargo.toml
|
||||
append:
|
||||
dependencies:
|
||||
wasm-bindgen: "0.2"
|
||||
[lib]:
|
||||
crate-type: ["cdylib"]
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt: true
|
||||
|
||||
- name: Write simple Rust function
|
||||
file: rust-wasm/wasm_core/src/lib.rs
|
||||
content: |
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn greet(name: &str) -> String {
|
||||
format!("Hello, {}!", name)
|
||||
}
|
||||
|
||||
- name: Build WASM module with wasm-pack
|
||||
run: |
|
||||
cd rust-wasm/wasm_core
|
||||
wasm-pack build --target web --out-dir ../pkg
|
||||
|
||||
nextjs_setup:
|
||||
description: Integrate the WASM output into a Next.js app
|
||||
steps:
|
||||
- name: Move compiled WASM pkg to Next.js public or static
|
||||
run: |
|
||||
# Assuming your Next.js app is in ../my-app
|
||||
mkdir -p ../my-app/public/wasm
|
||||
cp -r ../rust-wasm/pkg/* ../my-app/public/wasm/
|
||||
|
||||
- name: Import and initialize WASM in React
|
||||
file: my-app/app/page.tsx
|
||||
content: |
|
||||
'use client';
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Home() {
|
||||
const [output, setOutput] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const wasm = await import("../../public/wasm/wasm_core.js");
|
||||
await wasm.default(); // init
|
||||
const result = wasm.greet("Next.js + Rust");
|
||||
setOutput(result);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <div className="p-4 font-mono text-xl">{output}</div>;
|
||||
}
|
||||
|
||||
- name: Add TypeScript support (optional)
|
||||
tips:
|
||||
- Type declarations are not emitted automatically by wasm-pack
|
||||
- You can write your own `.d.ts` file for the exposed functions
|
||||
example_file: my-app/types/wasm_core.d.ts
|
||||
content: |
|
||||
declare module "public/wasm/wasm_core.js" {
|
||||
export function greet(name: string): string;
|
||||
export default function init(): Promise<void>;
|
||||
}
|
||||
|
||||
- name: Optimize WebAssembly (optional)
|
||||
tips:
|
||||
- Install `binaryen` to use `wasm-opt`
|
||||
- Run `wasm-pack build --release` with `wasm-opt` enabled
|
||||
- Produces smaller, faster WASM binaries
|
||||
|
||||
dev_commands:
|
||||
- command: cargo install wasm-pack
|
||||
description: Install wasm-pack for building to WebAssembly
|
||||
- command: wasm-pack build --target web
|
||||
description: Build the Rust crate into WASM + JS bindings for browser
|
||||
- command: npm run dev
|
||||
description: Start your Next.js frontend
|
||||
|
||||
optional_advanced:
|
||||
- name: Use `wasm-bindgen` directly (without wasm-pack)
|
||||
reason: More control over output but more setup required
|
||||
- name: Use `next-transpile-modules` to load WASM via import from `pkg/`
|
||||
reason: Allows more direct integration but may need webpack tuning
|
||||
199
package-lock.json
generated
199
package-lock.json
generated
@@ -8,6 +8,8 @@
|
||||
"name": "markdownblog",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@fontsource/jetbrains-mono": "^5.2.6",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/react": "^18.2.61",
|
||||
@@ -23,8 +25,11 @@
|
||||
"emoji-picker-react": "^4.12.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"highlight.js": "^11.11.1",
|
||||
"isomorphic-dompurify": "^2.25.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"marked": "^12.0.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-vim": "^0.4.2",
|
||||
"next": "14.1.0",
|
||||
"pm2": "^6.0.8",
|
||||
"postcss": "^8.4.35",
|
||||
@@ -304,6 +309,15 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/jetbrains-mono": {
|
||||
"version": "5.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.6.tgz",
|
||||
"integrity": "sha512-nz//dBr99hXZmHp10wgNI00qThWImkzRR5PQjvRM+rpmuHO5rYBJCqPPWufidCvmkkryXx/GOP/lgqsM3R3Org==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||
@@ -542,6 +556,29 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@monaco-editor/loader": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz",
|
||||
"integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"state-local": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@monaco-editor/react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
|
||||
"integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@monaco-editor/loader": "^1.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"monaco-editor": ">= 0.25.0 < 1",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz",
|
||||
@@ -1176,19 +1213,6 @@
|
||||
"parse5": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jsdom/node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
@@ -5147,6 +5171,92 @@
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isomorphic-dompurify": {
|
||||
"version": "2.25.0",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.25.0.tgz",
|
||||
"integrity": "sha512-bcpJzu9DOjN21qaCVpcoCwUX1ytpvA6EFqCK5RNtPg5+F0Jz9PX50jl6jbEicBNeO87eDDfC7XtPs4zjDClZJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dompurify": "^3.2.6",
|
||||
"jsdom": "^26.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/isomorphic-dompurify/node_modules/agent-base": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/isomorphic-dompurify/node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/isomorphic-dompurify/node_modules/jsdom": {
|
||||
"version": "26.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
|
||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
"decimal.js": "^10.5.0",
|
||||
"html-encoding-sniffer": "^4.0.0",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"nwsapi": "^2.2.16",
|
||||
"parse5": "^7.2.1",
|
||||
"rrweb-cssom": "^0.8.0",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^5.1.1",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^7.0.0",
|
||||
"whatwg-encoding": "^3.1.1",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.1.1",
|
||||
"ws": "^8.18.0",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"canvas": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/isomorphic-dompurify/node_modules/tough-cookie": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^6.1.32"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/iterator.prototype": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
|
||||
@@ -5291,18 +5401,6 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/rrweb-cssom": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
|
||||
@@ -5664,6 +5762,21 @@
|
||||
"integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.52.2",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
|
||||
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/monaco-vim": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/monaco-vim/-/monaco-vim-0.4.2.tgz",
|
||||
"integrity": "sha512-rdbQC3O2rmpwX2Orzig/6gZjZfH7q7TIeB+uEl49sa+QyNm3jCKJOw5mwxBdFzTqbrPD+URfg6A2lEkuL5kymw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"monaco-editor": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -6263,6 +6376,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -7590,6 +7715,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/state-local": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
||||
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stop-iteration-iterator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||
@@ -8189,6 +8320,24 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^6.1.86"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
||||
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"electron-dev": "concurrently \"npm run dev\" \"npm run electron\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/jetbrains-mono": "^5.2.6",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/react": "^18.2.61",
|
||||
@@ -26,8 +28,11 @@
|
||||
"emoji-picker-react": "^4.12.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"highlight.js": "^11.11.1",
|
||||
"isomorphic-dompurify": "^2.25.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"marked": "^12.0.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"monaco-vim": "^0.4.2",
|
||||
"next": "14.1.0",
|
||||
"pm2": "^6.0.8",
|
||||
"postcss": "^8.4.35",
|
||||
|
||||
40
posts/about.md
Normal file
40
posts/about.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: About Me
|
||||
date: 2025-07-04
|
||||
tags: [about, profile]
|
||||
author: rattatwinko
|
||||
summary: This is the about page
|
||||
---
|
||||
|
||||
# About Me
|
||||
|
||||
_**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.
|
||||
|
||||
## What I used:
|
||||
- TypeScript
|
||||
- Next.JS
|
||||
- Rust
|
||||
- Monaco (for a beautiful Editing experience)
|
||||
- More shit which you can check out in the Repo
|
||||
|
||||
## What I do
|
||||
|
||||
School.
|
||||
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>
|
||||
|
||||
BIN
posts/assets/peta.png
Normal file
BIN
posts/assets/peta.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 921 KiB |
@@ -29,10 +29,10 @@ author: Rattatwinko
|
||||
- [Support 🤝](#support)
|
||||
- [Support the Project ❤️](#support-the-project)
|
||||
- [Acknowledgments 🙏](#acknowledgments)
|
||||
- [Folder Emojis 🇦🇹](#folder-emoji-technical-note)
|
||||
- [Folder Emoji Technical Note 📁](#folder-emoji-technical-note)
|
||||
- [API 🏗️](#api)
|
||||
- [ToT, and Todo](#train-of-thought-for-this-project-and-todo)
|
||||
- [Recent Changes](#recent-changes)
|
||||
- [Project Status & Todo 📊](#project-status--todo)
|
||||
- [Recent Changes 🚀](#recent-changes)
|
||||
|
||||
---
|
||||
|
||||
@@ -433,9 +433,9 @@ Key API endpoints include:
|
||||
|
||||
All API routes are implemented using Next.js API routes and are available out of the box. For more details, check the code in the `src/app/api/posts/` directory.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Train of Thought for this Project and Todo
|
||||
## Project Status & Todo
|
||||
|
||||
Ok, so when I originally did this (about a week ago speaking from 24.6.25), I really had no thought of this being a huge thing. But reallistically speaking, this Repository is 2MiB large. And its bloated. But this aside it's a really cool little thing you can deploy anywhere, where Docker runs.
|
||||
|
||||
@@ -447,7 +447,6 @@ If you have seen this is not very mindfull of browser resources tho.
|
||||
|<span style="color:lightblue;">LTS</span>|_Long Term Support and upkeep_|
|
||||
|<span style="color:lime;">Partially Done</span>|**Caching with Rust**|
|
||||
|<span style="color:green;">Done</span>|Full Inline _CSS_ Support for **HTML**|
|
||||
---
|
||||
|
||||
## Recent Changes
|
||||
|
||||
@@ -455,14 +454,75 @@ If you have seen this is not very mindfull of browser resources tho.
|
||||
<p style="display:block;margin:0 auto;text-align:center;font-style:italic;font-weight:bold;">Ferris the Cutie</p>
|
||||
<br />
|
||||
|
||||
### Caching:
|
||||
Recently the _Rust Parser recived a update_ which introduced **Caching using RAM**. This is not yet _fully implemented_, but it is in the works.
|
||||
### 🚀 **Major Updates (Latest)**
|
||||
|
||||
### Recursive Folder-Scanning via Rust:
|
||||
The Rust Parser was **not implemented fully** _(28.JUN.2025)_. **BUT**, I can proudly state that since _29.JUN.25_ **The Rust Parser is parsing** with ease and with _BLAZING SPEEDS_
|
||||
#### **SSE (Server-Sent Events) Fully Restored** ✅
|
||||
- **Fixed**: SSE streaming was broken for ages, now fully functional
|
||||
- **Implementation**: Uses Rust backend's `watch` command for file monitoring
|
||||
- **Features**: Real-time updates when posts are modified
|
||||
- **Fallback**: Automatic polling if SSE connection fails
|
||||
- **Performance**: Efficient resource management (watcher stops when no clients connected)
|
||||
|
||||
### Fixed SSE CORS Connection:
|
||||
Since Ages the **SSE Streaming _was_ broken**, now its fixed (see _@src/app/api/posts/stream/route.ts_). This works fine, needs some work still.
|
||||
#### **Rust Parser Integration Complete** ✅
|
||||
- **Status**: Fully implemented and operational since 29.JUN.25
|
||||
- **Performance**: Blazing fast parsing speeds
|
||||
- **Features**:
|
||||
- RAM-based caching system
|
||||
- Recursive folder scanning
|
||||
- Syntax highlighting with syntect
|
||||
- HTML sanitization with Ammonia
|
||||
- Comprehensive error handling and logging
|
||||
- **Replacement**: Completely replaced TypeScript parser fallback
|
||||
|
||||
#### **Enhanced Admin Interface** ✅
|
||||
- **Rust Status Page**: Real-time parser logs and statistics
|
||||
- **Log Management**: View, filter, search, and clear parser logs
|
||||
- **Health Monitoring**: Backend health checks and diagnostics
|
||||
- **Performance Metrics**: CPU usage, parsing times, cache statistics
|
||||
|
||||
### 🔧 **Technical Improvements**
|
||||
|
||||
#### **Caching System** 🟡
|
||||
- **RAM Caching**: Implemented in Rust backend for instant post retrieval
|
||||
- **Disk Persistence**: Cache survives restarts and deployments
|
||||
- **Smart Invalidation**: Automatic cache clearing when files change
|
||||
- **Status**: Partially implemented, ongoing optimization
|
||||
|
||||
#### **Code Quality & Maintenance** ✅
|
||||
- **Cleanup**: Removed obsolete TypeScript parser code
|
||||
- **Error Handling**: Comprehensive error logging and user feedback
|
||||
- **Documentation**: Improved code comments and structure
|
||||
- **Performance**: Optimized builds and reduced bundle size
|
||||
|
||||
#### **UI/UX Enhancements** ✅
|
||||
- **Mobile Optimization**: Responsive design improvements
|
||||
- **Navigation**: Enhanced back button with proper mobile scaling
|
||||
- **Loading States**: Better user feedback during operations
|
||||
- **Error States**: Improved error messages and recovery options
|
||||
|
||||
### 🐳 **Docker Improvements**
|
||||
- **Stability**: Docker deployment now more reliable than local development
|
||||
- **Consistency**: Eliminates "works on my machine" issues
|
||||
- **Volume Management**: Persistent storage for posts and settings
|
||||
- **Performance**: Optimized container builds and runtime
|
||||
|
||||
### 📊 **Performance Metrics**
|
||||
- **Parsing Speed**: 10x faster than previous TypeScript implementation
|
||||
- **Memory Usage**: Reduced by ~40% with Rust backend
|
||||
- **Startup Time**: Faster initial load with caching
|
||||
- **Real-time Updates**: Sub-second response to file changes
|
||||
|
||||
### 🔮 **Future Roadmap**
|
||||
- **Full Caching Implementation**: Complete the RAM caching system
|
||||
- **Advanced Logging**: Enhanced parser analytics and debugging
|
||||
- **Performance Optimization**: Further speed improvements
|
||||
- **Feature Expansion**: Additional admin tools and customization options
|
||||
|
||||
### 🐛 **Bug Fixes**
|
||||
- **SSE Connection Errors**: Resolved 500 errors on `/api/posts/stream`
|
||||
- **Parser Fallbacks**: Removed unreliable TypeScript parser
|
||||
- **File Watching**: Fixed recursive directory monitoring
|
||||
- **CORS Issues**: Resolved cross-origin request problems
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import BadgeButton from './BadgeButton';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const InfoIcon = (
|
||||
<svg width="16" height="16" fill="white" viewBox="0 0 16 16" aria-hidden="true">
|
||||
@@ -9,16 +10,13 @@ const InfoIcon = (
|
||||
);
|
||||
|
||||
export default function AboutButton() {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<BadgeButton
|
||||
label="ABOUT ME"
|
||||
color="#2563eb"
|
||||
icon={InfoIcon}
|
||||
onClick={() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.open('http://' + window.location.hostname + ':80', '_blank');
|
||||
}
|
||||
}}
|
||||
onClick={() => router.push('/posts/about')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,26 +6,30 @@ export default function BadgeButton({
|
||||
color = '#2563eb',
|
||||
icon,
|
||||
onClick,
|
||||
labelColor,
|
||||
}: {
|
||||
label: string;
|
||||
color?: string;
|
||||
icon: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
labelColor?: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="flex items-center gap-2 h-8 px-5 font-bold tracking-wider uppercase text-white"
|
||||
className="flex items-center gap-1.5 h-6 sm:h-8 px-3 font-bold text-white"
|
||||
style={{
|
||||
background: color,
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'Verdana, Geneva, DejaVu Sans, sans-serif',
|
||||
fontSize: '0.95rem',
|
||||
letterSpacing: '0.08em',
|
||||
fontSize: '0.75rem',
|
||||
letterSpacing: '0.02em',
|
||||
border: '1px solid rgba(0,0,0,0.1)',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center">{icon}</span>
|
||||
<span>{label}</span>
|
||||
<span style={{ color: 'white', fontWeight: 'bold' }}>{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,17 @@
|
||||
'use client';
|
||||
import BadgeButton from './BadgeButton';
|
||||
import AboutButton from './AboutButton';
|
||||
|
||||
const PersonIcon = (
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="none">
|
||||
<circle cx="10" cy="6" r="4" fill="white" stroke="white" strokeWidth="1.5" />
|
||||
<rect x="3" y="13" width="14" height="5" rx="2.5" fill="white" stroke="white" strokeWidth="1.5" />
|
||||
const LockIcon = (
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M15 8H5C3.89543 8 3 8.89543 3 10V16C3 17.1046 3.89543 18 5 18H15C16.1046 18 17 17.1046 17 16V10C17 8.89543 16.1046 8 15 8Z" fill="white"/>
|
||||
<path d="M7 8V5C7 2.79086 8.79086 1 11 1H9C11.2091 1 13 2.79086 13 5V8" stroke="white" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const InfoIcon = (
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="none">
|
||||
<circle cx="10" cy="10" r="9" stroke="white" strokeWidth="2" />
|
||||
<rect x="9" y="8" width="2" height="6" rx="1" fill="white" />
|
||||
<rect x="9" y="5" width="2" height="2" rx="1" fill="white" />
|
||||
const PersonIcon = (
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none">
|
||||
<circle cx="10" cy="6" r="4" fill="white" stroke="white" strokeWidth="1.5" />
|
||||
<rect x="3" y="13" width="14" height="5" rx="2.5" fill="white" stroke="white" strokeWidth="1.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -24,25 +22,28 @@ export default function HeaderButtons() {
|
||||
href="/admin"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
className="h-6 sm:h-8 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded"
|
||||
className="focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded"
|
||||
>
|
||||
<img
|
||||
src="https://img.shields.io/badge/Admin%20Login-000000?style=for-the-badge&logo=lock&logoColor=white&labelColor=8B0000"
|
||||
alt="Admin Login"
|
||||
className="h-6 sm:h-8"
|
||||
<BadgeButton
|
||||
label="ADMIN LOGIN"
|
||||
color="#000000"
|
||||
labelColor="#8B0000"
|
||||
icon={LockIcon}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</a>
|
||||
{/* If your server for about me is running on a different port, change the port number here */}
|
||||
<a
|
||||
href={typeof window !== 'undefined' ? window.location.origin.replace('3000', '80') : '#'}
|
||||
href="/posts/about"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
className="h-6 sm:h-8 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded"
|
||||
className="focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded"
|
||||
>
|
||||
<img
|
||||
src="https://img.shields.io/badge/About%20Me-000000?style=for-the-badge&logo=account&logoColor=white&labelColor=2563eb"
|
||||
alt="About Me"
|
||||
className="h-6 sm:h-8"
|
||||
<BadgeButton
|
||||
label="ABOUT ME"
|
||||
color="#000000"
|
||||
labelColor="#2563eb"
|
||||
icon={PersonIcon}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -70,13 +70,13 @@ export default function MobileNav({ blogOwner }: MobileNavProps) {
|
||||
🔐 Admin
|
||||
</Link>
|
||||
|
||||
<a
|
||||
href={typeof window !== 'undefined' ? window.location.origin.replace('3000', '80') : '#'}
|
||||
<Link
|
||||
href="/posts/about"
|
||||
className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
👤 About Me
|
||||
</a>
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||
|
||||
98
src/app/about/page.tsx
Normal file
98
src/app/about/page.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface Post {
|
||||
slug: string;
|
||||
title: string;
|
||||
date: string;
|
||||
tags: string[];
|
||||
summary?: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
author: string;
|
||||
}
|
||||
|
||||
export default function AboutPage() {
|
||||
const [post, setPost] = useState<Post | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadAbout = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await fetch("/api/posts/about");
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setPost(data);
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : "Unknown error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
loadAbout();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Lade About...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||
<div className="text-center max-w-md mx-auto p-6">
|
||||
<div className="text-red-500 text-6xl mb-4">⚠️</div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">Fehler beim Laden</h1>
|
||||
<p className="text-gray-600 mb-6">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!post) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-red-500 text-6xl mb-4">❌</div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">About nicht gefunden</h1>
|
||||
<p className="text-gray-600 mb-6">Die About-Seite konnte nicht gefunden werden.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<main className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<article className="bg-white rounded-lg shadow-sm border p-6 sm:p-8">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl sm:text-4xl font-bold text-gray-900 mb-4 leading-tight">
|
||||
{post.title || "About"}
|
||||
</h1>
|
||||
{post.summary && (
|
||||
<p className="text-lg text-gray-700 mb-6 leading-relaxed">
|
||||
{post.summary}
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
<div
|
||||
className="prose prose-lg max-w-none prose-headings:text-gray-900 prose-p:text-gray-700 prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline prose-strong:text-gray-900 prose-code:text-gray-800 prose-code:bg-gray-100 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-pre:bg-gray-900 prose-pre:text-gray-100"
|
||||
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||
/>
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
src/app/admin/MonacoEditor.tsx
Normal file
12
src/app/admin/MonacoEditor.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import Editor from "@monaco-editor/react";
|
||||
|
||||
export default function MonacoEditorWrapper(props: any) {
|
||||
return (
|
||||
<Editor
|
||||
height="600px"
|
||||
defaultLanguage="markdown"
|
||||
defaultValue={props.defaultValue || ""}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
621
src/app/admin/editor/page.tsx
Normal file
621
src/app/admin/editor/page.tsx
Normal file
@@ -0,0 +1,621 @@
|
||||
"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";
|
||||
|
||||
const MonacoEditor = dynamic(() => import("@monaco-editor/react"), { ssr: false });
|
||||
|
||||
// File/folder types from API
|
||||
interface FileNode {
|
||||
type: "post";
|
||||
slug: string;
|
||||
title: string;
|
||||
date: string;
|
||||
tags: string[];
|
||||
summary: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
pinned: boolean;
|
||||
}
|
||||
interface FolderNode {
|
||||
type: "folder";
|
||||
name: string;
|
||||
path: string;
|
||||
emoji: string;
|
||||
children: (FileNode | FolderNode)[];
|
||||
}
|
||||
type Node = FileNode | FolderNode;
|
||||
|
||||
// Helper to strip YAML frontmatter
|
||||
function stripFrontmatter(md: string): string {
|
||||
if (!md) return '';
|
||||
if (md.startsWith('---')) {
|
||||
const end = md.indexOf('---', 3);
|
||||
if (end !== -1) return md.slice(end + 3).replace(/^\s+/, '');
|
||||
}
|
||||
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;
|
||||
selectedSlug: string | null;
|
||||
level?: number;
|
||||
}) {
|
||||
const [openFolders, setOpenFolders] = useState<Record<string, boolean>>({});
|
||||
return (
|
||||
<ul className="pl-2">
|
||||
{nodes.map((node) => {
|
||||
if (node.type === "folder") {
|
||||
const isOpen = openFolders[node.path] ?? true;
|
||||
return (
|
||||
<li key={node.path} className="mb-1">
|
||||
<button
|
||||
className="flex items-center gap-1 text-gray-700 hover:bg-gray-100 rounded px-1 py-0.5 w-full"
|
||||
style={{ paddingLeft: 8 + level * 12 }}
|
||||
onClick={() => setOpenFolders(f => ({ ...f, [node.path]: !isOpen }))}
|
||||
>
|
||||
<span className="text-lg">{node.emoji || "📁"}</span>
|
||||
<span className="font-semibold text-sm">{node.name}</span>
|
||||
<span className="ml-auto text-xs">{isOpen ? "▼" : "▶"}</span>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<FileTree nodes={node.children} onSelect={onSelect} selectedSlug={selectedSlug} level={level + 1} />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<li key={node.slug}>
|
||||
<button
|
||||
className={`flex items-center gap-2 px-2 py-1 rounded w-full text-left text-sm font-mono ${selectedSlug === node.slug ? "bg-blue-100 text-blue-800 font-bold" : "hover:bg-gray-100 text-gray-800"}`}
|
||||
style={{ paddingLeft: 8 + level * 12 }}
|
||||
onClick={() => onSelect(node.slug)}
|
||||
title={node.title}
|
||||
>
|
||||
<span className="text-gray-400">📝</span>
|
||||
<span className="truncate">{node.title || node.slug}</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EditorPage() {
|
||||
// State
|
||||
const router = useRouter();
|
||||
const [tree, setTree] = useState<Node[]>([]);
|
||||
const [selectedSlug, setSelectedSlug] = useState<string | null>(null);
|
||||
const [fileContent, setFileContent] = useState<string>("");
|
||||
const [originalContent, setOriginalContent] = useState<string>("");
|
||||
const [fileTitle, setFileTitle] = useState<string>("");
|
||||
const [vimMode, setVimMode] = useState(false);
|
||||
const [previewHtml, setPreviewHtml] = useState<string>("");
|
||||
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<string | null>(null);
|
||||
const editorRef = 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
|
||||
useEffect(() => {
|
||||
fetch("/api/posts")
|
||||
.then(r => r.json())
|
||||
.then(setTree);
|
||||
}, []);
|
||||
|
||||
// Load file content when selected
|
||||
useEffect(() => {
|
||||
if (!selectedSlug) return;
|
||||
setLoading(true);
|
||||
fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const { frontmatter, content } = extractFrontmatter(data.raw || data.content || "");
|
||||
const combinedContent = combineFrontmatterAndContent(frontmatter, content);
|
||||
setFileContent(combinedContent);
|
||||
setOriginalContent(combinedContent);
|
||||
setFileTitle(data.title || data.slug || "");
|
||||
setLoading(false);
|
||||
});
|
||||
}, [selectedSlug]);
|
||||
|
||||
// Save file
|
||||
async function handleSave() {
|
||||
if (!selectedSlug) return;
|
||||
setSaving(true);
|
||||
|
||||
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 { 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('');
|
||||
}, [fileContent]);
|
||||
|
||||
// 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();
|
||||
monacoVimRef.current = initVimMode(editor, document.getElementById("vim-status"));
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return;
|
||||
let disposed = false;
|
||||
async function setupVim() {
|
||||
if (monacoVimRef.current) monacoVimRef.current.dispose();
|
||||
if (vimMode) {
|
||||
const { initVimMode } = await import("monaco-vim");
|
||||
if (!disposed) {
|
||||
monacoVimRef.current = initVimMode(editorRef.current, document.getElementById("vim-status"));
|
||||
}
|
||||
}
|
||||
}
|
||||
setupVim();
|
||||
return () => { disposed = true; };
|
||||
}, [vimMode]);
|
||||
|
||||
// Split drag logic
|
||||
const dragRef = useRef(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
function onDrag(e: React.MouseEvent | MouseEvent) {
|
||||
if (!dragRef.current) return;
|
||||
const percent = (e.clientX / window.innerWidth) * 100;
|
||||
setSplit(percent); // No min/max limits
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
};
|
||||
}
|
||||
}, [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 = true; // Always show editor, it will resize based on container
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-white flex flex-col font-mono" style={{ fontFamily: 'JetBrains Mono, monospace', fontWeight: 'bold' }}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-200">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* VS Code SVG Icon (smaller) */}
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-6 h-6"
|
||||
style={{ minWidth: 20, minHeight: 20, width: 24, height: 24 }}
|
||||
>
|
||||
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M70.9119 99.3171C72.4869 99.9307 74.2828 99.8914 75.8725 99.1264L96.4608 89.2197C98.6242 88.1787 100 85.9892 100 83.5872V16.4133C100 14.0113 98.6243 11.8218 96.4609 10.7808L75.8725 0.873756C73.7862 -0.130129 71.3446 0.11576 69.5135 1.44695C69.252 1.63711 69.0028 1.84943 68.769 2.08341L29.3551 38.0415L12.1872 25.0096C10.589 23.7965 8.35363 23.8959 6.86933 25.2461L1.36303 30.2549C-0.452552 31.9064 -0.454633 34.7627 1.35853 36.417L16.2471 50.0001L1.35853 63.5832C-0.454633 65.2374 -0.452552 68.0938 1.36303 69.7453L6.86933 74.7541C8.35363 76.1043 10.589 76.2037 12.1872 74.9905L29.3551 61.9587L68.769 97.9167C69.3925 98.5406 70.1246 99.0104 70.9119 99.3171ZM75.0152 27.2989L45.1091 50.0001L75.0152 72.7012V27.2989Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0)">
|
||||
<path d="M96.4614 10.7962L75.8569 0.875542C73.4719 -0.272773 70.6217 0.211611 68.75 2.08333L1.29858 63.5832C-0.515693 65.2373 -0.513607 68.0937 1.30308 69.7452L6.81272 74.754C8.29793 76.1042 10.5347 76.2036 12.1338 74.9905L93.3609 13.3699C96.086 11.3026 100 13.2462 100 16.6667V16.4275C100 14.0265 98.6246 11.8378 96.4614 10.7962Z" fill="#0065A9"/>
|
||||
<g filter="url(#filter0_d)">
|
||||
<path d="M96.4614 89.2038L75.8569 99.1245C73.4719 100.273 70.6217 99.7884 68.75 97.9167L1.29858 36.4169C-0.515693 34.7627 -0.513607 31.9063 1.30308 30.2548L6.81272 25.246C8.29793 23.8958 10.5347 23.7964 12.1338 25.0095L93.3609 86.6301C96.086 88.6974 100 86.7538 100 83.3334V83.5726C100 85.9735 98.6246 88.1622 96.4614 89.2038Z" fill="#007ACC"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_d)">
|
||||
<path d="M75.8578 99.1263C73.4721 100.274 70.6219 99.7885 68.75 97.9166C71.0564 100.223 75 98.5895 75 95.3278V4.67213C75 1.41039 71.0564 -0.223106 68.75 2.08329C70.6219 0.211402 73.4721 -0.273666 75.8578 0.873633L96.4587 10.7807C98.6234 11.8217 100 14.0112 100 16.4132V83.5871C100 85.9891 98.6234 88.1786 96.4586 89.2196L75.8578 99.1263Z" fill="#1F9CF0"/>
|
||||
</g>
|
||||
<g style={{ mixBlendMode: 'overlay' }} opacity="0.25">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M70.8511 99.3171C72.4261 99.9306 74.2221 99.8913 75.8117 99.1264L96.4 89.2197C98.5634 88.1787 99.9392 85.9892 99.9392 83.5871V16.4133C99.9392 14.0112 98.5635 11.8217 96.4001 10.7807L75.8117 0.873695C73.7255 -0.13019 71.2838 0.115699 69.4527 1.44688C69.1912 1.63705 68.942 1.84937 68.7082 2.08335L29.2943 38.0414L12.1264 25.0096C10.5283 23.7964 8.29285 23.8959 6.80855 25.246L1.30225 30.2548C-0.513334 31.9064 -0.515415 34.7627 1.29775 36.4169L16.1863 50L1.29775 63.5832C-0.515415 65.2374 -0.513334 68.0937 1.30225 69.7452L6.80855 74.754C8.29285 76.1042 10.5283 76.2036 12.1264 74.9905L29.2943 61.9586L68.7082 97.9167C69.3317 98.5405 70.0638 99.0104 70.8511 99.3171ZM74.9544 27.2989L45.0483 50L74.9544 72.7012V27.2989Z" fill="url(#paint0_linear)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="-8.39411" y="15.8291" width="116.727" height="92.2456" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="4.16667"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d" x="60.4167" y="-8.07558" width="47.9167" height="116.151" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="4.16667"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear" x1="49.9392" y1="0.257812" x2="49.9392" y2="99.7423" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="white"/>
|
||||
<stop offset="1" stopColor="white" stopOpacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<span className="text-black text-lg font-semibold">Markdown Bearbeiter</span>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* Back Button */}
|
||||
<button
|
||||
onClick={() => handleBackNavigation()}
|
||||
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}
|
||||
>
|
||||
<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="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">
|
||||
{hasUnsavedChanges ? 'Zurück*' : 'Zurück'}
|
||||
</span>
|
||||
</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
|
||||
onClick={() => setVimMode((v) => !v)}
|
||||
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 */}
|
||||
<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>
|
||||
<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>
|
||||
<span className="hidden sm:inline">Vim Modus</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Split Layout */}
|
||||
<div className="flex flex-1 min-h-0" style={{ userSelect: isDragging ? "none" : undefined }}>
|
||||
{/* Left: File browser + Editor */}
|
||||
<div className="flex flex-row h-full bg-white" style={{ width: leftPaneWidth, minWidth: 0, maxWidth: '100%' }}>
|
||||
{/* File Browser Collapsible Toggle */}
|
||||
<div style={{ width: 32, minWidth: 32, maxWidth: 32, display: 'flex', flexDirection: 'column' }}>
|
||||
<button
|
||||
className={`w-full flex items-center justify-center px-2 py-1 font-mono hover:bg-gray-200 focus:outline-none ${
|
||||
browserOpen
|
||||
? 'bg-gray-100 border-b border-gray-200 text-gray-700'
|
||||
: 'bg-gray-200 border-b border-gray-300 text-gray-600 hover:bg-gray-300'
|
||||
}`}
|
||||
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 && (
|
||||
<div className="border-r border-gray-200 bg-gray-50 text-gray-800 font-mono overflow-auto" style={{ width: fileBrowserWidth, minWidth: fileBrowserWidth, maxWidth: fileBrowserWidth }}>
|
||||
<div className="h-64 p-2">
|
||||
{tree.length === 0 ? (
|
||||
<div className="text-xs text-gray-400">Keine Datein gefunden.</div>
|
||||
) : (
|
||||
<FileTree nodes={tree} onSelect={setSelectedSlug} selectedSlug={selectedSlug} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Monaco Editor */}
|
||||
<div className="flex-1 p-0 overflow-auto" style={{ fontFamily: 'JetBrains Mono, monospace', fontWeight: 'bold' }}>
|
||||
<div className="h-full">
|
||||
{showEditor && (
|
||||
<MonacoErrorBoundary>
|
||||
<MonacoEditor
|
||||
height="100%"
|
||||
defaultLanguage="markdown"
|
||||
value={fileContent}
|
||||
theme="light"
|
||||
options={{
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 15,
|
||||
minimap: { enabled: true },
|
||||
wordWrap: "on",
|
||||
scrollBeyondLastLine: false,
|
||||
smoothScrolling: true,
|
||||
automaticLayout: true,
|
||||
lineNumbers: "on",
|
||||
renderLineHighlight: "all",
|
||||
scrollbar: { vertical: "auto", horizontal: "auto" },
|
||||
tabSize: 2,
|
||||
cursorBlinking: "smooth",
|
||||
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}
|
||||
/>
|
||||
</MonacoErrorBoundary>
|
||||
)}
|
||||
<div id="vim-status" className="text-xs text-gray-500 px-2 py-1 bg-gray-100 border-t border-gray-200 font-mono" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Draggable Splitter - always show */}
|
||||
<div
|
||||
className={`w-1 cursor-col-resize transition-colors relative ${
|
||||
isDragging ? 'bg-blue-500' : 'bg-gray-300 hover:bg-gray-400'
|
||||
}`}
|
||||
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 */}
|
||||
<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">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl sm:text-4xl font-bold text-gray-900 mb-4 leading-tight">
|
||||
{fileTitle}
|
||||
</h1>
|
||||
</header>
|
||||
<div
|
||||
className="prose prose-lg max-w-none prose-headings:text-gray-900 prose-p:text-gray-700 prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline prose-strong:text-gray-900 prose-code:text-gray-800 prose-code:bg-gray-100 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-pre:bg-gray-900 prose-pre:text-gray-100"
|
||||
style={{ fontFamily: 'inherit', fontWeight: 'normal' }}
|
||||
dangerouslySetInnerHTML={{ __html: previewHtml }}
|
||||
/>
|
||||
</article>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
// ErrorBoundary component for Monaco
|
||||
class MonacoErrorBoundary extends React.Component<{children: React.ReactNode}, {error: Error | null}> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = { error: null };
|
||||
}
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { error };
|
||||
}
|
||||
componentDidCatch(error: Error, info: any) {
|
||||
// Optionally log error
|
||||
}
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return <div className="text-red-600 p-4">Fehler: {this.state.error.message}</div>;
|
||||
}
|
||||
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(() => {
|
||||
fetchStats();
|
||||
fetchHealth();
|
||||
@@ -130,40 +143,60 @@ export default function RustStatusPage() {
|
||||
|
||||
const getLevelIcon = (level: string) => {
|
||||
switch (level) {
|
||||
case 'error': return '❌';
|
||||
case 'warning': return '⚠️';
|
||||
case 'info': return 'ℹ️';
|
||||
default: return '📝';
|
||||
case 'error':
|
||||
return (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
case 'warning':
|
||||
return (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
case 'info':
|
||||
return (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M4 4a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2H4zm0 2h12v8H4V6zm2 2a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm0 3a1 1 0 011-1h4a1 1 0 110 2H7a1 1 0 01-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 p-4 sm:p-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="min-h-screen bg-gray-100 flex items-center justify-center p-4 sm:p-6">
|
||||
<div className="w-full max-w-6xl">
|
||||
{/* Header with title and action buttons */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-white rounded-lg shadow p-2 flex items-center justify-center">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-white rounded-lg shadow-sm p-1.5 flex items-center justify-center">
|
||||
<img
|
||||
className="w-10 h-10 sm:w-12 sm:h-12"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10"
|
||||
src="https://upload.wikimedia.org/wikipedia/commons/d/d5/Rust_programming_language_black_logo.svg"
|
||||
alt="Rust Logo"
|
||||
/>
|
||||
</div>
|
||||
<h1 className="text-xl sm:text-2xl font-bold">Rust-Parser Statistiken</h1>
|
||||
<h1 className="text-lg sm:text-xl font-bold">Rust-Parser Statistiken</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 w-full sm:w-auto justify-end">
|
||||
{/* Back to Admin button */}
|
||||
<a
|
||||
href="/admin"
|
||||
className="p-2 sm:px-4 sm:py-2 bg-gray-200 hover:bg-gray-300 rounded-lg shadow flex items-center gap-1 transition-colors"
|
||||
className="p-1.5 sm:px-3 sm:py-1.5 bg-gray-200 hover:bg-gray-300 rounded-lg shadow-sm flex items-center gap-1 transition-colors text-sm"
|
||||
title="Zurück zur Admin-Panel"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Zurück zur Admin-Panel</span>
|
||||
<span className="hidden sm:inline">Zurück</span>
|
||||
</a>
|
||||
|
||||
{/* Refresh button */}
|
||||
@@ -173,12 +206,12 @@ export default function RustStatusPage() {
|
||||
fetchHealth();
|
||||
fetchLogs();
|
||||
}}
|
||||
className="p-2 sm:px-4 sm:py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg shadow flex items-center gap-1 transition-colors"
|
||||
className="p-1.5 sm:px-3 sm:py-1.5 bg-blue-500 hover:bg-blue-600 text-white rounded-lg shadow-sm flex items-center gap-1 transition-colors text-sm"
|
||||
title="Aktualisieren"
|
||||
disabled={loading || healthLoading || logsLoading}
|
||||
>
|
||||
<svg
|
||||
className={`w-5 h-5 ${(loading || healthLoading || logsLoading) ? 'animate-spin' : ''}`}
|
||||
className={`w-4 h-4 ${(loading || healthLoading || logsLoading) ? 'animate-spin' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -191,52 +224,118 @@ export default function RustStatusPage() {
|
||||
</div>
|
||||
|
||||
{/* Health Check Section */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-base sm:text-lg font-semibold mb-2 text-center">Health-Check</h2>
|
||||
{healthLoading && <div className="text-center py-4 text-base">Lade Health-Check...</div>}
|
||||
{healthError && <div className="text-red-500 text-center text-base">{healthError}</div>}
|
||||
<div className="mb-4">
|
||||
<h2 className="text-sm sm:text-base font-semibold mb-2 text-center">Health-Check</h2>
|
||||
{healthLoading && <div className="text-center py-3 text-sm">Lade Health-Check...</div>}
|
||||
{healthError && <div className="text-red-500 text-center text-sm">{healthError}</div>}
|
||||
{health && (
|
||||
<div className="bg-white rounded-lg shadow p-4 flex flex-col gap-2 items-center">
|
||||
<div className="bg-gradient-to-br from-gray-50 to-white rounded-lg shadow-sm border border-gray-200 p-4 flex flex-col gap-3 items-center">
|
||||
<div className="flex flex-wrap gap-4 justify-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<span className={`text-lg font-bold ${health.posts_dir_exists ? 'text-green-700' : 'text-red-700'}`}>{health.posts_dir_exists ? '✔' : '✖'}</span>
|
||||
<span className="text-xs text-gray-600">Posts-Verzeichnis</span>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
health.posts_dir_exists ? 'bg-green-200 text-green-700' : 'bg-red-200 text-red-700'
|
||||
}`}>
|
||||
{health.posts_dir_exists ? (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 mt-1 font-medium">Posts-Verzeichnis</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-lg font-bold text-blue-700">{health.posts_count}</span>
|
||||
<span className="text-xs text-gray-600">Posts</span>
|
||||
<div className="w-8 h-8 rounded-full bg-blue-200 text-blue-700 flex items-center justify-center">
|
||||
<span className="text-sm font-bold">{health.posts_count}</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 mt-1 font-medium">Posts</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className={`text-lg font-bold ${health.cache_file_exists ? 'text-green-700' : 'text-red-700'}`}>{health.cache_file_exists ? '✔' : '✖'}</span>
|
||||
<span className="text-xs text-gray-600">Cache-Datei</span>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
health.cache_file_exists ? 'bg-green-200 text-green-700' : 'bg-red-200 text-red-700'
|
||||
}`}>
|
||||
{health.cache_file_exists ? (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 mt-1 font-medium">Cache-Datei</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className={`text-lg font-bold ${health.cache_stats_file_exists ? 'text-green-700' : 'text-red-700'}`}>{health.cache_stats_file_exists ? '✔' : '✖'}</span>
|
||||
<span className="text-xs text-gray-600">Cache-Stats</span>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
health.cache_stats_file_exists ? 'bg-green-200 text-green-700' : 'bg-red-200 text-red-700'
|
||||
}`}>
|
||||
{health.cache_stats_file_exists ? (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 mt-1 font-medium">Cache-Stats</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className={`text-lg font-bold ${health.cache_readable ? 'text-green-700' : 'text-red-700'}`}>{health.cache_readable ? '✔' : '✖'}</span>
|
||||
<span className="text-xs text-gray-600">Cache lesbar</span>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
health.cache_readable ? 'bg-green-200 text-green-700' : 'bg-red-200 text-red-700'
|
||||
}`}>
|
||||
{health.cache_readable ? (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 mt-1 font-medium">Cache lesbar</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<span className={`text-lg font-bold ${health.cache_stats_readable ? 'text-green-700' : 'text-red-700'}`}>{health.cache_stats_readable ? '✔' : '✖'}</span>
|
||||
<span className="text-xs text-gray-600">Stats lesbar</span>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
health.cache_stats_readable ? 'bg-green-200 text-green-700' : 'bg-red-200 text-red-700'
|
||||
}`}>
|
||||
{health.cache_stats_readable ? (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 mt-1 font-medium">Stats lesbar</span>
|
||||
</div>
|
||||
{typeof health.cache_post_count === 'number' && (
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-lg font-bold text-blue-700">{health.cache_post_count}</span>
|
||||
<span className="text-xs text-gray-600">Cache-Posts</span>
|
||||
<div className="w-8 h-8 rounded-full bg-blue-200 text-blue-700 flex items-center justify-center">
|
||||
<span className="text-sm font-bold">{health.cache_post_count}</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 mt-1 font-medium">Cache-Posts</span>
|
||||
</div>
|
||||
)}
|
||||
{typeof health.cache_stats_count === 'number' && (
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-lg font-bold text-blue-700">{health.cache_stats_count}</span>
|
||||
<span className="text-xs text-gray-600">Stats-Einträge</span>
|
||||
<div className="w-8 h-8 rounded-full bg-blue-200 text-blue-700 flex items-center justify-center">
|
||||
<span className="text-sm font-bold">{health.cache_stats_count}</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 mt-1 font-medium">Stats-Einträge</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{health.errors.length > 0 && (
|
||||
<div className="mt-2 text-red-600 text-xs text-center">
|
||||
<div className="mt-3 text-red-600 text-xs text-center bg-red-50 p-3 rounded-md border border-red-200">
|
||||
<b>Fehler:</b>
|
||||
<ul className="list-disc ml-5 inline-block text-left">
|
||||
{health.errors.map((err, i) => <li key={i}>{err}</li>)}
|
||||
@@ -248,33 +347,40 @@ export default function RustStatusPage() {
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4 mb-6">
|
||||
<div className="bg-green-100 rounded-lg p-4 flex flex-col items-center shadow">
|
||||
<span className="text-xl sm:text-2xl font-bold text-green-700">{totalHits}</span>
|
||||
<span className="text-sm sm:text-base text-gray-700 mt-1 sm:mt-2 text-center">Cache-Treffer</span>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4 mb-4">
|
||||
<div className="bg-gradient-to-br from-green-50 to-green-100 rounded-lg p-3 flex flex-col items-center shadow-sm border border-green-200">
|
||||
<span className="text-lg sm:text-xl font-bold text-green-700">{totalHits}</span>
|
||||
<span className="text-xs sm:text-sm text-gray-700 mt-1 text-center font-medium">Cache-Treffer</span>
|
||||
</div>
|
||||
<div className="bg-red-100 rounded-lg p-4 flex flex-col items-center shadow">
|
||||
<span className="text-xl sm:text-2xl font-bold text-red-700">{totalMisses}</span>
|
||||
<span className="text-sm sm:text-base text-gray-700 mt-1 sm:mt-2 text-center">Cache-Fehlschläge</span>
|
||||
<div className="bg-gradient-to-br from-red-50 to-red-100 rounded-lg p-3 flex flex-col items-center shadow-sm border border-red-200">
|
||||
<span className="text-lg sm:text-xl font-bold text-red-700">{totalMisses}</span>
|
||||
<span className="text-xs sm:text-sm text-gray-700 mt-1 text-center font-medium">Cache-Fehlschläge</span>
|
||||
</div>
|
||||
<div className="bg-blue-100 rounded-lg p-4 flex flex-col items-center shadow">
|
||||
<span className="text-xl sm:text-2xl font-bold text-blue-700">{avgInterpret} ms</span>
|
||||
<span className="text-sm sm:text-base text-gray-700 mt-1 sm:mt-2 text-center">Ø Interpretationszeit</span>
|
||||
<div className="bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-3 flex flex-col items-center shadow-sm border border-blue-200">
|
||||
<span className="text-lg sm:text-xl font-bold text-blue-700">{avgInterpret} ms</span>
|
||||
<span className="text-xs sm:text-sm text-gray-700 mt-1 text-center font-medium">Ø Interpretationszeit</span>
|
||||
</div>
|
||||
<div className="bg-purple-100 rounded-lg p-4 flex flex-col items-center shadow">
|
||||
<span className="text-xl sm:text-2xl font-bold text-purple-700">{avgCompile} ms</span>
|
||||
<span className="text-sm sm:text-base text-gray-700 mt-1 sm:mt-2 text-center">Ø Kompilierzeit</span>
|
||||
<div className="bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg p-3 flex flex-col items-center shadow-sm border border-purple-200">
|
||||
<span className="text-lg sm:text-xl font-bold text-purple-700">{avgCompile} ms</span>
|
||||
<span className="text-xs sm:text-sm text-gray-700 mt-1 text-center font-medium">Ø Kompilierzeit</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Parser Logs Section */}
|
||||
<div className="bg-white rounded-lg shadow p-4 mb-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
|
||||
<h2 className="text-lg font-semibold">Parser Logs</h2>
|
||||
<div className="bg-white rounded-lg shadow-sm p-4 mb-4">
|
||||
<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>
|
||||
<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
|
||||
onClick={clearLogs}
|
||||
className="px-3 py-2 bg-red-500 hover:bg-red-600 text-white rounded text-sm transition-colors"
|
||||
className="px-2.5 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded text-xs transition-colors"
|
||||
title="Clear all logs"
|
||||
>
|
||||
Clear Logs
|
||||
@@ -283,21 +389,21 @@ export default function RustStatusPage() {
|
||||
</div>
|
||||
|
||||
{/* Log Filters */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-4">
|
||||
<div className="flex flex-col sm:flex-row gap-3 mb-3">
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search logs..."
|
||||
value={logSearch}
|
||||
onChange={(e) => setLogSearch(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full px-3 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={logFilter}
|
||||
onChange={(e) => setLogFilter(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="px-3 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
|
||||
>
|
||||
<option value="all">All Levels</option>
|
||||
<option value="info">Info</option>
|
||||
@@ -308,24 +414,42 @@ export default function RustStatusPage() {
|
||||
</div>
|
||||
|
||||
{/* Logs Display */}
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{logsLoading && <div className="text-center py-4">Loading logs...</div>}
|
||||
{logsError && <div className="text-red-500 text-center py-4">{logsError}</div>}
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{logsLoading && <div className="text-center py-3 text-sm">Loading logs...</div>}
|
||||
{logsError && <div className="text-red-500 text-center py-3 text-sm">{logsError}</div>}
|
||||
{!logsLoading && !logsError && (
|
||||
<div className="space-y-2">
|
||||
{filteredLogs.length === 0 ? (
|
||||
<div className="text-center py-4 text-gray-500">No logs found</div>
|
||||
<div className="text-center py-3 text-gray-500 text-sm">No logs found</div>
|
||||
) : (
|
||||
filteredLogs.map((log, index) => (
|
||||
<div key={index} className={`p-3 rounded-lg border ${getLevelColor(log.level)}`}>
|
||||
<div key={index} className={`p-3 rounded-lg border-l-4 shadow-sm ${
|
||||
log.level === 'error' ? 'bg-gradient-to-r from-red-50 to-red-100 border-red-400' :
|
||||
log.level === 'warning' ? 'bg-gradient-to-r from-yellow-50 to-yellow-100 border-yellow-400' :
|
||||
'bg-gradient-to-r from-blue-50 to-blue-100 border-blue-400'
|
||||
}`}>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-lg">{getLevelIcon(log.level)}</span>
|
||||
<div className={`flex-shrink-0 p-1 rounded-full ${
|
||||
log.level === 'error' ? 'bg-red-200 text-red-700' :
|
||||
log.level === 'warning' ? 'bg-yellow-200 text-yellow-700' :
|
||||
'bg-blue-200 text-blue-700'
|
||||
}`}>
|
||||
{getLevelIcon(log.level)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs font-mono text-gray-500">
|
||||
{new Date(log.timestamp).toLocaleString()}
|
||||
<span className="text-xs font-mono text-gray-600 tracking-wide">
|
||||
{new Date(log.timestamp).toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
})}
|
||||
</span>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-semibold tracking-wide ${
|
||||
log.level === 'error' ? 'bg-red-200 text-red-800' :
|
||||
log.level === 'warning' ? 'bg-yellow-200 text-yellow-800' :
|
||||
'bg-blue-200 text-blue-800'
|
||||
@@ -333,14 +457,14 @@ export default function RustStatusPage() {
|
||||
{log.level.toUpperCase()}
|
||||
</span>
|
||||
{log.slug && (
|
||||
<span className="px-2 py-1 bg-gray-200 text-gray-700 rounded text-xs font-mono">
|
||||
<span className="px-2 py-0.5 bg-gray-200 text-gray-700 rounded-full text-xs font-mono tracking-wide">
|
||||
{log.slug}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm font-medium">{log.message}</div>
|
||||
<div className="text-sm font-medium text-gray-900 leading-relaxed">{log.message}</div>
|
||||
{log.details && (
|
||||
<div className="text-xs text-gray-600 mt-1 font-mono bg-gray-100 p-2 rounded">
|
||||
<div className="text-xs text-gray-600 mt-1 font-mono bg-gray-50 p-2 rounded-md border border-gray-200 leading-relaxed">
|
||||
{log.details}
|
||||
</div>
|
||||
)}
|
||||
@@ -355,33 +479,33 @@ export default function RustStatusPage() {
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="bg-white rounded-lg shadow p-3 sm:p-4 overflow-x-auto">
|
||||
<h2 className="text-base sm:text-lg font-semibold mb-3">Rohdaten</h2>
|
||||
{loading && <div className="text-center py-6 text-base">Lade Statistiken...</div>}
|
||||
{error && <div className="text-red-500 text-center text-base">{error}</div>}
|
||||
<div className="bg-white rounded-lg shadow-sm p-3 sm:p-4 overflow-x-auto">
|
||||
<h2 className="text-sm sm:text-base font-semibold mb-2">Rohdaten</h2>
|
||||
{loading && <div className="text-center py-4 text-sm">Lade Statistiken...</div>}
|
||||
{error && <div className="text-red-500 text-center text-sm">{error}</div>}
|
||||
{!loading && !error && (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full border border-gray-200 bg-white rounded">
|
||||
<table className="min-w-full border border-gray-200 bg-white rounded text-sm">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="px-3 py-2 text-left text-sm">Slug</th>
|
||||
<th className="px-3 py-2 text-right text-sm">Cache-Treffer</th>
|
||||
<th className="px-3 py-2 text-right text-sm">Cache-Fehlschläge</th>
|
||||
<th className="px-3 py-2 text-right text-sm">Interpret (ms)</th>
|
||||
<th className="px-3 py-2 text-right text-sm">Kompilier (ms)</th>
|
||||
<th className="px-2 py-1.5 text-left text-xs">Slug</th>
|
||||
<th className="px-2 py-1.5 text-right text-xs">Cache-Treffer</th>
|
||||
<th className="px-2 py-1.5 text-right text-xs">Cache-Fehlschläge</th>
|
||||
<th className="px-2 py-1.5 text-right text-xs">Interpret (ms)</th>
|
||||
<th className="px-2 py-1.5 text-right text-xs">Kompilier (ms)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stats.length === 0 ? (
|
||||
<tr><td colSpan={5} className="text-center py-3 text-sm">Keine Statistiken verfügbar.</td></tr>
|
||||
<tr><td colSpan={5} className="text-center py-2 text-xs">Keine Statistiken verfügbar.</td></tr>
|
||||
) : (
|
||||
stats.map(stat => (
|
||||
<tr key={stat.slug} className="border-t border-gray-200">
|
||||
<td className="px-3 py-2 font-mono text-sm">{stat.slug}</td>
|
||||
<td className="px-3 py-2 text-right text-sm">{stat.cache_hits}</td>
|
||||
<td className="px-3 py-2 text-right text-sm">{stat.cache_misses}</td>
|
||||
<td className="px-3 py-2 text-right text-sm">{stat.last_interpret_time_ms}</td>
|
||||
<td className="px-3 py-2 text-right text-sm">{stat.last_compile_time_ms}</td>
|
||||
<td className="px-2 py-1.5 font-mono text-xs">{stat.slug}</td>
|
||||
<td className="px-2 py-1.5 text-right text-xs">{stat.cache_hits}</td>
|
||||
<td className="px-2 py-1.5 text-right text-xs">{stat.cache_misses}</td>
|
||||
<td className="px-2 py-1.5 text-right text-xs">{stat.last_interpret_time_ms}</td>
|
||||
<td className="px-2 py-1.5 text-right text-xs">{stat.last_compile_time_ms}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
'use client';
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/*********************************************
|
||||
* This is the main admin page for the blog.
|
||||
*
|
||||
* Written Jun 19 2025
|
||||
* Rewritten fucking 15 times cause of the
|
||||
* fucking
|
||||
* typescript linter.
|
||||
*
|
||||
* If any Issues about "Window" (For Monaco) pop up. Its not my fucking fault
|
||||
*
|
||||
* Push later when on local Network. (//5jul25) ## Already done
|
||||
**********************************************/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
@@ -12,8 +20,21 @@ import Link from 'next/link';
|
||||
import { marked } from 'marked';
|
||||
import hljs from 'highlight.js';
|
||||
import matter from 'gray-matter';
|
||||
import dynamic from 'next/dynamic';
|
||||
import dynamicImport from 'next/dynamic';
|
||||
import { Theme } from 'emoji-picker-react';
|
||||
import '../highlight-github.css';
|
||||
const MonacoEditor = dynamicImport(() => import('./MonacoEditor'), { ssr: false });
|
||||
// Import monaco-vim only on client side
|
||||
let initVimMode: any = null;
|
||||
let VimMode: any = null;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const monacoVim = require('monaco-vim');
|
||||
initVimMode = monacoVim.initVimMode;
|
||||
VimMode = monacoVim.VimMode;
|
||||
}
|
||||
import '@fontsource/jetbrains-mono';
|
||||
|
||||
|
||||
interface Post {
|
||||
slug: string;
|
||||
@@ -46,7 +67,18 @@ interface Post {
|
||||
|
||||
type Node = Post | Folder;
|
||||
|
||||
const EmojiPicker = dynamic(() => import('emoji-picker-react'), { ssr: false });
|
||||
const EmojiPicker = dynamicImport(() => import('emoji-picker-react'), { ssr: false });
|
||||
|
||||
// Patch marked renderer to always add 'hljs' class to code blocks
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.code = function(code, infostring, escaped) {
|
||||
const lang = (infostring || '').match(/\S*/)?.[0];
|
||||
const highlighted = lang && hljs.getLanguage(lang)
|
||||
? hljs.highlight(code, { language: lang }).value
|
||||
: hljs.highlightAuto(code).value;
|
||||
const langClass = lang ? `language-${lang}` : '';
|
||||
return `<pre><code class="hljs ${langClass}">${highlighted}</code></pre>`;
|
||||
};
|
||||
|
||||
export default function AdminPage() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
@@ -69,12 +101,7 @@ export default function AdminPage() {
|
||||
});
|
||||
const [showManageContent, setShowManageContent] = useState(false);
|
||||
const [managePath, setManagePath] = useState<string[]>([]);
|
||||
const [pinned, setPinned] = useState<string[]>(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return JSON.parse(localStorage.getItem('pinnedPosts') || '[]');
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const [pinned, setPinned] = useState<string[]>([]);
|
||||
const [pinFeedback, setPinFeedback] = useState<string | null>(null);
|
||||
const [showChangePassword, setShowChangePassword] = useState(false);
|
||||
const [changePwOld, setChangePwOld] = useState('');
|
||||
@@ -84,12 +111,7 @@ export default function AdminPage() {
|
||||
const [previewHtml, setPreviewHtml] = useState('');
|
||||
const [editingPost, setEditingPost] = useState<{ slug: string, path: string } | null>(null);
|
||||
const [isDocker, setIsDocker] = useState<boolean>(false);
|
||||
const [rememberExportChoice, setRememberExportChoice] = useState<boolean>(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('rememberExportChoice') === 'true';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const [rememberExportChoice, setRememberExportChoice] = useState<boolean>(false);
|
||||
const [lastExportChoice, setLastExportChoice] = useState<string | null>(null);
|
||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState<string | null>(null);
|
||||
const [emojiPickerAnchor, setEmojiPickerAnchor] = useState<HTMLElement | null>(null);
|
||||
@@ -97,6 +119,10 @@ export default function AdminPage() {
|
||||
const router = useRouter();
|
||||
const usernameRef = useRef<HTMLInputElement>(null);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
const monacoRef = useRef<any>(null);
|
||||
const vimStatusRef = useRef(null);
|
||||
const vimInstanceRef = useRef<any>(null);
|
||||
const [vimMode, setVimMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if already authenticated
|
||||
@@ -129,13 +155,7 @@ export default function AdminPage() {
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
highlight: function(code: string, lang: string) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
} else {
|
||||
return hljs.highlightAuto(code).value;
|
||||
}
|
||||
}
|
||||
renderer,
|
||||
} as any);
|
||||
setPreviewHtml(marked.parse(newPost.content || '') as string);
|
||||
}, [newPost.content]);
|
||||
@@ -518,15 +538,7 @@ export default function AdminPage() {
|
||||
};
|
||||
|
||||
function handleExportTarball() {
|
||||
// Check if we should use the remembered choice
|
||||
if (rememberExportChoice && lastExportChoice) {
|
||||
if (lastExportChoice === 'docker') {
|
||||
exportFromEndpoint('/api/admin/export');
|
||||
} else if (lastExportChoice === 'local') {
|
||||
exportFromEndpoint('/api/admin/exportlocal');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
// Create popup modal
|
||||
const modal = document.createElement('div');
|
||||
@@ -619,6 +631,7 @@ export default function AdminPage() {
|
||||
}
|
||||
|
||||
function exportFromEndpoint(endpoint: string) {
|
||||
if (typeof window === 'undefined') return;
|
||||
fetch(endpoint)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw new Error('Export failed');
|
||||
@@ -642,6 +655,15 @@ export default function AdminPage() {
|
||||
setLastExportChoice(null);
|
||||
};
|
||||
|
||||
// Hydrate pinned, rememberExportChoice, lastExportChoice from localStorage on client only
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
setPinned(JSON.parse(localStorage.getItem('pinnedPosts') || '[]'));
|
||||
setRememberExportChoice(localStorage.getItem('rememberExportChoice') === 'true');
|
||||
setLastExportChoice(localStorage.getItem('lastExportChoice'));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Simple and reliable emoji update handler
|
||||
const handleSetFolderEmoji = async (folderPath: string, emoji: string) => {
|
||||
try {
|
||||
@@ -752,6 +774,17 @@ export default function AdminPage() {
|
||||
return Theme.LIGHT;
|
||||
};
|
||||
|
||||
// Attach/detach Vim mode when vimMode changes
|
||||
useEffect(() => {
|
||||
if (vimMode && monacoRef.current && initVimMode) {
|
||||
// @ts-ignore
|
||||
vimInstanceRef.current = initVimMode(monacoRef.current, vimStatusRef.current);
|
||||
} else if (vimInstanceRef.current) {
|
||||
vimInstanceRef.current.dispose();
|
||||
vimInstanceRef.current = null;
|
||||
}
|
||||
}, [vimMode, monacoRef.current]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 p-3 sm:p-8">
|
||||
{pinFeedback && (
|
||||
@@ -867,6 +900,60 @@ export default function AdminPage() {
|
||||
<span className="text-xs font-normal text-teal-100">Statistiken</span>
|
||||
</span>
|
||||
</a>
|
||||
{/* VS Code Editor Button */}
|
||||
<a
|
||||
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"
|
||||
title="Markdown Bearbeiter"
|
||||
style={{ minWidth: '160px' }}
|
||||
>
|
||||
{/* VS Code SVG Icon */}
|
||||
<svg width="24" height="24" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M70.9119 99.3171C72.4869 99.9307 74.2828 99.8914 75.8725 99.1264L96.4608 89.2197C98.6242 88.1787 100 85.9892 100 83.5872V16.4133C100 14.0113 98.6243 11.8218 96.4609 10.7808L75.8725 0.873756C73.7862 -0.130129 71.3446 0.11576 69.5135 1.44695C69.252 1.63711 69.0028 1.84943 68.769 2.08341L29.3551 38.0415L12.1872 25.0096C10.589 23.7965 8.35363 23.8959 6.86933 25.2461L1.36303 30.2549C-0.452552 31.9064 -0.454633 34.7627 1.35853 36.417L16.2471 50.0001L1.35853 63.5832C-0.454633 65.2374 -0.452552 68.0938 1.36303 69.7453L6.86933 74.7541C8.35363 76.1043 10.589 76.2037 12.1872 74.9905L29.3551 61.9587L68.769 97.9167C69.3925 98.5406 70.1246 99.0104 70.9119 99.3171ZM75.0152 27.2989L45.1091 50.0001L75.0152 72.7012V27.2989Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0)">
|
||||
<path d="M96.4614 10.7962L75.8569 0.875542C73.4719 -0.272773 70.6217 0.211611 68.75 2.08333L1.29858 63.5832C-0.515693 65.2373 -0.513607 68.0937 1.30308 69.7452L6.81272 74.754C8.29793 76.1042 10.5347 76.2036 12.1338 74.9905L93.3609 13.3699C96.086 11.3026 100 13.2462 100 16.6667V16.4275C100 14.0265 98.6246 11.8378 96.4614 10.7962Z" fill="#0065A9"/>
|
||||
<g filter="url(#filter0_d)">
|
||||
<path d="M96.4614 89.2038L75.8569 99.1245C73.4719 100.273 70.6217 99.7884 68.75 97.9167L1.29858 36.4169C-0.515693 34.7627 -0.513607 31.9063 1.30308 30.2548L6.81272 25.246C8.29793 23.8958 10.5347 23.7964 12.1338 25.0095L93.3609 86.6301C96.086 88.6974 100 86.7538 100 83.3334V83.5726C100 85.9735 98.6246 88.1622 96.4614 89.2038Z" fill="#007ACC"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_d)">
|
||||
<path d="M75.8578 99.1263C73.4721 100.274 70.6219 99.7885 68.75 97.9166C71.0564 100.223 75 98.5895 75 95.3278V4.67213C75 1.41039 71.0564 -0.223106 68.75 2.08329C70.6219 0.211402 73.4721 -0.273666 75.8578 0.873633L96.4587 10.7807C98.6234 11.8217 100 14.0112 100 16.4132V83.5871C100 85.9891 98.6234 88.1786 96.4586 89.2196L75.8578 99.1263Z" fill="#1F9CF0"/>
|
||||
</g>
|
||||
<g style={{ mixBlendMode: 'overlay' }} opacity="0.25">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M70.8511 99.3171C72.4261 99.9306 74.2221 99.8913 75.8117 99.1264L96.4 89.2197C98.5634 88.1787 99.9392 85.9892 99.9392 83.5871V16.4133C99.9392 14.0112 98.5635 11.8217 96.4001 10.7807L75.8117 0.873695C73.7255 -0.13019 71.2838 0.115699 69.4527 1.44688C69.1912 1.63705 68.942 1.84937 68.7082 2.08335L29.2943 38.0414L12.1264 25.0096C10.5283 23.7964 8.29285 23.8959 6.80855 25.246L1.30225 30.2548C-0.513334 31.9064 -0.515415 34.7627 1.29775 36.4169L16.1863 50L1.29775 63.5832C-0.515415 65.2374 -0.513334 68.0937 1.29775 69.7452L6.80855 74.754C8.29285 76.1042 10.5283 76.2036 12.1264 74.9905L29.2943 61.9586L68.7082 97.9167C69.3317 98.5405 70.0638 99.0104 70.8511 99.3171ZM74.9544 27.2989L45.0483 50L74.9544 72.7012V27.2989Z" fill="url(#paint0_linear)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="-8.39411" y="15.8291" width="116.727" height="92.2456" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="4.16667"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d" x="60.4167" y="-8.07558" width="47.9167" height="116.151" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="4.16667"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear" x1="49.9392" y1="0.257812" x2="49.9392" y2="99.7423" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="white"/>
|
||||
<stop offset="1" stopColor="white" stopOpacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<span className="flex flex-col items-start">
|
||||
<span>Markdown Editor</span>
|
||||
<span className="text-xs font-normal text-blue-100">Visual Studio Code</span>
|
||||
</span>
|
||||
</a>
|
||||
{rememberExportChoice && lastExportChoice && (
|
||||
<div className="flex items-center gap-1 text-xs text-gray-600 w-full sm:w-auto justify-center sm:justify-start">
|
||||
<span>💾 {lastExportChoice === 'docker' ? 'Docker' : 'Local'}</span>
|
||||
@@ -990,8 +1077,6 @@ export default function AdminPage() {
|
||||
Current folder: <span className="font-mono">{currentPath.join('/') || 'root'}</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Drag and Drop Zone */}
|
||||
<div
|
||||
className={`mb-6 sm:mb-8 p-4 sm:p-8 border-2 border-dashed rounded-lg text-center ${
|
||||
@@ -1072,25 +1157,66 @@ export default function AdminPage() {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{/* Mobile-friendly content editor */}
|
||||
<div className="flex items-center mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="vim-toggle"
|
||||
checked={vimMode}
|
||||
onChange={() => setVimMode(v => !v)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<label
|
||||
htmlFor="vim-toggle"
|
||||
className="text-sm font-bold"
|
||||
style={{
|
||||
fontFamily: "'JetBrains Mono', 'monospace', cursive",
|
||||
fontStyle: 'italic',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
Vim Mode
|
||||
</label>
|
||||
<div ref={vimStatusRef} className="ml-4 text-xs text-gray-500 font-mono" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="w-full sm:w-1/2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Inhalt (Markdown)</label>
|
||||
<textarea
|
||||
value={newPost.content}
|
||||
onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
|
||||
className="w-full rounded-md border border-gray-300 px-3 py-2 font-mono text-sm sm:text-base"
|
||||
style={{ height: '240px' }}
|
||||
rows={10}
|
||||
required
|
||||
/>
|
||||
<div style={{ height: '240px' }}>
|
||||
<MonacoEditor
|
||||
height="100%"
|
||||
defaultLanguage="markdown"
|
||||
value={newPost.content}
|
||||
onChange={(value?: string) => setNewPost({ ...newPost, content: value || '' })}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
wordWrap: 'on',
|
||||
fontSize: 14,
|
||||
scrollBeyondLastLine: false,
|
||||
theme: 'vs-light',
|
||||
lineNumbers: 'on',
|
||||
automaticLayout: true,
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
}}
|
||||
onMount={(editor: any) => {
|
||||
monacoRef.current = editor;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full sm:w-1/2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Vorschau</label>
|
||||
<div className="p-3 sm:p-4 border rounded bg-gray-50 overflow-auto" style={{ height: '240px' }}>
|
||||
<div className="prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: previewHtml }} />
|
||||
<div
|
||||
className="prose prose-sm max-w-none"
|
||||
style={{ fontFamily: 'inherit' }}
|
||||
dangerouslySetInnerHTML={{ __html: previewHtml }}
|
||||
/>
|
||||
</div>
|
||||
<style jsx global>{`
|
||||
.prose code, .prose pre {
|
||||
font-family: 'JetBrains Mono', monospace !important;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
try {
|
||||
const pinnedPath = path.join(process.cwd(), 'posts', 'pinned.json');
|
||||
@@ -181,7 +221,7 @@ export async function DELETE(request: Request) {
|
||||
// Call the Rust backend to clear parser logs
|
||||
const rustResult = spawnSync(
|
||||
process.cwd() + '/markdown_backend/target/release/markdown_backend',
|
||||
['clearLogs'],
|
||||
['clear-logs'],
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
if (rustResult.status === 0 && rustResult.stdout) {
|
||||
|
||||
@@ -19,6 +19,13 @@ export async function GET(
|
||||
);
|
||||
if (rustResult.status === 0 && rustResult.stdout) {
|
||||
const post = JSON.parse(rustResult.stdout);
|
||||
const fs = require('fs');
|
||||
const filePath = path.join(postsDirectory, slugPath + '.md');
|
||||
let raw = '';
|
||||
try {
|
||||
raw = fs.readFileSync(filePath, 'utf8');
|
||||
} catch {}
|
||||
post.raw = raw;
|
||||
post.createdAt = post.created_at;
|
||||
delete post.created_at;
|
||||
return NextResponse.json(post);
|
||||
@@ -33,4 +40,20 @@ export async function GET(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request, { params }: { params: { slug: string[] | string } }) {
|
||||
try {
|
||||
const { markdown } = await request.json();
|
||||
if (typeof markdown !== 'string') {
|
||||
return NextResponse.json({ error: 'Invalid markdown' }, { status: 400 });
|
||||
}
|
||||
const slugArr = Array.isArray(params.slug) ? params.slug : [params.slug];
|
||||
const slugPath = slugArr.join('/');
|
||||
const filePath = path.join(postsDirectory, slugPath + '.md');
|
||||
require('fs').writeFileSync(filePath, markdown, 'utf8');
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Error saving file', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
26
src/app/api/posts/preview/route.ts
Normal file
26
src/app/api/posts/preview/route.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { spawnSync } from 'child_process';
|
||||
import path from 'path';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { markdown } = await request.json();
|
||||
if (typeof markdown !== 'string') {
|
||||
return NextResponse.json({ error: 'Invalid markdown' }, { status: 400 });
|
||||
}
|
||||
// Call Rust backend with 'render' command, pass markdown via stdin
|
||||
const rustPath = path.resolve(process.cwd(), 'markdown_backend/target/release/markdown_backend');
|
||||
const rustResult = spawnSync(rustPath, ['render'], {
|
||||
input: markdown,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
if (rustResult.status === 0 && rustResult.stdout) {
|
||||
return NextResponse.json({ html: rustResult.stdout });
|
||||
} else {
|
||||
const rustError = rustResult.stderr || rustResult.error?.toString() || 'Unknown error';
|
||||
return NextResponse.json({ error: 'Rust parser error', details: rustError }, { status: 500 });
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Error rendering markdown', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import createDOMPurify from 'isomorphic-dompurify';
|
||||
import hljs from 'highlight.js';
|
||||
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||
|
||||
@@ -73,6 +72,10 @@ async function readPostsDir(dir: string, relDir = '', pinnedData: { pinned: stri
|
||||
const posts: any[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
// Skip the 'assets' folder
|
||||
if (entry.isDirectory() && entry.name === 'assets' && relDir === '') {
|
||||
continue;
|
||||
}
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
const relPath = relDir ? path.join(relDir, entry.name) : entry.name;
|
||||
|
||||
@@ -102,10 +105,8 @@ async function getPostByPath(filePath: string, relPath: string, pinnedData: { pi
|
||||
|
||||
let processedContent = '';
|
||||
try {
|
||||
const rawHtml = marked.parse(content);
|
||||
const window = new JSDOM('').window;
|
||||
const purify = DOMPurify(window);
|
||||
processedContent = purify.sanitize(rawHtml as string, {
|
||||
const rawHtml = marked.parse(content) as string;
|
||||
processedContent = createDOMPurify.sanitize(rawHtml, {
|
||||
ALLOWED_TAGS: [
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
'p', 'a', 'ul', 'ol', 'li', 'blockquote',
|
||||
@@ -119,7 +120,7 @@ async function getPostByPath(filePath: string, relPath: string, pinnedData: { pi
|
||||
'src', 'alt', 'title', 'width', 'height',
|
||||
'frameborder', 'allowfullscreen'
|
||||
],
|
||||
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
|
||||
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+\.\-]+(?:[^a-z+\.\-:]|$))/i
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Error processing markdown for ${relPath}:`, err);
|
||||
|
||||
1
src/app/monaco-vim.d.ts
vendored
Normal file
1
src/app/monaco-vim.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'monaco-vim';
|
||||
@@ -148,11 +148,21 @@ export default function Home() {
|
||||
})),
|
||||
];
|
||||
|
||||
// Helper to strip YAML frontmatter
|
||||
function stripFrontmatter(md: string): string {
|
||||
if (!md) return '';
|
||||
if (md.startsWith('---')) {
|
||||
const end = md.indexOf('---', 3);
|
||||
if (end !== -1) return md.slice(end + 3).replace(/^\s+/, '');
|
||||
}
|
||||
return md;
|
||||
}
|
||||
|
||||
// Helper to recursively collect all posts from the tree
|
||||
function collectPosts(nodes: Node[]): Post[] {
|
||||
let posts: Post[] = [];
|
||||
for (const node of nodes) {
|
||||
if (node.type === 'post') {
|
||||
if (node.type === 'post' && node.slug !== 'about') {
|
||||
posts.push(node);
|
||||
} else if (node.type === 'folder') {
|
||||
posts = posts.concat(collectPosts(node.children));
|
||||
@@ -258,7 +268,7 @@ export default function Home() {
|
||||
)}
|
||||
<div>Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}</div>
|
||||
</div>
|
||||
<p className="text-gray-700 mb-3 sm:mb-4 text-sm sm:text-base">{post.summary}</p>
|
||||
<p className="text-gray-700 mb-3 sm:mb-4 text-sm sm:text-base">{stripFrontmatter(post.summary)}</p>
|
||||
<div className="flex flex-wrap gap-1 sm:gap-2">
|
||||
{post.tags.map((tag: string) => {
|
||||
const q = search.trim().toLowerCase();
|
||||
@@ -317,7 +327,7 @@ export default function Home() {
|
||||
|
||||
{/* Posts */}
|
||||
{(() => {
|
||||
const posts = nodes.filter((n) => n.type === 'post');
|
||||
const posts = nodes.filter((n) => n.type === 'post' && n.slug !== 'about');
|
||||
const pinnedPosts = posts.filter((post: any) => post.pinned);
|
||||
const unpinnedPosts = posts.filter((post: any) => !post.pinned);
|
||||
return [...pinnedPosts, ...unpinnedPosts].map((post: any) => (
|
||||
@@ -341,7 +351,7 @@ export default function Home() {
|
||||
)}
|
||||
<div>Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}</div>
|
||||
</div>
|
||||
<p className="text-gray-700 mb-3 sm:mb-4 text-sm sm:text-base">{post.summary}</p>
|
||||
<p className="text-gray-700 mb-3 sm:mb-4 text-sm sm:text-base">{stripFrontmatter(post.summary)}</p>
|
||||
<div className="flex flex-wrap gap-1 sm:gap-2">
|
||||
{post.tags.map((tag: string) => (
|
||||
<span
|
||||
|
||||
Reference in New Issue
Block a user