Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53482a24b7 | ||
|
|
3badea2832 |
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
+2
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "book-test api",
|
"name": "book-test-api",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"typescript": "^7.0.2"
|
"typescript": "^7.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"argon2": "^0.45.1",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
|
|||||||
+89
-1
@@ -4,10 +4,11 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"api": {
|
"api": {
|
||||||
"name": "book-test",
|
"name": "book-test-api",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"argon2": "^0.45.1",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
@@ -40,6 +41,12 @@
|
|||||||
"linux"
|
"linux"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@epic-web/invariant": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.28.2",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
|
||||||
@@ -57,6 +64,15 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@phc/format": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/body-parser": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.6",
|
"version": "1.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||||
@@ -243,6 +259,22 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/argon2": {
|
||||||
|
"version": "0.45.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.45.1.tgz",
|
||||||
|
"integrity": "sha512-skm+/WCjkGqCQxF7FG1LuZXM5yvbFjgbfiCGsud2oLgaDhh6b6dbH0b1EkghbM+xx4Bj8Ape+KKgixoIlWZicQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@phc/format": "^1.0.0",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
|
"node-addon-api": "^8.9.0",
|
||||||
|
"node-gyp-build": "^4.8.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/basic-auth": {
|
"node_modules/basic-auth": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||||
@@ -296,6 +328,14 @@
|
|||||||
"resolved": "api",
|
"resolved": "api",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/book-test-api": {
|
||||||
|
"resolved": "api",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/book-test-web": {
|
||||||
|
"resolved": "web",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -444,6 +484,23 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-env": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@epic-web/invariant": "^1.0.0",
|
||||||
|
"cross-spawn": "^7.0.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"cross-env": "dist/bin/cross-env.js",
|
||||||
|
"cross-env-shell": "dist/bin/cross-env-shell.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -1078,6 +1135,26 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-addon-api": {
|
||||||
|
"version": "8.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz",
|
||||||
|
"integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18 || ^20 || >= 21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-gyp-build": {
|
||||||
|
"version": "4.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||||
|
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"node-gyp-build": "bin.js",
|
||||||
|
"node-gyp-build-optional": "optional.js",
|
||||||
|
"node-gyp-build-test": "build-test.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -1823,6 +1900,17 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"name": "book-test-web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^5.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/cli": "^4.3.3",
|
||||||
|
"tailwindcss": "^4.3.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+1242
-2
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
"web"
|
"web"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently -n \"API,WEB\" -c \"cyan,green\" \"npm run start --workspace=api\" \"npm run start --workspace=web\"",
|
"dev": "concurrently -n \"API,CSS,WEB\" -c \"cyan,blue,green\" \"npm run start --workspace=api\" \"npm run watch:css --workspace=web\" \"npm run dev:server --workspace=web\"",
|
||||||
"start:api": "npm run start --workspace=api",
|
"start:api": "npm run start --workspace=api",
|
||||||
"start:web": "npm run start --workspace=web",
|
"start:web": "npm run start --workspace=web",
|
||||||
"test:api": "npm run test --workspace=api",
|
"test:api": "npm run test --workspace=api",
|
||||||
|
|||||||
+9
-2
@@ -1,11 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "book-test web",
|
"name": "book-test-web",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsx src/index.ts"
|
"build:css": "tailwindcss -i ./src/input.css -o ./public/output.css --minify",
|
||||||
|
"watch:css": "tailwindcss -i ./src/input.css -o ./public/output.css --watch",
|
||||||
|
"start": "npm run build:css && tsx src/index.ts",
|
||||||
|
"dev:server": "tsx src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.2.1"
|
"express": "^5.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/cli": "^4.3.3",
|
||||||
|
"tailwindcss": "^4.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+51
-29
@@ -1,30 +1,32 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.WEB_PORT || 3001;
|
const PORT = process.env.WEB_PORT || 3001;
|
||||||
const API_URL = process.env.API_URL || 'http://localhost:8300';
|
const API_URL = process.env.API_URL || 'http://localhost:8300';
|
||||||
|
|
||||||
|
// 1. Tell Express to serve static files (like your output.css) out of the public folder
|
||||||
|
app.use(express.static(join(import.meta.dirname, '../public')));
|
||||||
|
|
||||||
app.get('/', async (req, res) => {
|
app.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// 1. Fetch the hierarchy structure from your API workspace
|
|
||||||
const apiResponse = await fetch(`${API_URL}/api/categories/tree`);
|
const apiResponse = await fetch(`${API_URL}/api/categories/tree`);
|
||||||
|
if (!apiResponse.ok) throw new Error(`API responded with status: ${apiResponse.status}`);
|
||||||
if (!apiResponse.ok) {
|
|
||||||
throw new Error(`API responded with status: ${apiResponse.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiData = await apiResponse.json();
|
const apiData = await apiResponse.json();
|
||||||
const categoriesTree = apiData.success ? apiData.data : [];
|
const categoriesTree = apiData.success ? apiData.data : [];
|
||||||
|
|
||||||
// 2. Recursive layout engine to convert nested JSON arrays to clean HTML lists
|
|
||||||
function renderTreeHTML(nodes: any[]): string {
|
function renderTreeHTML(nodes: any[]): string {
|
||||||
if (!nodes || nodes.length === 0) return '';
|
if (!nodes || nodes.length === 0) return '';
|
||||||
let html = '<ul>';
|
let html = '<ul class="pl-6 border-l border-dashed border-slate-200 my-1 space-y-2">';
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
html += `
|
html += `
|
||||||
<li>
|
<li class="relative transition-all duration-200">
|
||||||
<span class="folder-name">${node.name}</span>
|
<div class="folder-group inline-flex items-center gap-2 cursor-pointer select-none py-1 px-2 rounded-md hover:bg-slate-50">
|
||||||
<span class="path-tag">${node.path}</span>
|
<span class="folder-toggle-icon text-slate-400 font-mono text-xs inline-block transition-transform duration-200 w-4 text-center"></span>
|
||||||
|
<span class="folder-name font-semibold text-slate-700 hover:text-blue-600">${node.name}</span>
|
||||||
|
<span class="path-tag text-[10px] font-mono font-medium text-slate-400 bg-slate-100 border border-slate-200/60 px-1.5 py-0.5 rounded ml-2">${node.path}</span>
|
||||||
|
</div>
|
||||||
${renderTreeHTML(node.children)}
|
${renderTreeHTML(node.children)}
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
@@ -35,38 +37,58 @@ app.get('/', async (req, res) => {
|
|||||||
|
|
||||||
const treeHTML = renderTreeHTML(categoriesTree);
|
const treeHTML = renderTreeHTML(categoriesTree);
|
||||||
|
|
||||||
// 3. Serve the clean, dependency-free UI page
|
// 2. Link directly to the local static compiled stylesheet
|
||||||
res.send(`
|
res.send(`
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" class="h-full bg-slate-50">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Hierarchical Ltree Viewer</title>
|
<title>Hierarchical Ltree Viewer</title>
|
||||||
|
<!-- Linked to your local, compiled, minified CSS bundle -->
|
||||||
|
<link rel="stylesheet" href="/output.css">
|
||||||
<style>
|
<style>
|
||||||
body { font-family: -apple-system, sans-serif; padding: 3rem; background: #fafafa; color: #333; }
|
.collapsed > ul { display: none; }
|
||||||
h1 { color: #111; margin-bottom: 0.5rem; }
|
.collapsed > .folder-group .folder-toggle-icon { transform: rotate(-90deg); }
|
||||||
.subtitle { color: #666; margin-bottom: 2rem; font-size: 0.9rem; }
|
|
||||||
ul { list-style-type: none; padding-left: 20px; border-left: 1px dashed #ccc; margin: 5px 0; }
|
|
||||||
li { margin: 8px 0; position: relative; }
|
|
||||||
.folder-name { font-weight: 600; color: #2c3e50; }
|
|
||||||
.path-tag { font-size: 0.75rem; color: #95a5a6; background: #eef2f3; padding: 2px 6px; border-radius: 4px; margin-left: 8px; font-family: monospace; }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="h-full antialiased text-slate-800 p-6 md:p-12 max-w-4xl mx-auto">
|
||||||
<h1>Hierarchy Tree Explorer</h1>
|
<header class="mb-8 border-b border-slate-200 pb-6">
|
||||||
<div class="subtitle">Connected to API target: <code>${API_URL}</code></div>
|
<h1 class="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl">Hierarchy Tree Explorer</h1>
|
||||||
<div id="tree-container">
|
<p class="mt-2 text-sm text-slate-500">Connected to API cluster target: <code class="bg-slate-100 text-slate-700 px-2 py-1 rounded font-mono text-xs">${API_URL}</code></p>
|
||||||
${treeHTML || '<p>No root categories found. Add some paths to your ltree table!</p>'}
|
</header>
|
||||||
</div>
|
|
||||||
|
<main class="bg-white rounded-xl shadow-sm border border-slate-200 p-6 md:p-8">
|
||||||
|
<div id="tree-container" class="flow-root">
|
||||||
|
${treeHTML || '<p class="text-slate-500 text-center py-4">No root categories found. Add some paths to your ltree table!</p>'}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('.folder-group').forEach(group => {
|
||||||
|
const hasChildren = group.parentElement.querySelector('ul');
|
||||||
|
const iconSpan = group.querySelector('.folder-toggle-icon');
|
||||||
|
|
||||||
|
if (hasChildren) {
|
||||||
|
if (iconSpan) iconSpan.textContent = '▼';
|
||||||
|
group.addEventListener('click', () => {
|
||||||
|
group.parentElement.classList.toggle('collapsed');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (iconSpan) {
|
||||||
|
iconSpan.textContent = '•';
|
||||||
|
iconSpan.classList.remove('text-slate-400');
|
||||||
|
iconSpan.classList.add('text-slate-300', 'translate-y-[-1px]');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Frontend error fetching data:', error);
|
console.error('Frontend error fetching data:', error);
|
||||||
res.status(500).send(`
|
res.status(500).send(`<h1>500 Frontend Render Failure</h1><p>Could not reach your API application container.</p>`);
|
||||||
<h1>500 Frontend Render Failure</h1>
|
|
||||||
<p>Could not reach your API application container. Verify that the API server is active on port 3000.</p>
|
|
||||||
`);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
Reference in new issue
Block a user