Compare commits
20 Commits
b0033a5671
...
subpath
| Author | SHA1 | Date | |
|---|---|---|---|
| c3c74bb21f | |||
| e3d8ba1017 | |||
| a9879d9fa4 | |||
| 7d387080f5 | |||
| 09a673b65b | |||
| 6665f65529 | |||
| 7629164387 | |||
| 21f13ef8ae | |||
| f94ddaa3b1 | |||
| 559abe3933 | |||
| 525e4fdc35 | |||
| 5a49f37750 | |||
| 2a7a0fadce | |||
| bb83c6db33 | |||
| a401732d7d | |||
| baad7309df | |||
| 4da88915f1 | |||
| 5e2c95b08d | |||
| 00fe8e7107 | |||
| 24ef59f0ed |
@@ -1,6 +1,9 @@
|
|||||||
#-------------------------------------------------------------------- # -----------------------------------------------------------------------#
|
#-------------------------------------------------------------------- # -----------------------------------------------------------------------#
|
||||||
# In here you have to set your socials / links # Explenations of Variables #
|
# In here you have to set your socials / links # Explenations of Variables #
|
||||||
#-------------------------------------------------------------------- # -----------------------------------------------------------------------#
|
#-------------------------------------------------------------------- # -----------------------------------------------------------------------#
|
||||||
|
# Modify This before deploying with docker / locally #
|
||||||
|
#---------------------------------------------------------------------#
|
||||||
|
#
|
||||||
NEXT_PUBLIC_BLOG_OWNER=Rattatwinko # Your Name goes here #
|
NEXT_PUBLIC_BLOG_OWNER=Rattatwinko # Your Name goes here #
|
||||||
NEXT_ABOUT_ME_LINK="http://localhost:80" # Your WebPage goes here #
|
NEXT_ABOUT_ME_LINK="http://localhost:80" # Your WebPage goes here #
|
||||||
NEXT_SOCIAL_INSTAGRAM="http://instagram.com/rattatwinko" # Your Instagram Link goes here #
|
NEXT_SOCIAL_INSTAGRAM="http://instagram.com/rattatwinko" # Your Instagram Link goes here #
|
||||||
@@ -8,4 +11,5 @@ NEXT_SOCIAL_TWITTER="https://twitter.com/user" # I dont h
|
|||||||
NEXT_SOCIAL_GITHUB_STATE="true" # I Have GitHub so this is True (if you dont then set this to false) #
|
NEXT_SOCIAL_GITHUB_STATE="true" # I Have GitHub so this is True (if you dont then set this to false) #
|
||||||
NEXT_SOCIAL_GITHUB_LINK_IF_TRUE="http://github.com/ZockerKatze" # If you have GitHub then paste your link here #
|
NEXT_SOCIAL_GITHUB_LINK_IF_TRUE="http://github.com/ZockerKatze" # If you have GitHub then paste your link here #
|
||||||
NEXT_SOCIAL_BUYMEACOFFEE="https://coff.ee/rattatwinko"
|
NEXT_SOCIAL_BUYMEACOFFEE="https://coff.ee/rattatwinko"
|
||||||
PORT=8080 # This is unused. You can safely delete if you want. #
|
PORT=8080 # This is unused. You can safely delete if you want. #
|
||||||
|
BASE_URL=/blog # This is the subpath!
|
||||||
@@ -10,19 +10,52 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
run: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install Node.js dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run ESLint
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
markdown_backend/target
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cargo-
|
||||||
|
|
||||||
|
- name: Check Rust code
|
||||||
|
working-directory: markdown_backend
|
||||||
|
run: cargo check
|
||||||
|
|
||||||
- name: Install Docker
|
- name: Install Docker
|
||||||
run: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
run: docker build -t markdownblog .
|
run: docker build -t localhost:3002/rattatwinko/markdownblog:latest .
|
||||||
|
|
||||||
- name: Save Docker image as tarball
|
- name: Save Docker image as tarball
|
||||||
run: docker save markdownblog -o markdownblog-image.tar
|
run: docker save localhost:3002/rattatwinko/markdownblog:latest -o markdownblog-image.tar
|
||||||
|
|
||||||
- name: Upload Docker image artifact
|
- name: Upload Docker image artifact
|
||||||
run: actions/upload-artifact@v3 --name markdownblog-docker-image --path markdownblog-image.tar
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
- name: Push Docker image
|
name: markdownblog-docker-image
|
||||||
run: docker push 10.0.0.13:3002/rattatwinko/markdownblog:latest
|
path: markdownblog-image.tar
|
||||||
242
README.md
242
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
|
## 🚀 Key Features
|
||||||
|
|
||||||
- **📝 Markdown Blog Posts**: Write posts with frontmatter metadata (title, date, tags, summary)
|
- **📝 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
|
- **🎨 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
|
- **📌 Pin Posts**: Pin important posts to the top of your blog
|
||||||
- **📁 Folder Organization**: Organize posts in nested folders
|
- **📁 Folder Organization**: Organize posts in nested folders
|
||||||
- **🖥️ Desktop App**: Electron-based desktop application
|
- **🖥️ 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
|
- **📱 Responsive Design**: Mobile-friendly UI with Tailwind CSS
|
||||||
- **🎯 Content Management**: Drag & drop file uploads, post editing, and deletion
|
- **🎯 Content Management**: Drag & drop file uploads, post editing, and deletion
|
||||||
- **📦 Export Functionality**: Export all posts as tar.gz archive (Docker only)
|
- **📦 Export Functionality**: Export all posts as tar.gz archive (Docker only)
|
||||||
|
- **💾 Smart Caching**: RAM-based caching system for instant post retrieval
|
||||||
|
- **🔧 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
|
## 🛠️ Technology Stack
|
||||||
|
|
||||||
- **Frontend**: Next.js 14, React 18, TypeScript
|
- **Frontend**: Next.js 14, React 18, TypeScript
|
||||||
|
- **Backend**: Rust (markdown parsing, file watching, caching)
|
||||||
- **Styling**: Tailwind CSS, @tailwindcss/typography
|
- **Styling**: Tailwind CSS, @tailwindcss/typography
|
||||||
- **Markdown**: marked, gray-matter, highlight.js
|
- **Markdown**: pulldown-cmark, syntect (syntax highlighting), ammonia (HTML sanitization)
|
||||||
- **Desktop**: Electron
|
- **Desktop**: Electron
|
||||||
- **Security**: bcrypt, DOMPurify
|
- **Security**: bcrypt, DOMPurify
|
||||||
- **Deployment**: Docker, PM2
|
- **Deployment**: Docker, PM2
|
||||||
- **Development**: ESLint, PostCSS, Autoprefixer
|
- **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/
|
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/
|
├── src/
|
||||||
│ ├── app/ # Next.js 14 App Router
|
│ ├── app/ # Next.js 14 App Router
|
||||||
│ │ ├── admin/ # Admin dashboard pages
|
│ │ ├── admin/ # Admin dashboard pages
|
||||||
|
│ │ │ ├── editor/ # VS Code-style editor
|
||||||
|
│ │ │ │ └── page.tsx # Markdown editor with Monaco
|
||||||
│ │ │ ├── manage/ # Content management interface
|
│ │ │ ├── manage/ # Content management interface
|
||||||
│ │ │ │ └── page.tsx # Manage posts and folders
|
│ │ │ │ ├── page.tsx # Manage posts and folders
|
||||||
|
│ │ │ │ └── rust-status/ # Rust backend monitoring
|
||||||
|
│ │ │ │ └── page.tsx # Parser logs and performance metrics
|
||||||
│ │ │ └── page.tsx # Main admin dashboard
|
│ │ │ └── page.tsx # Main admin dashboard
|
||||||
│ │ ├── api/ # API routes (Next.js API routes)
|
│ │ ├── api/ # API routes (Next.js API routes)
|
||||||
│ │ │ ├── admin/ # Admin API endpoints
|
│ │ │ ├── admin/ # Admin API endpoints
|
||||||
@@ -70,10 +89,16 @@ markdownblog/
|
|||||||
│ │ │ └── posts/ # Public post API
|
│ │ │ └── posts/ # Public post API
|
||||||
│ │ │ ├── [slug]/ # Dynamic post API routes
|
│ │ │ ├── [slug]/ # Dynamic post API routes
|
||||||
│ │ │ │ └── route.ts
|
│ │ │ │ └── route.ts
|
||||||
|
│ │ │ ├── preview/ # Markdown preview API
|
||||||
|
│ │ │ │ └── route.ts
|
||||||
|
│ │ │ ├── stream/ # Server-Sent Events for real-time updates
|
||||||
|
│ │ │ │ └── route.ts
|
||||||
|
│ │ │ ├── webhook/ # Webhook endpoint
|
||||||
|
│ │ │ │ └── route.ts
|
||||||
│ │ │ └── route.ts # List all posts
|
│ │ │ └── route.ts # List all posts
|
||||||
│ │ ├── posts/ # Blog post pages
|
│ │ ├── posts/ # Blog post pages
|
||||||
│ │ │ └── [...slug]/ # Dynamic post routing (catch-all)
|
│ │ │ └── [...slug]/ # Dynamic post routing (catch-all)
|
||||||
│ │ │ └── page.tsx # Individual post page with anchor linking
|
│ │ │ └── page.tsx # Individual post page with anchor linking and SSE
|
||||||
│ │ ├── AboutButton.tsx # About page button component
|
│ │ ├── AboutButton.tsx # About page button component
|
||||||
│ │ ├── BadgeButton.tsx # Badge display component
|
│ │ ├── BadgeButton.tsx # Badge display component
|
||||||
│ │ ├── globals.css # Global styles and Tailwind imports
|
│ │ ├── globals.css # Global styles and Tailwind imports
|
||||||
@@ -81,16 +106,15 @@ markdownblog/
|
|||||||
│ │ ├── highlight-github.css # Code syntax highlighting styles
|
│ │ ├── highlight-github.css # Code syntax highlighting styles
|
||||||
│ │ ├── layout.tsx # Root layout with metadata
|
│ │ ├── layout.tsx # Root layout with metadata
|
||||||
│ │ ├── MobileNav.tsx # Mobile navigation component
|
│ │ ├── MobileNav.tsx # Mobile navigation component
|
||||||
|
│ │ ├── monaco-vim.d.ts # Monaco Vim typings
|
||||||
│ │ └── page.tsx # Homepage with post listing
|
│ │ └── page.tsx # Homepage with post listing
|
||||||
│ └── lib/ # Utility libraries
|
│ └── lib/ # Utility libraries
|
||||||
│ ├── markdown.ts # Markdown processing with marked.js
|
│ └── postsDirectory.ts # Post directory management and Rust integration
|
||||||
│ └── postsDirectory.ts # Post directory management and parsing
|
|
||||||
├── posts/ # Markdown blog posts storage
|
├── posts/ # Markdown blog posts storage
|
||||||
│ ├── pinned.json # Pinned posts configuration
|
│ ├── about.md
|
||||||
│ ├── welcome.md # Welcome post with frontmatter
|
│ ├── welcome.md
|
||||||
│ ├── mdtest.md # Test post with various markdown features
|
│ └── assets/
|
||||||
│ ├── anchor-test.md # Test post for anchor linking
|
│ └── peta.png
|
||||||
│ └── ii/ # Example nested folder structure
|
|
||||||
├── public/ # Static assets
|
├── public/ # Static assets
|
||||||
│ ├── android-chrome-192x192.png
|
│ ├── android-chrome-192x192.png
|
||||||
│ ├── android-chrome-512x512.png
|
│ ├── android-chrome-512x512.png
|
||||||
@@ -100,38 +124,45 @@ markdownblog/
|
|||||||
│ ├── favicon.ico
|
│ ├── favicon.ico
|
||||||
│ └── site.webmanifest
|
│ └── site.webmanifest
|
||||||
├── electron/ # Desktop application
|
├── electron/ # Desktop application
|
||||||
│ └── main.js # Electron main process configuration
|
│ └── main.js # Electron main process configuration
|
||||||
├── Dockerfile # Docker container configuration
|
├── Dockerfile # Docker container configuration
|
||||||
├── docker.sh # Docker deployment script
|
├── docker.sh # Docker deployment script
|
||||||
├── entrypoint.sh # Container entrypoint script
|
├── entrypoint.sh # Container entrypoint script
|
||||||
├── next-env.d.ts # Next.js TypeScript definitions
|
├── run-local-backend.sh # Local Rust backend runner
|
||||||
├── next.config.js # Next.js configuration
|
├── next-env.d.ts # Next.js TypeScript definitions
|
||||||
├── package-lock.json # npm lock file
|
├── next.config.js # Next.js configuration
|
||||||
├── package.json # Dependencies and scripts
|
├── package-lock.json # npm lock file
|
||||||
├── postcss.config.js # PostCSS configuration
|
├── package.json # Dependencies and scripts
|
||||||
├── tailwind.config.js # Tailwind CSS configuration
|
├── postcss.config.js # PostCSS configuration
|
||||||
├── tsconfig.json # TypeScript configuration
|
├── tailwind.config.js # Tailwind CSS configuration
|
||||||
└── LICENSE # MIT License
|
├── tsconfig.json # TypeScript configuration
|
||||||
|
└── LICENSE # MIT License
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Components
|
### 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)
|
#### Frontend (Next.js 14 App Router)
|
||||||
- **`src/app/page.tsx`**: Homepage with responsive post listing and search
|
- **`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/page.tsx`**: Admin dashboard with content management
|
||||||
- **`src/app/admin/manage/page.tsx`**: Advanced content management interface
|
- **`src/app/admin/manage/page.tsx`**: Advanced content management interface
|
||||||
|
- **`src/app/admin/rust-status/page.tsx`**: Rust backend monitoring and logs
|
||||||
|
|
||||||
#### API Routes
|
#### 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
|
- **Folder Management**: Create, delete, and organize content structure
|
||||||
- **Authentication**: Password management and validation
|
- **Authentication**: Password management and validation
|
||||||
- **Export**: Docker and local export functionality
|
- **Export**: Docker and local export functionality
|
||||||
- **Upload**: Drag & drop file upload handling
|
- **Upload**: Drag & drop file upload handling
|
||||||
|
- **SSE Streaming**: Real-time updates via Server-Sent Events
|
||||||
|
|
||||||
#### Utilities
|
#### Utilities
|
||||||
- **`src/lib/markdown.ts`**: Markdown processing with syntax highlighting
|
- **`src/lib/postsDirectory.ts`**: File system operations and Rust backend integration
|
||||||
- **`src/lib/postsDirectory.ts`**: File system operations and post parsing
|
|
||||||
|
|
||||||
#### Desktop App
|
#### Desktop App
|
||||||
- **`electron/main.js`**: Electron configuration for desktop application
|
- **`electron/main.js`**: Electron configuration for desktop application
|
||||||
@@ -140,6 +171,7 @@ markdownblog/
|
|||||||
- **`Dockerfile`**: Multi-stage build for production deployment
|
- **`Dockerfile`**: Multi-stage build for production deployment
|
||||||
- **`docker.sh`**: Automated deployment script with volume management
|
- **`docker.sh`**: Automated deployment script with volume management
|
||||||
- **`entrypoint.sh`**: Container initialization and post setup
|
- **`entrypoint.sh`**: Container initialization and post setup
|
||||||
|
- **`run-local-backend.sh`**: Local Rust backend runner
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -149,6 +181,7 @@ markdownblog/
|
|||||||
|
|
||||||
- **Node.js 18+**
|
- **Node.js 18+**
|
||||||
- **npm** or **yarn**
|
- **npm** or **yarn**
|
||||||
|
- **Rust** (for local development)
|
||||||
- **Docker** (for containerized deployment)
|
- **Docker** (for containerized deployment)
|
||||||
|
|
||||||
### Local Development
|
### Local Development
|
||||||
@@ -160,13 +193,20 @@ markdownblog/
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Start development server**:
|
2. **Build Rust backend**:
|
||||||
|
```bash
|
||||||
|
cd markdown_backend
|
||||||
|
cargo build --release
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start development server**:
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
Visit [http://localhost:3000](http://localhost:3000)
|
Visit [http://localhost:3000](http://localhost:3000)
|
||||||
|
|
||||||
3. **Desktop app development**:
|
4. **Desktop app development**:
|
||||||
```bash
|
```bash
|
||||||
npm run electron-dev
|
npm run electron-dev
|
||||||
```
|
```
|
||||||
@@ -174,6 +214,10 @@ markdownblog/
|
|||||||
### Production Build
|
### Production Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Build Rust backend
|
||||||
|
cd markdown_backend && cargo build --release && cd ..
|
||||||
|
|
||||||
|
# Build Next.js frontend
|
||||||
npm run build
|
npm run build
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
@@ -192,7 +236,7 @@ chmod +x docker.sh
|
|||||||
```
|
```
|
||||||
|
|
||||||
This script will:
|
This script will:
|
||||||
- Build the Docker image
|
- Build the Docker image (including Rust backend)
|
||||||
- Create a persistent volume for posts
|
- Create a persistent volume for posts
|
||||||
- Run the container on port 8080
|
- Run the container on port 8080
|
||||||
- Copy built-in posts to the volume
|
- Copy built-in posts to the volume
|
||||||
@@ -209,7 +253,7 @@ This script will:
|
|||||||
docker run -d \
|
docker run -d \
|
||||||
--name markdownblog \
|
--name markdownblog \
|
||||||
-p 8080:3000 \
|
-p 8080:3000 \
|
||||||
-v markdownblog-posts:/app/docker \
|
-v markdownblog-posts:/app/posts \
|
||||||
markdownblog
|
markdownblog
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -218,7 +262,7 @@ This script will:
|
|||||||
docker run -d \
|
docker run -d \
|
||||||
--name markdownblog \
|
--name markdownblog \
|
||||||
-p 8080:3000 \
|
-p 8080:3000 \
|
||||||
-v /path/to/your/posts:/app/docker \
|
-v /path/to/your/posts:/app/posts \
|
||||||
markdownblog
|
markdownblog
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -228,6 +272,7 @@ This script will:
|
|||||||
- **Export Functionality**: Export all posts as tar.gz (Docker only)
|
- **Export Functionality**: Export all posts as tar.gz (Docker only)
|
||||||
- **Auto-restart**: Container automatically restarts on failure
|
- **Auto-restart**: Container automatically restarts on failure
|
||||||
- **Built-in Posts**: Welcome and test posts included
|
- **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"
|
date: "2024-01-15"
|
||||||
tags: ["technology", "programming", "web"]
|
tags: ["technology", "programming", "web"]
|
||||||
summary: "A brief description of your post content"
|
summary: "A brief description of your post content"
|
||||||
|
author: "Your Name"
|
||||||
---
|
---
|
||||||
|
|
||||||
Your post content here...
|
Your post content here...
|
||||||
|
|
||||||
## Headers
|
## Headers
|
||||||
|
|
||||||
Regular Markdown syntax is supported.
|
Regular Markdown syntax is supported with automatic anchor linking.
|
||||||
|
|
||||||
### Code Blocks
|
### Code Blocks
|
||||||
|
|
||||||
@@ -262,13 +308,13 @@ console.log("Hello, World!");
|
|||||||
- Item 1
|
- Item 1
|
||||||
- Item 2
|
- Item 2
|
||||||
- Nested item
|
- Nested item
|
||||||
```
|
|
||||||
|
|
||||||
### Post Organization
|
### Post Organization
|
||||||
|
|
||||||
- **Root Level**: Posts directly in `posts/` folder
|
- **Root Level**: Posts directly in `posts/` folder
|
||||||
- **Folders**: Create subdirectories for organization
|
- **Folders**: Create subdirectories for organization
|
||||||
- **Nested Structure**: Support for unlimited nesting levels
|
- **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
|
- **📤 Upload Files**: Drag & drop Markdown files
|
||||||
- **🔐 Change Password**: Secure password management
|
- **🔐 Change Password**: Secure password management
|
||||||
- **📦 Export Posts**: Download all posts as archive (Docker only)
|
- **📦 Export Posts**: Download all posts as archive (Docker only)
|
||||||
|
- **📊 Rust Status**: Monitor parser performance, logs, and health
|
||||||
|
- **🔍 Log Management**: View, filter, and clear parser logs
|
||||||
|
- **🔧 VS Code Editor**: Monaco-based editor with YAML frontmatter preservation
|
||||||
|
- **🔄 Force Reparse**: Manual cache clearing and post reparsing
|
||||||
|
- **📁 Reliable Scanning**: Enhanced directory traversal with error recovery
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- **Password Hashing**: bcrypt with salt
|
- **Password Hashing**: bcrypt with salt
|
||||||
- **Session Management**: Local storage-based authentication
|
- **Session Management**: Local storage-based authentication
|
||||||
- **Input Sanitization**: DOMPurify for XSS protection
|
- **Input Sanitization**: Ammonia for XSS protection
|
||||||
- **File Validation**: Markdown file type checking
|
- **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
|
## 🎨 Customization
|
||||||
|
|
||||||
### Styling
|
### Styling
|
||||||
|
|
||||||
- **Tailwind CSS**: Utility-first CSS framework
|
- **Tailwind CSS**: Utility-first CSS framework
|
||||||
- **Typography**: @tailwindcss/typography for content styling
|
- **Typography**: @tailwindcss/typography for content styling
|
||||||
- **Syntax Highlighting**: highlight.js with GitHub theme
|
- **Syntax Highlighting**: syntect with GitHub theme
|
||||||
- **Responsive Design**: Mobile-first approach
|
- **Responsive Design**: Mobile-first approach
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
@@ -315,6 +400,7 @@ console.log("Hello, World!");
|
|||||||
- **Tailwind Config**: `tailwind.config.js`
|
- **Tailwind Config**: `tailwind.config.js`
|
||||||
- **TypeScript Config**: `tsconfig.json`
|
- **TypeScript Config**: `tsconfig.json`
|
||||||
- **PostCSS Config**: `postcss.config.js`
|
- **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
|
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
|
## 📄 License
|
||||||
@@ -342,7 +440,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|||||||
1. Fork the repository
|
1. Fork the repository
|
||||||
2. Create a feature branch
|
2. Create a feature branch
|
||||||
3. Make your changes
|
3. Make your changes
|
||||||
4. Test thoroughly
|
4. Test thoroughly (both frontend and Rust backend)
|
||||||
5. Submit a pull request
|
5. Submit a pull request
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -354,8 +452,74 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|||||||
- **Port conflicts**: Change port in `docker.sh` or Docker run command
|
- **Port conflicts**: Change port in `docker.sh` or Docker run command
|
||||||
- **Permission errors**: Ensure `docker.sh` is executable (`chmod +x docker.sh`)
|
- **Permission errors**: Ensure `docker.sh` is executable (`chmod +x docker.sh`)
|
||||||
- **Volume issues**: Check Docker volume exists and has proper permissions
|
- **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
|
### 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
|
||||||
|
|
||||||
|
## Configuring a Base URL for Proxy Hosting
|
||||||
|
|
||||||
|
If you want to host your app behind a subpath (e.g. `http://localhost:3000/blog/`), set the base URL in `.env.local`:
|
||||||
|
|
||||||
|
```
|
||||||
|
BASE_URL=/blog
|
||||||
|
```
|
||||||
|
|
||||||
|
This will automatically prefix all internal links, API calls, and static assets with `/blog`. Make sure your reverse proxy (e.g. nginx) is configured to forward requests from `/blog` to your app.
|
||||||
|
|
||||||
|
### Example nginx config
|
||||||
|
|
||||||
|
```
|
||||||
|
location /blog/ {
|
||||||
|
proxy_pass http://localhost:3000/blog/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
```
|
||||||
34
docker.sh
34
docker.sh
@@ -44,5 +44,37 @@ if ! docker ps | grep -q $CONTAINER_NAME; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Output with colors
|
||||||
|
GREEN='\033[1;32m' # Green
|
||||||
|
CYAN='\033[1;36m'
|
||||||
|
RESET='\033[0m'
|
||||||
|
|
||||||
|
echo ""
|
||||||
echo "Deployment complete!"
|
echo "Deployment complete!"
|
||||||
echo "App should be available at http://localhost:$PORT"
|
echo ""
|
||||||
|
echo -e " App is running at: ${GREEN}http://localhost:${PORT}${RESET}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Rainbow ASCII Art
|
||||||
|
RAINBOW=(
|
||||||
|
'\033[1;31m' # Red
|
||||||
|
'\033[1;33m' # Yellow
|
||||||
|
'\033[1;32m' # Green
|
||||||
|
'\033[1;36m' # Cyan
|
||||||
|
'\033[1;34m' # Blue
|
||||||
|
'\033[1;35m' # Magenta
|
||||||
|
)
|
||||||
|
|
||||||
|
ASCII=(
|
||||||
|
" __ ___ __ __ ____ __ "
|
||||||
|
" / |/ /___ ______/ /______/ /___ _ ______ / __ )/ /___ ____ _"
|
||||||
|
" / /|_/ / __ \`/ ___/ //_/ __ / __ \\ | /| / / __ \\/ __ / / __ \\/ __ \`/"
|
||||||
|
" / / / / /_/ / / / ,< / /_/ / /_/ / |/ |/ / / / / /_/ / / /_/ / /_/ / "
|
||||||
|
"/_/ /_/\\__,_/_/ /_/|_|\\__,_/\\____/|__/|__/_/ /_/_____/_/\\____/\\__, / "
|
||||||
|
" /____/ "
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in "${!ASCII[@]}"; do
|
||||||
|
color="${RAINBOW[$((i % ${#RAINBOW[@]}))]}"
|
||||||
|
echo -e "${color}${ASCII[$i]}${RESET}"
|
||||||
|
done
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ const { app, BrowserWindow } = require('electron');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
This will be discontinued in a bit.
|
||||||
|
Either move to Docker or get fucked.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
@@ -14,7 +21,9 @@ function createWindow() {
|
|||||||
|
|
||||||
// Load the Next.js app
|
// Load the Next.js app
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
mainWindow.loadURL('http://localhost:3000');
|
const baseUrl = process.env.BASE_URL || '';
|
||||||
|
const url = `http://localhost:3000${baseUrl}`;
|
||||||
|
mainWindow.loadURL(url);
|
||||||
mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools();
|
||||||
} else {
|
} else {
|
||||||
mainWindow.loadFile(path.join(__dirname, '../.next/server/pages/index.html'));
|
mainWindow.loadFile(path.join(__dirname, '../.next/server/pages/index.html'));
|
||||||
|
|||||||
357
flowcharts/backend.drawio
Normal file
357
flowcharts/backend.drawio
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0" version="27.2.0">
|
||||||
|
<diagram name="Markdown Backend Flowchart" id="eQoA7ipTtm_-JJKlHUD_">
|
||||||
|
<mxGraphModel dx="2374" dy="2271" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-440" value="CLI Command" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ff8f00;fontStyle=1;fontSize=14;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2040" y="900" width="110" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-441" value="Command Type" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ff8f00;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1855" y="900" width="110" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-442" value="get_all_posts" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2055" y="995" width="90" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-443" value="get_post_by_slug" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2045" y="1055" width="110" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-444" value="get_posts_by_tag" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2045" y="1115" width="110" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-445" value="watch_posts" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2060" y="1195" width="80" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-446" value="checkhealth" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2060" y="1255" width="80" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-447" value="force_reinterpret_all_posts" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2025" y="1315" width="150" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-539" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-448" target="KWtHj6Fe61qkRVlhPuuz-449">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-448" value="<div>Cache</div>" style="rhombus;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2250" y="995" width="60" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-540" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-449" target="KWtHj6Fe61qkRVlhPuuz-475">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-449" value="<div>Cache</div>" style="rhombus;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2250" y="1055" width="60" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-450" value="find_markdown_files" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2400" y="955" width="120" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-541" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-451" target="KWtHj6Fe61qkRVlhPuuz-453">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-451" value="get_posts_directory" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2400" y="1005" width="120" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-452" value="Scan for .md files" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2405" y="1085" width="110" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-542" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-453" target="KWtHj6Fe61qkRVlhPuuz-452">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-453" value="slug_to_path" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2415" y="1045" width="90" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-454" value="Read File Content" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2585" y="1045" width="110" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-455" value="File Size Check" style="rhombus;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2590" y="1095" width="100" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-543" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-456" target="KWtHj6Fe61qkRVlhPuuz-473">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-456" value="Parse Frontmatter" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2585" y="1135" width="110" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-457" value="gray_matter::parse" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2760" y="1135" width="120" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-458" value="process_anchor_links" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2925" y="1135" width="130" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-459" value="process_custom_tags" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2925" y="1185" width="130" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-460" value="Markdown Parser" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2765" y="1185" width="110" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-461" value="Parser::new_ext" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2770" y="1225" width="100" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-462" value="Event Processing Loop" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2920" y="1225" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-463" value="push_html" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2955" y="1275" width="70" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-464" value="AMMONIA Sanitization" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2920" y="1325" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-465" value="Create Post Struct" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2935" y="1365" width="110" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-545" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-466" target="KWtHj6Fe61qkRVlhPuuz-467">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<mxPoint x="2810" y="1320" as="targetPoint" />
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="2830" y="1430" />
|
||||||
|
<mxPoint x="2830" y="1270" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-466" value="Final Post Object" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2935" y="1415" width="110" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-467" value="Insert into POST_CACHE" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2565" y="1255" width="150" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-468" value="Update POST_STATS" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2575" y="1295" width="130" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-469" value="Return Post" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2600" y="1335" width="80" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-470" value="Sort by Date" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2420" y="1125" width="80" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-471" value="Update ALL_POSTS_CACHE" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2375" y="1165" width="170" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-472" value="Return Posts" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2415" y="1205" width="90" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-547" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-473" target="KWtHj6Fe61qkRVlhPuuz-467">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-473" value="Error: File too large" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffebee;strokeColor=#c62828;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2580" y="1175" width="120" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-475" value="Filter by Tag" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2240" y="1125" width="80" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-476" value="Return Filtered Posts" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2215" y="1165" width="130" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-477" value="Create Watcher" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2400" y="1255" width="100" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-478" value="Watch Directory" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2400" y="1295" width="100" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-479" value="Clear Caches" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2405" y="1335" width="90" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-480" value="Check Posts Directory" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2205" y="1255" width="130" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-481" value="Generate HealthReport" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2200" y="1295" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-482" value="Clear All Caches" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2045" y="1385" width="110" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-483" value="Reprocess All Files" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2040" y="1425" width="120" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-484" value="Save to Disk" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2060" y="1465" width="80" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-485" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-440" target="KWtHj6Fe61qkRVlhPuuz-441">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-486" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-441" target="KWtHj6Fe61qkRVlhPuuz-442">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="1910" y="1010" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-487" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-441" target="KWtHj6Fe61qkRVlhPuuz-443">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="1910" y="1010" />
|
||||||
|
<mxPoint x="2020" y="1070" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-488" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-441" target="KWtHj6Fe61qkRVlhPuuz-444">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="1910" y="1010" />
|
||||||
|
<mxPoint x="2030" y="1130" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-489" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-441" target="KWtHj6Fe61qkRVlhPuuz-445">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="1910" y="1010" />
|
||||||
|
<mxPoint x="2040" y="1210" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-490" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-441" target="KWtHj6Fe61qkRVlhPuuz-446">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="1910" y="1010" />
|
||||||
|
<mxPoint x="2040" y="1270" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-491" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-441" target="KWtHj6Fe61qkRVlhPuuz-447">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="1910" y="1010" />
|
||||||
|
<mxPoint x="2000" y="1300" />
|
||||||
|
<mxPoint x="2020" y="1330" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-492" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-442" target="KWtHj6Fe61qkRVlhPuuz-448">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-493" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-443" target="KWtHj6Fe61qkRVlhPuuz-449">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-494" style="exitX=1.009;exitY=0.493;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-448" target="KWtHj6Fe61qkRVlhPuuz-450">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="2360" y="970" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-495" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-450" target="KWtHj6Fe61qkRVlhPuuz-451">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-497" style="exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-449" target="KWtHj6Fe61qkRVlhPuuz-453">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-498" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-453" target="KWtHj6Fe61qkRVlhPuuz-454">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-499" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-454" target="KWtHj6Fe61qkRVlhPuuz-455">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-500" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-455" target="KWtHj6Fe61qkRVlhPuuz-456">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-501" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-456" target="KWtHj6Fe61qkRVlhPuuz-457">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-502" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-457" target="KWtHj6Fe61qkRVlhPuuz-458">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-503" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-458" target="KWtHj6Fe61qkRVlhPuuz-459">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-504" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-459" target="KWtHj6Fe61qkRVlhPuuz-460">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-505" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-460" target="KWtHj6Fe61qkRVlhPuuz-461">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-506" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-461" target="KWtHj6Fe61qkRVlhPuuz-462">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-507" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-462" target="KWtHj6Fe61qkRVlhPuuz-463">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-508" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-463" target="KWtHj6Fe61qkRVlhPuuz-464">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-509" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-464" target="KWtHj6Fe61qkRVlhPuuz-465">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-510" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-465" target="KWtHj6Fe61qkRVlhPuuz-466">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-512" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-467" target="KWtHj6Fe61qkRVlhPuuz-468">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-513" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-468" target="KWtHj6Fe61qkRVlhPuuz-469">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-514" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-452" target="KWtHj6Fe61qkRVlhPuuz-470">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-515" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-470" target="KWtHj6Fe61qkRVlhPuuz-471">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-516" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-471" target="KWtHj6Fe61qkRVlhPuuz-472">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-519" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-444" target="KWtHj6Fe61qkRVlhPuuz-475">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-520" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-475" target="KWtHj6Fe61qkRVlhPuuz-476">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-521" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-445" target="KWtHj6Fe61qkRVlhPuuz-477">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="2270" y="1210" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-522" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-477" target="KWtHj6Fe61qkRVlhPuuz-478">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-523" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-478" target="KWtHj6Fe61qkRVlhPuuz-479">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-524" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-446" target="KWtHj6Fe61qkRVlhPuuz-480">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-525" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-480" target="KWtHj6Fe61qkRVlhPuuz-481">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-526" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-447" target="KWtHj6Fe61qkRVlhPuuz-482">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-527" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-482" target="KWtHj6Fe61qkRVlhPuuz-483">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-528" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-483" target="KWtHj6Fe61qkRVlhPuuz-484">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-531" value="Hit" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2350" y="965" width="40" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-532" value="Miss" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2280" y="1025" width="40" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-533" value="Hit" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2350" y="1035" width="40" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-534" value="Miss" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2270" y="1085" width="40" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-535" value="OK" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2630" y="1065" width="40" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-536" value="Too Large" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2625" y="1115" width="70" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-537" value="&nbsp;" style="text;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2310" y="1300" width="30" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-546" value="&nbsp;" style="text;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2360" y="900" width="30" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-549" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="KWtHj6Fe61qkRVlhPuuz-548" target="KWtHj6Fe61qkRVlhPuuz-440">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="KWtHj6Fe61qkRVlhPuuz-548" value="<div>Frontend API</div>" style="shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="2035" y="740" width="120" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
266
flowcharts/docker_build_flowchart.drawio
Normal file
266
flowcharts/docker_build_flowchart.drawio
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0" version="27.2.0">
|
||||||
|
<diagram name="Docker Build Flowchart" id="docker-build-flow">
|
||||||
|
<mxGraphModel dx="1426" dy="795" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="1200" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
<mxCell id="start" value="Start Docker Build" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontStyle=1;fontSize=14;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="40" y="40" width="140" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="docker-check" value="Check Docker Daemon" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ff8f00;fontSize=12;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="220" y="50" width="140" height="40" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="docker-error" value="Error: Docker daemon not running" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffebee;strokeColor=#c62828;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="220" y="120" width="140" height="40" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="cleanup-start" value="Cleanup Phase" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontStyle=1;fontSize=12;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="400" y="50" width="140" height="40" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="stop-containers" value="Stop & Remove Containers" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="400" y="110" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="remove-volume" value="Remove Docker Volume" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="400" y="160" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="build-start" value="Build Phase" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontStyle=1;fontSize=12;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="580" y="50" width="140" height="40" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="stage1-start" value="Stage 1: Rust Build" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="580" y="110" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="rust-base" value="FROM rust:latest" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="580" y="160" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="copy-rust" value="COPY ./markdown_backend" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="580" y="210" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="install-musl" value="Install musl-tools" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="580" y="260" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="cargo-build" value="cargo build --release" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="580" y="310" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="stage2-start" value="Stage 2: Node.js Build" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="580" y="360" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="node-base" value="FROM node:20" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="580" y="410" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="copy-package" value="COPY package*.json" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="580" y="460" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="npm-install" value="RUN npm install" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="580" y="510" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Baa9tTqOcb9ER4_EkQ3s-4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" target="setup-start">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<mxPoint x="700" y="573" as="sourcePoint" />
|
||||||
|
<mxPoint x="727.3399999999999" y="70" as="targetPoint" />
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="730" y="574" />
|
||||||
|
<mxPoint x="730" y="70" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="copy-source" value="COPY . ." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="580" y="560" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="setup-start" value="Setup Phase" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontStyle=1;fontSize=12;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="750" y="50" width="140" height="40" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="create-posts" value="Create /app/posts" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="750" y="110" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="copy-posts" value="COPY posts/*" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="750" y="160" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="copy-binary" value="COPY Rust Binary" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="750" y="210" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="set-permissions" value="Set Permissions" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="750" y="260" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="npm-build" value="npm run build" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="750" y="310" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="create-docker-dir" value="Create /app/docker" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="750" y="360" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="set-env" value="ENV DOCKER_CONTAINER=true" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="750" y="410" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="expose-port" value="EXPOSE 3000" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="750" y="460" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="setup-entrypoint" value="Setup Entrypoint" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1f5fe;strokeColor=#0277bd;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="750" y="510" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="build-complete" value="Build Complete" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=12;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="930" y="50" width="140" height="40" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="run-start" value="Run Phase" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ff8f00;fontStyle=1;fontSize=12;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="930" y="110" width="140" height="40" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="docker-run" value="docker run" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ff8f00;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="930" y="170" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="port-mapping" value="Port Mapping 8080:3000" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ff8f00;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="930" y="220" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="volume-mount" value="Volume Mount" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff3e0;strokeColor=#ff8f00;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="930" y="270" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="entrypoint-start" value="Entrypoint Execution" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="930" y="320" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="check-volume" value="Check Volume Empty?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="930" y="370" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="copy-builtin" value="Copy Built-in Posts" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="930" y="420" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="start-app" value="Start Application" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f3e5f5;strokeColor=#7b1fa2;fontSize=11;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="930" y="470" width="140" height="30" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="container-running" value="Container Running" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=12;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="930" y="520" width="140" height="40" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge1" parent="1" source="start" target="docker-check" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge2" parent="1" source="docker-check" target="docker-error" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge3" parent="1" source="docker-check" target="cleanup-start" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge4" parent="1" source="cleanup-start" target="stop-containers" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge5" parent="1" source="stop-containers" target="remove-volume" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge6" parent="1" source="remove-volume" target="build-start" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="560" y="175" />
|
||||||
|
<mxPoint x="560" y="70" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge7" parent="1" source="build-start" target="stage1-start" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge8" parent="1" source="stage1-start" target="rust-base" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge9" parent="1" source="rust-base" target="copy-rust" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge10" parent="1" source="copy-rust" target="install-musl" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge11" parent="1" source="install-musl" target="cargo-build" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge13" parent="1" source="stage2-start" target="node-base" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge14" parent="1" source="node-base" target="copy-package" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge15" parent="1" source="copy-package" target="npm-install" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge16" parent="1" source="npm-install" target="copy-source" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge18" parent="1" source="setup-start" target="create-posts" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge19" parent="1" source="create-posts" target="copy-posts" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge20" parent="1" source="copy-posts" target="copy-binary" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge21" parent="1" source="copy-binary" target="set-permissions" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge22" parent="1" source="set-permissions" target="npm-build" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge23" parent="1" source="npm-build" target="create-docker-dir" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge24" parent="1" source="create-docker-dir" target="set-env" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge25" parent="1" source="set-env" target="expose-port" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge26" parent="1" source="expose-port" target="setup-entrypoint" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge27" parent="1" source="setup-entrypoint" target="build-complete" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="910" y="525" />
|
||||||
|
<mxPoint x="910" y="70" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge28" parent="1" source="build-complete" target="run-start" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge29" parent="1" source="run-start" target="docker-run" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge30" parent="1" source="docker-run" target="port-mapping" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge31" parent="1" source="port-mapping" target="volume-mount" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge32" parent="1" source="volume-mount" target="entrypoint-start" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge33" parent="1" source="entrypoint-start" target="check-volume" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge34" parent="1" source="check-volume" target="copy-builtin" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge36" parent="1" source="copy-builtin" target="start-app" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge37" parent="1" source="start-app" target="container-running" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge38" parent="1" source="container-running" edge="1">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<mxPoint x="1000" y="580" as="targetPoint" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="label1" value="Running" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="360" y="50" width="40" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="label2" value="Not Running" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="290" y="90" width="50" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="label3" value="Empty" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="1030" y="350" width="30" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="label4" value="Not Empty" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="1030" y="400" width="40" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Baa9tTqOcb9ER4_EkQ3s-2" value="Deployment Done." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f5e8;strokeColor=#2e7d32;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="930" y="580" width="140" height="40" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="Baa9tTqOcb9ER4_EkQ3s-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0.093;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="cargo-build" target="stage2-start">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
@@ -1,16 +1,32 @@
|
|||||||
#[warn(unused_imports)]
|
#[warn(unused_imports)]
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
mod markdown;
|
mod markdown;
|
||||||
use markdown::{get_all_posts, get_post_by_slug, get_posts_by_tag, watch_posts};
|
use markdown::{
|
||||||
|
get_all_posts,
|
||||||
|
get_post_by_slug,
|
||||||
|
get_posts_by_tag,
|
||||||
|
watch_posts,
|
||||||
|
get_parser_logs,
|
||||||
|
clear_parser_logs,
|
||||||
|
load_parser_logs_from_disk,
|
||||||
|
force_reinterpret_all_posts,
|
||||||
|
force_reparse_single_post
|
||||||
|
};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Read; // STD AYOOOOOOOOOOOOOO - Tsodin
|
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)]
|
#[derive(Parser)]
|
||||||
#[command(name = "Markdown Backend")]
|
#[command(name = "Markdown Backend")]
|
||||||
#[command(about = "A CLI for managing markdown blog posts", long_about = None)]
|
#[command(about = "Ein CLI für die Verwaltung von Markdown-Blogbeiträgen", long_about = None)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Commands,
|
command: Commands,
|
||||||
@@ -34,6 +50,16 @@ enum Commands {
|
|||||||
Rsparseinfo,
|
Rsparseinfo,
|
||||||
/// Check backend health
|
/// Check backend health
|
||||||
Checkhealth,
|
Checkhealth,
|
||||||
|
/// Get parser logs
|
||||||
|
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 markdown from file or stdin
|
||||||
Parse {
|
Parse {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -47,6 +73,7 @@ enum Commands {
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
markdown::load_post_cache_from_disk();
|
markdown::load_post_cache_from_disk();
|
||||||
|
load_parser_logs_from_disk();
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
match &cli.command {
|
match &cli.command {
|
||||||
Commands::List => {
|
Commands::List => {
|
||||||
@@ -76,9 +103,9 @@ fn main() {
|
|||||||
println!("{}", serde_json::to_string(&posts).unwrap());
|
println!("{}", serde_json::to_string(&posts).unwrap());
|
||||||
}
|
}
|
||||||
Commands::Watch => {
|
Commands::Watch => {
|
||||||
println!("Watching for changes in posts directory. Press Ctrl+C to exit.");
|
println!("Überwache Änderungen im Posts-Verzeichnis. Drücken Sie Strg+C zum Beenden.");
|
||||||
let _ = watch_posts(|| {
|
let _ = watch_posts(|| {
|
||||||
println!("Posts directory changed!");
|
println!("Posts-Verzeichnis hat sich geändert!");
|
||||||
});
|
});
|
||||||
// Keep the main thread alive
|
// Keep the main thread alive
|
||||||
loop {
|
loop {
|
||||||
@@ -92,39 +119,69 @@ fn main() {
|
|||||||
let health = markdown::checkhealth();
|
let health = markdown::checkhealth();
|
||||||
println!("{}", serde_json::to_string_pretty(&health).unwrap());
|
println!("{}", serde_json::to_string_pretty(&health).unwrap());
|
||||||
}
|
}
|
||||||
Commands::Parse { file, stdin, ast } => {
|
Commands::Logs => {
|
||||||
let input = if let Some(file_path) = file {
|
let logs = get_parser_logs();
|
||||||
match std::fs::read_to_string(file_path) {
|
println!("{}", serde_json::to_string_pretty(&logs).unwrap());
|
||||||
Ok(content) => content,
|
}
|
||||||
Err(e) => {
|
Commands::ClearLogs => {
|
||||||
eprintln!("Failed to read file: {}", e);
|
clear_parser_logs();
|
||||||
std::process::exit(1);
|
println!("{}", serde_json::to_string(&serde_json::json!({"success": true, "message": "Protokolle gelöscht"})).unwrap());
|
||||||
}
|
}
|
||||||
|
Commands::ReinterpretAll => {
|
||||||
|
match force_reinterpret_all_posts() {
|
||||||
|
Ok(posts) => {
|
||||||
|
println!("{}", serde_json::to_string(&serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"message": format!("Alle Beiträge erfolgreich neu interpretiert. {} Beiträge verarbeitet.", posts.len())
|
||||||
|
})).unwrap());
|
||||||
}
|
}
|
||||||
} else if *stdin {
|
Err(e) => {
|
||||||
let mut buffer = String::new();
|
eprintln!("{}", e);
|
||||||
if let Err(e) = io::stdin().read_to_string(&mut buffer) {
|
|
||||||
eprintln!("Failed to read from stdin: {}", e);
|
|
||||||
std::process::exit(1);
|
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!("Beitrag '{}' erfolgreich neu geparst", 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();
|
||||||
|
io::stdin().read_to_string(&mut buffer).unwrap();
|
||||||
buffer
|
buffer
|
||||||
|
} else if let Some(file_path) = file {
|
||||||
|
fs::read_to_string(file_path).unwrap()
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Please provide --file <path> or --stdin");
|
eprintln!("Entweder --file oder --stdin muss angegeben werden");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
if *ast {
|
if *ast {
|
||||||
// Print pulldown_cmark events as debug output
|
// Parse and output AST as debug format
|
||||||
let parser = pulldown_cmark::Parser::new_ext(&input, pulldown_cmark::Options::all());
|
let parser = pulldown_cmark::Parser::new_ext(&content, pulldown_cmark::Options::all());
|
||||||
for event in parser {
|
let events: Vec<_> = parser.collect();
|
||||||
|
for event in events {
|
||||||
println!("{:?}", event);
|
println!("{:?}", event);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Print HTML output
|
// Parse and output HTML
|
||||||
let parser = pulldown_cmark::Parser::new_ext(&input, pulldown_cmark::Options::all());
|
let parser = pulldown_cmark::Parser::new_ext(&content, pulldown_cmark::Options::all());
|
||||||
let mut html_output = String::new();
|
let mut html_output = String::new();
|
||||||
pulldown_cmark::html::push_html(&mut html_output, parser);
|
pulldown_cmark::html::push_html(&mut html_output, parser);
|
||||||
println!("{}", html_output);
|
println!("{}", html_output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,10 @@ const nextConfig = {
|
|||||||
experimental: {
|
experimental: {
|
||||||
serverComponentsExternalPackages: ['chokidar']
|
serverComponentsExternalPackages: ['chokidar']
|
||||||
},
|
},
|
||||||
|
basePath: process.env.BASE_URL || '',
|
||||||
|
env: {
|
||||||
|
NEXT_PUBLIC_BASE_URL: process.env.BASE_URL || '',
|
||||||
|
},
|
||||||
// Handle API routes that shouldn't be statically generated
|
// Handle API routes that shouldn't be statically generated
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
|
|||||||
199
package-lock.json
generated
199
package-lock.json
generated
@@ -8,6 +8,8 @@
|
|||||||
"name": "markdownblog",
|
"name": "markdownblog",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/jetbrains-mono": "^5.2.6",
|
||||||
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@types/node": "^20.11.24",
|
"@types/node": "^20.11.24",
|
||||||
"@types/react": "^18.2.61",
|
"@types/react": "^18.2.61",
|
||||||
@@ -23,8 +25,11 @@
|
|||||||
"emoji-picker-react": "^4.12.2",
|
"emoji-picker-react": "^4.12.2",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
"isomorphic-dompurify": "^2.25.0",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"marked": "^12.0.0",
|
"marked": "^12.0.0",
|
||||||
|
"monaco-editor": "^0.52.2",
|
||||||
|
"monaco-vim": "^0.4.2",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
"pm2": "^6.0.8",
|
"pm2": "^6.0.8",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
@@ -304,6 +309,15 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"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": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||||
@@ -542,6 +556,29 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/@next/env": {
|
||||||
"version": "14.1.0",
|
"version": "14.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz",
|
||||||
@@ -1176,19 +1213,6 @@
|
|||||||
"parse5": "^7.0.0"
|
"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": {
|
"node_modules/@types/json5": {
|
||||||
"version": "0.0.29",
|
"version": "0.0.29",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||||
@@ -5147,6 +5171,92 @@
|
|||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/iterator.prototype": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
|
||||||
@@ -5291,18 +5401,6 @@
|
|||||||
"node": ">= 14"
|
"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": {
|
"node_modules/jsdom/node_modules/rrweb-cssom": {
|
||||||
"version": "0.7.1",
|
"version": "0.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
|
||||||
@@ -5664,6 +5762,21 @@
|
|||||||
"integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
|
"integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -6263,6 +6376,18 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@@ -7590,6 +7715,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/stop-iteration-iterator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
"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"
|
"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": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"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\""
|
"electron-dev": "concurrently \"npm run dev\" \"npm run electron\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/jetbrains-mono": "^5.2.6",
|
||||||
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@types/node": "^20.11.24",
|
"@types/node": "^20.11.24",
|
||||||
"@types/react": "^18.2.61",
|
"@types/react": "^18.2.61",
|
||||||
@@ -26,8 +28,11 @@
|
|||||||
"emoji-picker-react": "^4.12.2",
|
"emoji-picker-react": "^4.12.2",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
"isomorphic-dompurify": "^2.25.0",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"marked": "^12.0.0",
|
"marked": "^12.0.0",
|
||||||
|
"monaco-editor": "^0.52.2",
|
||||||
|
"monaco-vim": "^0.4.2",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
"pm2": "^6.0.8",
|
"pm2": "^6.0.8",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
|
|||||||
9
posts/about.md
Normal file
9
posts/about.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: About Me
|
||||||
|
date: 2025-07-04
|
||||||
|
tags: [about, profile]
|
||||||
|
author: rattatwinko
|
||||||
|
summary: This is the about page
|
||||||
|
---
|
||||||
|
|
||||||
|
_**config this in the monaco editor in the admin panel**_
|
||||||
@@ -29,10 +29,10 @@ author: Rattatwinko
|
|||||||
- [Support 🤝](#support)
|
- [Support 🤝](#support)
|
||||||
- [Support the Project ❤️](#support-the-project)
|
- [Support the Project ❤️](#support-the-project)
|
||||||
- [Acknowledgments 🙏](#acknowledgments)
|
- [Acknowledgments 🙏](#acknowledgments)
|
||||||
- [Folder Emojis 🇦🇹](#folder-emoji-technical-note)
|
- [Folder Emoji Technical Note 📁](#folder-emoji-technical-note)
|
||||||
- [API 🏗️](#api)
|
- [API 🏗️](#api)
|
||||||
- [ToT, and Todo](#train-of-thought-for-this-project-and-todo)
|
- [Project Status & Todo 📊](#project-status--todo)
|
||||||
- [Recent Changes](#recent-changes)
|
- [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.
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Status & Todo
|
||||||
## Train of Thought for this Project and 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.
|
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:lightblue;">LTS</span>|_Long Term Support and upkeep_|
|
||||||
|<span style="color:lime;">Partially Done</span>|**Caching with Rust**|
|
|<span style="color:lime;">Partially Done</span>|**Caching with Rust**|
|
||||||
|<span style="color:green;">Done</span>|Full Inline _CSS_ Support for **HTML**|
|
|<span style="color:green;">Done</span>|Full Inline _CSS_ Support for **HTML**|
|
||||||
---
|
|
||||||
|
|
||||||
## Recent Changes
|
## 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>
|
<p style="display:block;margin:0 auto;text-align:center;font-style:italic;font-weight:bold;">Ferris the Cutie</p>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### Caching:
|
### 🚀 **Major Updates (Latest)**
|
||||||
Recently the _Rust Parser recived a update_ which introduced **Caching using RAM**. This is not yet _fully implemented_, but it is in the works.
|
|
||||||
|
|
||||||
### Recursive Folder-Scanning via Rust:
|
#### **SSE (Server-Sent Events) Fully Restored** ✅
|
||||||
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_
|
- **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:
|
#### **Rust Parser Integration Complete** ✅
|
||||||
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.
|
- **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 />
|
<br />
|
||||||
|
|
||||||
@@ -475,3 +535,9 @@ Since Ages the **SSE Streaming _was_ broken**, now its fixed (see _@src/app/api/
|
|||||||
> *"DEVELOPERS! DEVELOPERS! DEVELOPERS!"* - Steve Ballmer
|
> *"DEVELOPERS! DEVELOPERS! DEVELOPERS!"* - Steve Ballmer
|
||||||
>
|
>
|
||||||
> <cite>— Rattatwinko, 2025 Q3</cite>
|
> <cite>— Rattatwinko, 2025 Q3</cite>
|
||||||
|
|
||||||
|
## Hosting behind a subpath (nginx proxy)
|
||||||
|
|
||||||
|
If you want to serve your blog at a subpath (e.g. `/blog`), set `BASE_URL=/blog` in your `.env.local` file. All internal links and API calls will use this base path automatically.
|
||||||
|
|
||||||
|
Example: Your blog will be available at `http://localhost:3000/blog`
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import BadgeButton from './BadgeButton';
|
import BadgeButton from './BadgeButton';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { withBaseUrl } from '@/lib/baseUrl';
|
||||||
|
|
||||||
const InfoIcon = (
|
const InfoIcon = (
|
||||||
<svg width="16" height="16" fill="white" viewBox="0 0 16 16" aria-hidden="true">
|
<svg width="16" height="16" fill="white" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
@@ -9,16 +11,13 @@ const InfoIcon = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export default function AboutButton() {
|
export default function AboutButton() {
|
||||||
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<BadgeButton
|
<BadgeButton
|
||||||
label="ABOUT ME"
|
label="ABOUT ME"
|
||||||
color="#2563eb"
|
color="#2563eb"
|
||||||
icon={InfoIcon}
|
icon={InfoIcon}
|
||||||
onClick={() => {
|
onClick={() => router.push(withBaseUrl('/posts/about'))}
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.open('http://' + window.location.hostname + ':80', '_blank');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,26 +6,30 @@ export default function BadgeButton({
|
|||||||
color = '#2563eb',
|
color = '#2563eb',
|
||||||
icon,
|
icon,
|
||||||
onClick,
|
onClick,
|
||||||
|
labelColor,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
labelColor?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
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={{
|
style={{
|
||||||
background: color,
|
background: color,
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
fontFamily: 'Verdana, Geneva, DejaVu Sans, sans-serif',
|
fontFamily: 'Verdana, Geneva, DejaVu Sans, sans-serif',
|
||||||
fontSize: '0.95rem',
|
fontSize: '0.75rem',
|
||||||
letterSpacing: '0.08em',
|
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 className="flex items-center">{icon}</span>
|
||||||
<span>{label}</span>
|
<span style={{ color: 'white', fontWeight: 'bold' }}>{label}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import BadgeButton from './BadgeButton';
|
import BadgeButton from './BadgeButton';
|
||||||
import AboutButton from './AboutButton';
|
import { withBaseUrl } from '@/lib/baseUrl';
|
||||||
|
|
||||||
const PersonIcon = (
|
const LockIcon = (
|
||||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="none">
|
<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" />
|
<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"/>
|
||||||
<rect x="3" y="13" width="14" height="5" rx="2.5" fill="white" stroke="white" strokeWidth="1.5" />
|
<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>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
const InfoIcon = (
|
const PersonIcon = (
|
||||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="none">
|
<svg width="16" height="16" viewBox="0 0 20 20" fill="none">
|
||||||
<circle cx="10" cy="10" r="9" stroke="white" strokeWidth="2" />
|
<circle cx="10" cy="6" r="4" fill="white" stroke="white" strokeWidth="1.5" />
|
||||||
<rect x="9" y="8" width="2" height="6" rx="1" fill="white" />
|
<rect x="3" y="13" width="14" height="5" rx="2.5" fill="white" stroke="white" strokeWidth="1.5" />
|
||||||
<rect x="9" y="5" width="2" height="2" rx="1" fill="white" />
|
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -21,28 +20,31 @@ export default function HeaderButtons() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex gap-2 justify-center sm:justify-end">
|
<div className="flex gap-2 justify-center sm:justify-end">
|
||||||
<a
|
<a
|
||||||
href="/admin"
|
href={withBaseUrl('/admin')}
|
||||||
target="_self"
|
target="_self"
|
||||||
rel="noopener noreferrer"
|
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
|
<BadgeButton
|
||||||
src="https://img.shields.io/badge/Admin%20Login-000000?style=for-the-badge&logo=lock&logoColor=white&labelColor=8B0000"
|
label="ADMIN LOGIN"
|
||||||
alt="Admin Login"
|
color="#000000"
|
||||||
className="h-6 sm:h-8"
|
labelColor="#8B0000"
|
||||||
|
icon={LockIcon}
|
||||||
|
onClick={() => {}}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
{/* If your server for about me is running on a different port, change the port number here */}
|
|
||||||
<a
|
<a
|
||||||
href={typeof window !== 'undefined' ? window.location.origin.replace('3000', '80') : '#'}
|
href={withBaseUrl('/posts/about')}
|
||||||
target="_self"
|
target="_self"
|
||||||
rel="noopener noreferrer"
|
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
|
<BadgeButton
|
||||||
src="https://img.shields.io/badge/About%20Me-000000?style=for-the-badge&logo=account&logoColor=white&labelColor=2563eb"
|
label="ABOUT ME"
|
||||||
alt="About Me"
|
color="#000000"
|
||||||
className="h-6 sm:h-8"
|
labelColor="#2563eb"
|
||||||
|
icon={PersonIcon}
|
||||||
|
onClick={() => {}}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { withBaseUrl } from '@/lib/baseUrl';
|
||||||
|
|
||||||
interface MobileNavProps {
|
interface MobileNavProps {
|
||||||
blogOwner: string;
|
blogOwner: string;
|
||||||
@@ -54,27 +55,15 @@ export default function MobileNav({ blogOwner }: MobileNavProps) {
|
|||||||
<h2 className="text-lg font-bold mb-6">{blogOwner}'s Blog</h2>
|
<h2 className="text-lg font-bold mb-6">{blogOwner}'s Blog</h2>
|
||||||
|
|
||||||
<nav className="space-y-4">
|
<nav className="space-y-4">
|
||||||
<Link
|
<a href={withBaseUrl('/')} className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors" onClick={toggleMenu}>
|
||||||
href="/"
|
|
||||||
className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors"
|
|
||||||
onClick={toggleMenu}
|
|
||||||
>
|
|
||||||
🏠 Home
|
🏠 Home
|
||||||
</Link>
|
</a>
|
||||||
|
|
||||||
<Link
|
<a href={withBaseUrl('/admin')} className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors" onClick={toggleMenu}>
|
||||||
href="/admin"
|
|
||||||
className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors"
|
|
||||||
onClick={toggleMenu}
|
|
||||||
>
|
|
||||||
🔐 Admin
|
🔐 Admin
|
||||||
</Link>
|
</a>
|
||||||
|
|
||||||
<a
|
<a href={withBaseUrl('/posts/about')} className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors" onClick={toggleMenu}>
|
||||||
href={typeof window !== 'undefined' ? window.location.origin.replace('3000', '80') : '#'}
|
|
||||||
className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors"
|
|
||||||
onClick={toggleMenu}
|
|
||||||
>
|
|
||||||
👤 About Me
|
👤 About Me
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
99
src/app/about/page.tsx
Normal file
99
src/app/about/page.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { withBaseUrl } from '@/lib/baseUrl';
|
||||||
|
|
||||||
|
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(withBaseUrl('/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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
622
src/app/admin/editor/page.tsx
Normal file
622
src/app/admin/editor/page.tsx
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
"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";
|
||||||
|
import { withBaseUrl } from '@/lib/baseUrl';
|
||||||
|
|
||||||
|
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(withBaseUrl('/api/posts'))
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(setTree);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Load file content when selected
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedSlug) return;
|
||||||
|
setLoading(true);
|
||||||
|
fetch(withBaseUrl(`/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(withBaseUrl(`/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(withBaseUrl(`/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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { withBaseUrl } from '@/lib/baseUrl';
|
||||||
|
|
||||||
interface Post {
|
interface Post {
|
||||||
type: 'post';
|
type: 'post';
|
||||||
@@ -26,7 +27,7 @@ type Node = Post | Folder;
|
|||||||
// Helper to get folder details
|
// Helper to get folder details
|
||||||
async function getFolderDetails(path: string): Promise<{ created: string, items: number, size: number, error?: string }> {
|
async function getFolderDetails(path: string): Promise<{ created: string, items: number, size: number, error?: string }> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/admin/folders/details?path=${encodeURIComponent(path)}`);
|
const res = await fetch(withBaseUrl(`/api/admin/folders/details?path=${encodeURIComponent(path)}`));
|
||||||
if (!res.ok) throw new Error('API error');
|
if (!res.ok) throw new Error('API error');
|
||||||
return await res.json();
|
return await res.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -38,7 +39,7 @@ async function getFolderDetails(path: string): Promise<{ created: string, items:
|
|||||||
// Helper to get post size and creation date
|
// Helper to get post size and creation date
|
||||||
async function getPostSize(slug: string): Promise<{ size: number | null, created: string | null }> {
|
async function getPostSize(slug: string): Promise<{ size: number | null, created: string | null }> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/admin/posts/size?slug=${encodeURIComponent(slug)}`);
|
const res = await fetch(withBaseUrl(`/api/admin/posts/size?slug=${encodeURIComponent(slug)}`));
|
||||||
if (!res.ok) throw new Error('API error');
|
if (!res.ok) throw new Error('API error');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return { size: data.size, created: data.created };
|
return { size: data.size, created: data.created };
|
||||||
@@ -76,7 +77,7 @@ export default function ManagePage() {
|
|||||||
|
|
||||||
const loadContent = async () => {
|
const loadContent = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/posts');
|
const response = await fetch(withBaseUrl('/api/posts'));
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setNodes(data);
|
setNodes(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -140,7 +141,7 @@ export default function ManagePage() {
|
|||||||
type: deleteConfirm.item.type
|
type: deleteConfirm.item.type
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch('/api/admin/delete', {
|
const response = await fetch(withBaseUrl('/api/admin/delete'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -170,7 +171,7 @@ export default function ManagePage() {
|
|||||||
// Move post API call
|
// Move post API call
|
||||||
const movePost = async (post: Post, targetFolder: string[]) => {
|
const movePost = async (post: Post, targetFolder: string[]) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/admin/posts/move', {
|
const response = await fetch(withBaseUrl('/api/admin/posts/move'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -239,12 +240,12 @@ export default function ManagePage() {
|
|||||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-6 sm:mb-8 space-y-4 sm:space-y-0">
|
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-6 sm:mb-8 space-y-4 sm:space-y-0">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4">
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold">Inhaltsverwaltung</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold">Inhaltsverwaltung</h1>
|
||||||
<Link
|
<a
|
||||||
href="/admin"
|
href={withBaseUrl('/admin')}
|
||||||
className="px-4 py-3 sm:py-2 bg-gray-200 rounded hover:bg-gray-300 transition-colors text-base font-medium"
|
className="px-4 py-3 sm:py-2 bg-gray-200 rounded hover:bg-gray-300 transition-colors text-base font-medium"
|
||||||
>
|
>
|
||||||
Zum Admin-Panel
|
Zum Admin-Panel
|
||||||
</Link>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -461,7 +462,7 @@ export default function ManagePage() {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!deleteAllConfirm.folder) return;
|
if (!deleteAllConfirm.folder) return;
|
||||||
// Call delete API with recursive flag
|
// Call delete API with recursive flag
|
||||||
await fetch('/api/admin/delete', {
|
await fetch(withBaseUrl('/api/admin/delete'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { withBaseUrl } from '@/lib/baseUrl';
|
||||||
|
|
||||||
interface PostStats {
|
interface PostStats {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -21,6 +22,14 @@ interface HealthReport {
|
|||||||
errors: string[];
|
errors: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LogEntry {
|
||||||
|
timestamp: string;
|
||||||
|
level: string;
|
||||||
|
message: string;
|
||||||
|
slug?: string;
|
||||||
|
details?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function RustStatusPage() {
|
export default function RustStatusPage() {
|
||||||
const [stats, setStats] = useState<PostStats[]>([]);
|
const [stats, setStats] = useState<PostStats[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -28,6 +37,11 @@ export default function RustStatusPage() {
|
|||||||
const [health, setHealth] = useState<HealthReport | null>(null);
|
const [health, setHealth] = useState<HealthReport | null>(null);
|
||||||
const [healthLoading, setHealthLoading] = useState(true);
|
const [healthLoading, setHealthLoading] = useState(true);
|
||||||
const [healthError, setHealthError] = useState<string | null>(null);
|
const [healthError, setHealthError] = useState<string | null>(null);
|
||||||
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
|
const [logsLoading, setLogsLoading] = useState(true);
|
||||||
|
const [logsError, setLogsError] = useState<string | null>(null);
|
||||||
|
const [logFilter, setLogFilter] = useState<string>('all'); // 'all', 'info', 'warning', 'error'
|
||||||
|
const [logSearch, setLogSearch] = useState<string>('');
|
||||||
|
|
||||||
// Summary calculations
|
// Summary calculations
|
||||||
const totalHits = stats.reduce((sum, s) => sum + s.cache_hits, 0);
|
const totalHits = stats.reduce((sum, s) => sum + s.cache_hits, 0);
|
||||||
@@ -39,7 +53,7 @@ export default function RustStatusPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/admin/posts?rsparseinfo=1');
|
const res = await fetch(withBaseUrl('/api/admin/posts?rsparseinfo=1'));
|
||||||
if (!res.ok) throw new Error('Fehler beim Laden der Statistiken');
|
if (!res.ok) throw new Error('Fehler beim Laden der Statistiken');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setStats(data);
|
setStats(data);
|
||||||
@@ -54,7 +68,7 @@ export default function RustStatusPage() {
|
|||||||
setHealthLoading(true);
|
setHealthLoading(true);
|
||||||
setHealthError(null);
|
setHealthError(null);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/admin/posts?checkhealth=1');
|
const res = await fetch(withBaseUrl('/api/admin/posts?checkhealth=1'));
|
||||||
if (!res.ok) throw new Error('Fehler beim Laden des Health-Checks');
|
if (!res.ok) throw new Error('Fehler beim Laden des Health-Checks');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setHealth(data);
|
setHealth(data);
|
||||||
@@ -65,49 +79,140 @@ export default function RustStatusPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
setLogsLoading(true);
|
||||||
|
setLogsError(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch(withBaseUrl('/api/admin/posts?logs=1'));
|
||||||
|
if (!res.ok) throw new Error('Fehler beim Laden der Logs');
|
||||||
|
const data = await res.json();
|
||||||
|
setLogs(data);
|
||||||
|
} catch (e: any) {
|
||||||
|
setLogsError(e.message || 'Unbekannter Fehler');
|
||||||
|
} finally {
|
||||||
|
setLogsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearLogs = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(withBaseUrl('/api/admin/posts?clearLogs=1'), { method: 'DELETE' });
|
||||||
|
if (!res.ok) throw new Error('Fehler beim Löschen der Logs');
|
||||||
|
await fetchLogs(); // Refresh logs after clearing
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Error clearing logs:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reinterpretAllPosts = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(withBaseUrl('/api/admin/posts?reinterpretAll=1'));
|
||||||
|
if (!res.ok) throw new Error('Fehler beim Neuinterpretieren der Posts');
|
||||||
|
const data = await res.json();
|
||||||
|
console.log('Neu-Interpretier Ergebins:', data);
|
||||||
|
// Refresh all data after reinterpret
|
||||||
|
await Promise.all([fetchStats(), fetchHealth(), fetchLogs()]);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Fehler beim Neu-Interpretieren => ', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStats();
|
fetchStats();
|
||||||
fetchHealth();
|
fetchHealth();
|
||||||
|
fetchLogs();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Filter logs based on level and search term
|
||||||
|
const filteredLogs = logs.filter(log => {
|
||||||
|
const matchesLevel = logFilter === 'all' || log.level === logFilter;
|
||||||
|
const matchesSearch = !logSearch ||
|
||||||
|
log.message.toLowerCase().includes(logSearch.toLowerCase()) ||
|
||||||
|
(log.slug && log.slug.toLowerCase().includes(logSearch.toLowerCase())) ||
|
||||||
|
(log.details && log.details.toLowerCase().includes(logSearch.toLowerCase()));
|
||||||
|
return matchesLevel && matchesSearch;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getLevelColor = (level: string) => {
|
||||||
|
switch (level) {
|
||||||
|
case 'error': return 'text-red-600 bg-red-50';
|
||||||
|
case 'warning': return 'text-yellow-600 bg-yellow-50';
|
||||||
|
case 'info': return 'text-blue-600 bg-blue-50';
|
||||||
|
default: return 'text-gray-600 bg-gray-50';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLevelIcon = (level: string) => {
|
||||||
|
switch (level) {
|
||||||
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 p-4 sm:p-6">
|
<div className="min-h-screen bg-gray-100 flex items-center justify-center p-4 sm:p-6">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="w-full max-w-6xl">
|
||||||
{/* Header with title and action buttons */}
|
{/* 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 flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mb-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<div className="bg-white rounded-lg shadow p-2 flex items-center justify-center">
|
<div className="bg-white rounded-lg shadow-sm p-1.5 flex items-center justify-center">
|
||||||
<img
|
<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"
|
src="https://upload.wikimedia.org/wikipedia/commons/d/d5/Rust_programming_language_black_logo.svg"
|
||||||
alt="Rust Logo"
|
alt="Rust Logo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 w-full sm:w-auto justify-end">
|
<div className="flex items-center gap-2 w-full sm:w-auto justify-end">
|
||||||
{/* Back to Admin button */}
|
{/* Back to Admin button */}
|
||||||
<a
|
<a
|
||||||
href="/admin"
|
href={withBaseUrl('/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"
|
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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="hidden sm:inline">Zurück zur Admin-Panel</span>
|
<span className="hidden sm:inline">Zurück</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{/* Refresh button */}
|
{/* Refresh button */}
|
||||||
<button
|
<button
|
||||||
onClick={fetchStats}
|
onClick={() => {
|
||||||
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"
|
fetchStats();
|
||||||
|
fetchHealth();
|
||||||
|
fetchLogs();
|
||||||
|
}}
|
||||||
|
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"
|
title="Aktualisieren"
|
||||||
disabled={loading}
|
disabled={loading || healthLoading || logsLoading}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`}
|
className={`w-4 h-4 ${(loading || healthLoading || logsLoading) ? 'animate-spin' : ''}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -120,52 +225,118 @@ export default function RustStatusPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Health Check Section */}
|
{/* Health Check Section */}
|
||||||
<div className="mb-6">
|
<div className="mb-4">
|
||||||
<h2 className="text-base sm:text-lg font-semibold mb-2 text-center">Health-Check</h2>
|
<h2 className="text-sm sm:text-base font-semibold mb-2 text-center">Health-Check</h2>
|
||||||
{healthLoading && <div className="text-center py-4 text-base">Lade Health-Check...</div>}
|
{healthLoading && <div className="text-center py-3 text-sm">Lade Health-Check...</div>}
|
||||||
{healthError && <div className="text-red-500 text-center text-base">{healthError}</div>}
|
{healthError && <div className="text-red-500 text-center text-sm">{healthError}</div>}
|
||||||
{health && (
|
{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-wrap gap-4 justify-center">
|
||||||
<div className="flex flex-col items-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>
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||||
<span className="text-xs text-gray-600">Posts-Verzeichnis</span>
|
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>
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<span className="text-lg font-bold text-blue-700">{health.posts_count}</span>
|
<div className="w-8 h-8 rounded-full bg-blue-200 text-blue-700 flex items-center justify-center">
|
||||||
<span className="text-xs text-gray-600">Posts</span>
|
<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>
|
||||||
<div className="flex flex-col items-center">
|
<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>
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||||
<span className="text-xs text-gray-600">Cache-Datei</span>
|
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>
|
||||||
<div className="flex flex-col items-center">
|
<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>
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||||
<span className="text-xs text-gray-600">Cache-Stats</span>
|
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>
|
||||||
<div className="flex flex-col items-center">
|
<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>
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||||
<span className="text-xs text-gray-600">Cache lesbar</span>
|
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>
|
||||||
<div className="flex flex-col items-center">
|
<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>
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||||
<span className="text-xs text-gray-600">Stats lesbar</span>
|
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>
|
</div>
|
||||||
{typeof health.cache_post_count === 'number' && (
|
{typeof health.cache_post_count === 'number' && (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<span className="text-lg font-bold text-blue-700">{health.cache_post_count}</span>
|
<div className="w-8 h-8 rounded-full bg-blue-200 text-blue-700 flex items-center justify-center">
|
||||||
<span className="text-xs text-gray-600">Cache-Posts</span>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{typeof health.cache_stats_count === 'number' && (
|
{typeof health.cache_stats_count === 'number' && (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<span className="text-lg font-bold text-blue-700">{health.cache_stats_count}</span>
|
<div className="w-8 h-8 rounded-full bg-blue-200 text-blue-700 flex items-center justify-center">
|
||||||
<span className="text-xs text-gray-600">Stats-Einträge</span>
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{health.errors.length > 0 && (
|
{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>
|
<b>Fehler:</b>
|
||||||
<ul className="list-disc ml-5 inline-block text-left">
|
<ul className="list-disc ml-5 inline-block text-left">
|
||||||
{health.errors.map((err, i) => <li key={i}>{err}</li>)}
|
{health.errors.map((err, i) => <li key={i}>{err}</li>)}
|
||||||
@@ -177,53 +348,165 @@ export default function RustStatusPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary Cards */}
|
{/* Summary Cards */}
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4 mb-6">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4 mb-4">
|
||||||
<div className="bg-green-100 rounded-lg p-4 flex flex-col items-center shadow">
|
<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-xl sm:text-2xl font-bold text-green-700">{totalHits}</span>
|
<span className="text-lg sm:text-xl 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>
|
<span className="text-xs sm:text-sm text-gray-700 mt-1 text-center font-medium">Cache-Treffer</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-red-100 rounded-lg p-4 flex flex-col items-center shadow">
|
<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-xl sm:text-2xl font-bold text-red-700">{totalMisses}</span>
|
<span className="text-lg sm:text-xl 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>
|
<span className="text-xs sm:text-sm text-gray-700 mt-1 text-center font-medium">Cache-Fehlschläge</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-blue-100 rounded-lg p-4 flex flex-col items-center shadow">
|
<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-xl sm:text-2xl font-bold text-blue-700">{avgInterpret} ms</span>
|
<span className="text-lg sm:text-xl 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>
|
<span className="text-xs sm:text-sm text-gray-700 mt-1 text-center font-medium">Ø Interpretationszeit</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-purple-100 rounded-lg p-4 flex flex-col items-center shadow">
|
<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-xl sm:text-2xl font-bold text-purple-700">{avgCompile} ms</span>
|
<span className="text-lg sm:text-xl 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>
|
<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-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="Neuinterpretation aller Beiträge erzwingen"
|
||||||
|
>
|
||||||
|
Alle Posts neu Interpretieren?
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={clearLogs}
|
||||||
|
className="px-2.5 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded text-xs transition-colors"
|
||||||
|
title="Logs Leeren"
|
||||||
|
>
|
||||||
|
Logs Leeren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Log Filters */}
|
||||||
|
<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-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-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
|
||||||
|
>
|
||||||
|
<option value="all">Alle Stufen</option>
|
||||||
|
<option value="info">Info</option>
|
||||||
|
<option value="warning">Warnungen</option>
|
||||||
|
<option value="error">Fehler</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logs Display */}
|
||||||
|
<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-3 text-gray-500 text-sm">No logs found</div>
|
||||||
|
) : (
|
||||||
|
filteredLogs.map((log, index) => (
|
||||||
|
<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">
|
||||||
|
<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-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-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'
|
||||||
|
}`}>
|
||||||
|
{log.level.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
{log.slug && (
|
||||||
|
<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 text-gray-900 leading-relaxed">{log.message}</div>
|
||||||
|
{log.details && (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div className="bg-white rounded-lg shadow p-3 sm:p-4 overflow-x-auto">
|
<div className="bg-white rounded-lg shadow-sm p-3 sm:p-4 overflow-x-auto">
|
||||||
<h2 className="text-base sm:text-lg font-semibold mb-3">Rohdaten</h2>
|
<h2 className="text-sm sm:text-base font-semibold mb-2">Rohdaten</h2>
|
||||||
{loading && <div className="text-center py-6 text-base">Lade Statistiken...</div>}
|
{loading && <div className="text-center py-4 text-sm">Lade Statistiken...</div>}
|
||||||
{error && <div className="text-red-500 text-center text-base">{error}</div>}
|
{error && <div className="text-red-500 text-center text-sm">{error}</div>}
|
||||||
{!loading && !error && (
|
{!loading && !error && (
|
||||||
<div className="overflow-x-auto">
|
<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>
|
<thead>
|
||||||
<tr className="bg-gray-100">
|
<tr className="bg-gray-100">
|
||||||
<th className="px-3 py-2 text-left text-sm">Slug</th>
|
<th className="px-2 py-1.5 text-left text-xs">Slug</th>
|
||||||
<th className="px-3 py-2 text-right text-sm">Cache-Treffer</th>
|
<th className="px-2 py-1.5 text-right text-xs">Cache-Treffer</th>
|
||||||
<th className="px-3 py-2 text-right text-sm">Cache-Fehlschläge</th>
|
<th className="px-2 py-1.5 text-right text-xs">Cache-Fehlschläge</th>
|
||||||
<th className="px-3 py-2 text-right text-sm">Interpret (ms)</th>
|
<th className="px-2 py-1.5 text-right text-xs">Interpret (ms)</th>
|
||||||
<th className="px-3 py-2 text-right text-sm">Kompilier (ms)</th>
|
<th className="px-2 py-1.5 text-right text-xs">Kompilier (ms)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{stats.length === 0 ? (
|
{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 => (
|
stats.map(stat => (
|
||||||
<tr key={stat.slug} className="border-t border-gray-200">
|
<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-2 py-1.5 font-mono text-xs">{stat.slug}</td>
|
||||||
<td className="px-3 py-2 text-right text-sm">{stat.cache_hits}</td>
|
<td className="px-2 py-1.5 text-right text-xs">{stat.cache_hits}</td>
|
||||||
<td className="px-3 py-2 text-right text-sm">{stat.cache_misses}</td>
|
<td className="px-2 py-1.5 text-right text-xs">{stat.cache_misses}</td>
|
||||||
<td className="px-3 py-2 text-right text-sm">{stat.last_interpret_time_ms}</td>
|
<td className="px-2 py-1.5 text-right text-xs">{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 text-right text-xs">{stat.last_compile_time_ms}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
/*********************************************
|
/*********************************************
|
||||||
* This is the main admin page for the blog.
|
* This is the main admin page for the blog.
|
||||||
*
|
*
|
||||||
* Written Jun 19 2025
|
* 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';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
@@ -12,8 +20,22 @@ import Link from 'next/link';
|
|||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import hljs from 'highlight.js';
|
import hljs from 'highlight.js';
|
||||||
import matter from 'gray-matter';
|
import matter from 'gray-matter';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamicImport from 'next/dynamic';
|
||||||
import { Theme } from 'emoji-picker-react';
|
import { Theme } from 'emoji-picker-react';
|
||||||
|
import '../highlight-github.css';
|
||||||
|
import { withBaseUrl } from '@/lib/baseUrl';
|
||||||
|
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 {
|
interface Post {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -46,7 +68,18 @@ interface Post {
|
|||||||
|
|
||||||
type Node = Post | Folder;
|
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() {
|
export default function AdminPage() {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
@@ -69,12 +102,7 @@ export default function AdminPage() {
|
|||||||
});
|
});
|
||||||
const [showManageContent, setShowManageContent] = useState(false);
|
const [showManageContent, setShowManageContent] = useState(false);
|
||||||
const [managePath, setManagePath] = useState<string[]>([]);
|
const [managePath, setManagePath] = useState<string[]>([]);
|
||||||
const [pinned, setPinned] = useState<string[]>(() => {
|
const [pinned, setPinned] = useState<string[]>([]);
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
return JSON.parse(localStorage.getItem('pinnedPosts') || '[]');
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
const [pinFeedback, setPinFeedback] = useState<string | null>(null);
|
const [pinFeedback, setPinFeedback] = useState<string | null>(null);
|
||||||
const [showChangePassword, setShowChangePassword] = useState(false);
|
const [showChangePassword, setShowChangePassword] = useState(false);
|
||||||
const [changePwOld, setChangePwOld] = useState('');
|
const [changePwOld, setChangePwOld] = useState('');
|
||||||
@@ -84,12 +112,7 @@ export default function AdminPage() {
|
|||||||
const [previewHtml, setPreviewHtml] = useState('');
|
const [previewHtml, setPreviewHtml] = useState('');
|
||||||
const [editingPost, setEditingPost] = useState<{ slug: string, path: string } | null>(null);
|
const [editingPost, setEditingPost] = useState<{ slug: string, path: string } | null>(null);
|
||||||
const [isDocker, setIsDocker] = useState<boolean>(false);
|
const [isDocker, setIsDocker] = useState<boolean>(false);
|
||||||
const [rememberExportChoice, setRememberExportChoice] = useState<boolean>(() => {
|
const [rememberExportChoice, setRememberExportChoice] = useState<boolean>(false);
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
return localStorage.getItem('rememberExportChoice') === 'true';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
const [lastExportChoice, setLastExportChoice] = useState<string | null>(null);
|
const [lastExportChoice, setLastExportChoice] = useState<string | null>(null);
|
||||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState<string | null>(null);
|
const [emojiPickerOpen, setEmojiPickerOpen] = useState<string | null>(null);
|
||||||
const [emojiPickerAnchor, setEmojiPickerAnchor] = useState<HTMLElement | null>(null);
|
const [emojiPickerAnchor, setEmojiPickerAnchor] = useState<HTMLElement | null>(null);
|
||||||
@@ -97,6 +120,10 @@ export default function AdminPage() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const usernameRef = useRef<HTMLInputElement>(null);
|
const usernameRef = useRef<HTMLInputElement>(null);
|
||||||
const passwordRef = 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(() => {
|
useEffect(() => {
|
||||||
// Check if already authenticated
|
// Check if already authenticated
|
||||||
@@ -129,20 +156,14 @@ export default function AdminPage() {
|
|||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
gfm: true,
|
gfm: true,
|
||||||
breaks: true,
|
breaks: true,
|
||||||
highlight: function(code: string, lang: string) {
|
renderer,
|
||||||
if (lang && hljs.getLanguage(lang)) {
|
|
||||||
return hljs.highlight(code, { language: lang }).value;
|
|
||||||
} else {
|
|
||||||
return hljs.highlightAuto(code).value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} as any);
|
} as any);
|
||||||
setPreviewHtml(marked.parse(newPost.content || '') as string);
|
setPreviewHtml(marked.parse(newPost.content || '') as string);
|
||||||
}, [newPost.content]);
|
}, [newPost.content]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if docker is used
|
// Check if docker is used
|
||||||
fetch('/api/admin/docker')
|
fetch(withBaseUrl('/api/admin/docker'))
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => setIsDocker(!!data.docker))
|
.then(data => setIsDocker(!!data.docker))
|
||||||
.catch(() => setIsDocker(false));
|
.catch(() => setIsDocker(false));
|
||||||
@@ -150,7 +171,7 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
const loadContent = async () => {
|
const loadContent = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/posts');
|
const response = await fetch(withBaseUrl('/api/posts'));
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setNodes(data);
|
setNodes(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -169,7 +190,7 @@ export default function AdminPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check password via API
|
// Check password via API
|
||||||
const res = await fetch('/api/admin/password', {
|
const res = await fetch(withBaseUrl('/api/admin/password'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ password: pass, mode: 'login' }),
|
body: JSON.stringify({ password: pass, mode: 'login' }),
|
||||||
@@ -193,7 +214,7 @@ export default function AdminPage() {
|
|||||||
const handleCreatePost = async (e: React.FormEvent) => {
|
const handleCreatePost = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/admin/posts', {
|
const response = await fetch(withBaseUrl('/api/admin/posts'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -231,7 +252,7 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/admin/folders', {
|
const response = await fetch(withBaseUrl('/api/admin/folders'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -327,7 +348,7 @@ export default function AdminPage() {
|
|||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('path', currentPath.join('/'));
|
formData.append('path', currentPath.join('/'));
|
||||||
|
|
||||||
const response = await fetch('/api/admin/upload', {
|
const response = await fetch(withBaseUrl('/api/admin/upload'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
@@ -361,7 +382,7 @@ export default function AdminPage() {
|
|||||||
type: deleteConfirm.item.type
|
type: deleteConfirm.item.type
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await fetch('/api/admin/delete', {
|
const response = await fetch(withBaseUrl('/api/admin/delete'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -394,7 +415,7 @@ export default function AdminPage() {
|
|||||||
? prev.filter((s) => s !== slug)
|
? prev.filter((s) => s !== slug)
|
||||||
: [slug, ...prev];
|
: [slug, ...prev];
|
||||||
// Update pinned.json on the server
|
// Update pinned.json on the server
|
||||||
fetch('/api/admin/posts', {
|
fetch(withBaseUrl('/api/admin/posts'), {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ pinned: newPinned }),
|
body: JSON.stringify({ pinned: newPinned }),
|
||||||
@@ -433,7 +454,7 @@ export default function AdminPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check old password
|
// Check old password
|
||||||
const res = await fetch('/api/admin/password', {
|
const res = await fetch(withBaseUrl('/api/admin/password'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ password: changePwOld, mode: 'login' }),
|
body: JSON.stringify({ password: changePwOld, mode: 'login' }),
|
||||||
@@ -444,7 +465,7 @@ export default function AdminPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Set new password
|
// Set new password
|
||||||
const res2 = await fetch('/api/admin/password', {
|
const res2 = await fetch(withBaseUrl('/api/admin/password'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ password: changePwNew }),
|
body: JSON.stringify({ password: changePwNew }),
|
||||||
@@ -464,7 +485,7 @@ export default function AdminPage() {
|
|||||||
// Function to load a post's raw markdown
|
// Function to load a post's raw markdown
|
||||||
const loadPostRaw = async (slug: string, folderPath: string) => {
|
const loadPostRaw = async (slug: string, folderPath: string) => {
|
||||||
const params = new URLSearchParams({ slug, path: folderPath });
|
const params = new URLSearchParams({ slug, path: folderPath });
|
||||||
const res = await fetch(`/api/admin/posts/raw?${params.toString()}`);
|
const res = await fetch(withBaseUrl(`/api/admin/posts/raw?${params.toString()}`));
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
alert('Error loading post');
|
alert('Error loading post');
|
||||||
return;
|
return;
|
||||||
@@ -496,7 +517,7 @@ export default function AdminPage() {
|
|||||||
summary: newPost.summary,
|
summary: newPost.summary,
|
||||||
author: process.env.NEXT_PUBLIC_BLOG_OWNER + "'s" || 'Anonymous',
|
author: process.env.NEXT_PUBLIC_BLOG_OWNER + "'s" || 'Anonymous',
|
||||||
});
|
});
|
||||||
const response = await fetch('/api/admin/posts', {
|
const response = await fetch(withBaseUrl('/api/admin/posts'), {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -518,15 +539,7 @@ export default function AdminPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function handleExportTarball() {
|
function handleExportTarball() {
|
||||||
// Check if we should use the remembered choice
|
if (typeof window === 'undefined') return;
|
||||||
if (rememberExportChoice && lastExportChoice) {
|
|
||||||
if (lastExportChoice === 'docker') {
|
|
||||||
exportFromEndpoint('/api/admin/export');
|
|
||||||
} else if (lastExportChoice === 'local') {
|
|
||||||
exportFromEndpoint('/api/admin/exportlocal');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create popup modal
|
// Create popup modal
|
||||||
const modal = document.createElement('div');
|
const modal = document.createElement('div');
|
||||||
@@ -594,9 +607,9 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
closeModal();
|
closeModal();
|
||||||
if (choice === 'docker') {
|
if (choice === 'docker') {
|
||||||
exportFromEndpoint('/api/admin/export');
|
exportFromEndpoint(withBaseUrl('/api/admin/export'));
|
||||||
} else if (choice === 'local') {
|
} else if (choice === 'local') {
|
||||||
exportFromEndpoint('/api/admin/exportlocal');
|
exportFromEndpoint(withBaseUrl('/api/admin/exportlocal'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -619,6 +632,7 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function exportFromEndpoint(endpoint: string) {
|
function exportFromEndpoint(endpoint: string) {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
fetch(endpoint)
|
fetch(endpoint)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
if (!res.ok) throw new Error('Export failed');
|
if (!res.ok) throw new Error('Export failed');
|
||||||
@@ -642,6 +656,15 @@ export default function AdminPage() {
|
|||||||
setLastExportChoice(null);
|
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
|
// Simple and reliable emoji update handler
|
||||||
const handleSetFolderEmoji = async (folderPath: string, emoji: string) => {
|
const handleSetFolderEmoji = async (folderPath: string, emoji: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -671,7 +694,7 @@ export default function AdminPage() {
|
|||||||
// Save to JSON file in background
|
// Save to JSON file in background
|
||||||
try {
|
try {
|
||||||
console.log('Fetching current pinned data...');
|
console.log('Fetching current pinned data...');
|
||||||
const pinnedRes = await fetch('/api/admin/posts', { method: 'GET' });
|
const pinnedRes = await fetch(withBaseUrl('/api/admin/posts'), { method: 'GET' });
|
||||||
if (!pinnedRes.ok) {
|
if (!pinnedRes.ok) {
|
||||||
throw new Error(`Failed to fetch pinned data: ${pinnedRes.status}`);
|
throw new Error(`Failed to fetch pinned data: ${pinnedRes.status}`);
|
||||||
}
|
}
|
||||||
@@ -684,7 +707,7 @@ export default function AdminPage() {
|
|||||||
console.log('Updated folderEmojis:', folderEmojis);
|
console.log('Updated folderEmojis:', folderEmojis);
|
||||||
console.log('Saving to pinned.json...');
|
console.log('Saving to pinned.json...');
|
||||||
|
|
||||||
const saveRes = await fetch('/api/admin/posts', {
|
const saveRes = await fetch(withBaseUrl('/api/admin/posts'), {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ folderEmojis, pinned: pinnedData.pinned || [] }),
|
body: JSON.stringify({ folderEmojis, pinned: pinnedData.pinned || [] }),
|
||||||
@@ -752,6 +775,17 @@ export default function AdminPage() {
|
|||||||
return Theme.LIGHT;
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 p-3 sm:p-8">
|
<div className="min-h-screen bg-gray-100 p-3 sm:p-8">
|
||||||
{pinFeedback && (
|
{pinFeedback && (
|
||||||
@@ -853,7 +887,7 @@ export default function AdminPage() {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
href="/admin/manage/rust-status"
|
href={withBaseUrl('/admin/manage/rust-status')}
|
||||||
className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-gradient-to-r from-teal-500 to-blue-500 text-white rounded-xl shadow-lg flex items-center justify-center gap-2 text-sm sm:text-base font-semibold hover:from-teal-600 hover:to-blue-600 transition-all focus:outline-none focus:ring-2 focus:ring-teal-400"
|
className="w-full sm:w-auto px-4 py-3 sm:py-2 bg-gradient-to-r from-teal-500 to-blue-500 text-white rounded-xl shadow-lg flex items-center justify-center gap-2 text-sm sm:text-base font-semibold hover:from-teal-600 hover:to-blue-600 transition-all focus:outline-none focus:ring-2 focus:ring-teal-400"
|
||||||
title="View Rust Parser Dashboard"
|
title="View Rust Parser Dashboard"
|
||||||
style={{ minWidth: '160px' }}
|
style={{ minWidth: '160px' }}
|
||||||
@@ -867,6 +901,60 @@ export default function AdminPage() {
|
|||||||
<span className="text-xs font-normal text-teal-100">Statistiken</span>
|
<span className="text-xs font-normal text-teal-100">Statistiken</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
{/* VS Code Editor Button */}
|
||||||
|
<a
|
||||||
|
href={withBaseUrl('/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 && (
|
{rememberExportChoice && lastExportChoice && (
|
||||||
<div className="flex items-center gap-1 text-xs text-gray-600 w-full sm:w-auto justify-center sm:justify-start">
|
<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>
|
<span>💾 {lastExportChoice === 'docker' ? 'Docker' : 'Local'}</span>
|
||||||
@@ -990,8 +1078,6 @@ export default function AdminPage() {
|
|||||||
Current folder: <span className="font-mono">{currentPath.join('/') || 'root'}</span>
|
Current folder: <span className="font-mono">{currentPath.join('/') || 'root'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Drag and Drop Zone */}
|
{/* Drag and Drop Zone */}
|
||||||
<div
|
<div
|
||||||
className={`mb-6 sm:mb-8 p-4 sm:p-8 border-2 border-dashed rounded-lg text-center ${
|
className={`mb-6 sm:mb-8 p-4 sm:p-8 border-2 border-dashed rounded-lg text-center ${
|
||||||
@@ -1072,25 +1158,66 @@ export default function AdminPage() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</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="space-y-4">
|
||||||
<div className="flex flex-col sm:flex-row gap-4">
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
<div className="w-full sm:w-1/2">
|
<div className="w-full sm:w-1/2">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Inhalt (Markdown)</label>
|
<div style={{ height: '240px' }}>
|
||||||
<textarea
|
<MonacoEditor
|
||||||
value={newPost.content}
|
height="100%"
|
||||||
onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
|
defaultLanguage="markdown"
|
||||||
className="w-full rounded-md border border-gray-300 px-3 py-2 font-mono text-sm sm:text-base"
|
value={newPost.content}
|
||||||
style={{ height: '240px' }}
|
onChange={(value?: string) => setNewPost({ ...newPost, content: value || '' })}
|
||||||
rows={10}
|
options={{
|
||||||
required
|
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>
|
||||||
<div className="w-full sm:w-1/2">
|
<div className="w-full sm:w-1/2">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Vorschau</label>
|
<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="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>
|
</div>
|
||||||
|
<style jsx global>{`
|
||||||
|
.prose code, .prose pre {
|
||||||
|
font-family: 'JetBrains Mono', monospace !important;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1351,7 +1478,7 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<a href="/admin/manage" className="block mt-6 text-blue-600 hover:underline text-sm sm:text-base">Zur Inhaltsverwaltung</a>
|
<a href={withBaseUrl('/admin/manage')} className="block mt-6 text-blue-600 hover:underline text-sm sm:text-base">Zur Inhaltsverwaltung</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -90,6 +90,66 @@ export async function GET(request: Request) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const logs = searchParams.get('logs');
|
||||||
|
if (logs === '1') {
|
||||||
|
// Call the Rust backend for parser logs
|
||||||
|
const rustResult = spawnSync(
|
||||||
|
process.cwd() + '/markdown_backend/target/release/markdown_backend',
|
||||||
|
['logs'],
|
||||||
|
{ 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 reinterpretAll = searchParams.get('reinterpretAll');
|
||||||
|
if (reinterpretAll === '1') {
|
||||||
|
// Call the Rust backend to force reinterpret all posts
|
||||||
|
const rustResult = spawnSync(
|
||||||
|
process.cwd() + '/markdown_backend/target/release/markdown_backend',
|
||||||
|
['reinterpret-all'],
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
if (rustResult.status === 0 && rustResult.stdout) {
|
||||||
|
return new Response(rustResult.stdout, {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new Response(JSON.stringify({ error: rustResult.stderr || rustResult.error }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const reparsePost = searchParams.get('reparsePost');
|
||||||
|
if (reparsePost) {
|
||||||
|
// Call the Rust backend to reparse a specific post
|
||||||
|
const rustResult = spawnSync(
|
||||||
|
process.cwd() + '/markdown_backend/target/release/markdown_backend',
|
||||||
|
['reparse-post', reparsePost],
|
||||||
|
{ encoding: 'utf-8' }
|
||||||
|
);
|
||||||
|
if (rustResult.status === 0 && rustResult.stdout) {
|
||||||
|
return new Response(rustResult.stdout, {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new Response(JSON.stringify({ error: rustResult.stderr || rustResult.error }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
// Return the current pinned.json object
|
// Return the current pinned.json object
|
||||||
try {
|
try {
|
||||||
const pinnedPath = path.join(process.cwd(), 'posts', 'pinned.json');
|
const pinnedPath = path.join(process.cwd(), 'posts', 'pinned.json');
|
||||||
@@ -150,4 +210,36 @@ export async function PUT(request: Request) {
|
|||||||
console.error('Error editing post:', error);
|
console.error('Error editing post:', error);
|
||||||
return NextResponse.json({ error: 'Error editing post' }, { status: 500 });
|
return NextResponse.json({ error: 'Error editing post' }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: Request) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const clearLogs = searchParams.get('clearLogs');
|
||||||
|
|
||||||
|
if (clearLogs === '1') {
|
||||||
|
// Call the Rust backend to clear parser logs
|
||||||
|
const rustResult = spawnSync(
|
||||||
|
process.cwd() + '/markdown_backend/target/release/markdown_backend',
|
||||||
|
['clear-logs'],
|
||||||
|
{ 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 NextResponse.json({ error: 'Invalid delete operation' }, { status: 400 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing logs:', error);
|
||||||
|
return NextResponse.json({ error: 'Error clearing logs' }, { status: 500 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,174 +1,59 @@
|
|||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import matter from 'gray-matter';
|
|
||||||
import { marked } from 'marked';
|
|
||||||
import DOMPurify from 'dompurify';
|
|
||||||
import { JSDOM } from 'jsdom';
|
|
||||||
import hljs from 'highlight.js';
|
|
||||||
import { getPostsDirectory } from '@/lib/postsDirectory';
|
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||||
import { spawnSync } from 'child_process';
|
import { spawnSync } from 'child_process';
|
||||||
|
|
||||||
const postsDirectory = getPostsDirectory();
|
const postsDirectory = getPostsDirectory();
|
||||||
|
|
||||||
// Function to get file creation date
|
|
||||||
function getFileCreationDate(filePath: string): Date {
|
|
||||||
const stats = fs.statSync(filePath);
|
|
||||||
return stats.birthtime ?? stats.mtime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to generate ID from text (matches frontend logic)
|
|
||||||
function generateId(text: string): string {
|
|
||||||
return text
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9]+/g, '-')
|
|
||||||
.replace(/^-+|-+$/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderer = new marked.Renderer();
|
|
||||||
|
|
||||||
// Custom heading renderer to add IDs
|
|
||||||
renderer.heading = (text, level) => {
|
|
||||||
const id = generateId(text);
|
|
||||||
return `<h${level} id="${id}">${text}</h${level}>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
renderer.code = (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>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
marked.setOptions({
|
|
||||||
gfm: true,
|
|
||||||
breaks: true,
|
|
||||||
renderer,
|
|
||||||
});
|
|
||||||
|
|
||||||
async function getPostBySlug(slug: string) {
|
|
||||||
const realSlug = slug.replace(/\.md$/, '');
|
|
||||||
const fullPath = path.join(postsDirectory, `${realSlug}.md`);
|
|
||||||
let rustResult;
|
|
||||||
try {
|
|
||||||
// Try Rust backend first
|
|
||||||
rustResult = spawnSync(
|
|
||||||
path.resolve(process.cwd(), 'markdown_backend/target/release/markdown_backend'),
|
|
||||||
['show', realSlug],
|
|
||||||
{ encoding: 'utf-8' }
|
|
||||||
);
|
|
||||||
if (rustResult.status === 0 && rustResult.stdout) {
|
|
||||||
// Expect Rust to output a JSON object matching the post shape
|
|
||||||
const post = JSON.parse(rustResult.stdout);
|
|
||||||
// Map snake_case to camelCase for frontend compatibility
|
|
||||||
post.createdAt = post.created_at;
|
|
||||||
delete post.created_at;
|
|
||||||
return post;
|
|
||||||
} else {
|
|
||||||
console.error('[Rust parser error]', rustResult.stderr || rustResult.error);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[Rust parser exception]', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to TypeScript parser
|
|
||||||
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
|
||||||
const { data, content } = matter(fileContents);
|
|
||||||
const createdAt = getFileCreationDate(fullPath);
|
|
||||||
|
|
||||||
let processedContent = '';
|
|
||||||
try {
|
|
||||||
// Convert markdown to HTML
|
|
||||||
const rawHtml = marked.parse(content);
|
|
||||||
const window = new JSDOM('').window;
|
|
||||||
const purify = DOMPurify(window);
|
|
||||||
processedContent = purify.sanitize(rawHtml as string, {
|
|
||||||
ALLOWED_TAGS: [
|
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
||||||
'p', 'a', 'ul', 'ol', 'li', 'blockquote',
|
|
||||||
'pre', 'code', 'em', 'strong', 'del',
|
|
||||||
'hr', 'br', 'img', 'table', 'thead', 'tbody',
|
|
||||||
'tr', 'th', 'td', 'div', 'span', 'iframe'
|
|
||||||
],
|
|
||||||
ALLOWED_ATTR: [
|
|
||||||
'class', 'id', 'style',
|
|
||||||
'href', 'target', 'rel',
|
|
||||||
'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
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Error processing markdown for slug "${realSlug}":`, err);
|
|
||||||
processedContent = `<div class="error-message">
|
|
||||||
<p>Error processing markdown content. Please check the console for details.</p>
|
|
||||||
<pre>${err instanceof Error ? err.message : 'Unknown error'}</pre>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
slug: realSlug,
|
|
||||||
title: data.title,
|
|
||||||
date: data.date,
|
|
||||||
tags: data.tags || [],
|
|
||||||
summary: data.summary,
|
|
||||||
content: processedContent,
|
|
||||||
createdAt: createdAt.toISOString(),
|
|
||||||
author: (process.env.NEXT_PUBLIC_BLOG_OWNER || 'Anonymous') + "'s",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: { slug: string[] | string } }
|
{ params }: { params: { slug: string[] | string } }
|
||||||
) {
|
) {
|
||||||
let parser = 'typescript';
|
|
||||||
let rustError = '';
|
|
||||||
try {
|
try {
|
||||||
const slugArr = Array.isArray(params.slug) ? params.slug : [params.slug];
|
const slugArr = Array.isArray(params.slug) ? params.slug : [params.slug];
|
||||||
const slugPath = slugArr.join('/');
|
const slugPath = slugArr.join('/');
|
||||||
let post;
|
const rustResult = spawnSync(
|
||||||
try {
|
path.resolve(process.cwd(), 'markdown_backend/target/release/markdown_backend'),
|
||||||
const rustResult = spawnSync(
|
['show', slugPath],
|
||||||
path.resolve(process.cwd(), 'markdown_backend/target/release/markdown_backend'),
|
{ encoding: 'utf-8' }
|
||||||
['show', slugPath],
|
);
|
||||||
{ encoding: 'utf-8' }
|
if (rustResult.status === 0 && rustResult.stdout) {
|
||||||
);
|
const post = JSON.parse(rustResult.stdout);
|
||||||
if (rustResult.status === 0 && rustResult.stdout) {
|
const fs = require('fs');
|
||||||
post = JSON.parse(rustResult.stdout);
|
const filePath = path.join(postsDirectory, slugPath + '.md');
|
||||||
post.createdAt = post.created_at;
|
let raw = '';
|
||||||
delete post.created_at;
|
try {
|
||||||
parser = 'rust';
|
raw = fs.readFileSync(filePath, 'utf8');
|
||||||
} else {
|
} catch {}
|
||||||
rustError = rustResult.stderr || rustResult.error?.toString() || 'Unknown error';
|
post.raw = raw;
|
||||||
console.error('[Rust parser error]', rustError);
|
post.createdAt = post.created_at;
|
||||||
}
|
delete post.created_at;
|
||||||
} catch (e) {
|
return NextResponse.json(post);
|
||||||
rustError = e instanceof Error ? e.message : String(e);
|
} else {
|
||||||
console.error('[Rust parser exception]', rustError);
|
const rustError = rustResult.stderr || rustResult.error?.toString() || 'Unknown error';
|
||||||
|
return NextResponse.json({ error: 'Rust parser error', details: rustError }, { status: 500 });
|
||||||
}
|
}
|
||||||
if (!post) {
|
|
||||||
post = await getPostBySlug(slugPath);
|
|
||||||
}
|
|
||||||
const response = NextResponse.json(post);
|
|
||||||
response.headers.set('X-Parser', parser);
|
|
||||||
if (parser !== 'rust' && rustError) {
|
|
||||||
response.headers.set('X-Rust-Parser-Error', rustError);
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading post:', error);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{ error: 'Error loading post', details: error instanceof Error ? error.message : 'Unknown error' },
|
||||||
error: 'Error loading post',
|
|
||||||
details: error instanceof Error ? error.message : 'Unknown error'
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 path from 'path';
|
||||||
import matter from 'gray-matter';
|
import matter from 'gray-matter';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import DOMPurify from 'dompurify';
|
import createDOMPurify from 'isomorphic-dompurify';
|
||||||
import { JSDOM } from 'jsdom';
|
|
||||||
import hljs from 'highlight.js';
|
import hljs from 'highlight.js';
|
||||||
import { getPostsDirectory } from '@/lib/postsDirectory';
|
import { getPostsDirectory } from '@/lib/postsDirectory';
|
||||||
|
|
||||||
@@ -73,6 +72,10 @@ async function readPostsDir(dir: string, relDir = '', pinnedData: { pinned: stri
|
|||||||
const posts: any[] = [];
|
const posts: any[] = [];
|
||||||
|
|
||||||
for (const entry of entries) {
|
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 fullPath = path.join(dir, entry.name);
|
||||||
const relPath = relDir ? path.join(relDir, entry.name) : 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 = '';
|
let processedContent = '';
|
||||||
try {
|
try {
|
||||||
const rawHtml = marked.parse(content);
|
const rawHtml = marked.parse(content) as string;
|
||||||
const window = new JSDOM('').window;
|
processedContent = createDOMPurify.sanitize(rawHtml, {
|
||||||
const purify = DOMPurify(window);
|
|
||||||
processedContent = purify.sanitize(rawHtml as string, {
|
|
||||||
ALLOWED_TAGS: [
|
ALLOWED_TAGS: [
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||||
'p', 'a', 'ul', 'ol', 'li', 'blockquote',
|
'p', 'a', 'ul', 'ol', 'li', 'blockquote',
|
||||||
@@ -119,7 +120,7 @@ async function getPostByPath(filePath: string, relPath: string, pinnedData: { pi
|
|||||||
'src', 'alt', 'title', 'width', 'height',
|
'src', 'alt', 'title', 'width', 'height',
|
||||||
'frameborder', 'allowfullscreen'
|
'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) {
|
} catch (err) {
|
||||||
console.error(`Error processing markdown for ${relPath}:`, err);
|
console.error(`Error processing markdown for ${relPath}:`, err);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { watchPosts, stopWatching } from '@/lib/markdown';
|
import { spawn } from 'child_process';
|
||||||
|
|
||||||
// Prevent static generation of this route
|
// Prevent static generation of this route
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
@@ -37,35 +37,87 @@ export async function GET(request: NextRequest) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up file watcher if not already set up
|
// Set up Rust file watcher if not already set up
|
||||||
if (clients.size === 1) {
|
if (clients.size === 1) {
|
||||||
try {
|
try {
|
||||||
watchPosts(() => {
|
const rustWatcher = spawn(
|
||||||
// Notify all connected clients about the update
|
process.cwd() + '/markdown_backend/target/release/markdown_backend',
|
||||||
const message = JSON.stringify({ type: 'update', timestamp: new Date().toISOString() });
|
['watch'],
|
||||||
const clientsToRemove: ReadableStreamDefaultController[] = [];
|
{ stdio: ['pipe', 'pipe', 'pipe'] }
|
||||||
|
);
|
||||||
|
|
||||||
|
rustWatcher.stdout.on('data', (data) => {
|
||||||
|
const message = data.toString().trim();
|
||||||
|
console.log('Rust watcher output:', message);
|
||||||
|
|
||||||
clients.forEach(client => {
|
if (message.includes('Posts directory changed!')) {
|
||||||
try {
|
// Notify all connected clients about the update
|
||||||
client.enqueue(`data: ${message}\n\n`);
|
const updateMessage = JSON.stringify({ type: 'update', timestamp: new Date().toISOString() });
|
||||||
} catch (error) {
|
const clientsToRemove: ReadableStreamDefaultController[] = [];
|
||||||
// Mark client for removal
|
|
||||||
clientsToRemove.push(client);
|
clients.forEach(client => {
|
||||||
|
try {
|
||||||
|
client.enqueue(`data: ${updateMessage}\n\n`);
|
||||||
|
} catch (error) {
|
||||||
|
// Mark client for removal
|
||||||
|
clientsToRemove.push(client);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove disconnected clients
|
||||||
|
clientsToRemove.forEach(client => {
|
||||||
|
clients.delete(client);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop watching if no clients are connected
|
||||||
|
if (clients.size === 0) {
|
||||||
|
console.log('No clients connected, stopping watcher');
|
||||||
|
rustWatcher.kill();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Remove disconnected clients
|
|
||||||
clientsToRemove.forEach(client => {
|
|
||||||
clients.delete(client);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Stop watching if no clients are connected
|
|
||||||
if (clients.size === 0) {
|
|
||||||
stopWatching();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
rustWatcher.stderr.on('data', (data) => {
|
||||||
|
const errorMessage = data.toString().trim();
|
||||||
|
console.error('Rust watcher error:', errorMessage);
|
||||||
|
|
||||||
|
// Don't treat RecvError as a real error - it's expected when the process is terminated
|
||||||
|
if (!errorMessage.includes('RecvError')) {
|
||||||
|
// Send error to clients
|
||||||
|
const errorData = JSON.stringify({ type: 'error', message: errorMessage });
|
||||||
|
const clientsToRemove: ReadableStreamDefaultController[] = [];
|
||||||
|
|
||||||
|
clients.forEach(client => {
|
||||||
|
try {
|
||||||
|
client.enqueue(`data: ${errorData}\n\n`);
|
||||||
|
} catch (error) {
|
||||||
|
clientsToRemove.push(client);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clientsToRemove.forEach(client => {
|
||||||
|
clients.delete(client);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rustWatcher.on('error', (error) => {
|
||||||
|
console.error('Rust watcher spawn error:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
rustWatcher.on('close', (code) => {
|
||||||
|
console.log('Rust watcher closed with code:', code);
|
||||||
|
// Only restart if we still have clients
|
||||||
|
if (clients.size > 0) {
|
||||||
|
console.log('Restarting watcher due to unexpected close');
|
||||||
|
// The watcher will be restarted when the next client connects
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store the watcher process for cleanup
|
||||||
|
(controller as any).rustWatcher = rustWatcher;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting up file watcher:', error);
|
console.error('Error setting up Rust file watcher:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,16 +127,17 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
// Stop watching if no clients are connected
|
// Stop watching if no clients are connected
|
||||||
if (clients.size === 0) {
|
if (clients.size === 0) {
|
||||||
stopWatching();
|
const rustWatcher = (controller as any).rustWatcher;
|
||||||
|
if (rustWatcher) {
|
||||||
|
console.log('Last client disconnected, stopping watcher');
|
||||||
|
rustWatcher.kill();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
// Handle stream cancellation - we can't access the specific controller here
|
// Handle stream cancellation - this is called when the stream is cancelled
|
||||||
// The abort event handler will handle cleanup for the specific controller
|
// We can't access the specific controller here, so we'll handle cleanup in the abort event
|
||||||
if (clients.size === 0) {
|
|
||||||
stopWatching();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import AboutButton from './AboutButton';
|
|||||||
import BadgeButton from './BadgeButton';
|
import BadgeButton from './BadgeButton';
|
||||||
import HeaderButtons from './HeaderButtons';
|
import HeaderButtons from './HeaderButtons';
|
||||||
import MobileNav from './MobileNav';
|
import MobileNav from './MobileNav';
|
||||||
|
import { withBaseUrl } from '@/lib/baseUrl';
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] });
|
const inter = Inter({ subsets: ['latin'] });
|
||||||
|
|
||||||
@@ -46,11 +47,11 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="de" className="h-full">
|
<html lang="de" className="h-full">
|
||||||
<Head>
|
<Head>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href={withBaseUrl('/favicon.ico')} />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href={withBaseUrl('/apple-touch-icon.png')} />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href={withBaseUrl('/favicon-32x32.png')} />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href={withBaseUrl('/favicon-16x16.png')} />
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href={withBaseUrl('/site.webmanifest')} />
|
||||||
</Head>
|
</Head>
|
||||||
<body className={`${inter.className} h-full min-h-screen flex flex-col`}>
|
<body className={`${inter.className} h-full min-h-screen flex flex-col`}>
|
||||||
<MobileNav blogOwner={blogOwner} />
|
<MobileNav blogOwner={blogOwner} />
|
||||||
|
|||||||
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';
|
||||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { withBaseUrl } from '@/lib/baseUrl';
|
||||||
|
|
||||||
interface Post {
|
interface Post {
|
||||||
type: 'post';
|
type: 'post';
|
||||||
@@ -47,7 +48,7 @@ export default function Home() {
|
|||||||
|
|
||||||
const setupSSE = () => {
|
const setupSSE = () => {
|
||||||
try {
|
try {
|
||||||
eventSource = new EventSource('/api/posts/stream');
|
eventSource = new EventSource(withBaseUrl('/api/posts/stream'));
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
eventSource.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
@@ -101,7 +102,7 @@ export default function Home() {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const response = await fetch('/api/posts');
|
const response = await fetch(withBaseUrl('/api/posts'));
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`API error: ${response.status}`);
|
throw new Error(`API error: ${response.status}`);
|
||||||
}
|
}
|
||||||
@@ -148,11 +149,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
|
// Helper to recursively collect all posts from the tree
|
||||||
function collectPosts(nodes: Node[]): Post[] {
|
function collectPosts(nodes: Node[]): Post[] {
|
||||||
let posts: Post[] = [];
|
let posts: Post[] = [];
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (node.type === 'post') {
|
if (node.type === 'post' && node.slug !== 'about') {
|
||||||
posts.push(node);
|
posts.push(node);
|
||||||
} else if (node.type === 'folder') {
|
} else if (node.type === 'folder') {
|
||||||
posts = posts.concat(collectPosts(node.children));
|
posts = posts.concat(collectPosts(node.children));
|
||||||
@@ -258,7 +269,7 @@ export default function Home() {
|
|||||||
)}
|
)}
|
||||||
<div>Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}</div>
|
<div>Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}</div>
|
||||||
</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">
|
<div className="flex flex-wrap gap-1 sm:gap-2">
|
||||||
{post.tags.map((tag: string) => {
|
{post.tags.map((tag: string) => {
|
||||||
const q = search.trim().toLowerCase();
|
const q = search.trim().toLowerCase();
|
||||||
@@ -317,7 +328,7 @@ export default function Home() {
|
|||||||
|
|
||||||
{/* Posts */}
|
{/* 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 pinnedPosts = posts.filter((post: any) => post.pinned);
|
||||||
const unpinnedPosts = posts.filter((post: any) => !post.pinned);
|
const unpinnedPosts = posts.filter((post: any) => !post.pinned);
|
||||||
return [...pinnedPosts, ...unpinnedPosts].map((post: any) => (
|
return [...pinnedPosts, ...unpinnedPosts].map((post: any) => (
|
||||||
@@ -341,7 +352,7 @@ export default function Home() {
|
|||||||
)}
|
)}
|
||||||
<div>Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}</div>
|
<div>Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}</div>
|
||||||
</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">
|
<div className="flex flex-wrap gap-1 sm:gap-2">
|
||||||
{post.tags.map((tag: string) => (
|
{post.tags.map((tag: string) => (
|
||||||
<span
|
<span
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
16
src/lib/baseUrl.ts
Normal file
16
src/lib/baseUrl.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
declare const process: { env: { NEXT_PUBLIC_BASE_URL?: string } };
|
||||||
|
|
||||||
|
export function withBaseUrl(path: string): string {
|
||||||
|
let base = '';
|
||||||
|
if (typeof process !== 'undefined' && process.env && process.env.NEXT_PUBLIC_BASE_URL) {
|
||||||
|
base = process.env.NEXT_PUBLIC_BASE_URL;
|
||||||
|
}
|
||||||
|
if (!base || base === '/') return path;
|
||||||
|
// Ensure base starts with / and does not end with /
|
||||||
|
if (!base.startsWith('/')) base = '/' + base;
|
||||||
|
if (base.endsWith('/')) base = base.slice(0, -1);
|
||||||
|
// Ensure path starts with /
|
||||||
|
if (!path.startsWith('/')) path = '/' + path;
|
||||||
|
// Avoid double slashes
|
||||||
|
return base + path;
|
||||||
|
}
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
// This is the frontend Markdown parser.
|
|
||||||
// It is written in TypeScript
|
|
||||||
// While I was writing this, only I and God knew how it works.
|
|
||||||
// Now, only God knows.
|
|
||||||
//
|
|
||||||
// If you are trying to understand how it works , and optimize it. Please increse the counter
|
|
||||||
//
|
|
||||||
// Hours wasted here: 12
|
|
||||||
|
|
||||||
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 chokidar from 'chokidar';
|
|
||||||
import type { FSWatcher } from 'chokidar';
|
|
||||||
import hljs from 'highlight.js';
|
|
||||||
import { getPostsDirectory } from './postsDirectory';
|
|
||||||
|
|
||||||
export interface Post {
|
|
||||||
slug: string;
|
|
||||||
title: string;
|
|
||||||
date: string;
|
|
||||||
tags: string[];
|
|
||||||
summary: string;
|
|
||||||
content: string;
|
|
||||||
createdAt: Date;
|
|
||||||
author: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const postsDirectory = getPostsDirectory();
|
|
||||||
|
|
||||||
// Function to get file creation date
|
|
||||||
function getFileCreationDate(filePath: string): Date {
|
|
||||||
const stats = fs.statSync(filePath);
|
|
||||||
return stats.birthtime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to generate ID from text (matches frontend logic)
|
|
||||||
function generateId(text: string): string {
|
|
||||||
return text
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9]+/g, '-')
|
|
||||||
.replace(/^-+|-+$/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enhanced slugification function that matches GitHub-style anchor links
|
|
||||||
function slugify(text: string): string {
|
|
||||||
return text
|
|
||||||
.toLowerCase()
|
|
||||||
.trim()
|
|
||||||
.replace(/[^\w\s-]/g, '') // Remove special characters except spaces and hyphens
|
|
||||||
.replace(/[\s_-]+/g, '-') // Replace spaces, underscores, and multiple hyphens with single hyphen
|
|
||||||
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to process anchor links in markdown content
|
|
||||||
function processAnchorLinks(content: string): string {
|
|
||||||
// Find all markdown links that point to anchors (e.g., [text](#anchor))
|
|
||||||
return content.replace(/\[([^\]]+)\]\(#([^)]+)\)/g, (match, linkText, anchor) => {
|
|
||||||
// Only slugify if the anchor doesn't already look like a slug
|
|
||||||
// This prevents double-processing of already-correct anchor links
|
|
||||||
const isAlreadySlugified = /^[a-z0-9-]+$/.test(anchor);
|
|
||||||
const slugifiedAnchor = isAlreadySlugified ? anchor : slugify(anchor);
|
|
||||||
return `[${linkText}](#${slugifiedAnchor})`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility function to debug anchor links (for development)
|
|
||||||
export function debugAnchorLinks(content: string): void {
|
|
||||||
if (process.env.NODE_ENV !== 'development') return;
|
|
||||||
|
|
||||||
console.log('=== Anchor Link Debug Info ===');
|
|
||||||
|
|
||||||
// Extract all headings and their IDs
|
|
||||||
const headingRegex = /^(#{1,6})\s+(.+)$/gm;
|
|
||||||
const headings: Array<{ level: number; text: string; id: string }> = [];
|
|
||||||
|
|
||||||
let match;
|
|
||||||
while ((match = headingRegex.exec(content)) !== null) {
|
|
||||||
const level = match[1].length;
|
|
||||||
const text = match[2].trim();
|
|
||||||
const id = slugify(text);
|
|
||||||
headings.push({ level, text, id });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Generated heading IDs:');
|
|
||||||
headings.forEach(({ level, text, id }) => {
|
|
||||||
console.log(` H${level}: "${text}" -> id="${id}"`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Extract all anchor links
|
|
||||||
const anchorLinkRegex = /\[([^\]]+)\]\(#([^)]+)\)/g;
|
|
||||||
const anchorLinks: Array<{ linkText: string; originalAnchor: string; slugifiedAnchor: string }> = [];
|
|
||||||
|
|
||||||
while ((match = anchorLinkRegex.exec(content)) !== null) {
|
|
||||||
const linkText = match[1];
|
|
||||||
const originalAnchor = match[2];
|
|
||||||
const slugifiedAnchor = slugify(originalAnchor);
|
|
||||||
anchorLinks.push({ linkText, originalAnchor, slugifiedAnchor });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Anchor links found:');
|
|
||||||
anchorLinks.forEach(({ linkText, originalAnchor, slugifiedAnchor }) => {
|
|
||||||
const headingExists = headings.some(h => h.id === slugifiedAnchor);
|
|
||||||
const status = headingExists ? '✅' : '❌';
|
|
||||||
console.log(` ${status} [${linkText}](#${originalAnchor}) -> [${linkText}](#${slugifiedAnchor})`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show missing headings
|
|
||||||
const missingAnchors = anchorLinks.filter(({ slugifiedAnchor }) =>
|
|
||||||
!headings.some(h => h.id === slugifiedAnchor)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (missingAnchors.length > 0) {
|
|
||||||
console.warn('Missing headings for these anchor links:');
|
|
||||||
missingAnchors.forEach(({ linkText, originalAnchor, slugifiedAnchor }) => {
|
|
||||||
console.warn(` - [${linkText}](#${originalAnchor}) -> id="${slugifiedAnchor}"`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('=== End Debug Info ===');
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderer = new marked.Renderer();
|
|
||||||
|
|
||||||
// Custom heading renderer to add IDs
|
|
||||||
renderer.heading = (text, level) => {
|
|
||||||
const id = slugify(text);
|
|
||||||
return `<h${level} id="${id}">${text}</h${level}>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
renderer.code = (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>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
marked.setOptions({
|
|
||||||
gfm: true,
|
|
||||||
breaks: true,
|
|
||||||
renderer,
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function getPostBySlug(slug: string): Promise<Post> {
|
|
||||||
const realSlug = slug.replace(/\.md$/, '');
|
|
||||||
const fullPath = path.join(postsDirectory, `${realSlug}.md`);
|
|
||||||
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
|
||||||
const { data, content } = matter(fileContents);
|
|
||||||
const createdAt = getFileCreationDate(fullPath);
|
|
||||||
|
|
||||||
let processedContent = '';
|
|
||||||
try {
|
|
||||||
// Debug anchor links in development
|
|
||||||
debugAnchorLinks(content);
|
|
||||||
|
|
||||||
// Process anchor links before parsing markdown
|
|
||||||
const processedMarkdown = processAnchorLinks(content);
|
|
||||||
const rawHtml = marked.parse(processedMarkdown);
|
|
||||||
const window = new JSDOM('').window;
|
|
||||||
const purify = DOMPurify(window);
|
|
||||||
processedContent = purify.sanitize(rawHtml as string, {
|
|
||||||
ALLOWED_TAGS: [
|
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
||||||
'p', 'a', 'ul', 'ol', 'li', 'blockquote',
|
|
||||||
'pre', 'code', 'em', 'strong', 'del',
|
|
||||||
'hr', 'br', 'img', 'table', 'thead', 'tbody',
|
|
||||||
'tr', 'th', 'td', 'div', 'span', 'iframe'
|
|
||||||
],
|
|
||||||
ALLOWED_ATTR: [
|
|
||||||
'class', 'id', 'style',
|
|
||||||
'href', 'target', 'rel',
|
|
||||||
'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
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Error processing markdown for ${realSlug}:`, err);
|
|
||||||
processedContent = `<div class="error-message">
|
|
||||||
<p>Error processing markdown content. Please check the console for details.</p>
|
|
||||||
<pre>${err instanceof Error ? err.message : 'Unknown error'}</pre>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
slug: realSlug,
|
|
||||||
title: data.title,
|
|
||||||
date: data.date,
|
|
||||||
tags: data.tags || [],
|
|
||||||
summary: data.summary,
|
|
||||||
content: processedContent,
|
|
||||||
createdAt,
|
|
||||||
author: process.env.NEXT_PUBLIC_BLOG_OWNER || 'Anonymous',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAllPosts(): Promise<Post[]> {
|
|
||||||
const fileNames = fs.readdirSync(postsDirectory);
|
|
||||||
const allPostsData = await Promise.all(
|
|
||||||
fileNames
|
|
||||||
.filter((fileName) => fileName.endsWith('.md'))
|
|
||||||
.map(async (fileName) => {
|
|
||||||
const slug = fileName.replace(/\.md$/, '');
|
|
||||||
return getPostBySlug(slug);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sort by creation date (newest first)
|
|
||||||
return allPostsData.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPostsByTag(tag: string): Promise<Post[]> {
|
|
||||||
const allPosts = await getAllPosts();
|
|
||||||
return allPosts.filter((post) => post.tags.includes(tag));
|
|
||||||
}
|
|
||||||
|
|
||||||
// File watcher setup
|
|
||||||
let watcher: FSWatcher | null = null;
|
|
||||||
let onChangeCallback: (() => void) | null = null;
|
|
||||||
|
|
||||||
export function watchPosts(callback: () => void) {
|
|
||||||
if (watcher) {
|
|
||||||
watcher.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeCallback = callback;
|
|
||||||
watcher = chokidar.watch(postsDirectory, {
|
|
||||||
ignored: [
|
|
||||||
/(^|[\/\\])\../, // ignore dotfiles
|
|
||||||
/node_modules/,
|
|
||||||
/\.git/,
|
|
||||||
/\.next/,
|
|
||||||
/\.cache/,
|
|
||||||
/\.DS_Store/,
|
|
||||||
/Thumbs\.db/,
|
|
||||||
/\.tmp$/,
|
|
||||||
/\.temp$/
|
|
||||||
],
|
|
||||||
persistent: true,
|
|
||||||
ignoreInitial: true, // Don't trigger on initial scan
|
|
||||||
awaitWriteFinish: {
|
|
||||||
stabilityThreshold: 1000, // Wait 1 second after file changes
|
|
||||||
pollInterval: 100 // Check every 100ms
|
|
||||||
},
|
|
||||||
usePolling: false, // Use native file system events when possible
|
|
||||||
interval: 1000 // Fallback polling interval (only used if native events fail)
|
|
||||||
});
|
|
||||||
|
|
||||||
watcher
|
|
||||||
.on('add', handleFileChange)
|
|
||||||
.on('change', handleFileChange)
|
|
||||||
.on('unlink', handleFileChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFileChange() {
|
|
||||||
if (onChangeCallback) {
|
|
||||||
onChangeCallback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stopWatching() {
|
|
||||||
if (watcher) {
|
|
||||||
watcher.close();
|
|
||||||
watcher = null;
|
|
||||||
}
|
|
||||||
onChangeCallback = null;
|
|
||||||
}
|
|
||||||
15
tooling/ascii.py
Executable file
15
tooling/ascii.py
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/python
|
||||||
|
from pyfiglet import Figlet
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def asciiart(text: str) -> str:
|
||||||
|
f = Figlet(font="slant")
|
||||||
|
return f.renderText(text)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("Usage: python ascii.py \"TEXT\"")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
text = sys.argv[1]
|
||||||
|
print(asciiart(text))
|
||||||
Reference in New Issue
Block a user