Initial commit: Thingamablog web app (React + Node.js)
This commit is contained in:
commit
8fe45f5536
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
# Thingamablog v2 - Technical Design Document
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Thingamablog v2 is a migration and modernization project for Paul's legacy blog data from the early 2000s Thingamablog platform. The system extracts blog posts from an obsolete HSQLDB database, cleans and structures the data, and provides a modern web interface for browsing.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
There are two related projects:
|
||||||
|
|
||||||
|
1. **thingamablog-api** (`projects/thingamablog-api/`): Contains the Java export tool for data extraction
|
||||||
|
2. **thingamablog-v2** (`projects/thingamablog-v2/`): Contains the web app for browsing the extracted data
|
||||||
|
|
||||||
|
No Git repositories are set up for these folders yet.
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
- **Legacy Data Lock-In:** Blog posts stored in HSQLDB 1.8 (obsolete Java DB format from 2000s)
|
||||||
|
- **Data Extraction Challenges:** Binary format difficult to parse reliably with modern tools
|
||||||
|
- **User Experience:** No easy way to browse/search 20+ years of personal blog content
|
||||||
|
- **Future Integration:** Need structured data for AI/vector search (Open Brain project)
|
||||||
|
|
||||||
|
## Solution Architecture
|
||||||
|
|
||||||
|
### High-Level Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
[Legacy HSQLDB DB] → [Java Export Tool] → [blog-export.json] → [Node.js Backend] → [React Frontend]
|
||||||
|
↓
|
||||||
|
(Binary Data) (Clean JSON) (API Server) (Web UI)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
#### 1. Data Extraction Layer (Java CLI Tool)
|
||||||
|
- **Location:** `projects/thingamablog-api/ExportTool.java`
|
||||||
|
- **Purpose:** Bridge from legacy DB to modern JSON
|
||||||
|
- **Technology:** Java with HSQLDB 1.8.0.10 driver
|
||||||
|
- **Input:** HSQLDB database files (`database.script`, `database.data`)
|
||||||
|
- **Output:** `blog-export.json` with structured blog entries
|
||||||
|
- **Design Decision:** Standalone CLI tool avoids Spring Boot complexity
|
||||||
|
|
||||||
|
#### 2. Data Storage Layer (JSON File)
|
||||||
|
- **Format:** Clean JSON array of blog post objects
|
||||||
|
- **Schema:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "Digital Imaging Notes",
|
||||||
|
"date": "2003-11-03 16:41:22.053",
|
||||||
|
"author": "Paul",
|
||||||
|
"categories": "Hobbies",
|
||||||
|
"content": "<p>Full HTML content...</p>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Benefits:** Human-readable, easily parseable, version-controllable
|
||||||
|
|
||||||
|
#### 3. Backend API Layer (Node.js/Express)
|
||||||
|
- **Endpoints:**
|
||||||
|
- `GET /api/posts` - List posts with metadata
|
||||||
|
- `GET /api/posts/:id` - Full post content
|
||||||
|
- **Features:**
|
||||||
|
- Priority loading of `blog-export.json`
|
||||||
|
- Fallback to legacy HSQLDB parser
|
||||||
|
- Static image serving
|
||||||
|
- CORS support for frontend
|
||||||
|
|
||||||
|
#### 4. Frontend UI Layer (React/Material-UI)
|
||||||
|
- **Components:**
|
||||||
|
- PostList: Scrollable filtered list
|
||||||
|
- PostDetail: Full content viewer
|
||||||
|
- Filters: Category/date navigation
|
||||||
|
- **Responsive Design:** Desktop and mobile support
|
||||||
|
|
||||||
|
## Detailed Design
|
||||||
|
|
||||||
|
### Data Extraction Process
|
||||||
|
|
||||||
|
#### The "Bridge" Approach
|
||||||
|
1. **Java Tool Creation:**
|
||||||
|
- `ExportTool.java`: Simple class using JDBC to connect to HSQLDB
|
||||||
|
- Uses `hsqldb-1.8.0.10.jar` driver (downloaded via Maven)
|
||||||
|
- Executes SQL query: `SELECT * FROM ENTRY_TABLE_1096292361887`
|
||||||
|
- Maps result set to JSON objects
|
||||||
|
|
||||||
|
2. **Compilation & Execution:**
|
||||||
|
```bash
|
||||||
|
cd projects/thingamablog-api
|
||||||
|
javac -cp target/dependency/hsqldb-1.8.0.10.jar ExportTool.java
|
||||||
|
java -cp .:target/dependency/hsqldb-1.8.0.10.jar ExportTool > ../thingamablog-v2/backend/blog-export.json
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Data Cleaning:**
|
||||||
|
- Converts timestamps to readable format
|
||||||
|
- Preserves HTML content in body
|
||||||
|
- Extracts categories from metadata
|
||||||
|
- Assigns sequential IDs
|
||||||
|
|
||||||
|
#### Migration Results
|
||||||
|
- **Input:** Binary HSQLDB files (~2MB total)
|
||||||
|
- **Output:** Clean JSON (1.3MB, 467 entries)
|
||||||
|
- **Quality:** Perfect titles, dates, HTML content preserved
|
||||||
|
- **Performance:** One-time export, instant loading thereafter
|
||||||
|
|
||||||
|
### API Design
|
||||||
|
|
||||||
|
#### REST Endpoints
|
||||||
|
- **GET /api/posts**
|
||||||
|
- Response: Array of post summaries
|
||||||
|
- Filtering: None (client-side)
|
||||||
|
- Sorting: By date descending (for clean JSON)
|
||||||
|
|
||||||
|
- **GET /api/posts/:id**
|
||||||
|
- Response: Full post object
|
||||||
|
- Image URL rewriting: Converts Windows paths to HTTP URLs
|
||||||
|
- Error handling: 404 for missing posts
|
||||||
|
|
||||||
|
#### Data Flow
|
||||||
|
1. Frontend requests post list
|
||||||
|
2. Backend loads JSON/falls back to DB parsing
|
||||||
|
3. Backend serves filtered/sorted data
|
||||||
|
4. Frontend renders with Material-UI components
|
||||||
|
|
||||||
|
### Frontend Design
|
||||||
|
|
||||||
|
#### Component Hierarchy
|
||||||
|
```
|
||||||
|
App
|
||||||
|
├── Sidebar (Filters)
|
||||||
|
│ ├── AllPostsFilter
|
||||||
|
│ ├── CategoryAccordion
|
||||||
|
│ └── ArchiveAccordion
|
||||||
|
├── PostList (Scrollable)
|
||||||
|
└── PostViewer (Rich content)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### State Management
|
||||||
|
- React hooks for local state
|
||||||
|
- No external state library (simple app)
|
||||||
|
- URL-based state for selected post/filter
|
||||||
|
|
||||||
|
#### UI/UX Principles
|
||||||
|
- **Progressive Disclosure:** Filters collapsed by default
|
||||||
|
- **Responsive Grid:** 3-column desktop, stacked mobile
|
||||||
|
- **Accessibility:** Keyboard navigation, screen reader support
|
||||||
|
- **Performance:** Virtual scrolling for large post lists
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Technology Choices
|
||||||
|
|
||||||
|
| Component | Technology | Rationale |
|
||||||
|
|-----------|------------|----------|
|
||||||
|
| Export Tool | Java + HSQLDB Driver | Native compatibility with legacy DB |
|
||||||
|
| Data Format | JSON | Universal, human-readable |
|
||||||
|
| Backend | Node.js/Express | Simple, fast for file-based data |
|
||||||
|
| Frontend | React/Material-UI | Modern, component-based UI |
|
||||||
|
| Images | Static serving | Direct file access for performance |
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
```
|
||||||
|
projects/
|
||||||
|
├── thingamablog-api/ # Data extraction tool
|
||||||
|
│ ├── ExportTool.java # Java export tool
|
||||||
|
│ ├── pom.xml # Maven config
|
||||||
|
│ ├── src/ # Maven source structure
|
||||||
|
│ ├── target/ # Compiled classes + dependencies
|
||||||
|
│ └── api.log # Execution log
|
||||||
|
└── thingamablog-v2/ # Web application
|
||||||
|
├── backend/
|
||||||
|
│ ├── server.js # API server
|
||||||
|
│ ├── hsqldbParser.js # Fallback parser
|
||||||
|
│ └── blog-export.json # Clean data
|
||||||
|
└── frontend/src/
|
||||||
|
├── App.js # Main UI
|
||||||
|
├── theme.js # Styling
|
||||||
|
└── components/ # Reusable UI pieces
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment & Operations
|
||||||
|
|
||||||
|
#### Local Development
|
||||||
|
1. Extract data: `cd projects/thingamablog-api && java -cp .:target/dependency/hsqldb-1.8.0.10.jar ExportTool > ../thingamablog-v2/backend/blog-export.json`
|
||||||
|
2. Start backend: `cd projects/thingamablog-v2/backend && node server.js`
|
||||||
|
3. Start frontend: `cd projects/thingamablog-v2/frontend && npm start`
|
||||||
|
4. Access: `http://localhost:3000`
|
||||||
|
|
||||||
|
#### Production Considerations
|
||||||
|
- **Scalability:** JSON file size (1.3MB) is fine for personal use
|
||||||
|
- **Backup:** Version control the JSON export
|
||||||
|
- **Updates:** Re-run export tool if DB changes
|
||||||
|
- **Security:** Local-only access, no authentication needed
|
||||||
|
|
||||||
|
## Risks & Mitigations
|
||||||
|
|
||||||
|
| Risk | Mitigation |
|
||||||
|
|------|------------|
|
||||||
|
| HSQLDB driver availability | Downloaded and cached locally |
|
||||||
|
| Java version compatibility | Tested with Java 8+ |
|
||||||
|
| Data corruption | JSON validation on load |
|
||||||
|
| Performance with large datasets | Client-side pagination/filtering |
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- **Search:** Full-text search within posts
|
||||||
|
- **Tagging:** Enhanced category management
|
||||||
|
- **Export:** Additional formats (Markdown, PDF)
|
||||||
|
- **Open Brain Integration:** Vector embedding for AI queries
|
||||||
|
- **Multi-user:** User accounts and permissions
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
- **Data Integrity:** 100% posts extracted with correct metadata
|
||||||
|
- **Performance:** <2s page load, <500ms API response
|
||||||
|
- **Usability:** Intuitive filtering/navigation
|
||||||
|
- **Maintainability:** Clear code structure, comprehensive docs
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Thingamablog v2 successfully modernizes legacy blog data through a "bridge" approach, providing a clean migration path from obsolete technology to contemporary web standards. The modular design allows for easy maintenance and future AI integrations.
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
# Thingamablog v2 Project Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This project extracts and displays blog entries from Paul's old Thingamablog (a Windows blog platform from the early 2000s). The process involved multiple iterations:
|
||||||
|
|
||||||
|
1. **Initial Attempt:** Python script to parse HTML files (`blog.html`).
|
||||||
|
2. **Database Approach:** Discovered the blogs were stored in an HSQLDB database (old Java-based SQL DB).
|
||||||
|
3. **Java/Spring Boot:** Used old HSQLDB Java library in a Spring Boot app to extract data.
|
||||||
|
4. **Node.js Solution:** Final implementation using a Node.js app to read HSQLDB directly and export to JSON.
|
||||||
|
5. **Web App:** Built a full-stack web app (React frontend + Express backend) to browse and display the blog entries.
|
||||||
|
|
||||||
|
The result is a clean JSON export (`blog-export.json`) containing all blog posts, which can be browsed via a modern web interface.
|
||||||
|
|
||||||
|
## Data Extraction Process
|
||||||
|
|
||||||
|
- **Source:** Old HSQLDB database files in `docs/pauls-blogs/Paul/database/`
|
||||||
|
- **Method:** Custom Node.js script using an HSQLDB reader library (likely `hsqldb` or similar npm package)
|
||||||
|
- **Command to Generate blog-export.json:** (Not fully documented, but likely)
|
||||||
|
```bash
|
||||||
|
# Assumed command (run in backend directory)
|
||||||
|
node -e "
|
||||||
|
const { parseHSQLDB } = require('./hsqldbParser');
|
||||||
|
const entries = parseHSQLDB('/home/paulh/.openclaw/workspace/docs/pauls-blogs/Paul/database');
|
||||||
|
const cleanEntries = entries.map((e, idx) => ({
|
||||||
|
id: idx + 1,
|
||||||
|
title: e.TITLE || 'Untitled',
|
||||||
|
date: e.TIMESTAMP || '',
|
||||||
|
author: e.AUTHOR || 'Paul',
|
||||||
|
categories: e.CATEGORIES || '',
|
||||||
|
content: e.ENTRY || ''
|
||||||
|
}));
|
||||||
|
console.log(JSON.stringify(cleanEntries, null, 2));
|
||||||
|
" > blog-export.json
|
||||||
|
```
|
||||||
|
- This uses the fallback parser, but the actual export may have used a more robust library for better data extraction.
|
||||||
|
- **Output:** `blog-export.json` with structured blog entries (title, date, content, categories, etc.)
|
||||||
|
- **Challenges:** HSQLDB is an obsolete format; required finding compatible libraries. The JSON cleans up the raw DB data into readable format.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
projects/thingamablog-v2/
|
||||||
|
├── backend/ # Express.js server
|
||||||
|
│ ├── server.js # Main server file
|
||||||
|
│ ├── hsqldbParser.js # Legacy HSQLDB parser (fallback)
|
||||||
|
│ ├── blog-export.json # Exported blog data (1.7MB)
|
||||||
|
│ └── package.json
|
||||||
|
└── frontend/ # React app
|
||||||
|
├── src/
|
||||||
|
│ ├── App.js # Main component
|
||||||
|
│ ├── theme.js # Material-UI theme
|
||||||
|
│ └── index.js
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Web App Features
|
||||||
|
- **Backend:** Express server serving API endpoints for posts
|
||||||
|
- **Frontend:** React with Material-UI for modern UI
|
||||||
|
- **Filters:** Browse by category (hierarchical) or date archives
|
||||||
|
- **Images:** Served statically from original blog image folder
|
||||||
|
- **Responsive:** Works on desktop and mobile
|
||||||
|
|
||||||
|
## Running the Application
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Node.js installed
|
||||||
|
- Backend dependencies: `cd backend && npm install`
|
||||||
|
- Frontend dependencies: `cd frontend && npm install`
|
||||||
|
|
||||||
|
### Start Backend
|
||||||
|
```bash
|
||||||
|
cd projects/thingamablog-v2/backend
|
||||||
|
node server.js
|
||||||
|
```
|
||||||
|
- Runs on `http://localhost:3637`
|
||||||
|
- Loads `blog-export.json` if available, else falls back to HSQLDB parser
|
||||||
|
- Serves images from `/home/paulh/.openclaw/workspace/docs/pauls-blogs/Paul/1096292361887/web`
|
||||||
|
|
||||||
|
### Start Frontend
|
||||||
|
```bash
|
||||||
|
cd projects/thingamablog-v2/frontend
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
- Runs on `http://localhost:3000` (opens automatically)
|
||||||
|
- Connects to backend at `http://localhost:3637`
|
||||||
|
|
||||||
|
### Access the App
|
||||||
|
Open `http://localhost:3000` in your browser.
|
||||||
|
|
||||||
|
## UI Description
|
||||||
|
|
||||||
|
The web app provides a clean, modern interface to browse Paul's old blog posts:
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
- **Header:** "Thingamablog Archive" title
|
||||||
|
- **Sidebar (Left):** Browse filters
|
||||||
|
- "All Posts" - show everything
|
||||||
|
- "Categories" accordion - hierarchical categories (e.g., Hobbies > Car Maintenance)
|
||||||
|
- "Archives" accordion - posts by year
|
||||||
|
- **Post List (Middle):** Scrollable list of posts with title, date, author
|
||||||
|
- **Post Content (Right):** Full post display with images, categories as chips
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **Filtering:** Click categories or years to filter posts
|
||||||
|
- **Selection:** Click a post to view full content
|
||||||
|
- **Images:** Embedded images load from served static files
|
||||||
|
- **Responsive:** Adapts to screen size (stacks vertically on mobile)
|
||||||
|
|
||||||
|
### Sample View
|
||||||
|
- Categories include: Hobbies, Personal, Robotics, etc.
|
||||||
|
- Posts date back to 2000s, covering topics like 3D printing, car maintenance, tech projects
|
||||||
|
- Content includes HTML formatting, links, and images
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
- `GET /api/posts` - List all posts (id, title, date, category)
|
||||||
|
- `GET /api/posts/:id` - Get full post data
|
||||||
|
- Images served at `/1096292361887/web/*` or `/`
|
||||||
|
|
||||||
|
## Future Integration
|
||||||
|
|
||||||
|
This data can be ingested into Open Brain for vector search:
|
||||||
|
- Parse `blog-export.json` into chunks
|
||||||
|
- Embed with OpenAI
|
||||||
|
- Store in Supabase PGVector
|
||||||
|
- Enable semantic queries across Paul's 20+ year blog history
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- No tests or CI/CD set up
|
||||||
|
- Assumes local paths for images/database
|
||||||
|
- Backend prioritizes JSON export over HSQLDB parsing for speed
|
||||||
|
- Categories use `<Category>` and `<Parent - Child>` format
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,35 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function parseEntriesDb(dbFilePath) {
|
||||||
|
const raw = fs.readFileSync(dbFilePath, 'utf-8');
|
||||||
|
const entries = raw.split('ENTRY=');
|
||||||
|
const parsed = [];
|
||||||
|
for (const e of entries) {
|
||||||
|
if (!e.trim()) continue;
|
||||||
|
const lines = e.split('\n');
|
||||||
|
let data = {}, body = '', foundBody = false;
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('body=')) {
|
||||||
|
foundBody = true;
|
||||||
|
body += line.substring(5) + '\n';
|
||||||
|
} else if (foundBody && line.trim() !== 'ENDENTRY') {
|
||||||
|
body += line + '\n';
|
||||||
|
} else if (line.trim() === 'ENDENTRY') {
|
||||||
|
foundBody = false;
|
||||||
|
} else if (!foundBody && line.includes('=')) {
|
||||||
|
const idx = line.indexOf('=');
|
||||||
|
let key = line.substring(0, idx).trim();
|
||||||
|
let val = line.substring(idx+1).trim();
|
||||||
|
data[key] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parsed.push({
|
||||||
|
...data,
|
||||||
|
body: body.trim(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { parseEntriesDb };
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function parseHSQLDB(dbDir) {
|
||||||
|
const scriptPath = path.join(dbDir, 'database.script');
|
||||||
|
const dataPath = path.join(dbDir, 'database.data');
|
||||||
|
|
||||||
|
if (!fs.existsSync(scriptPath) || !fs.existsSync(dataPath)) {
|
||||||
|
throw new Error('Missing HSQLDB files (database.script or database.data)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataBuffer = fs.readFileSync(dataPath);
|
||||||
|
const script = fs.readFileSync(scriptPath, 'utf-8');
|
||||||
|
|
||||||
|
// Parse table structure from script
|
||||||
|
const entryTableMatch = script.match(
|
||||||
|
/CREATE CACHED TABLE (ENTRY_TABLE_\d+)\((.*?)\)/
|
||||||
|
);
|
||||||
|
if (!entryTableMatch) throw new Error('Could not find ENTRY_TABLE in script');
|
||||||
|
|
||||||
|
const tableName = entryTableMatch[1];
|
||||||
|
const tableDefStr = entryTableMatch[2];
|
||||||
|
const columns = tableDefStr
|
||||||
|
.split(',')
|
||||||
|
.map(col => col.trim().split(/\s+/)[0]);
|
||||||
|
|
||||||
|
// Parse INDEX line (note: no space between INDEX and ')
|
||||||
|
const indexMatch = script.match(
|
||||||
|
new RegExp(`SET TABLE ${tableName} INDEX'(.*?)'`)
|
||||||
|
);
|
||||||
|
if (!indexMatch) throw new Error('Could not find INDEX for ENTRY_TABLE');
|
||||||
|
|
||||||
|
const [pageIndex, rowCount] = indexMatch[1].split(' ').map(Number);
|
||||||
|
|
||||||
|
// HSQLDB binary format parsing (simplified for common structure)
|
||||||
|
// This is a basic extraction—HSQLDB uses a specific binary format
|
||||||
|
const entries = [];
|
||||||
|
const entries_raw = extractEntriesFromBinary(dataBuffer, pageIndex, rowCount);
|
||||||
|
|
||||||
|
for (const raw of entries_raw) {
|
||||||
|
if (raw && raw.length >= columns.length) {
|
||||||
|
const entry = {};
|
||||||
|
columns.forEach((col, idx) => {
|
||||||
|
entry[col] = raw[idx];
|
||||||
|
});
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractEntriesFromBinary(buffer, pageIndex, rowCount) {
|
||||||
|
// HSQLDB stores data in a binary format with page offsets
|
||||||
|
// This is a simplified extraction—looking for text patterns in the binary data
|
||||||
|
const entries = [];
|
||||||
|
const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true });
|
||||||
|
|
||||||
|
// Convert buffer to text, extract null-terminated strings
|
||||||
|
const bufStr = textDecoder.decode(buffer);
|
||||||
|
const nullSeparated = bufStr.split('\0');
|
||||||
|
|
||||||
|
// Group entries by looking for patterns (ID, timestamp, title, categories, entry, draft, modified, author)
|
||||||
|
let currentEntry = [];
|
||||||
|
for (const part of nullSeparated) {
|
||||||
|
if (part.trim()) {
|
||||||
|
currentEntry.push(part.trim());
|
||||||
|
// Rough heuristic: HSQLDB entries typically have ~8 fields
|
||||||
|
if (currentEntry.length === 8) {
|
||||||
|
entries.push([...currentEntry]);
|
||||||
|
currentEntry = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { parseHSQLDB };
|
||||||
|
|
@ -0,0 +1,854 @@
|
||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.6",
|
||||||
|
"express": "^5.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/accepts": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"negotiator": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/body-parser": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "^3.1.2",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"iconv-lite": "^0.7.0",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"qs": "^6.14.1",
|
||||||
|
"raw-body": "^3.0.1",
|
||||||
|
"type-is": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bytes": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/content-disposition": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/content-type": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie-signature": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cors": {
|
||||||
|
"version": "2.8.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
||||||
|
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"object-assign": "^4",
|
||||||
|
"vary": "^1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/depd": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ee-first": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/encodeurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/escape-html": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/etag": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "^2.0.0",
|
||||||
|
"body-parser": "^2.2.1",
|
||||||
|
"content-disposition": "^1.0.0",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"cookie": "^0.7.1",
|
||||||
|
"cookie-signature": "^1.2.1",
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"depd": "^2.0.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"finalhandler": "^2.1.0",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"merge-descriptors": "^2.0.0",
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"once": "^1.4.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"proxy-addr": "^2.0.7",
|
||||||
|
"qs": "^6.14.0",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"router": "^2.2.0",
|
||||||
|
"send": "^1.1.0",
|
||||||
|
"serve-static": "^2.2.0",
|
||||||
|
"statuses": "^2.0.1",
|
||||||
|
"type-is": "^2.0.1",
|
||||||
|
"vary": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/finalhandler": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"statuses": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/forwarded": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fresh": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-errors": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"inherits": "~2.0.4",
|
||||||
|
"setprototypeof": "~1.2.0",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"toidentifier": "~1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ipaddr.js": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-promise": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/media-typer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/merge-descriptors": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.54.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
|
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "^1.54.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/negotiator": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/on-finished": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": "1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parseurl": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-to-regexp": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/proxy-addr": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"forwarded": "0.2.0",
|
||||||
|
"ipaddr.js": "1.9.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
|
||||||
|
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/range-parser": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "~3.1.2",
|
||||||
|
"http-errors": "~2.0.1",
|
||||||
|
"iconv-lite": "~0.7.0",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/router": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"depd": "^2.0.0",
|
||||||
|
"is-promise": "^4.0.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"path-to-regexp": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/send": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.1",
|
||||||
|
"mime-types": "^3.0.2",
|
||||||
|
"ms": "^2.1.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"statuses": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/serve-static": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"send": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/setprototypeof": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-list": "^1.0.0",
|
||||||
|
"side-channel-map": "^1.0.1",
|
||||||
|
"side-channel-weakmap": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-list": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-map": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-weakmap": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-map": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/statuses": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/toidentifier": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/type-is": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"media-typer": "^1.1.0",
|
||||||
|
"mime-types": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unpipe": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vary": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.6",
|
||||||
|
"express": "^5.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { parseHSQLDB } = require('./hsqldbParser');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = 3637;
|
||||||
|
const DB_DIR = '/home/paulh/.openclaw/workspace/docs/pauls-blogs/Paul/database';
|
||||||
|
const EXPORT_FILE = path.join(__dirname, 'blog-export.json');
|
||||||
|
const IMAGE_DIR = '/home/paulh/.openclaw/workspace/docs/pauls-blogs/Paul/1096292361887/web';
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
// Serve blog images statically
|
||||||
|
// Assuming image paths in blog posts are relative or absolute within the blog structure
|
||||||
|
// We'll serve the specific blog's web folder at the root path or a specific prefix
|
||||||
|
// Let's check a sample post content to see how images are referenced first
|
||||||
|
app.use('/1096292361887/web', express.static(IMAGE_DIR));
|
||||||
|
// Also serve at root just in case paths are relative
|
||||||
|
app.use('/', express.static(IMAGE_DIR));
|
||||||
|
|
||||||
|
let cachedEntries = [];
|
||||||
|
let usingExport = false;
|
||||||
|
|
||||||
|
// Load entries on startup
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(EXPORT_FILE)) {
|
||||||
|
console.log('Loading clean data from blog-export.json...');
|
||||||
|
const rawData = fs.readFileSync(EXPORT_FILE, 'utf8');
|
||||||
|
cachedEntries = JSON.parse(rawData);
|
||||||
|
usingExport = true;
|
||||||
|
console.log(`Successfully loaded ${cachedEntries.length} clean entries from JSON export.`);
|
||||||
|
} else {
|
||||||
|
console.log('No export file found, falling back to HSQLDB parser...');
|
||||||
|
cachedEntries = parseHSQLDB(DB_DIR);
|
||||||
|
console.log(`Loaded ${cachedEntries.length} entries from HSQLDB (Legacy Parser)`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load database:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all posts with minimal data
|
||||||
|
app.get('/api/posts', (req, res) => {
|
||||||
|
const items = cachedEntries.map((e, idx) => {
|
||||||
|
if (usingExport) {
|
||||||
|
return {
|
||||||
|
id: e.id,
|
||||||
|
title: e.title || 'Untitled',
|
||||||
|
date: e.date || '',
|
||||||
|
author: e.author || 'Paul',
|
||||||
|
category: e.categories || '',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
id: idx,
|
||||||
|
title: e.TITLE || 'Untitled',
|
||||||
|
date: e.TIMESTAMP || '',
|
||||||
|
author: e.AUTHOR || 'Paul',
|
||||||
|
category: e.CATEGORIES || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Sort by date descending if using export (dates are cleaner)
|
||||||
|
if (usingExport) {
|
||||||
|
items.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||||
|
}
|
||||||
|
res.json(items);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get full data for one post
|
||||||
|
app.get('/api/posts/:id', (req, res) => {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
let post = null;
|
||||||
|
|
||||||
|
if (usingExport) {
|
||||||
|
post = cachedEntries.find(p => p.id === id);
|
||||||
|
} else {
|
||||||
|
post = cachedEntries[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!post) return res.status(404).json({error: 'Post not found.'});
|
||||||
|
|
||||||
|
if (usingExport) {
|
||||||
|
// Fix old Windows file paths in image src attributes
|
||||||
|
let body = post.content || '';
|
||||||
|
|
||||||
|
// Match: file:///\\paulspc2\...\Weblogs/FILENAME.jpg
|
||||||
|
// Replace with: http://HOSTNAME:3637/FILENAME.jpg (full URL to backend)
|
||||||
|
const hostname = req.get('host') || 'localhost:3637';
|
||||||
|
const protocol = req.protocol || 'http';
|
||||||
|
|
||||||
|
body = body.replace(
|
||||||
|
/src="file:\/\/\/\\\\[^"]*[\\\/]([^\\\/]+\.(jpg|jpeg|png|gif))"/gi,
|
||||||
|
`src="${protocol}://${hostname}/$1"`
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
title: post.title || 'Untitled',
|
||||||
|
date: post.date || '',
|
||||||
|
author: post.author || 'Paul',
|
||||||
|
category: post.categories || '',
|
||||||
|
body: body,
|
||||||
|
draft: false,
|
||||||
|
modified: '',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.json({
|
||||||
|
title: post.TITLE || 'Untitled',
|
||||||
|
date: post.TIMESTAMP || '',
|
||||||
|
author: post.AUTHOR || 'Paul',
|
||||||
|
category: post.CATEGORIES || '',
|
||||||
|
body: post.ENTRY || '',
|
||||||
|
draft: post.DRAFT || false,
|
||||||
|
modified: post.MODIFIED || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Thingamablog-v2 backend running on port ${PORT}`);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
# Getting Started with Create React App
|
||||||
|
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||||
|
|
||||||
|
The page will reload when you make changes.\
|
||||||
|
You may also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `npm test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.\
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.\
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.\
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `npm run eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||||
|
|
||||||
|
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||||
|
|
||||||
|
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||||
|
|
||||||
|
### Code Splitting
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||||
|
|
||||||
|
### Analyzing the Bundle Size
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||||
|
|
||||||
|
### Making a Progressive Web App
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||||
|
|
||||||
|
### `npm run build` fails to minify
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@mui/icons-material": "^7.3.8",
|
||||||
|
"@mui/material": "^7.3.8",
|
||||||
|
"@testing-library/dom": "^10.4.1",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
|
|
@ -0,0 +1,43 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
.App {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-logo {
|
||||||
|
height: 40vmin;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.App-logo {
|
||||||
|
animation: App-logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-header {
|
||||||
|
background-color: #282c34;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-link {
|
||||||
|
color: #61dafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes App-logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,334 @@
|
||||||
|
import React, { useEffect, useState, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
CssBaseline,
|
||||||
|
ThemeProvider,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemText,
|
||||||
|
CircularProgress,
|
||||||
|
Paper,
|
||||||
|
Divider,
|
||||||
|
Chip,
|
||||||
|
Accordion,
|
||||||
|
AccordionSummary,
|
||||||
|
AccordionDetails,
|
||||||
|
} from "@mui/material";
|
||||||
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
|
import theme from "./theme";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [posts, setPosts] = useState([]);
|
||||||
|
const [selectedId, setSelectedId] = useState(null);
|
||||||
|
const [selectedPost, setSelectedPost] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
// Filtering state
|
||||||
|
const [filterType, setFilterType] = useState("all"); // all, category, date
|
||||||
|
const [filterValue, setFilterValue] = useState(null);
|
||||||
|
|
||||||
|
// Determine API base URL dynamically
|
||||||
|
const API_BASE = `http://${window.location.hostname}:3637`;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`${API_BASE}/api/posts`)
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
setPosts(data);
|
||||||
|
setError("");
|
||||||
|
} else {
|
||||||
|
setPosts([]);
|
||||||
|
setError(typeof data.error === "string" ? data.error : "API did not return posts array.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setPosts([]);
|
||||||
|
setError("Failed to connect to backend API.");
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedId == null) return;
|
||||||
|
setLoading(true);
|
||||||
|
fetch(`${API_BASE}/api/posts/${selectedId}`)
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((data) => {
|
||||||
|
setSelectedPost(data);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [selectedId]);
|
||||||
|
|
||||||
|
// --- Derived Data for Filters ---
|
||||||
|
|
||||||
|
// Build a Category Tree: { "Hobbies": ["Car Maintenance", "Computer"], "Personal": [] }
|
||||||
|
const categoryTree = useMemo(() => {
|
||||||
|
const tree = {};
|
||||||
|
|
||||||
|
posts.forEach(p => {
|
||||||
|
if (!p.category) return;
|
||||||
|
// Categories look like "<Hobbies>\n<Hobbies - Digital Imaging>\n<Personal>"
|
||||||
|
const rawCats = p.category
|
||||||
|
.replace(/</g, '')
|
||||||
|
.replace(/>/g, '')
|
||||||
|
.split('\n')
|
||||||
|
.map(c => c.trim())
|
||||||
|
.filter(c => c);
|
||||||
|
|
||||||
|
rawCats.forEach(catStr => {
|
||||||
|
// Split "Parent - Child"
|
||||||
|
// Note: Using " - " as separator based on your convention
|
||||||
|
const parts = catStr.split(' - ');
|
||||||
|
|
||||||
|
if (parts.length === 1) {
|
||||||
|
// Top level category
|
||||||
|
const parent = parts[0];
|
||||||
|
if (!tree[parent]) tree[parent] = [];
|
||||||
|
} else if (parts.length >= 2) {
|
||||||
|
// Child category
|
||||||
|
const parent = parts[0];
|
||||||
|
const child = parts.slice(1).join(' - '); // Handle multi-level if needed
|
||||||
|
|
||||||
|
if (!tree[parent]) tree[parent] = [];
|
||||||
|
if (!tree[parent].includes(child)) tree[parent].push(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort keys and children
|
||||||
|
const sortedTree = {};
|
||||||
|
Object.keys(tree).sort().forEach(key => {
|
||||||
|
sortedTree[key] = tree[key].sort();
|
||||||
|
});
|
||||||
|
return sortedTree;
|
||||||
|
}, [posts]);
|
||||||
|
|
||||||
|
const archives = useMemo(() => {
|
||||||
|
const years = new Set();
|
||||||
|
posts.forEach(p => {
|
||||||
|
if (!p.date) return;
|
||||||
|
// Date format: 2003-11-03 16:41:22.053
|
||||||
|
const year = p.date.substring(0, 4);
|
||||||
|
if (year) years.add(year);
|
||||||
|
});
|
||||||
|
return Array.from(years).sort().reverse();
|
||||||
|
}, [posts]);
|
||||||
|
|
||||||
|
// --- Filtering Logic ---
|
||||||
|
|
||||||
|
const filteredPosts = useMemo(() => {
|
||||||
|
if (filterType === "all") return posts;
|
||||||
|
|
||||||
|
return posts.filter(p => {
|
||||||
|
if (filterType === "category") {
|
||||||
|
// Smart Filtering:
|
||||||
|
// If filtering by "Hobbies", include "Hobbies - Car Maintenance" too.
|
||||||
|
// Check if ANY category on the post matches the filter OR starts with filter + " -"
|
||||||
|
if (!p.category) return false;
|
||||||
|
|
||||||
|
const postCats = p.category
|
||||||
|
.replace(/</g, '')
|
||||||
|
.replace(/>/g, '')
|
||||||
|
.split('\n');
|
||||||
|
|
||||||
|
return postCats.some(cat =>
|
||||||
|
cat === filterValue || cat.startsWith(filterValue + " - ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (filterType === "date") {
|
||||||
|
return p.date && p.date.startsWith(filterValue);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [posts, filterType, filterValue]);
|
||||||
|
|
||||||
|
// --- Handlers ---
|
||||||
|
|
||||||
|
const handleFilter = (type, value) => {
|
||||||
|
setFilterType(type);
|
||||||
|
setFilterValue(value);
|
||||||
|
setSelectedId(null);
|
||||||
|
setSelectedPost(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<Container maxWidth="xl">
|
||||||
|
<Box my={4}>
|
||||||
|
<Typography variant="h4" color="primary" gutterBottom>
|
||||||
|
Thingamablog Archive
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: "flex", gap: 3, flexDirection: { xs: "column", md: "row" } }}>
|
||||||
|
|
||||||
|
{/* Sidebar (Filters) */}
|
||||||
|
<Paper sx={{ width: { xs: "100%", md: 250 }, p: 2, height: "fit-content" }}>
|
||||||
|
<Typography variant="h6" gutterBottom>Browse</Typography>
|
||||||
|
|
||||||
|
<List dense>
|
||||||
|
<ListItemButton
|
||||||
|
selected={filterType === "all"}
|
||||||
|
onClick={() => handleFilter("all", null)}
|
||||||
|
>
|
||||||
|
<ListItemText primary="All Posts" />
|
||||||
|
</ListItemButton>
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 1 }} />
|
||||||
|
|
||||||
|
<Accordion defaultExpanded disableGutters elevation={0}>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
|
<Typography variant="subtitle2">Categories</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails sx={{ p: 0 }}>
|
||||||
|
{Object.keys(categoryTree).map(parent => (
|
||||||
|
<React.Fragment key={parent}>
|
||||||
|
{/* Parent Category */}
|
||||||
|
<ListItemButton
|
||||||
|
dense
|
||||||
|
onClick={() => handleFilter("category", parent)}
|
||||||
|
selected={filterType === "category" && filterValue === parent}
|
||||||
|
sx={{ pl: 2 }}
|
||||||
|
>
|
||||||
|
<ListItemText primary={parent} />
|
||||||
|
{/* Show count of children or expand icon if needed */}
|
||||||
|
</ListItemButton>
|
||||||
|
|
||||||
|
{/* Child Categories (Indented) */}
|
||||||
|
{categoryTree[parent] && categoryTree[parent].length > 0 && (
|
||||||
|
<List disablePadding>
|
||||||
|
{categoryTree[parent].map(child => (
|
||||||
|
<ListItemButton
|
||||||
|
key={child}
|
||||||
|
dense
|
||||||
|
onClick={() => handleFilter("category", `${parent} - ${child}`)}
|
||||||
|
selected={filterType === "category" && filterValue === `${parent} - ${child}`}
|
||||||
|
sx={{ pl: 4 }} // Indent child categories
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={child}
|
||||||
|
primaryTypographyProps={{ variant: "body2", color: "text.secondary" }}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion defaultExpanded disableGutters elevation={0}>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
|
<Typography variant="subtitle2">Archives</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails sx={{ p: 0 }}>
|
||||||
|
<List dense>
|
||||||
|
{archives.map(year => (
|
||||||
|
<ListItemButton
|
||||||
|
key={year}
|
||||||
|
selected={filterType === "date" && filterValue === year}
|
||||||
|
onClick={() => handleFilter("date", year)}
|
||||||
|
>
|
||||||
|
<ListItemText primary={year} />
|
||||||
|
</ListItemButton>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Post List */}
|
||||||
|
<Paper sx={{ width: { xs: "100%", md: 350 }, height: "80vh", overflow: "auto" }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ p: 2, position: "sticky", top: 0, bgcolor: "background.paper", zIndex: 1 }}>
|
||||||
|
{filterType === "all" ? "All Posts" : `${filterValue} (${filteredPosts.length})`}
|
||||||
|
</Typography>
|
||||||
|
<Divider />
|
||||||
|
<List>
|
||||||
|
{filteredPosts.length === 0 && (
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText primary="No posts found." />
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
{filteredPosts.map((post) => (
|
||||||
|
<ListItem key={post.id} disablePadding divider>
|
||||||
|
<ListItemButton
|
||||||
|
onClick={() => setSelectedId(post.id)}
|
||||||
|
selected={selectedId === post.id}
|
||||||
|
alignItems="flex-start"
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={post.title}
|
||||||
|
secondary={
|
||||||
|
<>
|
||||||
|
<Typography component="span" variant="body2" color="text.primary">
|
||||||
|
{post.date ? post.date.substring(0, 10) : ""}
|
||||||
|
</Typography>
|
||||||
|
<br />
|
||||||
|
{post.author && `by ${post.author}`}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Post Content */}
|
||||||
|
<Box flex={1} sx={{ height: "80vh", overflow: "auto" }}>
|
||||||
|
{loading && <CircularProgress color="primary" sx={{ m: 4 }} />}
|
||||||
|
|
||||||
|
{selectedPost && !loading && (
|
||||||
|
<Paper sx={{ p: 4 }}>
|
||||||
|
<Typography variant="h4" color="primary" gutterBottom>
|
||||||
|
{selectedPost.title}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ mb: 3, display: "flex", gap: 1, flexWrap: "wrap", alignItems: "center" }}>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary">
|
||||||
|
{selectedPost.date}
|
||||||
|
</Typography>
|
||||||
|
{selectedPost.category && selectedPost.category.split('\n').map(c => (
|
||||||
|
<Chip key={c} label={c.replace(/[<>]/g, '')} size="small" />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ mb: 3 }} />
|
||||||
|
|
||||||
|
<Box
|
||||||
|
className="blog-content"
|
||||||
|
dangerouslySetInnerHTML={{ __html: selectedPost.body }}
|
||||||
|
sx={{
|
||||||
|
"& img": { maxWidth: "100%", height: "auto" },
|
||||||
|
"& a": { color: "primary.main" },
|
||||||
|
"& p": { mb: 2, lineHeight: 1.6 }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!selectedPost && !loading && (
|
||||||
|
<Paper sx={{ p: 4, textAlign: "center", color: "text.secondary" }}>
|
||||||
|
<Typography variant="h6">Select a post to read</Typography>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
test('renders learn react link', () => {
|
||||||
|
render(<App />);
|
||||||
|
const linkElement = screen.getByText(/learn react/i);
|
||||||
|
expect(linkElement).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals();
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
|
|
@ -0,0 +1,13 @@
|
||||||
|
const reportWebVitals = onPerfEntry => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { createTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
palette: {
|
||||||
|
mode: 'dark',
|
||||||
|
background: {
|
||||||
|
default: '#181818',
|
||||||
|
paper: '#232323',
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
main: '#6EA6FF', // Soft blue accent
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: '#B39DDB', // Muted lavender
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: '#F5F5F5', // Very light text
|
||||||
|
secondary: '#A1A1AA', // Soft grey text
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontFamily: 'Segoe UI, Roboto, Arial, sans-serif',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default theme;
|
||||||
Loading…
Reference in New Issue