@lexho111/plainblog 0.5.28 → 0.6.1

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 (64) hide show
  1. package/Article.js +73 -4
  2. package/Blog.js +342 -140
  3. package/Formatter.js +2 -11
  4. package/README.md +1 -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 +1051 -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 +2674 -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 +322 -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 +352 -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 +2078 -0
  45. package/index.js +25 -1
  46. package/model/DataModel.js +80 -0
  47. package/model/DatabaseModel.js +21 -8
  48. package/model/FileAdapter.js +44 -4
  49. package/model/FileModel.js +48 -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 +277 -0
  55. package/model/datastructures/BinarySearchTreeHashMap.js +90 -0
  56. package/model/datastructures/BinarySearchTreeTest.js +16 -0
  57. package/model/datastructures/FileList.js +53 -0
  58. package/package.json +9 -3
  59. package/public/fetchData.js +0 -0
  60. package/public/scripts.min.js +2 -1
  61. package/public/styles.min.css +2 -1
  62. package/src/fetchData.js +150 -30
  63. package/src/styles.css +29 -0
  64. package/utilities.js +142 -0
@@ -0,0 +1,277 @@
1
+ //import createDebug from "debug";
2
+ import { debuglog as createDebug } from "node:util";
3
+ import Article from "../../Article.js";
4
+
5
+ const debug = createDebug("plainblog:BinarySearchTree");
6
+
7
+ class BlogNode {
8
+ constructor(article) {
9
+ this.article = article;
10
+ this.left = null;
11
+ this.right = null;
12
+ }
13
+ }
14
+
15
+ export class BinarySearchTree {
16
+ constructor() {
17
+ this.root = null;
18
+ }
19
+
20
+ // O(n) - Linear search (Base class has no HashMap)
21
+ contains(id) {
22
+ return this.find(id) !== null;
23
+ }
24
+
25
+ // Insert a new article
26
+ insert(article) {
27
+ // Note: The tree is now sorted by date. ID-based methods will not work correctly.
28
+ // if (!article.id) article.id = this.getNextID();
29
+ // Add to the Set
30
+ const newNode = new BlogNode(article);
31
+ if (!this.root) {
32
+ this.root = newNode;
33
+ return;
34
+ }
35
+ let current = this.root;
36
+ const newTime = newNode.article.createdAt;
37
+ while (true) {
38
+ // Using getTime() for numeric comparison. Duplicates go to the right.
39
+ if (newTime < current.article.createdAt) {
40
+ if (!current.left) {
41
+ current.left = newNode;
42
+ break;
43
+ }
44
+ current = current.left;
45
+ } else {
46
+ if (!current.right) {
47
+ current.right = newNode;
48
+ break;
49
+ }
50
+ current = current.right;
51
+ }
52
+ }
53
+ }
54
+
55
+ update(id, title, content) {
56
+ const article = this.find(id);
57
+ article.title = title;
58
+ article.content = content;
59
+ }
60
+
61
+ search(query) {
62
+ debug("search %s ", query);
63
+ const lower = query.toLowerCase();
64
+ return this.getAllArticles().filter(
65
+ (a) =>
66
+ a.title.toLowerCase().includes(lower) ||
67
+ a.content.toLowerCase().includes(lower),
68
+ );
69
+ }
70
+
71
+ _findMinNode(node) {
72
+ while (node.left) node = node.left;
73
+ return node;
74
+ }
75
+
76
+ // Search for a specific article by ID
77
+ find(id) {
78
+ // Tree is sorted by Date, so we must traverse all nodes to find by ID (O(n))
79
+ return this._findNodeDFS(this.root, id);
80
+ }
81
+
82
+ _findNodeDFS(node, id) {
83
+ if (!node) return null;
84
+ if (id === node.article.id) return node.article;
85
+ const leftResult = this._findNodeDFS(node.left, id);
86
+ if (leftResult) return leftResult;
87
+ return this._findNodeDFS(node.right, id);
88
+ }
89
+
90
+ // Get all articles in order (Sorted by ID)
91
+ getAllArticles(order = "newest") {
92
+ const articles = [];
93
+ if (this.root) {
94
+ this._inOrder(this.root, articles, order);
95
+ }
96
+ return articles;
97
+ }
98
+
99
+ _inOrder(node, articles, order) {
100
+ const stack = [];
101
+ let current = node;
102
+
103
+ if (order === "newest") {
104
+ while (current || stack.length > 0) {
105
+ while (current) {
106
+ stack.push(current);
107
+ current = current.right;
108
+ }
109
+ current = stack.pop();
110
+ articles.push(current.article);
111
+ current = current.left;
112
+ }
113
+ } else {
114
+ while (current || stack.length > 0) {
115
+ while (current) {
116
+ stack.push(current);
117
+ current = current.left;
118
+ }
119
+ current = stack.pop();
120
+ articles.push(current.article);
121
+ current = current.right;
122
+ }
123
+ }
124
+ }
125
+
126
+ getRange(startdate, enddate, limit) {
127
+ const results = [];
128
+ // Optimization: Convert to timestamps once to avoid Date object creation/coercion in recursion
129
+ const start = startdate
130
+ ? new Date(
131
+ startdate instanceof Date
132
+ ? startdate
133
+ : isNaN(startdate)
134
+ ? startdate
135
+ : parseInt(startdate),
136
+ ).getTime()
137
+ : 0;
138
+ const end = enddate
139
+ ? new Date(
140
+ enddate instanceof Date
141
+ ? enddate
142
+ : isNaN(enddate)
143
+ ? enddate
144
+ : parseInt(enddate),
145
+ ).getTime()
146
+ : Infinity;
147
+ this._rangeSearch(this.root, start, end, limit, results);
148
+ return results;
149
+ }
150
+
151
+ _rangeSearch(node, start, end, limit, results) {
152
+ // Stop if node is null or we have reached the limit
153
+ if (!node || (limit !== null && results.length >= limit)) return;
154
+
155
+ // This is an efficient reverse in-order traversal for a date-sorted tree.
156
+ // It prunes branches that are outside the specified date range.
157
+
158
+ // 1. Traverse right (newer posts) if there's a possibility of finding matches.
159
+ if (node.article.createdAt <= end) {
160
+ this._rangeSearch(node.right, start, end, limit, results);
161
+ }
162
+
163
+ if (limit !== null && results.length >= limit) return;
164
+
165
+ // 2. Check if the current node is within the range.
166
+ if (node.article.createdAt >= start && node.article.createdAt <= end) {
167
+ results.push(node.article);
168
+ }
169
+
170
+ if (limit !== null && results.length >= limit) return;
171
+
172
+ // 3. Traverse left (older posts) if there's a possibility of finding matches.
173
+ if (node.article.createdAt > start) {
174
+ this._rangeSearch(node.left, start, end, limit, results);
175
+ }
176
+ }
177
+
178
+ // Private recursive helper
179
+ _countNodes(node) {
180
+ // 1. Base Case: If the branch is empty, return 0
181
+ if (!node) {
182
+ return 0;
183
+ }
184
+
185
+ // 2. Recursive Step:
186
+ // Count the current node (1)
187
+ // PLUS whatever the left side has
188
+ // PLUS whatever the right side has
189
+ return 1 + this._countNodes(node.left) + this._countNodes(node.right);
190
+ }
191
+
192
+ remove(data) {
193
+ const id = data instanceof Article ? data.id : data;
194
+ const date = data instanceof Article ? data.createdAt : null;
195
+ this.root = this._removeIterative(this.root, id, date);
196
+ }
197
+
198
+ _removeIterative(root, id, date) {
199
+ let parent = null;
200
+ let current = root;
201
+ let isLeftChild = false;
202
+
203
+ // 1. Find the node
204
+ if (date !== null) {
205
+ // Optimized search using BST property (Date)
206
+ while (current) {
207
+ if (current.article.id == id) break;
208
+ parent = current;
209
+ if (date < current.article.createdAt) {
210
+ current = current.left;
211
+ isLeftChild = true;
212
+ } else {
213
+ current = current.right;
214
+ isLeftChild = false;
215
+ }
216
+ }
217
+ } else {
218
+ // Full scan required (Iterative DFS) if we don't have the date
219
+ const stack = [{ node: root, parent: null, isLeft: false }];
220
+ current = null;
221
+ while (stack.length > 0) {
222
+ const item = stack.pop();
223
+ if (item.node.article.id == id) {
224
+ current = item.node;
225
+ parent = item.parent;
226
+ isLeftChild = item.isLeft;
227
+ break;
228
+ }
229
+ if (item.node.right)
230
+ stack.push({
231
+ node: item.node.right,
232
+ parent: item.node,
233
+ isLeft: false,
234
+ });
235
+ if (item.node.left)
236
+ stack.push({ node: item.node.left, parent: item.node, isLeft: true });
237
+ }
238
+ }
239
+
240
+ if (!current) return root; // Not found
241
+
242
+ // 2. Delete node
243
+ if (!current.left && !current.right) {
244
+ if (!parent) return null; // Root was deleted
245
+ if (isLeftChild) parent.left = null;
246
+ else parent.right = null;
247
+ } else if (!current.right) {
248
+ if (!parent) return current.left;
249
+ if (isLeftChild) parent.left = current.left;
250
+ else parent.right = current.left;
251
+ } else if (!current.left) {
252
+ if (!parent) return current.right;
253
+ if (isLeftChild) parent.left = current.right;
254
+ else parent.right = current.right;
255
+ } else {
256
+ // Two children: Find successor (min in right subtree)
257
+ let successorParent = current;
258
+ let successor = current.right;
259
+ while (successor.left) {
260
+ successorParent = successor;
261
+ successor = successor.left;
262
+ }
263
+ current.article = successor.article;
264
+ if (successorParent === current) successorParent.right = successor.right;
265
+ else successorParent.left = successor.right;
266
+ }
267
+ return root;
268
+ }
269
+
270
+ clear() {
271
+ this.root = null;
272
+ }
273
+
274
+ size() {
275
+ return this._countNodes(this.root);
276
+ }
277
+ }
@@ -0,0 +1,90 @@
1
+ //import createDebug from "debug";
2
+ import { debuglog as createDebug } from "node:util";
3
+ import { BinarySearchTree } from "./BinarySearchTree.js";
4
+
5
+ // Initialize the debugger with a specific namespace
6
+ const debug = createDebug("plainblog:BinarySearchTreeHashMap");
7
+
8
+ export class BinarySearchTreeHashMap extends BinarySearchTree {
9
+ constructor() {
10
+ super();
11
+ this.ids = new Map(); // Fast lookup storage (ID -> Article)
12
+ }
13
+
14
+ // O(1) - Instant lookup
15
+ contains(id) {
16
+ return this.ids.has(id);
17
+ }
18
+
19
+ // Insert a new article
20
+ insert(article) {
21
+ // Note: The tree is now sorted by date. ID-based methods will not work correctly.
22
+ // if (!article.id) article.id = this.getNextID();
23
+ // Add to the Map
24
+ this.ids.set(article.id, article);
25
+ super.insert(article);
26
+ }
27
+
28
+ update(id, title, content) {
29
+ // Check for both string and number ID
30
+ let article = this.ids.get(id);
31
+ if (!article) article = this.ids.get(parseInt(id));
32
+
33
+ if (article) {
34
+ article.title = title;
35
+ article.content = content;
36
+ return true;
37
+ }
38
+ return false;
39
+ }
40
+
41
+ search(query) {
42
+ debug("search %s ", query);
43
+ const lower = query.toLowerCase();
44
+ const results = this.getAllArticles().filter(
45
+ (a) =>
46
+ a.title.toLowerCase().includes(lower) ||
47
+ a.content.toLowerCase().includes(lower),
48
+ );
49
+ debug("%d results found", results.length);
50
+ return results;
51
+ }
52
+
53
+ // Search for a specific article by ID
54
+ find(id) {
55
+ return this.ids.get(id);
56
+ }
57
+
58
+ // Get all articles in order (Sorted by ID)
59
+ getAllArticles(order = "newest") {
60
+ return super.getAllArticles(order);
61
+ }
62
+
63
+ getRange(startdate, enddate, limit) {
64
+ return super.getRange(startdate, enddate, limit);
65
+ }
66
+
67
+ remove(data) {
68
+ const id = data && data.id ? data.id : data;
69
+
70
+ let article = this.ids.get(id);
71
+ if (!article) article = this.ids.get(parseInt(id));
72
+
73
+ if (article) {
74
+ this.ids.delete(article.id);
75
+ return super.remove(article); // Pass article to use optimized date-based removal
76
+ }
77
+
78
+ this.ids.delete(id);
79
+ return super.remove(data);
80
+ }
81
+
82
+ clear() {
83
+ super.clear();
84
+ this.ids.clear();
85
+ }
86
+
87
+ size() {
88
+ return super.size();
89
+ }
90
+ }
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexho111/plainblog",
3
- "version": "0.5.28",
3
+ "version": "0.6.1",
4
4
  "description": "A tool for creating and serving a minimalist, single-page blog.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -8,6 +8,8 @@
8
8
  "dev": "node index.js",
9
9
  "postinstall": "node postinstall.js",
10
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",
11
13
  "lint": "eslint ."
12
14
  },
13
15
  "keywords": [
@@ -18,6 +20,8 @@
18
20
  "author": "lexho111",
19
21
  "license": "ISC",
20
22
  "devDependencies": {
23
+ "@babel/core": "^7.28.6",
24
+ "@babel/preset-env": "^7.28.6",
21
25
  "@eslint/js": "^9.39.2",
22
26
  "@types/node": "^25.0.3",
23
27
  "autoprefixer": "^10.4.23",
@@ -30,8 +34,10 @@
30
34
  "pg-hstore": "^2.3.4",
31
35
  "postcss": "^8.5.6",
32
36
  "sequelize": "^6.37.7",
33
- "sqlite3": "^5.1.7",
37
+ "sqlite3": "^5.0.2",
34
38
  "typescript": "^5.9.3",
39
+ "uglify-js": "^3.19.3",
35
40
  "w3c-css-validator": "^1.4.1"
36
- }
41
+ },
42
+ "dependencies": {}
37
43
  }
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;function debounce(t,n=300){let a;return(...e)=>{clearTimeout(a),a=setTimeout(()=>{t.apply(this,e)},n)}}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;var r,d=document.createDocumentFragment();for(r of i)fillWithContent(r.title,r.content,r.createdAt,r.id,d)&&e++;document.getElementById("articles").appendChild(d),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,t){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 i,r,d,c,e=new Date(e),s=(null!=o&&l.setAttribute("data-id",o),l.setAttribute("data-date",e.getTime()),document.createElement("h2")),u=document.createElement("span"),m=document.createElement("p");function g(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"]')&&(i=document.createElement("div"),r=document.createElement("div"),d=document.createElement("div"),c=document.createElement("div"),i.setAttribute("class","buttons"),r.setAttribute("class","button edit"),d.setAttribute("class","button delete"),c.setAttribute("class","button delete"),r.textContent="edit",d.textContent="delete",c.textContent="something",i.appendChild(r),i.appendChild(d),i.appendChild(c),r.addEventListener("click",()=>g("edit")),d.addEventListener("click",()=>g("delete")),l.appendChild(i)),s.textContent=n,u.textContent=getFormattedDate(e),m.textContent=a,l.appendChild(s),l.appendChild(u),l.appendChild(m),(t||document.getElementById("articles")).appendChild(l),!0}document.addEventListener("DOMContentLoaded",()=>{window.addEventListener("scroll",handleScroll);var e=document.getElementById("search");e&&e.addEventListener("input",debounce(async e=>{e=e.target.value;if(0<e.length){var e=await(await fetch("/api/articles?q="+encodeURIComponent(e))).json(),n=document.getElementById("articles");n.innerHTML="";let t=document.createDocumentFragment();(e.articles?e.articles.slice(0,50):[]).forEach(e=>fillWithContent(e.title,e.content,e.createdAt,e.id,t)),n.appendChild(t)}else window.location.reload()},300))});
2
+ /* source-hash: 913e06cbf98050a2e5c5af66e7322d711048940869b45f472aaff9ce03fc19b2 */
@@ -1 +1,2 @@
1
- body{font-family:Arial;margin:0}nav{margin-top:10px}.box,h1,nav{margin-left:10px}.grid{border:0 solid #000;display:grid;grid-gap:.25rem;gap:.25rem;grid-template-columns:1fr}.grid article{border:0 solid #ccc;border-radius:4px;min-width:0;word-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;-webkit-text-decoration:underline;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%}}
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 */