@lexho111/plainblog 0.6.8 → 0.6.10

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.
package/Blog.js CHANGED
@@ -17,6 +17,7 @@ import DataModel from "./model/DataModel.js";
17
17
  import { BinarySearchTreeHashMap } from "./model/datastructures/BinarySearchTreeHashMap.js";
18
18
  import createDebug from "./debug-loader.js";
19
19
  import { readFile } from "node:fs/promises";
20
+ import { login, logout, api, new1, getPages } from "./router.js";
20
21
 
21
22
  // Initialize the debugger with a specific namespace
22
23
  const debug = createDebug("plainblog:Blog");
@@ -47,6 +48,7 @@ export default class Blog {
47
48
  this.compiledStyles = "";
48
49
  //this.compiledScripts = "";
49
50
  this.reloadStylesOnGET = false;
51
+ this.angular = false;
50
52
  this.sessions = new Set();
51
53
 
52
54
  this.#version = pkg.version;
@@ -377,27 +379,13 @@ export default class Blog {
377
379
 
378
380
  const server = http.createServer(async (req, res) => {
379
381
  //debug("query %s", req.url);
380
-
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
- // ---------------------------------------------
392
- if (req.url.startsWith("/api")) {
393
- await this.#jsonAPI(req, res);
394
- return;
395
- }
396
-
397
- // Web Page Routes
398
- if (req.url === "/login") {
399
- if (req.method === "GET") {
400
- res.writeHead(200, { "Content-Type": "text/html" });
382
+ await login(
383
+ req,
384
+ res,
385
+ async (req, res) => {
386
+ await this.#handleLogin(req, res);
387
+ },
388
+ async (req, res) => {
401
389
  res.end(`${header("My Blog")}<body>
402
390
  <form class="loginform" id="loginForm">
403
391
  <h1>Blog</h1>
@@ -445,126 +433,141 @@ export default class Blog {
445
433
  });
446
434
  </script>
447
435
  </body></html>`);
448
- return;
449
- /*res.end(`${header("My Blog")}
450
- <body>
451
- <form class="loginform" method="POST">
452
- <h1>Blog</h1>
453
- <h2>Login</h2>
454
- <input type="password" class="form_element password" name="password" placeholder="Password" />
455
- <button class="btn">Login</button></form>
456
- </body></html>`);
457
- return;*/
458
- } else if (req.method === "POST") {
459
- await this.#handleLogin(req, res);
460
- return;
461
- }
462
- }
436
+ },
437
+ );
438
+ if (res.headersSent) return;
463
439
 
464
- if (req.url === "/logout") {
440
+ await logout(req, res, async (req, res) => {
465
441
  this.#handleLogout(req, res);
466
- return;
467
- }
468
- // load articles
469
- // GET artciles
470
- if (req.method === "GET" && req.url === "/") {
471
- // reload styles and scripts on (every) request
472
- if (this.reloadStylesOnGET) {
473
- if (this.#stylesheetPath != null && this.compilestyle) {
474
- await this.#processStylesheets(this.#stylesheetPath);
475
- }
476
- const __filename = fileURLToPath(import.meta.url);
477
- const __dirname = path.dirname(__filename);
478
- const srcScriptPath = path.join(__dirname, "src", "fetchData.js");
479
- await this.#processScripts(srcScriptPath);
480
- }
481
-
482
- let loggedin = false;
483
- if (!this.#isAuthenticated(req)) {
484
- // login
485
- loggedin = false;
486
- } else {
487
- // logout
488
- loggedin = true;
489
- }
442
+ });
443
+ if (res.headersSent) return;
490
444
 
491
- // use built in view engine
492
- try {
493
- const html = await this.toHTML(loggedin); // render this blog to HTML
494
- res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
495
- res.end(html);
496
- } catch (err) {
497
- console.error(err);
498
- res.writeHead(500, { "Content-Type": "text/plain" });
499
- res.end("Internal Server Error");
500
- }
445
+ await api(req, res, async (req, res) => {
446
+ await this.#jsonAPI(req, res);
447
+ });
448
+ if (res.headersSent) return;
449
+
450
+ await new1(req, res, (req, res) => {
451
+ return new Promise((resolve, reject) => {
452
+ if (!this.#isAuthenticated(req)) {
453
+ debug("not authenticated");
454
+ res.writeHead(403, { "Content-Type": "application/json" });
455
+ res.end(JSON.stringify({ error: "Forbidden" }));
456
+ resolve();
457
+ return;
458
+ }
459
+ let body = "";
501
460
 
502
- // use angular frontend
503
- /*const filePath = path.join(this.#publicDir, "index.html");
461
+ // 1. Collect data chunks
462
+ req.on("data", (chunk) => {
463
+ body += chunk.toString();
464
+ });
504
465
 
505
- debug("%s", filePath);
506
- try {
507
- const data = await readFile(filePath);
508
- // Manual MIME type detection (simplified)
509
- const ext = path.extname(filePath);
510
- const mimeTypes = {
511
- ".html": "text/html",
512
- ".css": "text/css",
513
- ".js": "text/javascript",
514
- };
515
-
516
- res.writeHead(200, {
517
- "Content-Type": mimeTypes[ext] || "application/octet-stream",
466
+ req.on("end", async () => {
467
+ try {
468
+ // 2. Parse x-www-form-urlencoded using URLSearchParams
469
+ const params = new URLSearchParams(body);
470
+
471
+ // 3. Convert to a plain object
472
+ const articleData = Object.fromEntries(params.entries());
473
+
474
+ console.log("New Article Data:", articleData);
475
+ // local
476
+ await this.#databaseModel.save(articleData); // --> to api server
477
+ this.postArticle(articleData);
478
+ // external
479
+
480
+ // Success response
481
+ res.writeHead(302, { Location: "/" });
482
+ res.end();
483
+ resolve();
484
+ } catch (err) {
485
+ if (!res.headersSent) {
486
+ res.writeHead(400, { "Content-Type": "application/json" });
487
+ res.end(JSON.stringify({ error: "Failed to parse form data" }));
488
+ }
489
+ resolve();
490
+ }
518
491
  });
519
- res.end(data);
520
- } catch (err) {
521
- if (err) {
522
- res.writeHead(404);
523
- return res.end("Index-File Not Found");
492
+ req.on("error", reject);
493
+ });
494
+
495
+ /*await this.#handleLogin(req, res, async () => {
496
+ await this.#handleLogin(req, res);
497
+ })*/
498
+ });
499
+ if (res.headersSent) return;
500
+
501
+ await getPages(
502
+ req,
503
+ res,
504
+ async (req, res) => {
505
+ // reload styles and scripts on (every) request
506
+ if (this.reloadStylesOnGET) {
507
+ if (this.#stylesheetPath != null && this.compilestyle) {
508
+ await this.#processStylesheets(this.#stylesheetPath);
509
+ }
510
+ const __filename = fileURLToPath(import.meta.url);
511
+ const __dirname = path.dirname(__filename);
512
+ const srcScriptPath = path.join(__dirname, "src", "fetchData.js");
513
+ await this.#processScripts(srcScriptPath);
524
514
  }
525
- }*/
526
515
 
527
- try {
528
- //const html = await this.toHTML(loggedin); // render this blog to HTML
529
- //res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
530
- //res.end(html);
531
- } catch (err) {
532
- console.error(err);
533
- res.writeHead(500, { "Content-Type": "text/plain" });
534
- res.end("Internal Server Error");
535
- }
536
- } else {
537
- // Try to serve static files from public folder
538
- // Normalize path to prevent directory traversal attacks
539
- const safePath = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, "");
540
- const filePath = path.join(
541
- this.#publicDir,
542
- safePath === "/" ? "index.html" : safePath,
543
- );
516
+ let loggedin = false;
517
+ if (!this.#isAuthenticated(req)) {
518
+ // login
519
+ loggedin = false;
520
+ } else {
521
+ // logout
522
+ loggedin = true;
523
+ }
544
524
 
545
- debug("%s", filePath);
546
- try {
547
- const data = await readFile(filePath);
548
- // Manual MIME type detection (simplified)
549
- const ext = path.extname(filePath);
550
- const mimeTypes = {
551
- ".html": "text/html",
552
- ".css": "text/css",
553
- ".js": "text/javascript",
554
- };
555
-
556
- res.writeHead(200, {
557
- "Content-Type": mimeTypes[ext] || "application/octet-stream",
558
- });
559
- res.end(data);
560
- } catch (err) {
561
- if (err) {
562
- res.writeHead(404);
563
- return res.end("File Not Found");
525
+ if (this.angular) {
526
+ // use angular frontend
527
+ const filePath = path.join(this.#publicDir, "index.html");
528
+
529
+ debug("%s", filePath);
530
+ try {
531
+ const data = await readFile(filePath);
532
+ // Manual MIME type detection (simplified)
533
+ const ext = path.extname(filePath);
534
+ const mimeTypes = {
535
+ ".html": "text/html",
536
+ ".css": "text/css",
537
+ ".js": "text/javascript",
538
+ };
539
+
540
+ res.writeHead(200, {
541
+ "Content-Type": mimeTypes[ext] || "application/octet-stream",
542
+ });
543
+ res.end(data);
544
+ } catch (err) {
545
+ if (err) {
546
+ res.writeHead(404);
547
+ return res.end("Index-File Not Found");
548
+ }
549
+ }
550
+ } else {
551
+ // use built in view engine
552
+ try {
553
+ const html = await this.toHTML(loggedin); // render this blog to HTML
554
+ res.writeHead(200, {
555
+ "Content-Type": "text/html; charset=UTF-8",
556
+ });
557
+ res.end(html);
558
+ return;
559
+ } catch (err) {
560
+ console.error(err);
561
+ if (!res.headersSent) {
562
+ res.writeHead(500, { "Content-Type": "text/plain" });
563
+ res.end("Internal Server Error");
564
+ }
565
+ return;
566
+ }
564
567
  }
565
- }
566
- return;
567
- }
568
+ },
569
+ this.#publicDir,
570
+ );
568
571
  });
569
572
 
570
573
  this.#server = server;
@@ -754,7 +757,7 @@ export default class Blog {
754
757
  // local
755
758
  await this.#databaseModel.save(newArticle); // --> to api server
756
759
  this.postArticle(newArticle);
757
- // extern
760
+ // external
758
761
  res.writeHead(201, { "Content-Type": "application/json" });
759
762
  res.end(JSON.stringify(newArticle));
760
763
  } else if (req.method === "DELETE") {
@@ -833,8 +836,13 @@ export default class Blog {
833
836
  login: "",
834
837
  };
835
838
 
836
- if (loggedin) data.login = `<a href="/logout">logout</a>`;
837
- else data.login = `<a href="/login">login</a>`;
839
+ if (loggedin)
840
+ data.login = `<form action="/logout" method="POST" style="display:inline;">
841
+ <button type="submit" class="btn">
842
+ logout
843
+ </button>
844
+ </form>`;
845
+ else data.login = `<a class="btn login" href="/login">login</a>`;
838
846
 
839
847
  //debug("typeof data: %o", typeof data);
840
848
  //debug("typeof data.articles: %o", typeof data.articles);
package/Formatter.js CHANGED
@@ -26,26 +26,26 @@ export function formatHTML(data) {
26
26
  const button = "";
27
27
  let form1 = "";
28
28
  if (data.loggedin) {
29
- form1 = `<form id="createNew" action="/" method="POST">
30
- <h3>Add a New Article</h3>
29
+ form1 = `<form id="createNew" action="/new" method="POST">
30
+ <h2>Add a New Article</h2>
31
31
  <input type="text" id="title" class="form_element new_title wide" name="title" placeholder="Article Title" required>
32
32
  <textarea id="content" class="form_element new_content wide" name="content" placeholder="Article Content" required></textarea>
33
- <button type="submit">Add Article</button>${button}
33
+ <button type="submit" class="btn">Add Article</button>${button}
34
34
  </form>
35
35
  <hr>`;
36
36
  }
37
37
  const form = form1;
38
38
  return `${header(data.title)}
39
39
  <body>
40
+ <div id="wrapper">
40
41
  <nav>
42
+ ${data.login}
41
43
  <input type="text" id="search" placeholder="Search...">
42
- ${data.login}
43
44
  </nav>
44
45
  <div id="header">
45
46
  <h1>${data.title}</h1>
46
47
  <img src="headerphoto.jpg" onerror="this.classList.add('hide-image')" />
47
48
  </div>
48
- <div id="wrapper">
49
49
  ${form}
50
50
  <section id="articles" class="articles">
51
51
  ${data.articles.map((article) => article.toHTML(data.loggedin)).join("")}
package/README.md CHANGED
@@ -30,6 +30,15 @@ Now you can open your blog in your webbrowser on `http://localhost:8080`. Login
30
30
 
31
31
  To add a headerphoto to your blog simply name it "headerphoto.jpg" and put it in the _public_ folder.
32
32
 
33
+ ### change view engine
34
+
35
+ You can put your own angular theme in the public folder.
36
+
37
+ ```
38
+ ...
39
+ blog.angular = true;
40
+ ```
41
+
33
42
  ### set a Database Adapter
34
43
 
35
44
  #### connect to a sqlite database
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexho111/plainblog",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
4
4
  "description": "A tool for creating and serving a minimalist, single-page blog.",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/public/index.html CHANGED
@@ -10,8 +10,8 @@
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
11
  <style>@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBGEe.woff2) format('woff2');unicode-range:U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBGEe.woff2) format('woff2');unicode-range:U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBGEe.woff2) format('woff2');unicode-range:U+1F00-1FFF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBGEe.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBGEe.woff2) format('woff2');unicode-range:U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBGEe.woff2) format('woff2');unicode-range:U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBGEe.woff2) format('woff2');unicode-range:U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:300;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBGEe.woff2) format('woff2');unicode-range:U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBGEe.woff2) format('woff2');unicode-range:U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBGEe.woff2) format('woff2');unicode-range:U+1F00-1FFF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBGEe.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBGEe.woff2) format('woff2');unicode-range:U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBGEe.woff2) format('woff2');unicode-range:U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBGEe.woff2) format('woff2');unicode-range:U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBGEe.woff2) format('woff2');unicode-range:U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBGEe.woff2) format('woff2');unicode-range:U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBGEe.woff2) format('woff2');unicode-range:U+1F00-1FFF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBGEe.woff2) format('woff2');unicode-range:U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBGEe.woff2) format('woff2');unicode-range:U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBGEe.woff2) format('woff2');unicode-range:U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBGEe.woff2) format('woff2');unicode-range:U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;}@font-face{font-family:'Roboto';font-style:normal;font-weight:500;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;}</style>
12
12
  <style>@font-face{font-family:'Material Icons';font-style:normal;font-weight:400;src:url(https://fonts.gstatic.com/s/materialicons/v145/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2) format('woff2');}.material-icons{font-family:'Material Icons';font-weight:normal;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased;}</style>
13
- <style>:root{--blue:hsl(224, 100%, 41%);--yellow:hsl(53, 100%, 50%);--orange:hsl(44, 100%, 50%);--poisongreen:hsl(88, 100%, 50%);--green:hsl(85, 100%, 40%);--pink:hsl(303, 100%, 40%);--basecolor:var(--orange);--primary:var(--basecolor);--primary-inverted:rgb(from var(--primary) calc(1 - r) calc(1 - g) calc(1 - b) );--header_bg:var(--basecolor);--header_darker:hsl(from var(--primary) h s l / .4);--header_darker_high-contrast:hsl(from var(--primary) h s l);--gray:rgba(255, 255, 255, .3);--darkgray:rgba(54, 54, 54, .5);--white:#fefefe;--white_darker:#eeeeee;--red:hsla(0, 100%, 38%, .7);--black:black;--contrast_text:rgb( from var(--header_darker) clamp(0, (r * .299 + g * .587 + b * .114 - 128) * -1000, 255) clamp(0, (r * .299 + g * .587 + b * .114 - 128) * -1000, 255) clamp(0, (r * .299 + g * .587 + b * .114 - 128) * -1000, 255) );--text-primary:var(--black);--text-secondary:var(--white)}html,body{margin:0}body{color:var(--text-primary);background:var(--white_darker);font-family:Arial,sans-serif}@media print{body{font-size:12pt;color:#000;background:#fff}}</style><link rel="stylesheet" href="styles-I55BTQOK.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-I55BTQOK.css"></noscript></head>
13
+ <style>@font-face{font-family:Freckle Face;font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/freckleface/v16/AMOWz4SXrmKHCvXTohxY-YIEWli389k.woff2) format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Freckle Face;font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/freckleface/v16/AMOWz4SXrmKHCvXTohxY-YIEVFi3.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Fredoka;font-style:normal;font-weight:300 700;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/fredoka/v17/X7n64b87HvSqjb_WIi2yDCRwoQ_k7367_DWs89XyHw.woff2) format("woff2");unicode-range:U+0307-0308,U+0590-05FF,U+200C-2010,U+20AA,U+25CC,U+FB1D-FB4F}@font-face{font-family:Fredoka;font-style:normal;font-weight:300 700;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/fredoka/v17/X7n64b87HvSqjb_WIi2yDCRwoQ_k7367_DWg89XyHw.woff2) format("woff2");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Fredoka;font-style:normal;font-weight:300 700;font-stretch:100%;font-display:swap;src:url(https://fonts.gstatic.com/s/fredoka/v17/X7n64b87HvSqjb_WIi2yDCRwoQ_k7367_DWu89U.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--blue:hsl(224, 100%, 41%);--yellow:hsl(53, 100%, 50%);--orange:hsl(44, 100%, 50%);--poisongreen:hsl(88, 100%, 50%);--green:hsl(85, 100%, 40%);--pink:hsl(303, 100%, 40%);--basecolor:var(--green);--primary:var(--basecolor);--primary-inverted:rgb(from var(--primary) calc(1 - r) calc(1 - g) calc(1 - b) );--header_bg:var(--basecolor);--header_darker:hsl(from var(--primary) h s l / .4);--header_darker_high-contrast:hsl(from var(--primary) h s l);--gray:rgba(255, 255, 255, .3);--darkgray:rgba(54, 54, 54, .5);--white:#fefefe;--white_darker:#eeeeee;--red:hsla(0, 100%, 38%, .7);--black:black;--contrast_text:rgb( from var(--header_darker) clamp(0, (r * .299 + g * .587 + b * .114 - 128) * -1000, 255) clamp(0, (r * .299 + g * .587 + b * .114 - 128) * -1000, 255) clamp(0, (r * .299 + g * .587 + b * .114 - 128) * -1000, 255) );--text-primary:var(--black);--text-secondary:var(--white);--font:"Freckle Face", system-ui;--buttonfont:Arial, sans-serif;--readable-font:"Fredoka", sans-serif}html,body{margin:0}body{color:var(--text-primary);background:var(--white_darker);font-family:var(--font)}@media print{body{font-size:12pt;color:#000;background:#fff}}</style><link rel="stylesheet" href="styles-CRQIYMR5.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-CRQIYMR5.css"></noscript></head>
14
14
  <body>
15
15
  <app-root></app-root>
16
- <script src="main-LAI5FAZI.js" type="module"></script></body>
16
+ <script src="main-V4OTOWYB.js" type="module"></script></body>
17
17
  </html>