@lexho111/plainblog 0.5.27 → 0.6.0

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 (65) hide show
  1. package/Article.js +73 -4
  2. package/Blog.js +348 -149
  3. package/Formatter.js +5 -12
  4. package/README.md +5 -1
  5. package/{blog → blog_test_empty.db} +0 -0
  6. package/blog_test_load.db +0 -0
  7. package/build-scripts.js +54 -0
  8. package/coverage/clover.xml +1043 -0
  9. package/coverage/coverage-final.json +20 -0
  10. package/coverage/lcov-report/base.css +224 -0
  11. package/coverage/lcov-report/block-navigation.js +87 -0
  12. package/coverage/lcov-report/favicon.png +0 -0
  13. package/coverage/lcov-report/index.html +161 -0
  14. package/coverage/lcov-report/package/Article.js.html +406 -0
  15. package/coverage/lcov-report/package/Blog.js.html +2635 -0
  16. package/coverage/lcov-report/package/Formatter.js.html +379 -0
  17. package/coverage/lcov-report/package/build-scripts.js.html +247 -0
  18. package/coverage/lcov-report/package/build-styles.js.html +367 -0
  19. package/coverage/lcov-report/package/index.html +191 -0
  20. package/coverage/lcov-report/package/model/APIModel.js.html +190 -0
  21. package/coverage/lcov-report/package/model/ArrayList.js.html +382 -0
  22. package/coverage/lcov-report/package/model/ArrayListHashMap.js.html +379 -0
  23. package/coverage/lcov-report/package/model/BinarySearchTree.js.html +856 -0
  24. package/coverage/lcov-report/package/model/BinarySearchTreeHashMap.js.html +346 -0
  25. package/coverage/lcov-report/package/model/DataModel.js.html +307 -0
  26. package/coverage/lcov-report/package/model/DatabaseModel.js.html +232 -0
  27. package/coverage/lcov-report/package/model/FileAdapter.js.html +394 -0
  28. package/coverage/lcov-report/package/model/FileList.js.html +244 -0
  29. package/coverage/lcov-report/package/model/FileModel.js.html +358 -0
  30. package/coverage/lcov-report/package/model/SequelizeAdapter.js.html +538 -0
  31. package/coverage/lcov-report/package/model/SqliteAdapter.js.html +247 -0
  32. package/coverage/lcov-report/package/model/datastructures/ArrayList.js.html +439 -0
  33. package/coverage/lcov-report/package/model/datastructures/ArrayListHashMap.js.html +196 -0
  34. package/coverage/lcov-report/package/model/datastructures/BinarySearchTree.js.html +913 -0
  35. package/coverage/lcov-report/package/model/datastructures/BinarySearchTreeHashMap.js.html +346 -0
  36. package/coverage/lcov-report/package/model/datastructures/FileList.js.html +244 -0
  37. package/coverage/lcov-report/package/model/datastructures/index.html +176 -0
  38. package/coverage/lcov-report/package/model/index.html +206 -0
  39. package/coverage/lcov-report/package/utilities.js.html +511 -0
  40. package/coverage/lcov-report/prettify.css +1 -0
  41. package/coverage/lcov-report/prettify.js +2 -0
  42. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  43. package/coverage/lcov-report/sorter.js +210 -0
  44. package/coverage/lcov.info +2063 -0
  45. package/index.js +25 -1
  46. package/model/DataModel.js +79 -0
  47. package/model/DatabaseModel.js +20 -8
  48. package/model/FileAdapter.js +43 -4
  49. package/model/FileModel.js +47 -9
  50. package/model/SequelizeAdapter.js +11 -3
  51. package/model/datastructures/ArrayList.js +118 -0
  52. package/model/datastructures/ArrayListHashMap.js +37 -0
  53. package/model/datastructures/ArrayListHashMap.js.bk +90 -0
  54. package/model/datastructures/BinarySearchTree.js +276 -0
  55. package/model/datastructures/BinarySearchTreeHashMap.js +89 -0
  56. package/model/datastructures/BinarySearchTreeTest.js +16 -0
  57. package/model/datastructures/FileList.js +53 -0
  58. package/package.json +11 -2
  59. package/postinstall.js +89 -0
  60. package/public/fetchData.js +0 -0
  61. package/public/scripts.min.js +2 -1
  62. package/public/styles.min.css +2 -2
  63. package/src/fetchData.js +150 -30
  64. package/src/styles.css +47 -0
  65. package/utilities.js +142 -0
@@ -0,0 +1,276 @@
1
+ import createDebug from "debug";
2
+ import Article from "../../Article.js";
3
+
4
+ const debug = createDebug("plainblog:BinarySearchTree");
5
+
6
+ class BlogNode {
7
+ constructor(article) {
8
+ this.article = article;
9
+ this.left = null;
10
+ this.right = null;
11
+ }
12
+ }
13
+
14
+ export class BinarySearchTree {
15
+ constructor() {
16
+ this.root = null;
17
+ }
18
+
19
+ // O(n) - Linear search (Base class has no HashMap)
20
+ contains(id) {
21
+ return this.find(id) !== null;
22
+ }
23
+
24
+ // Insert a new article
25
+ insert(article) {
26
+ // Note: The tree is now sorted by date. ID-based methods will not work correctly.
27
+ // if (!article.id) article.id = this.getNextID();
28
+ // Add to the Set
29
+ const newNode = new BlogNode(article);
30
+ if (!this.root) {
31
+ this.root = newNode;
32
+ return;
33
+ }
34
+ let current = this.root;
35
+ const newTime = newNode.article.createdAt;
36
+ while (true) {
37
+ // Using getTime() for numeric comparison. Duplicates go to the right.
38
+ if (newTime < current.article.createdAt) {
39
+ if (!current.left) {
40
+ current.left = newNode;
41
+ break;
42
+ }
43
+ current = current.left;
44
+ } else {
45
+ if (!current.right) {
46
+ current.right = newNode;
47
+ break;
48
+ }
49
+ current = current.right;
50
+ }
51
+ }
52
+ }
53
+
54
+ update(id, title, content) {
55
+ const article = this.find(id);
56
+ article.title = title;
57
+ article.content = content;
58
+ }
59
+
60
+ search(query) {
61
+ debug("search %s ", query);
62
+ const lower = query.toLowerCase();
63
+ return this.getAllArticles().filter(
64
+ (a) =>
65
+ a.title.toLowerCase().includes(lower) ||
66
+ a.content.toLowerCase().includes(lower),
67
+ );
68
+ }
69
+
70
+ _findMinNode(node) {
71
+ while (node.left) node = node.left;
72
+ return node;
73
+ }
74
+
75
+ // Search for a specific article by ID
76
+ find(id) {
77
+ // Tree is sorted by Date, so we must traverse all nodes to find by ID (O(n))
78
+ return this._findNodeDFS(this.root, id);
79
+ }
80
+
81
+ _findNodeDFS(node, id) {
82
+ if (!node) return null;
83
+ if (id === node.article.id) return node.article;
84
+ const leftResult = this._findNodeDFS(node.left, id);
85
+ if (leftResult) return leftResult;
86
+ return this._findNodeDFS(node.right, id);
87
+ }
88
+
89
+ // Get all articles in order (Sorted by ID)
90
+ getAllArticles(order = "newest") {
91
+ const articles = [];
92
+ if (this.root) {
93
+ this._inOrder(this.root, articles, order);
94
+ }
95
+ return articles;
96
+ }
97
+
98
+ _inOrder(node, articles, order) {
99
+ const stack = [];
100
+ let current = node;
101
+
102
+ if (order === "newest") {
103
+ while (current || stack.length > 0) {
104
+ while (current) {
105
+ stack.push(current);
106
+ current = current.right;
107
+ }
108
+ current = stack.pop();
109
+ articles.push(current.article);
110
+ current = current.left;
111
+ }
112
+ } else {
113
+ while (current || stack.length > 0) {
114
+ while (current) {
115
+ stack.push(current);
116
+ current = current.left;
117
+ }
118
+ current = stack.pop();
119
+ articles.push(current.article);
120
+ current = current.right;
121
+ }
122
+ }
123
+ }
124
+
125
+ getRange(startdate, enddate, limit) {
126
+ const results = [];
127
+ // Optimization: Convert to timestamps once to avoid Date object creation/coercion in recursion
128
+ const start = startdate
129
+ ? new Date(
130
+ startdate instanceof Date
131
+ ? startdate
132
+ : isNaN(startdate)
133
+ ? startdate
134
+ : parseInt(startdate),
135
+ ).getTime()
136
+ : 0;
137
+ const end = enddate
138
+ ? new Date(
139
+ enddate instanceof Date
140
+ ? enddate
141
+ : isNaN(enddate)
142
+ ? enddate
143
+ : parseInt(enddate),
144
+ ).getTime()
145
+ : Infinity;
146
+ this._rangeSearch(this.root, start, end, limit, results);
147
+ return results;
148
+ }
149
+
150
+ _rangeSearch(node, start, end, limit, results) {
151
+ // Stop if node is null or we have reached the limit
152
+ if (!node || (limit !== null && results.length >= limit)) return;
153
+
154
+ // This is an efficient reverse in-order traversal for a date-sorted tree.
155
+ // It prunes branches that are outside the specified date range.
156
+
157
+ // 1. Traverse right (newer posts) if there's a possibility of finding matches.
158
+ if (node.article.createdAt <= end) {
159
+ this._rangeSearch(node.right, start, end, limit, results);
160
+ }
161
+
162
+ if (limit !== null && results.length >= limit) return;
163
+
164
+ // 2. Check if the current node is within the range.
165
+ if (node.article.createdAt >= start && node.article.createdAt <= end) {
166
+ results.push(node.article);
167
+ }
168
+
169
+ if (limit !== null && results.length >= limit) return;
170
+
171
+ // 3. Traverse left (older posts) if there's a possibility of finding matches.
172
+ if (node.article.createdAt > start) {
173
+ this._rangeSearch(node.left, start, end, limit, results);
174
+ }
175
+ }
176
+
177
+ // Private recursive helper
178
+ _countNodes(node) {
179
+ // 1. Base Case: If the branch is empty, return 0
180
+ if (!node) {
181
+ return 0;
182
+ }
183
+
184
+ // 2. Recursive Step:
185
+ // Count the current node (1)
186
+ // PLUS whatever the left side has
187
+ // PLUS whatever the right side has
188
+ return 1 + this._countNodes(node.left) + this._countNodes(node.right);
189
+ }
190
+
191
+ remove(data) {
192
+ const id = data instanceof Article ? data.id : data;
193
+ const date = data instanceof Article ? data.createdAt : null;
194
+ this.root = this._removeIterative(this.root, id, date);
195
+ }
196
+
197
+ _removeIterative(root, id, date) {
198
+ let parent = null;
199
+ let current = root;
200
+ let isLeftChild = false;
201
+
202
+ // 1. Find the node
203
+ if (date !== null) {
204
+ // Optimized search using BST property (Date)
205
+ while (current) {
206
+ if (current.article.id == id) break;
207
+ parent = current;
208
+ if (date < current.article.createdAt) {
209
+ current = current.left;
210
+ isLeftChild = true;
211
+ } else {
212
+ current = current.right;
213
+ isLeftChild = false;
214
+ }
215
+ }
216
+ } else {
217
+ // Full scan required (Iterative DFS) if we don't have the date
218
+ const stack = [{ node: root, parent: null, isLeft: false }];
219
+ current = null;
220
+ while (stack.length > 0) {
221
+ const item = stack.pop();
222
+ if (item.node.article.id == id) {
223
+ current = item.node;
224
+ parent = item.parent;
225
+ isLeftChild = item.isLeft;
226
+ break;
227
+ }
228
+ if (item.node.right)
229
+ stack.push({
230
+ node: item.node.right,
231
+ parent: item.node,
232
+ isLeft: false,
233
+ });
234
+ if (item.node.left)
235
+ stack.push({ node: item.node.left, parent: item.node, isLeft: true });
236
+ }
237
+ }
238
+
239
+ if (!current) return root; // Not found
240
+
241
+ // 2. Delete node
242
+ if (!current.left && !current.right) {
243
+ if (!parent) return null; // Root was deleted
244
+ if (isLeftChild) parent.left = null;
245
+ else parent.right = null;
246
+ } else if (!current.right) {
247
+ if (!parent) return current.left;
248
+ if (isLeftChild) parent.left = current.left;
249
+ else parent.right = current.left;
250
+ } else if (!current.left) {
251
+ if (!parent) return current.right;
252
+ if (isLeftChild) parent.left = current.right;
253
+ else parent.right = current.right;
254
+ } else {
255
+ // Two children: Find successor (min in right subtree)
256
+ let successorParent = current;
257
+ let successor = current.right;
258
+ while (successor.left) {
259
+ successorParent = successor;
260
+ successor = successor.left;
261
+ }
262
+ current.article = successor.article;
263
+ if (successorParent === current) successorParent.right = successor.right;
264
+ else successorParent.left = successor.right;
265
+ }
266
+ return root;
267
+ }
268
+
269
+ clear() {
270
+ this.root = null;
271
+ }
272
+
273
+ size() {
274
+ return this._countNodes(this.root);
275
+ }
276
+ }
@@ -0,0 +1,89 @@
1
+ import createDebug from "debug";
2
+ import { BinarySearchTree } from "./BinarySearchTree.js";
3
+
4
+ // Initialize the debugger with a specific namespace
5
+ const debug = createDebug("plainblog:BinarySearchTreeHashMap");
6
+
7
+ export class BinarySearchTreeHashMap extends BinarySearchTree {
8
+ constructor() {
9
+ super();
10
+ this.ids = new Map(); // Fast lookup storage (ID -> Article)
11
+ }
12
+
13
+ // O(1) - Instant lookup
14
+ contains(id) {
15
+ return this.ids.has(id);
16
+ }
17
+
18
+ // Insert a new article
19
+ insert(article) {
20
+ // Note: The tree is now sorted by date. ID-based methods will not work correctly.
21
+ // if (!article.id) article.id = this.getNextID();
22
+ // Add to the Map
23
+ this.ids.set(article.id, article);
24
+ super.insert(article);
25
+ }
26
+
27
+ update(id, title, content) {
28
+ // Check for both string and number ID
29
+ let article = this.ids.get(id);
30
+ if (!article) article = this.ids.get(parseInt(id));
31
+
32
+ if (article) {
33
+ article.title = title;
34
+ article.content = content;
35
+ return true;
36
+ }
37
+ return false;
38
+ }
39
+
40
+ search(query) {
41
+ debug("search %s ", query);
42
+ const lower = query.toLowerCase();
43
+ const results = this.getAllArticles().filter(
44
+ (a) =>
45
+ a.title.toLowerCase().includes(lower) ||
46
+ a.content.toLowerCase().includes(lower),
47
+ );
48
+ debug("%d results found", results.length);
49
+ return results;
50
+ }
51
+
52
+ // Search for a specific article by ID
53
+ find(id) {
54
+ return this.ids.get(id);
55
+ }
56
+
57
+ // Get all articles in order (Sorted by ID)
58
+ getAllArticles(order = "newest") {
59
+ return super.getAllArticles(order);
60
+ }
61
+
62
+ getRange(startdate, enddate, limit) {
63
+ return super.getRange(startdate, enddate, limit);
64
+ }
65
+
66
+ remove(data) {
67
+ const id = data && data.id ? data.id : data;
68
+
69
+ let article = this.ids.get(id);
70
+ if (!article) article = this.ids.get(parseInt(id));
71
+
72
+ if (article) {
73
+ this.ids.delete(article.id);
74
+ return super.remove(article); // Pass article to use optimized date-based removal
75
+ }
76
+
77
+ this.ids.delete(id);
78
+ return super.remove(data);
79
+ }
80
+
81
+ clear() {
82
+ super.clear();
83
+ this.ids.clear();
84
+ }
85
+
86
+ size() {
87
+ return super.size();
88
+ }
89
+ }
@@ -0,0 +1,16 @@
1
+ import Article from "../../Article.js";
2
+ import { BinarySearchTree } from "./BinarySearchTree.js";
3
+
4
+ const bst = new BinarySearchTree();
5
+
6
+ // Adding articles
7
+ //myBlog.insert(new Article(101, "Hello World", "Welcome to my JS blog."));
8
+ //myBlog.insert(new Article(50, "Intro to Trees", "Binary trees are useful..."));
9
+ //myBlog.insert(new Article(150, "JavaScript in 2026", "The state of JS today."));
10
+
11
+ // Find a specific article
12
+ const post = bst.find(101);
13
+ console.log("Found:", post.title);
14
+
15
+ // List all articles (Sorted by ID)
16
+ console.log("All Articles:", bst.getAllArticles());
@@ -0,0 +1,53 @@
1
+ import fs from "node:fs";
2
+ import {
3
+ saveInfo,
4
+ loadInfo,
5
+ appendArticleSync, // Change to synchronous version
6
+ loadArticlesSync, // Change to synchronous version
7
+ initFiles,
8
+ } from "../FileModel.js";
9
+
10
+ export default class FileList {
11
+ constructor(filepath) {
12
+ this.filepath = filepath;
13
+ }
14
+
15
+ clear() {
16
+ // Optionally: fs.writeFileSync(this.filepath, "");
17
+ }
18
+
19
+ // Removed 'async'
20
+ insert(newArticle) {
21
+ /*if (!newArticle.createdAt) {
22
+ newArticle.createdAt = new Date().toISOString();
23
+ }
24
+ if (!newArticle.id) {
25
+ newArticle.id = Date.now();
26
+ }
27
+ // Must call a synchronous function here
28
+ appendArticleSync(this.filepath, newArticle);*/
29
+ }
30
+
31
+ remove(article) {}
32
+
33
+ contains(id) {}
34
+
35
+ // Removed 'async'
36
+ getAllArticles() {
37
+ return loadArticlesSync(this.filepath);
38
+ }
39
+
40
+ size() {
41
+ try {
42
+ const content = fs.readFileSync(this.filepath, "utf8");
43
+ // filter(Boolean) prevents empty last lines from being counted as articles
44
+ return content.split("\n").filter((line) => line.trim()).length;
45
+ } catch (err) {
46
+ return 0;
47
+ }
48
+ }
49
+
50
+ length() {
51
+ return this.size();
52
+ }
53
+ }
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@lexho111/plainblog",
3
- "version": "0.5.27",
3
+ "version": "0.6.0",
4
4
  "description": "A tool for creating and serving a minimalist, single-page blog.",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "dev": "node index.js",
9
+ "postinstall": "node postinstall.js",
9
10
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
11
+ "coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage & start coverage/lcov-report/index.html",
12
+ "view-coverage": "start coverage/lcov-report/index.html",
10
13
  "lint": "eslint ."
11
14
  },
12
15
  "keywords": [
@@ -17,6 +20,8 @@
17
20
  "author": "lexho111",
18
21
  "license": "ISC",
19
22
  "devDependencies": {
23
+ "@babel/core": "^7.28.6",
24
+ "@babel/preset-env": "^7.28.6",
20
25
  "@eslint/js": "^9.39.2",
21
26
  "@types/node": "^25.0.3",
22
27
  "autoprefixer": "^10.4.23",
@@ -29,8 +34,12 @@
29
34
  "pg-hstore": "^2.3.4",
30
35
  "postcss": "^8.5.6",
31
36
  "sequelize": "^6.37.7",
32
- "sqlite3": "^5.1.7",
37
+ "sqlite3": "^5.0.2",
33
38
  "typescript": "^5.9.3",
39
+ "uglify-js": "^3.19.3",
34
40
  "w3c-css-validator": "^1.4.1"
41
+ },
42
+ "dependencies": {
43
+ "debug": "^4.4.3"
35
44
  }
36
45
  }
package/postinstall.js ADDED
@@ -0,0 +1,89 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import process from "node:process";
5
+
6
+ // This script is executed after the package is installed.
7
+ // Its purpose is to copy default static assets from the package's `public`
8
+ // directory to the user's project's `public` directory.
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ async function main() {
14
+ try {
15
+ // The source is the 'public' directory inside our package.
16
+ // We use __dirname to be sure we are looking inside the package itself.
17
+ const sourceDir = path.join(__dirname, "public");
18
+
19
+ // The destination is the 'public' folder in the user's project root.
20
+ // process.env.INIT_CWD is the directory where `npm install` was run.
21
+ // If not available, we fallback to traversing up from node_modules.
22
+ const projectRoot =
23
+ process.env.INIT_CWD || path.resolve(__dirname, "../..");
24
+ const destDir = path.join(projectRoot, "public");
25
+
26
+ console.log(
27
+ `[plainblog] Postinstall: Copying assets from ${sourceDir} to ${destDir}`
28
+ );
29
+
30
+ // 1. Check if source directory exists
31
+ try {
32
+ await fs.promises.access(sourceDir);
33
+ } catch {
34
+ console.log(
35
+ `[plainblog] Source directory '${sourceDir}' not found. Skipping.`
36
+ );
37
+ return;
38
+ }
39
+
40
+ // 2. Avoid copying if source and destination are the same (e.g. local dev install)
41
+ if (path.relative(sourceDir, destDir) === "") {
42
+ console.log(
43
+ "[plainblog] Source and destination are the same. Skipping copy."
44
+ );
45
+ return;
46
+ }
47
+
48
+ // Ensure the user's public directory exists, creating it if not.
49
+ await fs.promises.mkdir(destDir, { recursive: true });
50
+
51
+ const filesToCopy = await fs.promises.readdir(sourceDir);
52
+
53
+ for (const file of filesToCopy) {
54
+ const sourceFile = path.join(sourceDir, file);
55
+ const destFile = path.join(destDir, file);
56
+
57
+ try {
58
+ // Check if destination file exists.
59
+ await fs.promises.access(destFile);
60
+ // If it exists, we do nothing to avoid overwriting user files.
61
+ } catch (error) {
62
+ if (error.code === "ENOENT") {
63
+ // File doesn't exist in the destination, so copy it.
64
+ try {
65
+ await fs.promises.copyFile(sourceFile, destFile);
66
+ console.log(
67
+ `[plainblog] Copied default asset '${file}' to project's '/public' directory.`
68
+ );
69
+ } catch (copyError) {
70
+ console.error(
71
+ `[plainblog] Error copying asset '${file}':`,
72
+ copyError
73
+ );
74
+ }
75
+ } else {
76
+ console.error(
77
+ `[plainblog] Error checking asset '${destFile}':`,
78
+ error
79
+ );
80
+ }
81
+ }
82
+ }
83
+ } catch (error) {
84
+ // We log errors but don't fail the entire installation.
85
+ console.error("[plainblog] Postinstall error:", error);
86
+ }
87
+ }
88
+
89
+ main();
File without changes
@@ -1 +1,2 @@
1
- function _createForOfIteratorHelper(e,t){var r,n,o,a,i="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(i)return o=!(n=!0),{s:function(){i=i.call(e)},n:function(){var e=i.next();return n=e.done,e},e:function(e){o=!0,r=e},f:function(){try{n||null==i.return||i.return()}finally{if(o)throw r}}};if(Array.isArray(e)||(i=_unsupportedIterableToArray(e))||t&&e&&"number"==typeof e.length)return i&&(e=i),a=0,{s:t=function(){},n:function(){return a>=e.length?{done:!0}:{done:!1,value:e[a++]}},e:function(e){throw e},f:t};throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _unsupportedIterableToArray(e,t){var r;if(e)return"string"==typeof e?_arrayLikeToArray(e,t):"Map"===(r="Object"===(r={}.toString.call(e).slice(8,-1))&&e.constructor?e.constructor.name:r)||"Set"===r?Array.from(e):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?_arrayLikeToArray(e,t):void 0}function _arrayLikeToArray(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r<t;r++)n[r]=e[r];return n}function _regenerator(){var g,e="function"==typeof Symbol?Symbol:{},t=e.iterator||"@@iterator",r=e.toStringTag||"@@toStringTag";function n(e,t,r,n){var o,a,i,c,l,u,f,s,d,t=t&&t.prototype instanceof h?t:h,t=Object.create(t.prototype);return _regeneratorDefine2(t,"_invoke",(o=e,a=r,f=n||[],s=!1,d={p:u=0,n:0,v:g,a:p,f:p.bind(g,4),d:function(e,t){return i=e,c=0,l=g,d.n=t,y}},function(e,t,r){if(1<u)throw TypeError("Generator is already running");for(s&&1===t&&p(t,r),c=t,l=r;(m=c<2?g:l)||!s;){i||(c?c<3?(1<c&&(d.n=-1),p(c,l)):d.n=l:d.v=l);try{if(u=2,i){if(m=i[e=c?e:"next"]){if(!(m=m.call(i,l)))throw TypeError("iterator result is not an object");if(!m.done)return m;l=m.value,c<2&&(c=0)}else 1===c&&(m=i.return)&&m.call(i),c<2&&(l=TypeError("The iterator does not provide a '"+e+"' method"),c=1);i=g}else if((m=(s=d.n<0)?l:o.call(a,d))!==y)break}catch(e){i=g,c=1,l=e}finally{u=1}}return{value:m,done:s}}),!0),t;function p(e,t){for(c=e,l=t,m=0;!s&&u&&!r&&m<f.length;m++){var r,n=f[m],o=d.p,a=n[2];3<e?(r=a===t)&&(l=n[(c=n[4])?5:c=3],n[4]=n[5]=g):n[0]<=o&&((r=e<2&&o<n[1])?(c=0,d.v=t,d.n=n[1]):o<a&&(r=e<3||n[0]>t||a<t)&&(n[4]=e,n[5]=t,d.n=a,c=0))}if(r||1<e)return y;throw s=!0,t}}var y={};function h(){}function o(){}function a(){}var m=Object.getPrototypeOf,e=[][t]?m(m([][t]())):(_regeneratorDefine2(m={},t,function(){return this}),m),i=a.prototype=h.prototype=Object.create(e);function c(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,a):(e.__proto__=a,_regeneratorDefine2(e,r,"GeneratorFunction")),e.prototype=Object.create(i),e}return _regeneratorDefine2(i,"constructor",o.prototype=a),_regeneratorDefine2(a,"constructor",o),_regeneratorDefine2(a,r,o.displayName="GeneratorFunction"),_regeneratorDefine2(i),_regeneratorDefine2(i,r,"Generator"),_regeneratorDefine2(i,t,function(){return this}),_regeneratorDefine2(i,"toString",function(){return"[object Generator]"}),(_regenerator=function(){return{w:n,m:c}})()}function _regeneratorDefine2(e,t,r,n){var a=Object.defineProperty;try{a({},"",{})}catch(e){a=0}(_regeneratorDefine2=function(e,t,r,n){function o(t,r){_regeneratorDefine2(e,t,function(e){return this._invoke(t,r,e)})}t?a?a(e,t,{value:r,enumerable:!n,configurable:!n,writable:!n}):e[t]=r:(o("next",0),o("throw",1),o("return",2))})(e,t,r,n)}function asyncGeneratorStep(e,t,r,n,o,a,i){try{var c=e[a](i),l=c.value}catch(e){return void r(e)}c.done?t(l):Promise.resolve(l).then(n,o)}function _asyncToGenerator(c){return function(){var e=this,i=arguments;return new Promise(function(t,r){var n=c.apply(e,i);function o(e){asyncGeneratorStep(n,t,r,o,a,"next",e)}function a(e){asyncGeneratorStep(n,t,r,o,a,"throw",e)}o(void 0)})}}var isLoading=!1;function handleScroll(){return _handleScroll.apply(this,arguments)}function _handleScroll(){return(_handleScroll=_asyncToGenerator(_regenerator().m(function e(){var t,r;return _regenerator().w(function(e){for(;;)switch(e.n){case 0:if(isLoading)return e.a(2);e.n=1;break;case 1:if(t=window.scrollY||document.documentElement.scrollTop||document.body.scrollTop,r=window.innerHeight||document.documentElement.clientHeight,(document.documentElement.scrollHeight||document.body.scrollHeight)-t-r<100)return e.n=2,loadMore();e.n=2;break;case 2:return e.a(2)}},e)}))).apply(this,arguments)}function loadMore(){return _loadMore.apply(this,arguments)}function _loadMore(){return(_loadMore=_asyncToGenerator(_regenerator().m(function e(){var t,r,n,o,a,i,c,l,u;return _regenerator().w(function(e){for(;;)switch(e.p=e.n){case 0:if(console.log("load more"),t=document.getElementById("articles"),t=t.querySelectorAll("article"),t=t[t.length-1],console.log("lastArticle",t),t){e.n=1;break}return e.a(2);case 1:if(r=parseInt(t.getAttribute("data-id")),console.log("lastId ".concat(r)),isNaN(r))return e.a(2);e.n=2;break;case 2:if((n=r-1)<1)return console.log("nextId < 1 ".concat(n<1)),window.removeEventListener("scroll",handleScroll),e.a(2);e.n=3;break;case 3:return o=!(isLoading=!0),a="/api/articles?startID=".concat(n,"&limit=5"),console.log("load more ".concat(a)),e.p=4,e.n=5,fetch(a);case 5:if((a=e.v).ok)return e.n=6,a.json();e.n=7;break;case 6:if((u=e.v).articles&&0<u.articles.length){i=_createForOfIteratorHelper(u.articles);try{for(i.s();!(c=i.n()).done;)fillWithContent((l=c.value).title,l.content,l.createdAt,l.id)}catch(e){i.e(e)}finally{i.f()}o=!0}case 7:e.n=9;break;case 8:e.p=8,u=e.v,console.error("Failed to load articles:",u);case 9:isLoading=!1,o&&handleScroll();case 10:return e.a(2)}},e,null,[[4,8]])}))).apply(this,arguments)}function getFormattedDate(e){var t=e.getFullYear(),r=String(e.getMonth()+1).padStart(2,"0"),n=String(e.getDate()).padStart(2,"0"),o=String(e.getHours()).padStart(2,"0"),e=String(e.getMinutes()).padStart(2,"0");return"".concat(t,"/").concat(r,"/").concat(n," ").concat(o,":").concat(e)}function fillWithContent(e,t,r,n){var o=document.createElement("article"),n=(n&&o.setAttribute("data-id",n),document.createElement("h2")),a=document.createElement("span"),i=document.createElement("p");n.textContent=e,a.textContent=getFormattedDate(new Date(r)),i.textContent=t,o.appendChild(n),o.appendChild(a),o.appendChild(i),document.getElementById("articles").appendChild(o)}function test(){console.log("123")}document.addEventListener("DOMContentLoaded",function(){window.addEventListener("scroll",handleScroll,{passive:!0}),handleScroll()});
1
+ let isLoading=!1;async function handleScroll(){isLoading||document.documentElement.scrollHeight-window.innerHeight-window.scrollY<100&&await loadMore()}async function loadMore(){console.log("load more");var e=document.getElementById("articles"),t=Array.from(e.querySelectorAll("article"));let n=NaN;for(let e=t.length-1;0<=e;e--){var a=t[e].getAttribute("data-date");if(a){a=parseInt(a,10);if(!isNaN(a)){n=a;break}}}if(console.log("lastTimestamp "+n),isNaN(n))window.removeEventListener("scroll",handleScroll);else{var e=n,o=(isLoading=!0,`/api/articles?enddate=${e}&limit=51`);console.log("load more "+o),console.log("endDate: "+new Date(e));try{var l=await fetch(o);if(l.ok){var i=(await l.json()).articles||[];if(0<i.length){let e=0;for(var r of i)fillWithContent(r.title,r.content,r.createdAt,r.id)&&e++;e<50&&window.removeEventListener("scroll",handleScroll)}else window.removeEventListener("scroll",handleScroll)}}catch(e){console.error("Failed to load articles:",e)}isLoading=!1}}function getFormattedDate(e){return e.getFullYear()+`/${String(e.getMonth()+1).padStart(2,"0")}/${String(e.getDate()).padStart(2,"0")} ${String(e.getHours()).padStart(2,"0")}:`+String(e.getMinutes()).padStart(2,"0")}function fillWithContent(n,a,e,o){if(o&&document.querySelector(`#articles > article[data-id='${o}']`))return console.log("article found. not adding it."),console.log(o+` ${n} `+e),!1;let l=document.createElement("article");var t,i,r,d,e=new Date(e),c=(null!=o&&l.setAttribute("data-id",o),l.setAttribute("data-date",e.getTime()),document.createElement("h2")),s=document.createElement("span"),m=document.createElement("p");function u(e){var t;"delete"===e?confirm("Delete this article?")&&fetch("/api/articles?id="+o,{method:"DELETE"}).then(e=>{e.ok?l.remove():alert("Error: "+e.statusText)}):"edit"===e&&(e=prompt("New Title",n),t=prompt("New Content",a),e)&&t&&fetch("/api/articles?id="+o,{method:"PUT",body:JSON.stringify({title:e,content:t})}).then(e=>{e.ok?window.location.reload():alert("Error: "+e.statusText)})}return null!==document.querySelector('a[href="/logout"]')&&(t=document.createElement("div"),i=document.createElement("div"),r=document.createElement("div"),d=document.createElement("div"),t.setAttribute("class","buttons"),i.setAttribute("class","button edit"),r.setAttribute("class","button delete"),d.setAttribute("class","button delete"),i.textContent="edit",r.textContent="delete",d.textContent="something",t.appendChild(i),t.appendChild(r),t.appendChild(d),i.addEventListener("click",()=>u("edit")),r.addEventListener("click",()=>u("delete")),l.appendChild(t)),c.textContent=n,s.textContent=getFormattedDate(e),m.textContent=a,l.appendChild(c),l.appendChild(s),l.appendChild(m),document.getElementById("articles").appendChild(l),!0}document.addEventListener("DOMContentLoaded",()=>{window.addEventListener("scroll",handleScroll);var e=document.getElementById("search");e&&e.addEventListener("input",async e=>{var e=e.target.value;0<e.length?(e=await(await fetch("/api/articles?q="+encodeURIComponent(e))).json(),document.getElementById("articles").innerHTML="",e.articles.forEach(e=>fillWithContent(e.title,e.content,e.createdAt,e.id))):window.location.reload()})});
2
+ /* source-hash: 2b1ef8919234bc5888e0b423e7411b07aab439a1feaebec95b729fe308f2fa32 */
@@ -1,2 +1,2 @@
1
- body{font-family:Arial;font-family:Arial,sans-serif}h1{color:#333}.grid{border:0 solid #000;display:grid;gap:.25rem;grid-template-columns:1fr}.grid article{border:0 solid #ccc;border-radius:4px;min-width:0;overflow-wrap:break-word;padding:.25rem}.grid article h2{color:#353535;margin-bottom:5px}.grid article .datetime{color:#757575;margin:0}.grid article p{margin-bottom:0;margin-top:10px}article a,article a:visited,h1{color:#696969}nav a{color:#3b40c1;font-size:20px;text-decoration:underline}nav a:visited{color:#3b40c1;text-decoration-color:#3b40c1}#wrapper{max-width:500px;width:100%}@media screen and (max-width:1000px){*{font-size:4vw}#wrapper{box-sizing:border-box;max-width:100%;padding:0 10px;width:100%}}
2
- /* source-hash: a07f631befba4b6bc703f8709f5ef455faafeff4e5f00b62f835576eea7fb529 */
1
+ body{font-family:Arial,sans-serif}h1{color:#333}body{font-family:Arial;margin:0}nav{margin-top:10px}.box,h1,nav{margin-left:10px}.grid{border:0 solid #000;display:grid;gap:.25rem;grid-template-columns:1fr}.grid article{border:0 solid #ccc;border-radius:4px;min-width:0;overflow-wrap:break-word;padding:.25rem}.grid article h2{color:#353535;margin-bottom:5px}.grid article .datetime{color:#757575;margin:0}.grid article p{margin-bottom:0;margin-top:10px}article a,article a:visited,h1{color:#696969}nav a{color:#3b40c1;font-size:20px;text-decoration:underline}nav a:visited{color:#3b40c1;text-decoration-color:#3b40c1}#wrapper{max-width:500px;width:100%}.hide-image{display:none}@media screen and (max-width:1000px){*{font-size:4vw}#wrapper{box-sizing:border-box;max-width:100%;padding:0 10px;width:100%}}h2{margin-top:0}.buttons,h2{border:0 solid #000}.buttons{height:25px;margin-bottom:0;width:100%}.button{border:1px solid #000;cursor:pointer;float:left;height:19px;padding:2px;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100px}.button:focus{outline:2px solid orange}.edit{background-color:blue}.delete{background-color:red}
2
+ /* source-hash: 22048a393d87cbbefd7a61fb300fff916360a30934da974ee2fd45739e446492 */