@lexho111/plainblog 0.6.4 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/Article.js +1 -1
  2. package/Blog.js +249 -138
  3. package/Formatter.js +5 -7
  4. package/model/DataModel.js +6 -2
  5. package/model/datastructures/BinarySearchTree.js +27 -0
  6. package/model/datastructures/BinarySearchTreeHashMap.js +14 -5
  7. package/package.json +1 -1
  8. package/public/favicon.ico +0 -0
  9. package/public/index.html +17 -0
  10. package/public/main-LAI5FAZI.js +7 -0
  11. package/public/main-SKL6R4NB.js +7 -0
  12. package/public/scripts.min.js +2 -2
  13. package/public/styles-I55BTQOK.css +1 -0
  14. package/public/styles.min.css +2 -2
  15. package/src/fetchData.js +53 -29
  16. package/src/styles.css +152 -42
  17. package/.vscode/settings.json +0 -2
  18. package/blog_test_empty.db +0 -0
  19. package/blog_test_load.db +0 -0
  20. package/coverage/clover.xml +0 -1051
  21. package/coverage/coverage-final.json +0 -20
  22. package/coverage/lcov-report/base.css +0 -224
  23. package/coverage/lcov-report/block-navigation.js +0 -87
  24. package/coverage/lcov-report/favicon.png +0 -0
  25. package/coverage/lcov-report/index.html +0 -161
  26. package/coverage/lcov-report/package/Article.js.html +0 -406
  27. package/coverage/lcov-report/package/Blog.js.html +0 -2674
  28. package/coverage/lcov-report/package/Formatter.js.html +0 -379
  29. package/coverage/lcov-report/package/build-scripts.js.html +0 -247
  30. package/coverage/lcov-report/package/build-styles.js.html +0 -367
  31. package/coverage/lcov-report/package/index.html +0 -191
  32. package/coverage/lcov-report/package/model/APIModel.js.html +0 -190
  33. package/coverage/lcov-report/package/model/ArrayList.js.html +0 -382
  34. package/coverage/lcov-report/package/model/ArrayListHashMap.js.html +0 -379
  35. package/coverage/lcov-report/package/model/BinarySearchTree.js.html +0 -856
  36. package/coverage/lcov-report/package/model/BinarySearchTreeHashMap.js.html +0 -346
  37. package/coverage/lcov-report/package/model/DataModel.js.html +0 -325
  38. package/coverage/lcov-report/package/model/DatabaseModel.js.html +0 -235
  39. package/coverage/lcov-report/package/model/FileAdapter.js.html +0 -397
  40. package/coverage/lcov-report/package/model/FileList.js.html +0 -244
  41. package/coverage/lcov-report/package/model/FileModel.js.html +0 -361
  42. package/coverage/lcov-report/package/model/SequelizeAdapter.js.html +0 -538
  43. package/coverage/lcov-report/package/model/SqliteAdapter.js.html +0 -247
  44. package/coverage/lcov-report/package/model/datastructures/ArrayList.js.html +0 -439
  45. package/coverage/lcov-report/package/model/datastructures/ArrayListHashMap.js.html +0 -196
  46. package/coverage/lcov-report/package/model/datastructures/BinarySearchTree.js.html +0 -916
  47. package/coverage/lcov-report/package/model/datastructures/BinarySearchTreeHashMap.js.html +0 -355
  48. package/coverage/lcov-report/package/model/datastructures/FileList.js.html +0 -244
  49. package/coverage/lcov-report/package/model/datastructures/index.html +0 -176
  50. package/coverage/lcov-report/package/model/index.html +0 -206
  51. package/coverage/lcov-report/package/utilities.js.html +0 -511
  52. package/coverage/lcov-report/prettify.css +0 -1
  53. package/coverage/lcov-report/prettify.js +0 -2
  54. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  55. package/coverage/lcov-report/sorter.js +0 -210
  56. package/coverage/lcov.info +0 -2078
  57. package/eslint.config.js +0 -27
  58. package/plainblog - Verkn/303/274pfung.lnk +0 -0
package/Article.js CHANGED
@@ -52,7 +52,7 @@ export default class Article {
52
52
  if (this.content == null) return "";
53
53
  const moreButton = this.content.length > 400 ? `<a href="#">more</a>` : "";
54
54
  const buttons = loggedin
55
- ? `<div class="buttons"><div id="editButton${this.id}" class="button edit">edit</div><div id="deleteButton${this.id}" class="button delete">delete</div></div>`
55
+ ? `<div class="buttons"><div id="editButton${this.id}" class="btn edit">edit</div><div id="deleteButton${this.id}" class="btn delete">delete</div><div id="somethingelseButton${this.id}" class="btn light">something else</div></div>`
56
56
  : "";
57
57
  const script = loggedin
58
58
  ? `<script>
package/Blog.js CHANGED
@@ -16,6 +16,7 @@ import FileAdapter from "./model/FileAdapter.js";
16
16
  import DataModel from "./model/DataModel.js";
17
17
  import { BinarySearchTreeHashMap } from "./model/datastructures/BinarySearchTreeHashMap.js";
18
18
  import createDebug from "./debug-loader.js";
19
+ import { readFile } from "node:fs/promises";
19
20
 
20
21
  // Initialize the debugger with a specific namespace
21
22
  const debug = createDebug("plainblog:Blog");
@@ -92,6 +93,7 @@ export default class Blog {
92
93
  #stylesheetPath = "";
93
94
  compilestyle = false;
94
95
  #initPromise = null;
96
+ #publicDir = path.join(process.cwd(), "public");
95
97
 
96
98
  setTitle(title) {
97
99
  this.#title = title;
@@ -161,8 +163,8 @@ export default class Blog {
161
163
 
162
164
  #isAuthenticated(req) {
163
165
  if (!req.headers.cookie) return false;
164
- const params = new URLSearchParams(req.headers.cookie.replace(/; /g, "&"));
165
- return this.sessions.has(params.get("session"));
166
+ const match = req.headers.cookie.match(/(?:^|;\s*)session=([^;]*)/);
167
+ return match ? this.sessions.has(match[1]) : false;
166
168
  }
167
169
 
168
170
  async #handleLogin(req, res) {
@@ -184,13 +186,17 @@ export default class Blog {
184
186
  });
185
187
  res.end();
186
188
  } else {
187
- res.writeHead(401, { "Content-Type": "text/html" });
188
- res.end(`${header("My Blog")}
189
- <body>
190
- <h1>Unauthorized</h1><div class="box"><p>Please enter the password.<form method="POST">
191
- <input type="password" name="password" placeholder="Password" />
192
- <button style="margin: 2px;">Login</button></form></div>
193
- </body></html>`);
189
+ debug("login failed");
190
+ debug("password did not match %s", params.get("password"));
191
+ res.writeHead(401, { "Content-Type": "application/json" });
192
+
193
+ // Send the JSON string
194
+ res.end(
195
+ JSON.stringify({
196
+ error: "unauthorized",
197
+ message: "Please enter the correct password.",
198
+ }),
199
+ );
194
200
  }
195
201
  }
196
202
 
@@ -232,11 +238,7 @@ export default class Blog {
232
238
  const __filename = fileURLToPath(import.meta.url);
233
239
  const __dirname = path.dirname(__filename);
234
240
  const srcStylePath = path.join(__dirname, "src", "styles.css");
235
- const publicStylePath = path.join(
236
- process.cwd(),
237
- "public",
238
- "styles.min.css",
239
- );
241
+ const publicStylePath = path.join(this.#publicDir, "styles.min.css");
240
242
 
241
243
  let publicHash = null;
242
244
  let srcStyles = "";
@@ -348,43 +350,25 @@ export default class Blog {
348
350
  return this.#initPromise;
349
351
  }
350
352
 
351
- /** post a blog article */
352
- async postArticle(req, res) {
353
- const body = await new Promise((resolve, reject) => {
354
- let data = "";
355
- req.on("data", (chunk) => (data += chunk.toString()));
356
- req.on("end", () => resolve(data));
357
- req.on("error", reject);
358
- });
359
-
360
- //req.on("end", async () => {
361
- const params = new URLSearchParams(body);
362
- const title = params.get("title");
363
- const content = params.get("content");
364
-
365
- if (title && content) {
366
- const newArticleData = { title, content };
367
- try {
368
- // Save the new article to the database via the ApiServer
369
- const promises = [];
370
- if (this.#databaseModel)
371
- promises.push(this.#databaseModel.save(newArticleData));
372
- if (this.#isExternalAPI)
373
- promises.push(postData(this.#apiUrl, newArticleData));
374
- await Promise.all(promises);
375
- // Add the article to the local list for immediate display
376
- this.#articles.insert(Article.createNew(title, content));
377
- // remove sample entries
378
- this.#articles.remove(1); // "Sample Entry #1"
379
- this.#articles.remove(2); // "Sample Entry #2"
380
- } catch (error) {
381
- console.error("Failed to post new article to API:", error);
382
- }
353
+ async postArticle(newArticle) {
354
+ try {
355
+ // Save the new article to the database via the ApiServer
356
+ const promises = [];
357
+ //if (this.#databaseModel)
358
+ // promises.push(this.#databaseModel.save(newArticle));
359
+ if (this.#isExternalAPI)
360
+ promises.push(postData(this.#apiUrl, newArticle));
361
+ await Promise.all(promises);
362
+ const title = newArticle.title;
363
+ const content = newArticle.content;
364
+ // Add the article to the local list for immediate display
365
+ this.#articles.insert(Article.createNew(title, content));
366
+ // remove sample entries
367
+ this.#articles.remove(1); // "Sample Entry #1"
368
+ this.#articles.remove(2); // "Sample Entry #2"
369
+ } catch (error) {
370
+ console.error("Failed to post new article to API:", error);
383
371
  }
384
-
385
- res.writeHead(303, { Location: "/" });
386
- res.end();
387
- //});
388
372
  }
389
373
 
390
374
  /** start a http server with default port 8080 */
@@ -393,7 +377,18 @@ export default class Blog {
393
377
 
394
378
  const server = http.createServer(async (req, res) => {
395
379
  //debug("query %s", req.url);
380
+
396
381
  // API routes
382
+ // workaround for angular frontend
383
+ if (req.url === "/api/login" && req.method === "POST") {
384
+ await this.#handleLogin(req, res);
385
+ return;
386
+ }
387
+ if (req.url === "/api/logout") {
388
+ this.#handleLogout(req, res);
389
+ return;
390
+ }
391
+ // ---------------------------------------------
397
392
  if (req.url.startsWith("/api")) {
398
393
  await this.#jsonAPI(req, res);
399
394
  return;
@@ -403,13 +398,61 @@ export default class Blog {
403
398
  if (req.url === "/login") {
404
399
  if (req.method === "GET") {
405
400
  res.writeHead(200, { "Content-Type": "text/html" });
406
- res.end(`${header("My Blog")}
407
- <body>
408
- <h1>Login</h1><div class="box"><form method="POST">
409
- <input type="password" name="password" placeholder="Password" />
410
- <button style="margin: 2px;">Login</button></form></div>
401
+ res.end(`${header("My Blog")}<body>
402
+ <form class="loginform" id="loginForm">
403
+ <h1>Blog</h1>
404
+ <!-- Message container -->
405
+ <div id="statusMessage"></div>
406
+
407
+ <input type="password" class="form_element" name="password" placeholder="Password" required />
408
+ <button class="btn" type="submit">Login</button>
409
+ </form>
410
+
411
+ <script>
412
+ const loginForm = document.getElementById('loginForm');
413
+ const statusDiv = document.getElementById('statusMessage');
414
+
415
+ loginForm.addEventListener('submit', async (e) => {
416
+ e.preventDefault(); // Prevent page reload
417
+
418
+ // Clear previous messages
419
+ statusDiv.innerHTML = '';
420
+
421
+ const formData = new FormData(loginForm);
422
+ const body = new URLSearchParams(formData); // Replicates form-urlencoded
423
+
424
+ try {
425
+ const response = await fetch('/login', {
426
+ method: 'POST',
427
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
428
+ body: body.toString()
429
+ });
430
+
431
+ if (response.ok) {
432
+ window.location.href = '/'; // Redirect on success
433
+ } else if (response.status === 401) {
434
+ // Handle Unauthorized status
435
+ statusDiv.innerHTML = '<h2>Unauthorized</h2><p>Please enter the correct password.</p>';
436
+ } else {
437
+ statusDiv.innerHTML = '<p>Something went wrong. Please try again.</p>';
438
+ }
439
+ } catch (error) {
440
+ console.error('Network Error:', error);
441
+ statusDiv.innerHTML = '<p>Unable to connect to server.</p>';
442
+ }
443
+ });
444
+ </script>
411
445
  </body></html>`);
412
446
  return;
447
+ /*res.end(`${header("My Blog")}
448
+ <body>
449
+ <form class="loginform" method="POST">
450
+ <h1>Blog</h1>
451
+ <h2>Login</h2>
452
+ <input type="password" class="form_element password" name="password" placeholder="Password" />
453
+ <button class="btn">Login</button></form>
454
+ </body></html>`);
455
+ return;*/
413
456
  } else if (req.method === "POST") {
414
457
  await this.#handleLogin(req, res);
415
458
  return;
@@ -420,18 +463,9 @@ export default class Blog {
420
463
  this.#handleLogout(req, res);
421
464
  return;
422
465
  }
423
- // POST new article
424
- if (req.method === "POST" && req.url === "/") {
425
- if (!this.#isAuthenticated(req)) {
426
- res.writeHead(403, { "Content-Type": "text/plain" });
427
- res.end("Forbidden");
428
- return;
429
- }
430
- await this.postArticle(req, res);
431
- // GET artciles
432
- } else if (req.method === "GET" && req.url === "/") {
433
- // load articles
434
-
466
+ // load articles
467
+ // GET artciles
468
+ if (req.method === "GET" && req.url === "/") {
435
469
  // reload styles and scripts on (every) request
436
470
  if (this.reloadStylesOnGET) {
437
471
  if (this.#stylesheetPath != null && this.compilestyle) {
@@ -452,6 +486,7 @@ export default class Blog {
452
486
  loggedin = true;
453
487
  }
454
488
 
489
+ // use built in view engine
455
490
  try {
456
491
  const html = await this.toHTML(loggedin); // render this blog to HTML
457
492
  res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
@@ -461,48 +496,72 @@ export default class Blog {
461
496
  res.writeHead(500, { "Content-Type": "text/plain" });
462
497
  res.end("Internal Server Error");
463
498
  }
499
+
500
+ // use angular frontend
501
+ /*const filePath = path.join(this.#publicDir, "index.html");
502
+
503
+ debug("%s", filePath);
504
+ try {
505
+ const data = await readFile(filePath);
506
+ // Manual MIME type detection (simplified)
507
+ const ext = path.extname(filePath);
508
+ const mimeTypes = {
509
+ ".html": "text/html",
510
+ ".css": "text/css",
511
+ ".js": "text/javascript",
512
+ };
513
+
514
+ res.writeHead(200, {
515
+ "Content-Type": mimeTypes[ext] || "application/octet-stream",
516
+ });
517
+ res.end(data);
518
+ } catch (err) {
519
+ if (err) {
520
+ res.writeHead(404);
521
+ return res.end("Index-File Not Found");
522
+ }
523
+ }*/
524
+
525
+ try {
526
+ //const html = await this.toHTML(loggedin); // render this blog to HTML
527
+ //res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
528
+ //res.end(html);
529
+ } catch (err) {
530
+ console.error(err);
531
+ res.writeHead(500, { "Content-Type": "text/plain" });
532
+ res.end("Internal Server Error");
533
+ }
464
534
  } else {
465
535
  // Try to serve static files from public folder
536
+ // Normalize path to prevent directory traversal attacks
537
+ const safePath = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, "");
538
+ const filePath = path.join(
539
+ this.#publicDir,
540
+ safePath === "/" ? "index.html" : safePath,
541
+ );
542
+
543
+ debug("%s", filePath);
466
544
  try {
467
- const publicDir = path.join(process.cwd(), "public");
468
- const parsedUrl = new URL(
469
- req.url,
470
- `http://${req.headers.host || "localhost"}`,
471
- );
472
- const filePath = path.join(publicDir, parsedUrl.pathname);
473
-
474
- if (filePath.startsWith(publicDir)) {
475
- const stats = await fs.promises.stat(filePath);
476
- if (stats.isFile()) {
477
- const ext = path.extname(filePath).toLowerCase();
478
- const mimeTypes = {
479
- ".html": "text/html",
480
- ".js": "text/javascript",
481
- ".css": "text/css",
482
- ".json": "application/json",
483
- ".png": "image/png",
484
- ".jpg": "image/jpeg",
485
- ".gif": "image/gif",
486
- ".svg": "image/svg+xml",
487
- ".ico": "image/x-icon",
488
- };
489
- const contentType = mimeTypes[ext] || "application/octet-stream";
490
- res.writeHead(200, { "Content-Type": contentType });
491
- fs.createReadStream(filePath).pipe(res);
492
- return;
493
- }
494
- }
545
+ const data = await readFile(filePath);
546
+ // Manual MIME type detection (simplified)
547
+ const ext = path.extname(filePath);
548
+ const mimeTypes = {
549
+ ".html": "text/html",
550
+ ".css": "text/css",
551
+ ".js": "text/javascript",
552
+ };
553
+
554
+ res.writeHead(200, {
555
+ "Content-Type": mimeTypes[ext] || "application/octet-stream",
556
+ });
557
+ res.end(data);
495
558
  } catch (err) {
496
- // ENOENT (file not found) is expected, leading to a 404.
497
- // Only log other, unexpected errors.
498
- if (err.code !== "ENOENT") {
499
- console.error(err);
559
+ if (err) {
560
+ res.writeHead(404);
561
+ return res.end("File Not Found");
500
562
  }
501
563
  }
502
-
503
- // Error 404
504
- res.writeHead(404, { "Content-Type": "application/json" });
505
- res.end(JSON.stringify({ message: "Not Found" }));
564
+ return;
506
565
  }
507
566
  });
508
567
 
@@ -588,11 +647,27 @@ export default class Blog {
588
647
  // controller
589
648
  /** everything that happens in /api */
590
649
  async #jsonAPI(req, res) {
650
+ const origin = req.headers.origin;
651
+ if (origin) {
652
+ res.setHeader("Access-Control-Allow-Origin", origin);
653
+ res.setHeader("Access-Control-Allow-Credentials", "true");
654
+ res.setHeader(
655
+ "Access-Control-Allow-Methods",
656
+ "GET, POST, PUT, DELETE, OPTIONS",
657
+ );
658
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
659
+ }
660
+ if (req.method === "OPTIONS") {
661
+ res.writeHead(204);
662
+ res.end();
663
+ return;
664
+ }
591
665
  const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
592
666
  const pathname = url.pathname;
593
667
 
594
668
  if (req.method === "GET") {
595
669
  if (pathname === "/api" || pathname === "/api/") {
670
+ debug("GET /api");
596
671
  res.writeHead(200, { "Content-Type": "application/json" });
597
672
  const data = {
598
673
  title: this.title,
@@ -601,20 +676,46 @@ export default class Blog {
601
676
  }
602
677
  // Search
603
678
  if (url.searchParams.has("q")) {
679
+ debug("GET search article by query");
604
680
  const query = url.searchParams.get("q");
681
+ const pLimit = parseInt(url.searchParams.get("limit"));
682
+ const limit = !isNaN(pLimit) ? pLimit : null;
683
+ debug("GET search article by query %s with limit %s", query, limit);
605
684
  res.writeHead(200, { "Content-Type": "application/json" });
606
- const results = this.#articles.search(query);
685
+ const results = this.#articles.search(query, limit);
607
686
  res.end(JSON.stringify({ articles: results }));
608
687
  return;
609
688
  }
689
+ // GET article by ID
690
+ // /api/articles/1
691
+ const match = pathname.match(/^\/api\/articles\/(\d+)$/);
692
+ if (match) {
693
+ debug("GET article by id");
694
+ const id = parseInt(match[1]);
695
+ debug("GET article by id %d", id);
696
+ //console.log(this.#articles.getAllArticles());
697
+ const article = this.#articles.get(id);
698
+ if (article) {
699
+ res.writeHead(200, { "Content-Type": "application/json" });
700
+ res.end(JSON.stringify(article));
701
+ } else {
702
+ res.writeHead(404, { "Content-Type": "application/json" });
703
+ res.end(JSON.stringify({ error: "Not Found" }));
704
+ }
705
+ return;
706
+ }
610
707
  // GET all blog data
611
708
  if (pathname === "/api/articles") {
709
+ debug("GET all articles");
612
710
  // Use 'offset' param as startId (filter) to get items starting at ID
613
711
  const pLimit = parseInt(url.searchParams.get("limit"));
614
712
  const limit = !isNaN(pLimit) ? pLimit : null;
615
713
 
616
714
  const start = url.searchParams.get("startdate");
617
715
  const end = url.searchParams.get("enddate");
716
+
717
+ debug("startdate: %d, enddate: %d, limit: %d", start, end, limit);
718
+
618
719
  //const parsedStart = parseDateParam(qStartdate);
619
720
  //const parsedEnd = parseDateParam(qEnddate, true);
620
721
 
@@ -633,7 +734,9 @@ export default class Blog {
633
734
 
634
735
  // POST a new article
635
736
  } else if (req.method === "POST" && pathname === "/api/articles") {
737
+ debug("POST an article");
636
738
  if (!this.#isAuthenticated(req)) {
739
+ debug("not authenticated");
637
740
  res.writeHead(403, { "Content-Type": "application/json" });
638
741
  res.end(JSON.stringify({ error: "Forbidden" }));
639
742
  return;
@@ -645,45 +748,57 @@ export default class Blog {
645
748
  req.on("error", reject);
646
749
  });
647
750
  const newArticle = JSON.parse(body);
751
+ debug("new article: %s", newArticle.title);
648
752
  // local
649
753
  await this.#databaseModel.save(newArticle); // --> to api server
754
+ this.postArticle(newArticle);
650
755
  // extern
651
756
  res.writeHead(201, { "Content-Type": "application/json" });
652
757
  res.end(JSON.stringify(newArticle));
653
- } else if (req.method === "DELETE" && pathname === "/api/articles") {
758
+ } else if (req.method === "DELETE") {
759
+ debug("DELETE an article");
760
+ const match = pathname.match(/^\/api\/articles\/(\d+)$/);
761
+ if (pathname === "/api/articles" || match) {
762
+ if (!this.#isAuthenticated(req)) {
763
+ res.writeHead(403, { "Content-Type": "application/json" });
764
+ res.end(JSON.stringify({ error: "Forbidden" }));
765
+ return;
766
+ }
767
+ const id = match ? match[1] : url.searchParams.get("id");
768
+ debug("delete an article by id $d", id);
769
+ if (id) {
770
+ this.#articles.remove(parseInt(id));
771
+ if (this.#databaseModel) {
772
+ await this.#databaseModel.remove(parseInt(id));
773
+ }
774
+ res.writeHead(200, { "Content-Type": "application/json" });
775
+ res.end(JSON.stringify({ status: "deleted", id }));
776
+ }
777
+ }
778
+ } else if (req.method === "PUT") {
779
+ debug("PUT an article");
654
780
  if (!this.#isAuthenticated(req)) {
655
781
  res.writeHead(403, { "Content-Type": "application/json" });
656
782
  res.end(JSON.stringify({ error: "Forbidden" }));
657
783
  return;
658
784
  }
659
- const id = url.searchParams.get("id");
660
- if (id) {
661
- this.#articles.remove(parseInt(id));
785
+ const match = pathname.match(/^\/api\/articles\/(\d+)$/);
786
+ if (pathname === "/api/articles" || match) {
787
+ const id = match ? match[1] : url.searchParams.get("id");
788
+ debug("PUT article id: %d", id);
789
+ const body = await new Promise((resolve) => {
790
+ let data = "";
791
+ req.on("data", (chunk) => (data += chunk));
792
+ req.on("end", () => resolve(data));
793
+ });
794
+ const { title, content } = JSON.parse(body);
795
+ this.#articles.update(parseInt(id), title, content);
662
796
  if (this.#databaseModel) {
663
- await this.#databaseModel.remove(parseInt(id));
797
+ await this.#databaseModel.update(parseInt(id), { title, content });
664
798
  }
665
799
  res.writeHead(200, { "Content-Type": "application/json" });
666
- res.end(JSON.stringify({ status: "deleted", id }));
800
+ res.end(JSON.stringify({ status: "updated", id }));
667
801
  }
668
- } else if (req.method === "PUT" && pathname === "/api/articles") {
669
- if (!this.#isAuthenticated(req)) {
670
- res.writeHead(403, { "Content-Type": "application/json" });
671
- res.end(JSON.stringify({ error: "Forbidden" }));
672
- return;
673
- }
674
- const id = url.searchParams.get("id");
675
- const body = await new Promise((resolve) => {
676
- let data = "";
677
- req.on("data", (chunk) => (data += chunk));
678
- req.on("end", () => resolve(data));
679
- });
680
- const { title, content } = JSON.parse(body);
681
- this.#articles.update(parseInt(id), title, content);
682
- if (this.#databaseModel) {
683
- await this.#databaseModel.update(parseInt(id), { title, content });
684
- }
685
- res.writeHead(200, { "Content-Type": "application/json" });
686
- res.end(JSON.stringify({ status: "updated", id }));
687
802
  }
688
803
  }
689
804
 
@@ -781,11 +896,9 @@ export default class Blog {
781
896
  this.compiledStyles = await compileStyles(fileData);
782
897
 
783
898
  // generate a file
784
- const publicDir = path.join(process.cwd(), "public");
785
-
786
- await fs.promises.mkdir(publicDir, { recursive: true });
899
+ await fs.promises.mkdir(this.#publicDir, { recursive: true });
787
900
  await fs.promises.writeFile(
788
- path.join(publicDir, "styles.min.css"),
901
+ path.join(this.#publicDir, "styles.min.css"),
789
902
  this.compiledStyles + `\n/* source-hash: ${currentHash} */`,
790
903
  );
791
904
  } else {
@@ -828,9 +941,8 @@ export default class Blog {
828
941
 
829
942
  if (!this.#scriptsHash) {
830
943
  try {
831
- const publicDir = path.join(process.cwd(), "public");
832
944
  const existing = await fs.promises.readFile(
833
- path.join(publicDir, "scripts.min.js"),
945
+ path.join(this.#publicDir, "scripts.min.js"),
834
946
  "utf-8",
835
947
  );
836
948
  const match = existing.match(/\/\* source-hash: ([a-f0-9]{64}) \*\//);
@@ -846,10 +958,9 @@ export default class Blog {
846
958
 
847
959
  const compiledScripts = await compileScripts(fileData);
848
960
 
849
- const publicDir = path.join(process.cwd(), "public");
850
- await fs.promises.mkdir(publicDir, { recursive: true });
961
+ await fs.promises.mkdir(this.#publicDir, { recursive: true });
851
962
  await fs.promises.writeFile(
852
- path.join(publicDir, "scripts.min.js"),
963
+ path.join(this.#publicDir, "scripts.min.js"),
853
964
  compiledScripts + `\n/* source-hash: ${currentHash} */`,
854
965
  );
855
966
  }
package/Formatter.js CHANGED
@@ -27,20 +27,18 @@ export function formatHTML(data) {
27
27
  let form1 = "";
28
28
  if (data.loggedin) {
29
29
  form1 = `<form action="/" method="POST">
30
- <div class="box">
31
30
  <h3>Add a New Article</h3>
32
- <input type="text" id="title" name="title" placeholder="Article Title" required style="display: block; width: 300px; margin-bottom: 10px;">
33
- <textarea id="content" name="content" placeholder="Article Content" required style="display: block; width: 300px; height: 100px; margin-bottom: 10px;"></textarea>
31
+ <input type="text" id="title" class="form_element new_title wide" name="title" placeholder="Article Title" required>
32
+ <textarea id="content" class="form_element new_content wide" name="content" placeholder="Article Content" required></textarea>
34
33
  <button type="submit">Add Article</button>${button}
35
34
  </form>
36
- </div>
37
35
  <hr>`;
38
36
  }
39
- const form = form1;
37
+ const form = ""; //form1;
40
38
  return `${header(data.title)}
41
39
  <body>
42
40
  <nav>
43
- <input type="text" id="search" placeholder="Search..." style="float:right; margin: 5px;">
41
+ <input type="text" id="search" placeholder="Search...">
44
42
  ${data.login}
45
43
  </nav>
46
44
  <div id="header">
@@ -49,7 +47,7 @@ export function formatHTML(data) {
49
47
  </div>
50
48
  <div id="wrapper">
51
49
  ${form}
52
- <section id="articles" class="grid">
50
+ <section id="articles" class="articles">
53
51
  ${data.articles.map((article) => article.toHTML(data.loggedin)).join("")}
54
52
  </section>
55
53
  </div>
@@ -34,10 +34,10 @@ export default class DataModel {
34
34
  }
35
35
  }
36
36
 
37
- search(query) {
37
+ search(query, limit) {
38
38
  debug("search");
39
39
  if (this.storage.search) {
40
- return this.storage.search(query);
40
+ return this.storage.search(query, limit);
41
41
  }
42
42
  }
43
43
 
@@ -46,6 +46,10 @@ export default class DataModel {
46
46
  this.storage.remove(article);
47
47
  }
48
48
 
49
+ get(id) {
50
+ return this.storage.get(id);
51
+ }
52
+
49
53
  getAllArticles() {
50
54
  debug("get all articles");
51
55
  const order = "newest";
@@ -122,6 +122,33 @@ export class BinarySearchTree {
122
122
  }
123
123
  }
124
124
 
125
+ *traverse(order = "newest") {
126
+ const stack = [];
127
+ let current = this.root;
128
+
129
+ if (order === "newest") {
130
+ while (current || stack.length > 0) {
131
+ while (current) {
132
+ stack.push(current);
133
+ current = current.right;
134
+ }
135
+ current = stack.pop();
136
+ yield current.article;
137
+ current = current.left;
138
+ }
139
+ } else {
140
+ while (current || stack.length > 0) {
141
+ while (current) {
142
+ stack.push(current);
143
+ current = current.left;
144
+ }
145
+ current = stack.pop();
146
+ yield current.article;
147
+ current = current.right;
148
+ }
149
+ }
150
+ }
151
+
125
152
  getRange(startdate, enddate, limit) {
126
153
  const results = [];
127
154
  // Optimization: Convert to timestamps once to avoid Date object creation/coercion in recursion