Compare commits
8 Commits
f94ddaa3b1
...
subpath
| Author | SHA1 | Date | |
|---|---|---|---|
| c3c74bb21f | |||
| e3d8ba1017 | |||
| a9879d9fa4 | |||
| 7d387080f5 | |||
| 09a673b65b | |||
| 6665f65529 | |||
| 7629164387 | |||
| 21f13ef8ae |
@@ -1,6 +1,9 @@
|
||||
#-------------------------------------------------------------------- # -----------------------------------------------------------------------#
|
||||
# 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_ABOUT_ME_LINK="http://localhost:80" # Your WebPage 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_LINK_IF_TRUE="http://github.com/ZockerKatze" # If you have GitHub then paste your link here #
|
||||
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
|
||||
steps:
|
||||
- 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
|
||||
run: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build Docker image
|
||||
run: docker build -t markdownblog .
|
||||
run: docker build -t localhost:3002/rattatwinko/markdownblog:latest .
|
||||
|
||||
- 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
|
||||
run: actions/upload-artifact@v3 --name markdownblog-docker-image --path markdownblog-image.tar
|
||||
|
||||
- name: Push Docker image
|
||||
run: docker push 10.0.0.13:3002/rattatwinko/markdownblog:latest
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: markdownblog-docker-image
|
||||
path: markdownblog-image.tar
|
||||
129
README.md
129
README.md
@@ -20,6 +20,9 @@ A modern, feature-rich blog system built with **Next.js 14**, **TypeScript**, **
|
||||
- **🎯 Content Management**: Drag & drop file uploads, post editing, and deletion
|
||||
- **📦 Export Functionality**: Export all posts as tar.gz archive (Docker only)
|
||||
- **💾 Smart Caching**: RAM-based caching system for instant post retrieval
|
||||
- **🔧 VS Code-Style Editor**: Monaco editor with YAML frontmatter support and live preview
|
||||
- **🔄 Force Reparse**: Manual cache clearing and post reparsing for immediate updates
|
||||
- **📁 Reliable Directory Scanning**: Robust file system traversal with error handling
|
||||
|
||||
---
|
||||
|
||||
@@ -43,13 +46,15 @@ A modern, feature-rich blog system built with **Next.js 14**, **TypeScript**, **
|
||||
markdownblog/
|
||||
├── markdown_backend/ # Rust backend for markdown processing
|
||||
│ ├── src/
|
||||
│ │ ├── main.rs # CLI interface and command handling
|
||||
│ │ └── markdown.rs # Markdown parsing, caching, and file watching
|
||||
│ ├── Cargo.toml # Rust dependencies and configuration
|
||||
│ └── target/ # Compiled Rust binaries
|
||||
│ │ ├── main.rs # CLI interface and command handling
|
||||
│ │ └── markdown.rs # Markdown parsing, caching, and file watching
|
||||
│ ├── Cargo.toml # Rust dependencies and configuration
|
||||
│ └── target/ # Compiled Rust binaries
|
||||
├── src/
|
||||
│ ├── app/ # Next.js 14 App Router
|
||||
│ ├── app/ # Next.js 14 App Router
|
||||
│ │ ├── admin/ # Admin dashboard pages
|
||||
│ │ │ ├── editor/ # VS Code-style editor
|
||||
│ │ │ │ └── page.tsx # Markdown editor with Monaco
|
||||
│ │ │ ├── manage/ # Content management interface
|
||||
│ │ │ │ ├── page.tsx # Manage posts and folders
|
||||
│ │ │ │ └── rust-status/ # Rust backend monitoring
|
||||
@@ -84,8 +89,12 @@ markdownblog/
|
||||
│ │ │ └── posts/ # Public post API
|
||||
│ │ │ ├── [slug]/ # Dynamic post API routes
|
||||
│ │ │ │ └── route.ts
|
||||
│ │ │ ├── preview/ # Markdown preview API
|
||||
│ │ │ │ └── route.ts
|
||||
│ │ │ ├── stream/ # Server-Sent Events for real-time updates
|
||||
│ │ │ │ └── route.ts
|
||||
│ │ │ ├── webhook/ # Webhook endpoint
|
||||
│ │ │ │ └── route.ts
|
||||
│ │ │ └── route.ts # List all posts
|
||||
│ │ ├── posts/ # Blog post pages
|
||||
│ │ │ └── [...slug]/ # Dynamic post routing (catch-all)
|
||||
@@ -97,15 +106,15 @@ markdownblog/
|
||||
│ │ ├── highlight-github.css # Code syntax highlighting styles
|
||||
│ │ ├── layout.tsx # Root layout with metadata
|
||||
│ │ ├── MobileNav.tsx # Mobile navigation component
|
||||
│ │ ├── monaco-vim.d.ts # Monaco Vim typings
|
||||
│ │ └── page.tsx # Homepage with post listing
|
||||
│ └── lib/ # Utility libraries
|
||||
│ └── postsDirectory.ts # Post directory management and Rust integration
|
||||
├── posts/ # Markdown blog posts storage
|
||||
│ ├── pinned.json # Pinned posts configuration
|
||||
│ ├── welcome.md # Welcome post with frontmatter
|
||||
│ ├── mdtest.md # Test post with various markdown features
|
||||
│ ├── anchor-test.md # Test post for anchor linking
|
||||
│ └── ii/ # Example nested folder structure
|
||||
│ ├── about.md
|
||||
│ ├── welcome.md
|
||||
│ └── assets/
|
||||
│ └── peta.png
|
||||
├── public/ # Static assets
|
||||
│ ├── android-chrome-192x192.png
|
||||
│ ├── android-chrome-512x512.png
|
||||
@@ -115,19 +124,19 @@ markdownblog/
|
||||
│ ├── favicon.ico
|
||||
│ └── site.webmanifest
|
||||
├── electron/ # Desktop application
|
||||
│ └── main.js # Electron main process configuration
|
||||
│ └── main.js # Electron main process configuration
|
||||
├── Dockerfile # Docker container configuration
|
||||
├── docker.sh # Docker deployment script
|
||||
├── entrypoint.sh # Container entrypoint script
|
||||
├── run-local-backend.sh # Local Rust backend runner
|
||||
├── next-env.d.ts # Next.js TypeScript definitions
|
||||
├── next.config.js # Next.js configuration
|
||||
├── package-lock.json # npm lock file
|
||||
├── package.json # Dependencies and scripts
|
||||
├── postcss.config.js # PostCSS configuration
|
||||
├── tailwind.config.js # Tailwind CSS configuration
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
└── LICENSE # MIT License
|
||||
├── entrypoint.sh # Container entrypoint script
|
||||
├── run-local-backend.sh # Local Rust backend runner
|
||||
├── next-env.d.ts # Next.js TypeScript definitions
|
||||
├── next.config.js # Next.js configuration
|
||||
├── package-lock.json # npm lock file
|
||||
├── package.json # Dependencies and scripts
|
||||
├── postcss.config.js # PostCSS configuration
|
||||
├── tailwind.config.js # Tailwind CSS configuration
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
└── LICENSE # MIT License
|
||||
```
|
||||
|
||||
### Key Components
|
||||
@@ -329,6 +338,9 @@ console.log("Hello, World!");
|
||||
- **📦 Export Posts**: Download all posts as archive (Docker only)
|
||||
- **📊 Rust Status**: Monitor parser performance, logs, and health
|
||||
- **🔍 Log Management**: View, filter, and clear parser logs
|
||||
- **🔧 VS Code Editor**: Monaco-based editor with YAML frontmatter preservation
|
||||
- **🔄 Force Reparse**: Manual cache clearing and post reparsing
|
||||
- **📁 Reliable Scanning**: Enhanced directory traversal with error recovery
|
||||
|
||||
### Security
|
||||
|
||||
@@ -350,6 +362,9 @@ console.log("Hello, World!");
|
||||
- **📁 Recursive Scanning**: Efficient folder traversal and file discovery
|
||||
- **💾 Smart Caching**: RAM-based caching with disk persistence
|
||||
- **📊 Performance Monitoring**: Real-time metrics and logging
|
||||
- **🔄 Force Reparse**: Manual cache invalidation and post reparsing
|
||||
- **📁 Reliable Directory Scanning**: Robust error handling and recovery
|
||||
- **🔧 Single Post Reparse**: Efficient individual post cache clearing
|
||||
|
||||
### Real-Time Updates
|
||||
|
||||
@@ -358,6 +373,16 @@ console.log("Hello, World!");
|
||||
- **⚡ Instant Updates**: Sub-second response to file modifications
|
||||
- **🔄 Fallback Polling**: Graceful degradation if SSE fails
|
||||
|
||||
### VS Code-Style Editor
|
||||
|
||||
- **🔧 Monaco Editor**: Professional code editor with syntax highlighting
|
||||
- **📄 YAML Frontmatter**: Preserved and editable at the top of files
|
||||
- **👁️ Live Preview**: Real-time Markdown rendering
|
||||
- **💾 Save & Reparse**: Automatic cache clearing and post reparsing
|
||||
- **⌨️ Vim Mode**: Optional Vim keybindings for power users
|
||||
- **📱 Responsive Design**: Works on desktop and mobile devices
|
||||
- **🎨 Custom Styling**: JetBrains Mono font and VS Code-like appearance
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Customization
|
||||
@@ -398,6 +423,8 @@ cargo build --release # Build optimized binary
|
||||
cargo run -- watch # Watch for file changes
|
||||
cargo run -- logs # View parser logs
|
||||
cargo run -- checkhealth # Check backend health
|
||||
cargo run -- reinterpret-all # Force reparse all posts
|
||||
cargo run -- reparse-post <slug> # Force reparse single post
|
||||
```
|
||||
|
||||
---
|
||||
@@ -434,7 +461,65 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
||||
- **File watching issues**: Check file permissions and inotify limits
|
||||
- **Performance issues**: Monitor logs via admin interface
|
||||
- **Cache problems**: Clear cache via admin interface or restart
|
||||
- **Directory scanning errors**: Check file permissions and hidden files
|
||||
- **Reparse failures**: Verify post slugs and file existence
|
||||
- **Memory issues**: Monitor cache size and clear if necessary
|
||||
|
||||
### Support
|
||||
|
||||
For issues and questions, please check the project structure and API documentation in the codebase. The admin interface includes comprehensive monitoring tools for the Rust backend.
|
||||
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
|
||||
fi
|
||||
|
||||
# Output with colors
|
||||
GREEN='\033[1;32m' # Green
|
||||
CYAN='\033[1;36m'
|
||||
RESET='\033[0m'
|
||||
|
||||
echo ""
|
||||
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 isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
/*
|
||||
|
||||
This will be discontinued in a bit.
|
||||
Either move to Docker or get fucked.
|
||||
|
||||
*/
|
||||
|
||||
function createWindow() {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
@@ -14,7 +21,9 @@ function createWindow() {
|
||||
|
||||
// Load the Next.js app
|
||||
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();
|
||||
} else {
|
||||
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,7 +1,17 @@
|
||||
#[warn(unused_imports)]
|
||||
use clap::{Parser, Subcommand};
|
||||
mod markdown;
|
||||
use markdown::{get_all_posts, get_post_by_slug, get_posts_by_tag, watch_posts, get_parser_logs, clear_parser_logs, load_parser_logs_from_disk};
|
||||
use markdown::{
|
||||
get_all_posts,
|
||||
get_post_by_slug,
|
||||
get_posts_by_tag,
|
||||
watch_posts,
|
||||
get_parser_logs,
|
||||
clear_parser_logs,
|
||||
load_parser_logs_from_disk,
|
||||
force_reinterpret_all_posts,
|
||||
force_reparse_single_post
|
||||
};
|
||||
use serde_json;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
@@ -16,7 +26,7 @@ use std::io::Read; // STD AYOOOOOOOOOOOOOO - Tsodin
|
||||
|
||||
#[derive(Parser)]
|
||||
#[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 {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
@@ -44,6 +54,12 @@ enum Commands {
|
||||
Logs,
|
||||
/// Clear parser logs
|
||||
ClearLogs,
|
||||
/// Force reinterpret all posts (clear cache and re-parse)
|
||||
ReinterpretAll,
|
||||
/// Force reparse a single post (clear cache and re-parse)
|
||||
ReparsePost {
|
||||
slug: String,
|
||||
},
|
||||
/// Parse markdown from file or stdin
|
||||
Parse {
|
||||
#[arg(long)]
|
||||
@@ -87,9 +103,9 @@ fn main() {
|
||||
println!("{}", serde_json::to_string(&posts).unwrap());
|
||||
}
|
||||
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(|| {
|
||||
println!("Posts directory changed!");
|
||||
println!("Posts-Verzeichnis hat sich geändert!");
|
||||
});
|
||||
// Keep the main thread alive
|
||||
loop {
|
||||
@@ -109,7 +125,36 @@ fn main() {
|
||||
}
|
||||
Commands::ClearLogs => {
|
||||
clear_parser_logs();
|
||||
println!("{}", serde_json::to_string(&serde_json::json!({"success": true, "message": "Logs cleared"})).unwrap());
|
||||
println!("{}", serde_json::to_string(&serde_json::json!({"success": true, "message": "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());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::ReparsePost { slug } => {
|
||||
match force_reparse_single_post(slug) {
|
||||
Ok(post) => {
|
||||
println!("{}", serde_json::to_string(&serde_json::json!({
|
||||
"success": true,
|
||||
"message": format!("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 {
|
||||
@@ -119,7 +164,7 @@ fn main() {
|
||||
} else if let Some(file_path) = file {
|
||||
fs::read_to_string(file_path).unwrap()
|
||||
} else {
|
||||
eprintln!("Either --file or --stdin must be specified");
|
||||
eprintln!("Entweder --file oder --stdin muss angegeben werden");
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
@@ -139,4 +184,4 @@ fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,6 +180,18 @@ static AMMONIA: Lazy<ammonia::Builder<'static>> = Lazy::new(|| {
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
fn ensure_cache_directory() {
|
||||
let cache_dir = PathBuf::from("./cache");
|
||||
if !cache_dir.exists() {
|
||||
if let Err(e) = fs::create_dir_all(&cache_dir) {
|
||||
eprintln!("Fehler beim Erstellen des Cache-Verzeichnisses: {}", e);
|
||||
add_log("error", &format!("Fehler beim Erstellen des Cache-Verzeichnisses: {}", e), None, None);
|
||||
} else {
|
||||
add_log("info", "Cache-Verzeichnis erstellt: ./cache", None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_posts_directory() -> PathBuf {
|
||||
let is_docker = std::env::var("DOCKER_CONTAINER").is_ok()
|
||||
|| std::env::var("KUBERNETES_SERVICE_HOST").is_ok()
|
||||
@@ -207,29 +219,101 @@ fn get_posts_directory() -> PathBuf {
|
||||
for candidate in candidates.iter() {
|
||||
let path = PathBuf::from(candidate);
|
||||
if path.exists() && path.is_dir() {
|
||||
add_log("info", &format!("Verwende Posts-Verzeichnis: {:?}", path), None, None);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
// Fallback: default to ./posts
|
||||
PathBuf::from("./posts")
|
||||
|
||||
// Fallback: create ./posts if it doesn't exist
|
||||
let fallback_path = PathBuf::from("./posts");
|
||||
if !fallback_path.exists() {
|
||||
if let Err(e) = fs::create_dir_all(&fallback_path) {
|
||||
add_log("error", &format!("Fehler beim Erstellen des Posts-Verzeichnisses: {}", e), None, None);
|
||||
} else {
|
||||
add_log("info", "Posts-Verzeichnis erstellt: ./posts", None, None);
|
||||
}
|
||||
}
|
||||
fallback_path
|
||||
}
|
||||
|
||||
// Function to find Markdown files.
|
||||
// This will scan Directories recursively
|
||||
// Function to find Markdown files with improved reliability
|
||||
fn find_markdown_files(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
|
||||
let mut files = Vec::new();
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
files.extend(find_markdown_files(&path)?);
|
||||
} else if path.extension().map(|e| e == "md").unwrap_or(false) {
|
||||
files.push(path);
|
||||
let mut errors = Vec::new();
|
||||
|
||||
if !dir.exists() {
|
||||
let error_msg = format!("Verzeichnis existiert nicht: {:?}", dir);
|
||||
add_log("error", &error_msg, None, None);
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, error_msg));
|
||||
}
|
||||
|
||||
if !dir.is_dir() {
|
||||
let error_msg = format!("Pfad ist kein Verzeichnis: {:?}", dir);
|
||||
add_log("error", &error_msg, None, None);
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, error_msg));
|
||||
}
|
||||
|
||||
// Try to read directory with retry logic
|
||||
let entries = match fs::read_dir(dir) {
|
||||
Ok(entries) => entries,
|
||||
Err(e) => {
|
||||
add_log("error", &format!("Fehler beim Lesen des Verzeichnisses {:?}: {}", dir, e), None, None);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
for entry_result in entries {
|
||||
match entry_result {
|
||||
Ok(entry) => {
|
||||
let path = entry.path();
|
||||
|
||||
// Skip hidden files and directories
|
||||
if let Some(name) = path.file_name() {
|
||||
if name.to_string_lossy().starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
// Recursively scan subdirectories
|
||||
match find_markdown_files(&path) {
|
||||
Ok(subfiles) => files.extend(subfiles),
|
||||
Err(e) => {
|
||||
let error_msg = format!("Fehler beim Scannen des Unterverzeichnisses {:?}: {}", path, e);
|
||||
add_log("warning", &error_msg, None, None);
|
||||
errors.push(error_msg);
|
||||
}
|
||||
}
|
||||
} else if path.extension().map(|e| e == "md").unwrap_or(false) {
|
||||
// Verify the file is readable
|
||||
match fs::metadata(&path) {
|
||||
Ok(metadata) => {
|
||||
if metadata.is_file() {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Datei nicht zugänglich {:?}: {}", path, e);
|
||||
add_log("warning", &error_msg, None, None);
|
||||
errors.push(error_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Fehler beim Lesen des Verzeichniseintrags: {}", e);
|
||||
add_log("warning", &error_msg, None, None);
|
||||
errors.push(error_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log summary
|
||||
add_log("info", &format!("{} Markdown-Dateien in {:?} gefunden", files.len(), dir), None, None);
|
||||
if !errors.is_empty() {
|
||||
add_log("warning", &format!("{} Fehler während der Verzeichnissuche aufgetreten", errors.len()), None, None);
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
@@ -312,11 +396,11 @@ fn process_custom_tags(content: &str) -> String {
|
||||
|
||||
// Handle simple tags without parameters
|
||||
let simple_tags = [
|
||||
("<mytag />", "<div class=\"custom-tag mytag\">This is my custom tag content!</div>"),
|
||||
("<warning />", "<div class=\"custom-tag warning\" style=\"background: #fff3cd; border: 1px solid #ffeaa7; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">⚠️ Warning: This is a custom warning tag!</div>"),
|
||||
("<info />", "<div class=\"custom-tag info\" style=\"background: #d1ecf1; border: 1px solid #bee5eb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">ℹ️ Info: This is a custom info tag!</div>"),
|
||||
("<success />", "<div class=\"custom-tag success\" style=\"background: #d4edda; border: 1px solid #c3e6cb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">✅ Success: This is a custom success tag!</div>"),
|
||||
("<error />", "<div class=\"custom-tag error\" style=\"background: #f8d7da; border: 1px solid #f5c6cb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">❌ Error: This is a custom error tag!</div>"),
|
||||
("<mytag />", "<div class=\"custom-tag mytag\">Dies ist mein benutzerdefinierter Tag-Inhalt!</div>"),
|
||||
("<warning />", "<div class=\"custom-tag warning\" style=\"background: #fff3cd; border: 1px solid #ffeaa7; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">⚠️ Warnung: Dies ist ein benutzerdefiniertes Warnungs-Tag!</div>"),
|
||||
("<info />", "<div class=\"custom-tag info\" style=\"background: #d1ecf1; border: 1px solid #bee5eb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">ℹ️ Info: Dies ist ein benutzerdefiniertes Info-Tag!</div>"),
|
||||
("<success />", "<div class=\"custom-tag success\" style=\"background: #d4edda; border: 1px solid #c3e6cb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">✅ Erfolg: Dies ist ein benutzerdefiniertes Erfolgs-Tag!</div>"),
|
||||
("<error />", "<div class=\"custom-tag error\" style=\"background: #f8d7da; border: 1px solid #f5c6cb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">❌ Fehler: Dies ist ein benutzerdefiniertes Fehler-Tag!</div>"),
|
||||
];
|
||||
|
||||
for (tag, replacement) in simple_tags.iter() {
|
||||
@@ -331,18 +415,18 @@ fn process_custom_tags(content: &str) -> String {
|
||||
|
||||
match tag_name {
|
||||
"mytag" => {
|
||||
format!("<div class=\"custom-tag mytag\" data-params=\"{}\">Custom content with params: {}</div>", params, params)
|
||||
format!("<div class=\"custom-tag mytag\" data-params=\"{}\">Benutzerdefinierter Inhalt mit Parametern: {}</div>", params, params)
|
||||
},
|
||||
"alert" => {
|
||||
if params.contains("type=\"warning\"") {
|
||||
"<div class=\"custom-tag alert warning\" style=\"background: #fff3cd; border: 1px solid #ffeaa7; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">⚠️ Warning Alert!</div>".to_string()
|
||||
"<div class=\"custom-tag alert warning\" style=\"background: #fff3cd; border: 1px solid #ffeaa7; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">⚠️ Warnungs-Alert!</div>".to_string()
|
||||
} else if params.contains("type=\"error\"") {
|
||||
"<div class=\"custom-tag alert error\" style=\"background: #f8d7da; border: 1px solid #f5c6cb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">❌ Error Alert!</div>".to_string()
|
||||
"<div class=\"custom-tag alert error\" style=\"background: #f8d7da; border: 1px solid #f5c6cb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">❌ Fehler-Alert!</div>".to_string()
|
||||
} else {
|
||||
"<div class=\"custom-tag alert info\" style=\"background: #d1ecf1; border: 1px solid #bee5eb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">ℹ️ Info Alert!</div>".to_string()
|
||||
"<div class=\"custom-tag alert info\" style=\"background: #d1ecf1; border: 1px solid #bee5eb; padding: 1rem; border-radius: 4px; margin: 1rem 0;\">ℹ️ Info-Alert!</div>".to_string()
|
||||
}
|
||||
},
|
||||
_ => format!("<div class=\"custom-tag {}\">Unknown custom tag: {}</div>", tag_name, tag_name)
|
||||
_ => format!("<div class=\"custom-tag {}\">Unbekanntes benutzerdefiniertes Tag: {}</div>", tag_name, tag_name)
|
||||
}
|
||||
}).to_string();
|
||||
|
||||
@@ -372,7 +456,7 @@ fn add_log(level: &str, message: &str, slug: Option<&str>, details: Option<&str>
|
||||
}
|
||||
|
||||
fn save_parser_logs_to_disk_inner(logs: &VecDeque<LogEntry>) -> std::io::Result<()> {
|
||||
let _ = std::fs::create_dir_all("./cache");
|
||||
ensure_cache_directory();
|
||||
let logs_vec: Vec<_> = logs.iter().cloned().collect();
|
||||
let json = serde_json::to_string(&logs_vec)?;
|
||||
std::fs::write(PARSER_LOGS_PATH, json)?;
|
||||
@@ -406,7 +490,7 @@ pub fn rsparseinfo() -> String {
|
||||
// This Function gets the Post by its Slugified Version.
|
||||
// This is basically only used for Caching (loading from it).
|
||||
pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>> {
|
||||
add_log("info", "Starting post parsing", Some(slug), None);
|
||||
add_log("info", "Starte Post-Parsing", Some(slug), None);
|
||||
|
||||
let mut sys = System::new_with_specifics(RefreshKind::new().with_processes(ProcessRefreshKind::everything()).with_cpu(CpuRefreshKind::everything()));
|
||||
sys.refresh_processes();
|
||||
@@ -428,7 +512,7 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
|
||||
entry.last_cache_status = "hit".to_string();
|
||||
sys.refresh_process(pid);
|
||||
entry.last_cpu_usage_percent = sys.process(pid).map(|p| p.cpu_usage()).unwrap_or(0.0) - before_cpu;
|
||||
add_log("info", "Cache hit", Some(slug), None);
|
||||
add_log("info", "Cache-Treffer", Some(slug), None);
|
||||
return Ok(post);
|
||||
}
|
||||
|
||||
@@ -440,16 +524,16 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
|
||||
let file_path = slug_to_path(slug, &posts_dir);
|
||||
|
||||
if !file_path.exists() {
|
||||
let error_msg = format!("File not found: {:?}", file_path);
|
||||
let error_msg = format!("Datei nicht gefunden: {:?}", file_path);
|
||||
add_log("error", &error_msg, Some(slug), None);
|
||||
return Err(error_msg.into());
|
||||
}
|
||||
|
||||
let file_content = fs::read_to_string(&file_path)?;
|
||||
add_log("info", &format!("File loaded: {} bytes", file_content.len()), Some(slug), None);
|
||||
add_log("info", &format!("Datei geladen: {} Bytes", file_content.len()), Some(slug), None);
|
||||
|
||||
if file_content.len() > MAX_FILE_SIZE {
|
||||
let error_msg = format!("File too large: {} bytes (max: {} bytes)", file_content.len(), MAX_FILE_SIZE);
|
||||
let error_msg = format!("Datei zu groß: {} Bytes (max: {} Bytes)", file_content.len(), MAX_FILE_SIZE);
|
||||
add_log("error", &error_msg, Some(slug), None);
|
||||
return Err(error_msg.into());
|
||||
}
|
||||
@@ -461,21 +545,21 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
|
||||
match data.deserialize() {
|
||||
Ok(front) => front,
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to deserialize frontmatter: {}", e);
|
||||
let error_msg = format!("Fehler beim Deserialisieren des Frontmatters: {}", e);
|
||||
add_log("error", &error_msg, Some(slug), None);
|
||||
return Err(error_msg.into());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
add_log("error", "No frontmatter found", Some(slug), None);
|
||||
return Err("No frontmatter found".into());
|
||||
add_log("error", "Kein Frontmatter gefunden", Some(slug), None);
|
||||
return Err("Kein Frontmatter gefunden".into());
|
||||
};
|
||||
|
||||
let created_at = get_file_creation_date(&file_path)?;
|
||||
let processed_markdown = process_anchor_links(&result.content);
|
||||
let processed_markdown = process_custom_tags(&processed_markdown);
|
||||
|
||||
add_log("info", "Starting markdown parsing", Some(slug), Some(&format!("Content length: {} chars", processed_markdown.len())));
|
||||
add_log("info", "Starte Markdown-Parsing", Some(slug), Some(&format!("Inhaltslänge: {} Zeichen", processed_markdown.len())));
|
||||
|
||||
let parser = Parser::new_ext(&processed_markdown, Options::all());
|
||||
let mut html_output = String::new();
|
||||
@@ -496,8 +580,8 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
|
||||
for event in parser {
|
||||
event_count += 1;
|
||||
if start_parsing.elapsed().as_secs() > PARSING_TIMEOUT_SECS {
|
||||
let error_msg = "Parsing timeout - file too large";
|
||||
add_log("error", error_msg, Some(slug), Some(&format!("Processed {} events", event_count)));
|
||||
let error_msg = "Parsing-Timeout - Datei zu groß";
|
||||
add_log("error", error_msg, Some(slug), Some(&format!("{} Events verarbeitet", event_count)));
|
||||
return Err(error_msg.into());
|
||||
}
|
||||
|
||||
@@ -550,7 +634,7 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
|
||||
}
|
||||
}
|
||||
|
||||
add_log("info", "Markdown parsing completed", Some(slug), Some(&format!("Processed {} events", event_count)));
|
||||
add_log("info", "Markdown-Parsing abgeschlossen", Some(slug), Some(&format!("{} Events verarbeitet", event_count)));
|
||||
|
||||
html::push_html(&mut html_output, events.into_iter());
|
||||
let sanitized_html = AMMONIA.clean(&html_output).to_string();
|
||||
@@ -565,7 +649,7 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
|
||||
summary: front.summary,
|
||||
content: sanitized_html,
|
||||
created_at: created_at.to_rfc3339(),
|
||||
author: std::env::var("BLOG_OWNER").unwrap_or_else(|_| "Anonymous".to_string()),
|
||||
author: std::env::var("BLOG_OWNER").unwrap_or_else(|_| "Anonym".to_string()),
|
||||
};
|
||||
let compile_time = compile_start.elapsed();
|
||||
|
||||
@@ -584,7 +668,7 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
|
||||
sys.refresh_process(pid);
|
||||
entry.last_cpu_usage_percent = sys.process(pid).map(|p| p.cpu_usage()).unwrap_or(0.0) - before_cpu;
|
||||
|
||||
add_log("info", "Post parsing completed successfully", Some(slug), Some(&format!("Interpret: {}ms, Compile: {}ms", interpret_time.as_millis(), compile_time.as_millis())));
|
||||
add_log("info", "Post-Parsing erfolgreich abgeschlossen", Some(slug), Some(&format!("Interpretation: {}ms, Kompilierung: {}ms", interpret_time.as_millis(), compile_time.as_millis())));
|
||||
|
||||
Ok(post)
|
||||
}
|
||||
@@ -631,7 +715,7 @@ pub fn watch_posts<F: Fn() + Send + 'static>(on_change: F) -> notify::Result<Rec
|
||||
on_change();
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("watch error: {:?}", e);
|
||||
eprintln!("Überwachungsfehler: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -654,12 +738,11 @@ pub fn load_post_cache_from_disk() {
|
||||
}
|
||||
|
||||
pub fn save_post_cache_to_disk() {
|
||||
ensure_cache_directory();
|
||||
if let Ok(map) = serde_json::to_string(&*POST_CACHE.read().unwrap()) {
|
||||
let _ = fs::create_dir_all("./cache");
|
||||
let _ = fs::write(POSTS_CACHE_PATH, map);
|
||||
}
|
||||
if let Ok(map) = serde_json::to_string(&*POST_STATS.read().unwrap()) {
|
||||
let _ = fs::create_dir_all("./cache");
|
||||
let _ = fs::write(POST_STATS_PATH, map);
|
||||
}
|
||||
}
|
||||
@@ -677,10 +760,10 @@ pub fn checkhealth() -> HealthReport {
|
||||
.filter(|e| e.path().extension().map(|ext| ext == "md").unwrap_or(false))
|
||||
.count();
|
||||
},
|
||||
Err(e) => errors.push(format!("Failed to read posts dir: {}", e)),
|
||||
Err(e) => errors.push(format!("Fehler beim Lesen des Posts-Verzeichnisses: {}", e)),
|
||||
}
|
||||
} else {
|
||||
errors.push("Posts directory does not exist".to_string());
|
||||
errors.push("Posts-Verzeichnis existiert nicht".to_string());
|
||||
}
|
||||
|
||||
let cache_file_exists = Path::new(POSTS_CACHE_PATH).exists();
|
||||
@@ -695,10 +778,10 @@ pub fn checkhealth() -> HealthReport {
|
||||
cache_readable = true;
|
||||
cache_post_count = Some(map.len());
|
||||
},
|
||||
Err(e) => errors.push(format!("Cache file not valid JSON: {}", e)),
|
||||
Err(e) => errors.push(format!("Cache-Datei ist kein gültiges JSON: {}", e)),
|
||||
}
|
||||
},
|
||||
Err(e) => errors.push(format!("Failed to read cache file: {}", e)),
|
||||
Err(e) => errors.push(format!("Fehler beim Lesen der Cache-Datei: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,10 +794,10 @@ pub fn checkhealth() -> HealthReport {
|
||||
cache_stats_readable = true;
|
||||
cache_stats_count = Some(map.len());
|
||||
},
|
||||
Err(e) => errors.push(format!("Cache stats file not valid JSON: {}", e)),
|
||||
Err(e) => errors.push(format!("Cache-Statistik-Datei ist kein gültiges JSON: {}", e)),
|
||||
}
|
||||
},
|
||||
Err(e) => errors.push(format!("Failed to read cache stats file: {}", e)),
|
||||
Err(e) => errors.push(format!("Fehler beim Lesen der Cache-Statistik-Datei: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,7 +822,90 @@ pub fn get_parser_logs() -> Vec<LogEntry> {
|
||||
}
|
||||
|
||||
pub fn clear_parser_logs() {
|
||||
let mut logs = PARSER_LOGS.write().unwrap();
|
||||
logs.clear();
|
||||
let _ = std::fs::remove_file(PARSER_LOGS_PATH);
|
||||
PARSER_LOGS.write().unwrap().clear();
|
||||
if let Err(e) = save_parser_logs_to_disk_inner(&VecDeque::new()) {
|
||||
eprintln!("Fehler beim Speichern leerer Protokolle auf Festplatte: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Force reinterpret all posts by clearing cache and re-parsing
|
||||
pub fn force_reinterpret_all_posts() -> Result<Vec<Post>, Box<dyn std::error::Error>> {
|
||||
add_log("info", "Starte erzwungene Neuinterpretation aller Posts", None, None);
|
||||
|
||||
// Clear all caches
|
||||
POST_CACHE.write().unwrap().clear();
|
||||
ALL_POSTS_CACHE.write().unwrap().take();
|
||||
POST_STATS.write().unwrap().clear();
|
||||
|
||||
add_log("info", "Alle Caches geleert", None, None);
|
||||
|
||||
// Get posts directory and find all markdown files
|
||||
let posts_dir = get_posts_directory();
|
||||
let markdown_files = find_markdown_files(&posts_dir)?;
|
||||
|
||||
add_log("info", &format!("{} Markdown-Dateien zur Neuinterpretation gefunden", markdown_files.len()), None, None);
|
||||
|
||||
let mut posts = Vec::new();
|
||||
let mut success_count = 0;
|
||||
let mut error_count = 0;
|
||||
|
||||
for file_path in markdown_files {
|
||||
let slug = path_to_slug(&file_path, &posts_dir);
|
||||
match get_post_by_slug(&slug) {
|
||||
Ok(post) => {
|
||||
posts.push(post);
|
||||
success_count += 1;
|
||||
add_log("info", &format!("Erfolgreich neuinterpretiert: {}", slug), Some(&slug), None);
|
||||
}
|
||||
Err(e) => {
|
||||
error_count += 1;
|
||||
add_log("error", &format!("Fehler bei der Neuinterpretation von {}: {}", slug, e), Some(&slug), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the all posts cache
|
||||
ALL_POSTS_CACHE.write().unwrap().replace(posts.clone());
|
||||
|
||||
// Save cache to disk
|
||||
save_post_cache_to_disk();
|
||||
|
||||
add_log("info", &format!("Erzwungene Neuinterpretation abgeschlossen. Erfolgreich: {}, Fehler: {}", success_count, error_count), None, None);
|
||||
|
||||
Ok(posts)
|
||||
}
|
||||
|
||||
// Force reparse a single post by clearing its cache and re-parsing
|
||||
pub fn force_reparse_single_post(slug: &str) -> Result<Post, Box<dyn std::error::Error>> {
|
||||
add_log("info", &format!("Starte erzwungenes Neuparsing des Posts: {}", slug), Some(slug), None);
|
||||
|
||||
// Clear this specific post from all caches
|
||||
POST_CACHE.write().unwrap().remove(slug);
|
||||
POST_STATS.write().unwrap().remove(slug);
|
||||
|
||||
// Clear the all posts cache since it might contain this post
|
||||
ALL_POSTS_CACHE.write().unwrap().take();
|
||||
|
||||
add_log("info", &format!("Cache für Post geleert: {}", slug), Some(slug), None);
|
||||
|
||||
// Re-parse the post
|
||||
let post = get_post_by_slug(slug)?;
|
||||
|
||||
// Update the all posts cache with the new post
|
||||
let mut all_posts_cache = ALL_POSTS_CACHE.write().unwrap();
|
||||
if let Some(ref mut posts) = *all_posts_cache {
|
||||
// Remove old version if it exists
|
||||
posts.retain(|p| p.slug != slug);
|
||||
// Add new version
|
||||
posts.push(post.clone());
|
||||
// Sort by creation date
|
||||
posts.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
}
|
||||
|
||||
// Save cache to disk
|
||||
save_post_cache_to_disk();
|
||||
|
||||
add_log("info", &format!("Post erfolgreich neugeparst: {}", slug), Some(slug), None);
|
||||
|
||||
Ok(post)
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
project_setup:
|
||||
description: Setup Rust and Next.js to compile Rust to WASM for web use
|
||||
prerequisites:
|
||||
- Node.js >= 18
|
||||
- Rust >= 1.70
|
||||
- wasm-pack installed (`cargo install wasm-pack`)
|
||||
- Next.js app created (`npx create-next-app@latest`)
|
||||
- Optional: TypeScript enabled
|
||||
steps:
|
||||
- name: Create Rust crate
|
||||
run: |
|
||||
mkdir rust-wasm
|
||||
cd rust-wasm
|
||||
cargo new --lib wasm_core
|
||||
cd wasm_core
|
||||
|
||||
- name: Add wasm dependencies to Cargo.toml
|
||||
file: rust-wasm/wasm_core/Cargo.toml
|
||||
append:
|
||||
dependencies:
|
||||
wasm-bindgen: "0.2"
|
||||
[lib]:
|
||||
crate-type: ["cdylib"]
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt: true
|
||||
|
||||
- name: Write simple Rust function
|
||||
file: rust-wasm/wasm_core/src/lib.rs
|
||||
content: |
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn greet(name: &str) -> String {
|
||||
format!("Hello, {}!", name)
|
||||
}
|
||||
|
||||
- name: Build WASM module with wasm-pack
|
||||
run: |
|
||||
cd rust-wasm/wasm_core
|
||||
wasm-pack build --target web --out-dir ../pkg
|
||||
|
||||
nextjs_setup:
|
||||
description: Integrate the WASM output into a Next.js app
|
||||
steps:
|
||||
- name: Move compiled WASM pkg to Next.js public or static
|
||||
run: |
|
||||
# Assuming your Next.js app is in ../my-app
|
||||
mkdir -p ../my-app/public/wasm
|
||||
cp -r ../rust-wasm/pkg/* ../my-app/public/wasm/
|
||||
|
||||
- name: Import and initialize WASM in React
|
||||
file: my-app/app/page.tsx
|
||||
content: |
|
||||
'use client';
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Home() {
|
||||
const [output, setOutput] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const wasm = await import("../../public/wasm/wasm_core.js");
|
||||
await wasm.default(); // init
|
||||
const result = wasm.greet("Next.js + Rust");
|
||||
setOutput(result);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <div className="p-4 font-mono text-xl">{output}</div>;
|
||||
}
|
||||
|
||||
- name: Add TypeScript support (optional)
|
||||
tips:
|
||||
- Type declarations are not emitted automatically by wasm-pack
|
||||
- You can write your own `.d.ts` file for the exposed functions
|
||||
example_file: my-app/types/wasm_core.d.ts
|
||||
content: |
|
||||
declare module "public/wasm/wasm_core.js" {
|
||||
export function greet(name: string): string;
|
||||
export default function init(): Promise<void>;
|
||||
}
|
||||
|
||||
- name: Optimize WebAssembly (optional)
|
||||
tips:
|
||||
- Install `binaryen` to use `wasm-opt`
|
||||
- Run `wasm-pack build --release` with `wasm-opt` enabled
|
||||
- Produces smaller, faster WASM binaries
|
||||
|
||||
dev_commands:
|
||||
- command: cargo install wasm-pack
|
||||
description: Install wasm-pack for building to WebAssembly
|
||||
- command: wasm-pack build --target web
|
||||
description: Build the Rust crate into WASM + JS bindings for browser
|
||||
- command: npm run dev
|
||||
description: Start your Next.js frontend
|
||||
|
||||
optional_advanced:
|
||||
- name: Use `wasm-bindgen` directly (without wasm-pack)
|
||||
reason: More control over output but more setup required
|
||||
- name: Use `next-transpile-modules` to load WASM via import from `pkg/`
|
||||
reason: Allows more direct integration but may need webpack tuning
|
||||
@@ -7,6 +7,10 @@ const nextConfig = {
|
||||
experimental: {
|
||||
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
|
||||
async headers() {
|
||||
return [
|
||||
|
||||
@@ -5,26 +5,5 @@ tags: [about, profile]
|
||||
author: rattatwinko
|
||||
summary: This is the about page
|
||||
---
|
||||
# About Me
|
||||
|
||||
Hi! I'm Rattatwinko, a passionate developer who loves building self-hosted tools and beautiful web experiences.
|
||||
|
||||

|
||||
|
||||
## Skills
|
||||
- TypeScript, JavaScript
|
||||
- Rust
|
||||
- React, Next.js
|
||||
- Tailwind CSS
|
||||
- Docker
|
||||
|
||||
## Experience
|
||||
- Indie developer, 2020–present
|
||||
- Open source contributor
|
||||
|
||||
## Projects
|
||||
- **MarkdownBlog**: The site you're reading now! A fast, modern, and hackable markdown blog platform.
|
||||
|
||||
## Contact
|
||||
- [GitHub](https://github.com/rattatwinko)
|
||||
- [Email](mailto:me@example.com)
|
||||
_**config this in the monaco editor in the admin panel**_
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 921 KiB |
@@ -535,3 +535,9 @@ If you have seen this is not very mindfull of browser resources tho.
|
||||
> *"DEVELOPERS! DEVELOPERS! DEVELOPERS!"* - Steve Ballmer
|
||||
>
|
||||
> <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,6 +1,7 @@
|
||||
'use client';
|
||||
import BadgeButton from './BadgeButton';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
const InfoIcon = (
|
||||
<svg width="16" height="16" fill="white" viewBox="0 0 16 16" aria-hidden="true">
|
||||
@@ -16,7 +17,7 @@ export default function AboutButton() {
|
||||
label="ABOUT ME"
|
||||
color="#2563eb"
|
||||
icon={InfoIcon}
|
||||
onClick={() => router.push('/posts/about')}
|
||||
onClick={() => router.push(withBaseUrl('/posts/about'))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import BadgeButton from './BadgeButton';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
const LockIcon = (
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="none">
|
||||
@@ -19,7 +20,7 @@ export default function HeaderButtons() {
|
||||
return (
|
||||
<div className="flex gap-2 justify-center sm:justify-end">
|
||||
<a
|
||||
href="/admin"
|
||||
href={withBaseUrl('/admin')}
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
className="focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded"
|
||||
@@ -33,7 +34,7 @@ export default function HeaderButtons() {
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="/posts/about"
|
||||
href={withBaseUrl('/posts/about')}
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
className="focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
interface MobileNavProps {
|
||||
blogOwner: string;
|
||||
@@ -54,29 +55,17 @@ export default function MobileNav({ blogOwner }: MobileNavProps) {
|
||||
<h2 className="text-lg font-bold mb-6">{blogOwner}'s Blog</h2>
|
||||
|
||||
<nav className="space-y-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<a href={withBaseUrl('/')} className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors" onClick={toggleMenu}>
|
||||
🏠 Home
|
||||
</Link>
|
||||
</a>
|
||||
|
||||
<Link
|
||||
href="/admin"
|
||||
className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<a href={withBaseUrl('/admin')} className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors" onClick={toggleMenu}>
|
||||
🔐 Admin
|
||||
</Link>
|
||||
</a>
|
||||
|
||||
<Link
|
||||
href="/posts/about"
|
||||
className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<a href={withBaseUrl('/posts/about')} className="block py-2 px-3 rounded-lg hover:bg-gray-100 transition-colors" onClick={toggleMenu}>
|
||||
👤 About Me
|
||||
</Link>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
interface Post {
|
||||
slug: string;
|
||||
@@ -23,7 +24,7 @@ export default function AboutPage() {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await fetch("/api/posts/about");
|
||||
const response = await fetch(withBaseUrl('/api/posts/about'));
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"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 });
|
||||
|
||||
@@ -37,6 +39,27 @@ function stripFrontmatter(md: string): string {
|
||||
return md;
|
||||
}
|
||||
|
||||
// Helper to extract YAML frontmatter
|
||||
function extractFrontmatter(md: string): { frontmatter: string; content: string } {
|
||||
if (!md) return { frontmatter: '', content: '' };
|
||||
if (md.startsWith('---')) {
|
||||
const end = md.indexOf('---', 3);
|
||||
if (end !== -1) {
|
||||
const frontmatter = md.slice(0, end + 3);
|
||||
const content = md.slice(end + 3).replace(/^\s+/, '');
|
||||
return { frontmatter, content };
|
||||
}
|
||||
}
|
||||
return { frontmatter: '', content: md };
|
||||
}
|
||||
|
||||
// Helper to combine frontmatter and content
|
||||
function combineFrontmatterAndContent(frontmatter: string, content: string): string {
|
||||
if (!frontmatter) return content;
|
||||
if (!content) return frontmatter;
|
||||
return frontmatter + '\n\n' + content;
|
||||
}
|
||||
|
||||
function FileTree({ nodes, onSelect, selectedSlug, level = 0 }: {
|
||||
nodes: Node[];
|
||||
onSelect: (slug: string) => void;
|
||||
@@ -87,22 +110,76 @@ function FileTree({ nodes, onSelect, selectedSlug, level = 0 }: {
|
||||
|
||||
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
|
||||
const [split, setSplit] = useState(50); // percent - default to 50/50 split
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [browserOpen, setBrowserOpen] = useState(true);
|
||||
const [showUnsavedDialog, setShowUnsavedDialog] = useState(false);
|
||||
const [pendingNavigation, setPendingNavigation] = useState<string | null>(null);
|
||||
const editorRef = useRef<any>(null);
|
||||
const monacoVimRef = useRef<any>(null);
|
||||
|
||||
// Check if there are unsaved changes
|
||||
const hasUnsavedChanges = fileContent !== originalContent;
|
||||
|
||||
// Handle browser beforeunload event
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
if (hasUnsavedChanges) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
}, [hasUnsavedChanges]);
|
||||
|
||||
// Handle back navigation with unsaved changes check
|
||||
const handleBackNavigation = () => {
|
||||
if (hasUnsavedChanges) {
|
||||
setShowUnsavedDialog(true);
|
||||
setPendingNavigation('/admin');
|
||||
} else {
|
||||
router.push('/admin');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle unsaved changes dialog actions
|
||||
const handleUnsavedDialogAction = (action: 'save' | 'discard' | 'cancel') => {
|
||||
if (action === 'save') {
|
||||
handleSave().then(() => {
|
||||
setOriginalContent(fileContent); // Reset unsaved state after save
|
||||
setShowUnsavedDialog(false);
|
||||
setPendingNavigation(null);
|
||||
if (pendingNavigation) {
|
||||
router.push(pendingNavigation);
|
||||
}
|
||||
});
|
||||
} else if (action === 'discard') {
|
||||
setFileContent(originalContent); // Revert to last saved
|
||||
setShowUnsavedDialog(false);
|
||||
setPendingNavigation(null);
|
||||
if (pendingNavigation) {
|
||||
router.push(pendingNavigation);
|
||||
}
|
||||
} else {
|
||||
setShowUnsavedDialog(false);
|
||||
setPendingNavigation(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch file tree
|
||||
useEffect(() => {
|
||||
fetch("/api/posts")
|
||||
fetch(withBaseUrl('/api/posts'))
|
||||
.then(r => r.json())
|
||||
.then(setTree);
|
||||
}, []);
|
||||
@@ -111,10 +188,13 @@ export default function EditorPage() {
|
||||
useEffect(() => {
|
||||
if (!selectedSlug) return;
|
||||
setLoading(true);
|
||||
fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`)
|
||||
fetch(withBaseUrl(`/api/posts/${encodeURIComponent(selectedSlug)}`))
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
setFileContent(stripFrontmatter(data.raw || data.content || ""));
|
||||
const { frontmatter, content } = extractFrontmatter(data.raw || data.content || "");
|
||||
const combinedContent = combineFrontmatterAndContent(frontmatter, content);
|
||||
setFileContent(combinedContent);
|
||||
setOriginalContent(combinedContent);
|
||||
setFileTitle(data.title || data.slug || "");
|
||||
setLoading(false);
|
||||
});
|
||||
@@ -124,18 +204,40 @@ export default function EditorPage() {
|
||||
async function handleSave() {
|
||||
if (!selectedSlug) return;
|
||||
setSaving(true);
|
||||
await fetch(`/api/posts/${encodeURIComponent(selectedSlug)}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ markdown: fileContent })
|
||||
});
|
||||
setSaving(false);
|
||||
|
||||
try {
|
||||
// First save the file
|
||||
const saveResponse = await fetch(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 html = typeof marked.parse === 'function' ? marked.parse(stripFrontmatter(fileContent)) : '';
|
||||
const { content } = extractFrontmatter(fileContent);
|
||||
const html = typeof marked.parse === 'function' ? marked.parse(content) : '';
|
||||
if (typeof html === 'string') setPreviewHtml(html);
|
||||
else if (html instanceof Promise) html.then(setPreviewHtml);
|
||||
else setPreviewHtml('');
|
||||
@@ -144,6 +246,17 @@ export default function EditorPage() {
|
||||
// Monaco Vim integration
|
||||
async function handleEditorDidMount(editor: any, monaco: any) {
|
||||
editorRef.current = editor;
|
||||
|
||||
// Ensure editor resizes properly
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
editor.layout();
|
||||
});
|
||||
|
||||
const editorContainer = editor.getContainerDomNode();
|
||||
if (editorContainer) {
|
||||
resizeObserver.observe(editorContainer);
|
||||
}
|
||||
|
||||
if (vimMode) {
|
||||
const { initVimMode } = await import("monaco-vim");
|
||||
if (monacoVimRef.current) monacoVimRef.current.dispose();
|
||||
@@ -168,25 +281,57 @@ export default function EditorPage() {
|
||||
|
||||
// Split drag logic
|
||||
const dragRef = useRef(false);
|
||||
function onDrag(e: React.MouseEvent) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
function onDrag(e: React.MouseEvent | MouseEvent) {
|
||||
if (!dragRef.current) return;
|
||||
const percent = (e.clientX / window.innerWidth) * 100;
|
||||
setSplit(Math.max(20, Math.min(80, percent)));
|
||||
setSplit(percent); // No min/max limits
|
||||
}
|
||||
function onDragStart() { dragRef.current = true; document.body.style.cursor = "col-resize"; }
|
||||
function onDragEnd() { dragRef.current = false; document.body.style.cursor = ""; }
|
||||
|
||||
function onDragStart() {
|
||||
dragRef.current = true;
|
||||
setIsDragging(true);
|
||||
document.body.style.cursor = "col-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
dragRef.current = false;
|
||||
setIsDragging(false);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function onMove(e: MouseEvent) { onDrag(e as any); }
|
||||
function onUp() { onDragEnd(); }
|
||||
if (dragRef.current) {
|
||||
function onMove(e: MouseEvent) {
|
||||
if (dragRef.current) {
|
||||
onDrag(e);
|
||||
}
|
||||
}
|
||||
|
||||
function onUp() {
|
||||
if (dragRef.current) {
|
||||
onDragEnd();
|
||||
}
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
window.addEventListener("mousemove", onMove);
|
||||
window.addEventListener("mouseup", onUp);
|
||||
return () => { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); };
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", onMove);
|
||||
window.removeEventListener("mouseup", onUp);
|
||||
};
|
||||
}
|
||||
}, [dragRef.current]);
|
||||
}, [isDragging]);
|
||||
|
||||
// Layout logic for left pane (file browser + editor)
|
||||
const leftPaneWidth = `${split}%`;
|
||||
const fileBrowserWidth = 240;
|
||||
|
||||
// Only render MonacoEditor if the editor pane is visible and has width
|
||||
const showEditor = browserOpen ? true : split > 5;
|
||||
const showEditor = true; // Always show editor, it will resize based on container
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-white flex flex-col font-mono" style={{ fontFamily: 'JetBrains Mono, monospace', fontWeight: 'bold' }}>
|
||||
@@ -241,50 +386,101 @@ export default function EditorPage() {
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<span className="text-black text-lg font-semibold">Markdown Editor</span>
|
||||
<span className="text-black text-lg font-semibold">Markdown Bearbeiter</span>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* Back Button */}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className={`flex items-center gap-2 px-3 py-1 rounded transition-colors border border-blue-400 bg-blue-500 text-white hover:bg-blue-600 font-mono ${saving ? 'opacity-60 cursor-wait' : ''}`}
|
||||
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-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a2 2 0 01-2 2H7a2 2 0 01-2-2V7a2 2 0 012-2h4a2 2 0 012 2v1" /></svg>
|
||||
<span>Save</span>
|
||||
<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-2 px-3 py-1 rounded transition-colors border border-gray-300 ${vimMode ? "bg-green-600 text-white" : "bg-white text-gray-700 hover:bg-gray-100"}`}
|
||||
className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1 rounded transition-colors border border-gray-300 text-sm sm:text-base ${
|
||||
vimMode
|
||||
? "bg-green-600 text-white"
|
||||
: "bg-white text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{/* Actual Vim SVG Icon */}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" className="w-5 h-5" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" className="w-4 h-4 sm:w-5 sm:h-5" fill="currentColor">
|
||||
<title>vim</title>
|
||||
<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 Mode</span>
|
||||
<span className="hidden sm:inline">Vim Modus</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Split Layout */}
|
||||
<div className="flex flex-1 min-h-0" style={{ userSelect: dragRef.current ? "none" : undefined }}>
|
||||
<div className="flex flex-1 min-h-0" style={{ userSelect: isDragging ? "none" : undefined }}>
|
||||
{/* Left: File browser + Editor */}
|
||||
<div className="flex flex-col" style={{ width: browserOpen ? `${split}%` : '48px', minWidth: browserOpen ? 240 : 48, maxWidth: 900, background: "#fff" }}>
|
||||
<div className="flex flex-row h-full bg-white" style={{ width: leftPaneWidth, minWidth: 0, maxWidth: '100%' }}>
|
||||
{/* File Browser Collapsible Toggle */}
|
||||
<button
|
||||
className="w-full flex items-center gap-2 px-2 py-1 bg-gray-100 border-b border-gray-200 text-gray-700 font-mono hover:bg-gray-200 focus:outline-none"
|
||||
onClick={() => setBrowserOpen(o => !o)}
|
||||
style={{ fontFamily: 'JetBrains Mono, monospace' }}
|
||||
>
|
||||
<span className="text-lg">{browserOpen ? '▼' : '▶'}</span>
|
||||
<span className="font-bold text-sm">Files</span>
|
||||
</button>
|
||||
<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="h-48 border-b border-gray-200 p-2 overflow-auto bg-gray-50 text-gray-800 font-mono">
|
||||
{tree.length === 0 ? (
|
||||
<div className="text-xs text-gray-400">No files found.</div>
|
||||
) : (
|
||||
<FileTree nodes={tree} onSelect={setSelectedSlug} selectedSlug={selectedSlug} />
|
||||
)}
|
||||
<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 */}
|
||||
@@ -301,7 +497,7 @@ export default function EditorPage() {
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 15,
|
||||
minimap: { enabled: false },
|
||||
minimap: { enabled: true },
|
||||
wordWrap: "on",
|
||||
scrollBeyondLastLine: false,
|
||||
smoothScrolling: true,
|
||||
@@ -314,6 +510,16 @@ export default function EditorPage() {
|
||||
cursorStyle: "line",
|
||||
fixedOverflowWidgets: true,
|
||||
readOnly: loading,
|
||||
folding: true,
|
||||
foldingStrategy: "indentation",
|
||||
showFoldingControls: "always",
|
||||
foldingHighlight: true,
|
||||
foldingImportsByDefault: true,
|
||||
unfoldOnClickAfterEndOfLine: false,
|
||||
links: true,
|
||||
colorDecorators: true,
|
||||
formatOnPaste: true,
|
||||
formatOnType: true,
|
||||
}}
|
||||
onChange={v => setFileContent(v ?? "")}
|
||||
onMount={handleEditorDidMount}
|
||||
@@ -324,11 +530,18 @@ export default function EditorPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Draggable Splitter */}
|
||||
{/* Draggable Splitter - always show */}
|
||||
<div
|
||||
className="w-2 cursor-col-resize bg-gray-200 hover:bg-gray-300 transition-colors"
|
||||
className={`w-1 cursor-col-resize transition-colors relative ${
|
||||
isDragging ? 'bg-blue-500' : 'bg-gray-300 hover:bg-gray-400'
|
||||
}`}
|
||||
onMouseDown={onDragStart}
|
||||
/>
|
||||
>
|
||||
{/* 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">
|
||||
@@ -345,6 +558,45 @@ export default function EditorPage() {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -363,7 +615,7 @@ class MonacoErrorBoundary extends React.Component<{children: React.ReactNode}, {
|
||||
}
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return <div className="text-red-600 p-4">Editor error: {this.state.error.message}</div>;
|
||||
return <div className="text-red-600 p-4">Fehler: {this.state.error.message}</div>;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
interface Post {
|
||||
type: 'post';
|
||||
@@ -26,7 +27,7 @@ type Node = Post | Folder;
|
||||
// Helper to get folder details
|
||||
async function getFolderDetails(path: string): Promise<{ created: string, items: number, size: number, error?: string }> {
|
||||
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');
|
||||
return await res.json();
|
||||
} catch (e) {
|
||||
@@ -38,7 +39,7 @@ async function getFolderDetails(path: string): Promise<{ created: string, items:
|
||||
// Helper to get post size and creation date
|
||||
async function getPostSize(slug: string): Promise<{ size: number | null, created: string | null }> {
|
||||
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');
|
||||
const data = await res.json();
|
||||
return { size: data.size, created: data.created };
|
||||
@@ -76,7 +77,7 @@ export default function ManagePage() {
|
||||
|
||||
const loadContent = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/posts');
|
||||
const response = await fetch(withBaseUrl('/api/posts'));
|
||||
const data = await response.json();
|
||||
setNodes(data);
|
||||
} catch (error) {
|
||||
@@ -140,7 +141,7 @@ export default function ManagePage() {
|
||||
type: deleteConfirm.item.type
|
||||
});
|
||||
|
||||
const response = await fetch('/api/admin/delete', {
|
||||
const response = await fetch(withBaseUrl('/api/admin/delete'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -170,7 +171,7 @@ export default function ManagePage() {
|
||||
// Move post API call
|
||||
const movePost = async (post: Post, targetFolder: string[]) => {
|
||||
try {
|
||||
const response = await fetch('/api/admin/posts/move', {
|
||||
const response = await fetch(withBaseUrl('/api/admin/posts/move'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
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:items-center gap-3 sm:gap-4">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold">Inhaltsverwaltung</h1>
|
||||
<Link
|
||||
href="/admin"
|
||||
<a
|
||||
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"
|
||||
>
|
||||
Zum Admin-Panel
|
||||
</Link>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
@@ -461,7 +462,7 @@ export default function ManagePage() {
|
||||
onClick={async () => {
|
||||
if (!deleteAllConfirm.folder) return;
|
||||
// Call delete API with recursive flag
|
||||
await fetch('/api/admin/delete', {
|
||||
await fetch(withBaseUrl('/api/admin/delete'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
interface PostStats {
|
||||
slug: string;
|
||||
@@ -52,7 +53,7 @@ export default function RustStatusPage() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
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');
|
||||
const data = await res.json();
|
||||
setStats(data);
|
||||
@@ -67,7 +68,7 @@ export default function RustStatusPage() {
|
||||
setHealthLoading(true);
|
||||
setHealthError(null);
|
||||
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');
|
||||
const data = await res.json();
|
||||
setHealth(data);
|
||||
@@ -82,7 +83,7 @@ export default function RustStatusPage() {
|
||||
setLogsLoading(true);
|
||||
setLogsError(null);
|
||||
try {
|
||||
const res = await fetch('/api/admin/posts?logs=1');
|
||||
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);
|
||||
@@ -95,7 +96,7 @@ export default function RustStatusPage() {
|
||||
|
||||
const clearLogs = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/admin/posts?clearLogs=1', { method: 'DELETE' });
|
||||
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) {
|
||||
@@ -103,6 +104,19 @@ export default function RustStatusPage() {
|
||||
}
|
||||
};
|
||||
|
||||
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(() => {
|
||||
fetchStats();
|
||||
fetchHealth();
|
||||
@@ -176,7 +190,7 @@ export default function RustStatusPage() {
|
||||
<div className="flex items-center gap-2 w-full sm:w-auto justify-end">
|
||||
{/* Back to Admin button */}
|
||||
<a
|
||||
href="/admin"
|
||||
href={withBaseUrl('/admin')}
|
||||
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"
|
||||
>
|
||||
@@ -358,12 +372,19 @@ export default function RustStatusPage() {
|
||||
<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="Clear all logs"
|
||||
title="Logs Leeren"
|
||||
>
|
||||
Clear Logs
|
||||
Logs Leeren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -385,10 +406,10 @@ export default function RustStatusPage() {
|
||||
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">All Levels</option>
|
||||
<option value="all">Alle Stufen</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="error">Error</option>
|
||||
<option value="warning">Warnungen</option>
|
||||
<option value="error">Fehler</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ export const dynamic = "force-dynamic";
|
||||
*
|
||||
* If any Issues about "Window" (For Monaco) pop up. Its not my fucking fault
|
||||
*
|
||||
* Push later when on local Network. (//5jul25)
|
||||
* Push later when on local Network. (//5jul25) ## Already done
|
||||
**********************************************/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
@@ -23,6 +23,7 @@ import matter from 'gray-matter';
|
||||
import dynamicImport from 'next/dynamic';
|
||||
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;
|
||||
@@ -162,7 +163,7 @@ export default function AdminPage() {
|
||||
|
||||
useEffect(() => {
|
||||
// Check if docker is used
|
||||
fetch('/api/admin/docker')
|
||||
fetch(withBaseUrl('/api/admin/docker'))
|
||||
.then(res => res.json())
|
||||
.then(data => setIsDocker(!!data.docker))
|
||||
.catch(() => setIsDocker(false));
|
||||
@@ -170,7 +171,7 @@ export default function AdminPage() {
|
||||
|
||||
const loadContent = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/posts');
|
||||
const response = await fetch(withBaseUrl('/api/posts'));
|
||||
const data = await response.json();
|
||||
setNodes(data);
|
||||
} catch (error) {
|
||||
@@ -189,7 +190,7 @@ export default function AdminPage() {
|
||||
return;
|
||||
}
|
||||
// Check password via API
|
||||
const res = await fetch('/api/admin/password', {
|
||||
const res = await fetch(withBaseUrl('/api/admin/password'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: pass, mode: 'login' }),
|
||||
@@ -213,7 +214,7 @@ export default function AdminPage() {
|
||||
const handleCreatePost = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const response = await fetch('/api/admin/posts', {
|
||||
const response = await fetch(withBaseUrl('/api/admin/posts'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -251,7 +252,7 @@ export default function AdminPage() {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/folders', {
|
||||
const response = await fetch(withBaseUrl('/api/admin/folders'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -347,7 +348,7 @@ export default function AdminPage() {
|
||||
formData.append('file', file);
|
||||
formData.append('path', currentPath.join('/'));
|
||||
|
||||
const response = await fetch('/api/admin/upload', {
|
||||
const response = await fetch(withBaseUrl('/api/admin/upload'), {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
@@ -381,7 +382,7 @@ export default function AdminPage() {
|
||||
type: deleteConfirm.item.type
|
||||
});
|
||||
|
||||
const response = await fetch('/api/admin/delete', {
|
||||
const response = await fetch(withBaseUrl('/api/admin/delete'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -414,7 +415,7 @@ export default function AdminPage() {
|
||||
? prev.filter((s) => s !== slug)
|
||||
: [slug, ...prev];
|
||||
// Update pinned.json on the server
|
||||
fetch('/api/admin/posts', {
|
||||
fetch(withBaseUrl('/api/admin/posts'), {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pinned: newPinned }),
|
||||
@@ -453,7 +454,7 @@ export default function AdminPage() {
|
||||
return;
|
||||
}
|
||||
// Check old password
|
||||
const res = await fetch('/api/admin/password', {
|
||||
const res = await fetch(withBaseUrl('/api/admin/password'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: changePwOld, mode: 'login' }),
|
||||
@@ -464,7 +465,7 @@ export default function AdminPage() {
|
||||
return;
|
||||
}
|
||||
// Set new password
|
||||
const res2 = await fetch('/api/admin/password', {
|
||||
const res2 = await fetch(withBaseUrl('/api/admin/password'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: changePwNew }),
|
||||
@@ -484,7 +485,7 @@ export default function AdminPage() {
|
||||
// Function to load a post's raw markdown
|
||||
const loadPostRaw = async (slug: string, folderPath: string) => {
|
||||
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) {
|
||||
alert('Error loading post');
|
||||
return;
|
||||
@@ -516,7 +517,7 @@ export default function AdminPage() {
|
||||
summary: newPost.summary,
|
||||
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',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -606,9 +607,9 @@ export default function AdminPage() {
|
||||
}
|
||||
closeModal();
|
||||
if (choice === 'docker') {
|
||||
exportFromEndpoint('/api/admin/export');
|
||||
exportFromEndpoint(withBaseUrl('/api/admin/export'));
|
||||
} else if (choice === 'local') {
|
||||
exportFromEndpoint('/api/admin/exportlocal');
|
||||
exportFromEndpoint(withBaseUrl('/api/admin/exportlocal'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -693,7 +694,7 @@ export default function AdminPage() {
|
||||
// Save to JSON file in background
|
||||
try {
|
||||
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) {
|
||||
throw new Error(`Failed to fetch pinned data: ${pinnedRes.status}`);
|
||||
}
|
||||
@@ -706,7 +707,7 @@ export default function AdminPage() {
|
||||
console.log('Updated folderEmojis:', folderEmojis);
|
||||
console.log('Saving to pinned.json...');
|
||||
|
||||
const saveRes = await fetch('/api/admin/posts', {
|
||||
const saveRes = await fetch(withBaseUrl('/api/admin/posts'), {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ folderEmojis, pinned: pinnedData.pinned || [] }),
|
||||
@@ -886,7 +887,7 @@ export default function AdminPage() {
|
||||
</span>
|
||||
</button>
|
||||
<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"
|
||||
title="View Rust Parser Dashboard"
|
||||
style={{ minWidth: '160px' }}
|
||||
@@ -902,9 +903,9 @@ export default function AdminPage() {
|
||||
</a>
|
||||
{/* VS Code Editor Button */}
|
||||
<a
|
||||
href="/admin/editor"
|
||||
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 Editor (VS Code Style)"
|
||||
title="Markdown Bearbeiter"
|
||||
style={{ minWidth: '160px' }}
|
||||
>
|
||||
{/* VS Code SVG Icon */}
|
||||
@@ -950,8 +951,8 @@ export default function AdminPage() {
|
||||
</defs>
|
||||
</svg>
|
||||
<span className="flex flex-col items-start">
|
||||
<span>Editor</span>
|
||||
<span className="text-xs font-normal text-blue-100">VS Code</span>
|
||||
<span>Markdown Editor</span>
|
||||
<span className="text-xs font-normal text-blue-100">Visual Studio Code</span>
|
||||
</span>
|
||||
</a>
|
||||
{rememberExportChoice && lastExportChoice && (
|
||||
@@ -1477,7 +1478,7 @@ export default function AdminPage() {
|
||||
</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>
|
||||
|
||||
@@ -110,6 +110,46 @@ export async function GET(request: Request) {
|
||||
});
|
||||
}
|
||||
}
|
||||
const reinterpretAll = searchParams.get('reinterpretAll');
|
||||
if (reinterpretAll === '1') {
|
||||
// Call the Rust backend to force reinterpret all posts
|
||||
const rustResult = spawnSync(
|
||||
process.cwd() + '/markdown_backend/target/release/markdown_backend',
|
||||
['reinterpret-all'],
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
if (rustResult.status === 0 && rustResult.stdout) {
|
||||
return new Response(rustResult.stdout, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} else {
|
||||
return new Response(JSON.stringify({ error: rustResult.stderr || rustResult.error }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
}
|
||||
const reparsePost = searchParams.get('reparsePost');
|
||||
if (reparsePost) {
|
||||
// Call the Rust backend to reparse a specific post
|
||||
const rustResult = spawnSync(
|
||||
process.cwd() + '/markdown_backend/target/release/markdown_backend',
|
||||
['reparse-post', reparsePost],
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
if (rustResult.status === 0 && rustResult.stdout) {
|
||||
return new Response(rustResult.stdout, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} else {
|
||||
return new Response(JSON.stringify({ error: rustResult.stderr || rustResult.error }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
}
|
||||
// Return the current pinned.json object
|
||||
try {
|
||||
const pinnedPath = path.join(process.cwd(), 'posts', 'pinned.json');
|
||||
|
||||
@@ -7,6 +7,7 @@ import AboutButton from './AboutButton';
|
||||
import BadgeButton from './BadgeButton';
|
||||
import HeaderButtons from './HeaderButtons';
|
||||
import MobileNav from './MobileNav';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
@@ -46,11 +47,11 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="de" className="h-full">
|
||||
<Head>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="icon" href={withBaseUrl('/favicon.ico')} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={withBaseUrl('/apple-touch-icon.png')} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={withBaseUrl('/favicon-32x32.png')} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={withBaseUrl('/favicon-16x16.png')} />
|
||||
<link rel="manifest" href={withBaseUrl('/site.webmanifest')} />
|
||||
</Head>
|
||||
<body className={`${inter.className} h-full min-h-screen flex flex-col`}>
|
||||
<MobileNav blogOwner={blogOwner} />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { format } from 'date-fns';
|
||||
import React from 'react';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
interface Post {
|
||||
type: 'post';
|
||||
@@ -47,7 +48,7 @@ export default function Home() {
|
||||
|
||||
const setupSSE = () => {
|
||||
try {
|
||||
eventSource = new EventSource('/api/posts/stream');
|
||||
eventSource = new EventSource(withBaseUrl('/api/posts/stream'));
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
@@ -101,7 +102,7 @@ export default function Home() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const response = await fetch('/api/posts');
|
||||
const response = await fetch(withBaseUrl('/api/posts'));
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status}`);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { withBaseUrl } from '@/lib/baseUrl';
|
||||
|
||||
interface Post {
|
||||
slug: string;
|
||||
@@ -55,7 +56,7 @@ export default function PostPage({ params }: { params: { slug: string[] } }) {
|
||||
|
||||
const setupSSE = () => {
|
||||
try {
|
||||
eventSource = new EventSource('/api/posts/stream');
|
||||
eventSource = new EventSource(withBaseUrl('/api/posts/stream'));
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
@@ -109,7 +110,7 @@ export default function PostPage({ params }: { params: { slug: string[] } }) {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await fetch(`/api/posts/${encodeURIComponent(slugPath)}`);
|
||||
const response = await fetch(withBaseUrl(`/api/posts/${encodeURIComponent(slugPath)}`));
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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