@lexho111/plainblog 0.5.28 → 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.
- package/Article.js +73 -4
- package/Blog.js +341 -140
- package/Formatter.js +2 -11
- package/README.md +1 -1
- package/{blog → blog_test_empty.db} +0 -0
- package/blog_test_load.db +0 -0
- package/build-scripts.js +54 -0
- package/coverage/clover.xml +1043 -0
- package/coverage/coverage-final.json +20 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +161 -0
- package/coverage/lcov-report/package/Article.js.html +406 -0
- package/coverage/lcov-report/package/Blog.js.html +2635 -0
- package/coverage/lcov-report/package/Formatter.js.html +379 -0
- package/coverage/lcov-report/package/build-scripts.js.html +247 -0
- package/coverage/lcov-report/package/build-styles.js.html +367 -0
- package/coverage/lcov-report/package/index.html +191 -0
- package/coverage/lcov-report/package/model/APIModel.js.html +190 -0
- package/coverage/lcov-report/package/model/ArrayList.js.html +382 -0
- package/coverage/lcov-report/package/model/ArrayListHashMap.js.html +379 -0
- package/coverage/lcov-report/package/model/BinarySearchTree.js.html +856 -0
- package/coverage/lcov-report/package/model/BinarySearchTreeHashMap.js.html +346 -0
- package/coverage/lcov-report/package/model/DataModel.js.html +307 -0
- package/coverage/lcov-report/package/model/DatabaseModel.js.html +232 -0
- package/coverage/lcov-report/package/model/FileAdapter.js.html +394 -0
- package/coverage/lcov-report/package/model/FileList.js.html +244 -0
- package/coverage/lcov-report/package/model/FileModel.js.html +358 -0
- package/coverage/lcov-report/package/model/SequelizeAdapter.js.html +538 -0
- package/coverage/lcov-report/package/model/SqliteAdapter.js.html +247 -0
- package/coverage/lcov-report/package/model/datastructures/ArrayList.js.html +439 -0
- package/coverage/lcov-report/package/model/datastructures/ArrayListHashMap.js.html +196 -0
- package/coverage/lcov-report/package/model/datastructures/BinarySearchTree.js.html +913 -0
- package/coverage/lcov-report/package/model/datastructures/BinarySearchTreeHashMap.js.html +346 -0
- package/coverage/lcov-report/package/model/datastructures/FileList.js.html +244 -0
- package/coverage/lcov-report/package/model/datastructures/index.html +176 -0
- package/coverage/lcov-report/package/model/index.html +206 -0
- package/coverage/lcov-report/package/utilities.js.html +511 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +2063 -0
- package/index.js +25 -1
- package/model/DataModel.js +79 -0
- package/model/DatabaseModel.js +20 -8
- package/model/FileAdapter.js +43 -4
- package/model/FileModel.js +47 -9
- package/model/SequelizeAdapter.js +11 -3
- package/model/datastructures/ArrayList.js +118 -0
- package/model/datastructures/ArrayListHashMap.js +37 -0
- package/model/datastructures/ArrayListHashMap.js.bk +90 -0
- package/model/datastructures/BinarySearchTree.js +276 -0
- package/model/datastructures/BinarySearchTreeHashMap.js +89 -0
- package/model/datastructures/BinarySearchTreeTest.js +16 -0
- package/model/datastructures/FileList.js +53 -0
- package/package.json +10 -2
- package/public/fetchData.js +0 -0
- package/public/scripts.min.js +2 -1
- package/public/styles.min.css +2 -1
- package/src/fetchData.js +150 -30
- package/src/styles.css +29 -0
- package/utilities.js +142 -0
package/src/fetchData.js
CHANGED
|
@@ -2,8 +2,45 @@ let isLoading = false;
|
|
|
2
2
|
|
|
3
3
|
document.addEventListener("DOMContentLoaded", () => {
|
|
4
4
|
window.addEventListener("scroll", handleScroll);
|
|
5
|
+
|
|
6
|
+
const searchInput = document.getElementById("search");
|
|
7
|
+
if (searchInput) {
|
|
8
|
+
searchInput.addEventListener(
|
|
9
|
+
"input",
|
|
10
|
+
debounce(async (e) => {
|
|
11
|
+
const query = e.target.value;
|
|
12
|
+
if (query.length > 0) {
|
|
13
|
+
const res = await fetch(
|
|
14
|
+
`/api/articles?q=${encodeURIComponent(query)}`,
|
|
15
|
+
);
|
|
16
|
+
const data = await res.json();
|
|
17
|
+
const container = document.getElementById("articles");
|
|
18
|
+
container.innerHTML = "";
|
|
19
|
+
const fragment = document.createDocumentFragment();
|
|
20
|
+
// Limit results to 50 to prevent browser freeze
|
|
21
|
+
const articles = data.articles ? data.articles.slice(0, 50) : [];
|
|
22
|
+
articles.forEach((a) =>
|
|
23
|
+
fillWithContent(a.title, a.content, a.createdAt, a.id, fragment),
|
|
24
|
+
);
|
|
25
|
+
container.appendChild(fragment);
|
|
26
|
+
} else {
|
|
27
|
+
window.location.reload();
|
|
28
|
+
}
|
|
29
|
+
}, 300),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
5
32
|
});
|
|
6
33
|
|
|
34
|
+
function debounce(func, timeout = 300) {
|
|
35
|
+
let timer;
|
|
36
|
+
return (...args) => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
timer = setTimeout(() => {
|
|
39
|
+
func.apply(this, args);
|
|
40
|
+
}, timeout);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
7
44
|
async function handleScroll() {
|
|
8
45
|
if (isLoading) return;
|
|
9
46
|
const scrollable = document.documentElement.scrollHeight - window.innerHeight;
|
|
@@ -22,42 +59,69 @@ async function handleScroll() {
|
|
|
22
59
|
async function loadMore() {
|
|
23
60
|
console.log(`load more`);
|
|
24
61
|
const articlesContainer = document.getElementById("articles");
|
|
25
|
-
const articles = articlesContainer.querySelectorAll("article");
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
62
|
+
const articles = Array.from(articlesContainer.querySelectorAll("article"));
|
|
63
|
+
|
|
64
|
+
// Find the last article that has a valid data-date
|
|
65
|
+
let lastTimestamp = NaN;
|
|
66
|
+
for (let i = articles.length - 1; i >= 0; i--) {
|
|
67
|
+
const val = articles[i].getAttribute("data-date");
|
|
68
|
+
if (val) {
|
|
69
|
+
const parsed = parseInt(val, 10);
|
|
70
|
+
if (!isNaN(parsed)) {
|
|
71
|
+
lastTimestamp = parsed;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`lastTimestamp ${lastTimestamp}`);
|
|
78
|
+
if (isNaN(lastTimestamp)) {
|
|
38
79
|
window.removeEventListener("scroll", handleScroll);
|
|
39
80
|
return;
|
|
40
81
|
}
|
|
41
82
|
|
|
83
|
+
//const lastDate = new Date(lastTimestamp);
|
|
84
|
+
/*let endDate = null;
|
|
85
|
+
const lastArticle = articles[articles.length - 1]; //
|
|
86
|
+
if (lastArticle) {
|
|
87
|
+
endDate = lastArticle.dataset.date;
|
|
88
|
+
}*/
|
|
89
|
+
const endDate = lastTimestamp;
|
|
90
|
+
|
|
42
91
|
isLoading = true;
|
|
43
|
-
const
|
|
92
|
+
const limit = 50;
|
|
93
|
+
// Fetch one more than needed to handle the duplicate last item.
|
|
94
|
+
const url = `/api/articles?enddate=${endDate}&limit=${limit + 1}`;
|
|
44
95
|
console.log(`load more ${url}`);
|
|
96
|
+
console.log(`endDate: ${new Date(endDate)}`);
|
|
45
97
|
try {
|
|
46
98
|
const response = await fetch(url);
|
|
47
99
|
if (response.ok) {
|
|
48
100
|
const result = await response.json();
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
101
|
+
let newArticles = result.articles || [];
|
|
102
|
+
|
|
103
|
+
if (newArticles.length > 0) {
|
|
104
|
+
let addedCount = 0;
|
|
105
|
+
const fragment = document.createDocumentFragment();
|
|
106
|
+
for (const article of newArticles) {
|
|
107
|
+
if (
|
|
108
|
+
fillWithContent(
|
|
109
|
+
article.title,
|
|
110
|
+
article.content,
|
|
111
|
+
article.createdAt,
|
|
112
|
+
article.id,
|
|
113
|
+
fragment,
|
|
114
|
+
)
|
|
115
|
+
) {
|
|
116
|
+
addedCount++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
document.getElementById("articles").appendChild(fragment);
|
|
120
|
+
if (addedCount < limit) {
|
|
121
|
+
window.removeEventListener("scroll", handleScroll);
|
|
57
122
|
}
|
|
58
123
|
} else {
|
|
59
|
-
|
|
60
|
-
//window.removeEventListener("scroll", handleScroll);
|
|
124
|
+
window.removeEventListener("scroll", handleScroll);
|
|
61
125
|
}
|
|
62
126
|
}
|
|
63
127
|
} catch (error) {
|
|
@@ -73,22 +137,78 @@ function getFormattedDate(date) {
|
|
|
73
137
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
74
138
|
return `${year}/${month}/${day} ${hours}:${minutes}`;
|
|
75
139
|
}
|
|
76
|
-
function fillWithContent(title, content, date, id) {
|
|
140
|
+
function fillWithContent(title, content, date, id, targetContainer) {
|
|
141
|
+
if (id) {
|
|
142
|
+
// Check if an article with this ID already exists in the container.
|
|
143
|
+
if (document.querySelector(`#articles > article[data-id='${id}']`)) {
|
|
144
|
+
console.log("article found. not adding it.");
|
|
145
|
+
console.log(`${id} ${title} ${date}`);
|
|
146
|
+
return false; // Indicate that nothing was added.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
77
150
|
const article = document.createElement("article");
|
|
78
|
-
|
|
151
|
+
const articleDate = new Date(date);
|
|
152
|
+
|
|
153
|
+
if (id !== null && id !== undefined) article.setAttribute("data-id", id);
|
|
154
|
+
article.setAttribute("data-date", articleDate.getTime());
|
|
79
155
|
|
|
80
156
|
const heading = document.createElement("h2");
|
|
81
157
|
const time = document.createElement("span");
|
|
82
158
|
const text = document.createElement("p");
|
|
83
159
|
|
|
160
|
+
const loggedin = document.querySelector('a[href="/logout"]') !== null;
|
|
161
|
+
if (loggedin) {
|
|
162
|
+
const buttons = document.createElement("div");
|
|
163
|
+
const editButton = document.createElement("div");
|
|
164
|
+
const deleteButton = document.createElement("div");
|
|
165
|
+
const somethingButton = document.createElement("div");
|
|
166
|
+
buttons.setAttribute("class", "buttons");
|
|
167
|
+
editButton.setAttribute("class", "button edit");
|
|
168
|
+
deleteButton.setAttribute("class", "button delete");
|
|
169
|
+
somethingButton.setAttribute("class", "button delete");
|
|
170
|
+
editButton.textContent = "edit";
|
|
171
|
+
deleteButton.textContent = "delete";
|
|
172
|
+
somethingButton.textContent = "something";
|
|
173
|
+
buttons.appendChild(editButton);
|
|
174
|
+
buttons.appendChild(deleteButton);
|
|
175
|
+
buttons.appendChild(somethingButton);
|
|
176
|
+
|
|
177
|
+
editButton.addEventListener("click", () => handleAction("edit"));
|
|
178
|
+
deleteButton.addEventListener("click", () => handleAction("delete"));
|
|
179
|
+
article.appendChild(buttons);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function handleAction(button) {
|
|
183
|
+
if (button === "delete") {
|
|
184
|
+
if (confirm("Delete this article?")) {
|
|
185
|
+
fetch("/api/articles?id=" + id, { method: "DELETE" }).then((res) => {
|
|
186
|
+
if (res.ok) article.remove();
|
|
187
|
+
else alert("Error: " + res.statusText);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
} else if (button === "edit") {
|
|
191
|
+
const newTitle = prompt("New Title", title);
|
|
192
|
+
const newContent = prompt("New Content", content);
|
|
193
|
+
if (newTitle && newContent) {
|
|
194
|
+
fetch("/api/articles?id=" + id, {
|
|
195
|
+
method: "PUT",
|
|
196
|
+
body: JSON.stringify({ title: newTitle, content: newContent }),
|
|
197
|
+
}).then((res) => {
|
|
198
|
+
if (res.ok) window.location.reload();
|
|
199
|
+
else alert("Error: " + res.statusText);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
84
205
|
heading.textContent = title;
|
|
85
|
-
time.textContent = getFormattedDate(
|
|
206
|
+
time.textContent = getFormattedDate(articleDate);
|
|
86
207
|
text.textContent = content;
|
|
87
208
|
|
|
88
|
-
//article.textContent = "article1";
|
|
89
209
|
article.appendChild(heading);
|
|
90
210
|
article.appendChild(time);
|
|
91
211
|
article.appendChild(text);
|
|
92
|
-
|
|
93
|
-
|
|
212
|
+
(targetContainer || document.getElementById("articles")).appendChild(article);
|
|
213
|
+
return true; // Indicate that an article was added.
|
|
94
214
|
}
|
package/src/styles.css
CHANGED
|
@@ -82,3 +82,32 @@ nav a:visited {
|
|
|
82
82
|
box-sizing: border-box;
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
h2 {
|
|
87
|
+
margin-top: 0;
|
|
88
|
+
border: 0px solid black;
|
|
89
|
+
}
|
|
90
|
+
.buttons {
|
|
91
|
+
width: 100%;
|
|
92
|
+
height: 25px;
|
|
93
|
+
border: 0px solid black;
|
|
94
|
+
margin-bottom: 0px;
|
|
95
|
+
}
|
|
96
|
+
.button {
|
|
97
|
+
border: 1px solid black;
|
|
98
|
+
width: 100px;
|
|
99
|
+
height: 19px;
|
|
100
|
+
float: left;
|
|
101
|
+
padding: 2px;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
user-select: none; /* Prevents text selection on double-click */
|
|
104
|
+
}
|
|
105
|
+
.button:focus {
|
|
106
|
+
outline: 2px solid orange; /* Vital for keyboard accessibility */
|
|
107
|
+
}
|
|
108
|
+
.edit {
|
|
109
|
+
background-color: blue;
|
|
110
|
+
}
|
|
111
|
+
.delete {
|
|
112
|
+
background-color: red;
|
|
113
|
+
}
|
package/utilities.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function generateTitle(number) {
|
|
4
|
+
return numberToGerman(number % 1000);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function generateContent(number) {
|
|
8
|
+
return (
|
|
9
|
+
numberToGerman(number % 1000) +
|
|
10
|
+
". " +
|
|
11
|
+
numberToGerman(number % 1000) +
|
|
12
|
+
". " +
|
|
13
|
+
numberToGerman(number % 1000) +
|
|
14
|
+
". " +
|
|
15
|
+
numberToGerman(number % 1000) +
|
|
16
|
+
". " +
|
|
17
|
+
numberToGerman(number % 1000) +
|
|
18
|
+
". " +
|
|
19
|
+
numberToGerman(number % 1000) +
|
|
20
|
+
". " +
|
|
21
|
+
numberToGerman(number % 1000) +
|
|
22
|
+
". " +
|
|
23
|
+
numberToGerman(number % 1000) +
|
|
24
|
+
". " +
|
|
25
|
+
numberToGerman(number % 1000) +
|
|
26
|
+
". " +
|
|
27
|
+
numberToGerman(number % 1000) +
|
|
28
|
+
". "
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function numberToGerman(n) {
|
|
33
|
+
if (n === 0) return "Null";
|
|
34
|
+
|
|
35
|
+
const units = [
|
|
36
|
+
"",
|
|
37
|
+
"eins",
|
|
38
|
+
"zwei",
|
|
39
|
+
"drei",
|
|
40
|
+
"vier",
|
|
41
|
+
"fünf",
|
|
42
|
+
"sechs",
|
|
43
|
+
"sieben",
|
|
44
|
+
"acht",
|
|
45
|
+
"neun",
|
|
46
|
+
];
|
|
47
|
+
const teens = [
|
|
48
|
+
"zehn",
|
|
49
|
+
"elf",
|
|
50
|
+
"zwölf",
|
|
51
|
+
"dreizehn",
|
|
52
|
+
"vierzehn",
|
|
53
|
+
"fünfzehn",
|
|
54
|
+
"sechzehn",
|
|
55
|
+
"siebzehn",
|
|
56
|
+
"achtzehn",
|
|
57
|
+
"neunzehn",
|
|
58
|
+
];
|
|
59
|
+
const tens = [
|
|
60
|
+
"",
|
|
61
|
+
"",
|
|
62
|
+
"zwanzig",
|
|
63
|
+
"dreißig",
|
|
64
|
+
"vierzig",
|
|
65
|
+
"fünfzig",
|
|
66
|
+
"sechzig",
|
|
67
|
+
"siebzig",
|
|
68
|
+
"achtzig",
|
|
69
|
+
"neunzig",
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
function convert(num) {
|
|
73
|
+
if (num < 10) return units[num];
|
|
74
|
+
if (num < 20) return teens[num - 10];
|
|
75
|
+
if (num < 100) {
|
|
76
|
+
const t = Math.floor(num / 10);
|
|
77
|
+
const u = num % 10;
|
|
78
|
+
if (u === 0) return tens[t];
|
|
79
|
+
return (u === 1 ? "ein" : units[u]) + "und" + tens[t];
|
|
80
|
+
}
|
|
81
|
+
if (num < 1000) {
|
|
82
|
+
const h = Math.floor(num / 100);
|
|
83
|
+
const r = num % 100;
|
|
84
|
+
let str = (h === 1 ? "ein" : units[h]) + "hundert";
|
|
85
|
+
if (r > 0) str += convert(r);
|
|
86
|
+
return str;
|
|
87
|
+
}
|
|
88
|
+
if (num < 1000000) {
|
|
89
|
+
const t = Math.floor(num / 1000);
|
|
90
|
+
const r = num % 1000;
|
|
91
|
+
let str = convert(t);
|
|
92
|
+
if (str.endsWith("eins")) str = str.slice(0, -1);
|
|
93
|
+
str += "tausend";
|
|
94
|
+
if (r > 0) str += convert(r);
|
|
95
|
+
return str;
|
|
96
|
+
}
|
|
97
|
+
return num.toString();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const result = convert(n);
|
|
101
|
+
return result.charAt(0).toUpperCase() + result.slice(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function generateDateList(startStr, endStr) {
|
|
105
|
+
const start = new Date(startStr);
|
|
106
|
+
// Optional: Reset minutes/seconds if you only want clean hourly markers
|
|
107
|
+
start.setMinutes(0, 0, 0);
|
|
108
|
+
|
|
109
|
+
const end = new Date(endStr);
|
|
110
|
+
const dates = [];
|
|
111
|
+
|
|
112
|
+
let current = new Date(start);
|
|
113
|
+
|
|
114
|
+
while (current <= end) {
|
|
115
|
+
// Push a new Date instance to avoid mutating existing array elements
|
|
116
|
+
dates.push(new Date(current));
|
|
117
|
+
|
|
118
|
+
// Increment by 1 hour
|
|
119
|
+
current.setHours(current.getHours() + 1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return dates;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function table(articles) {
|
|
126
|
+
const tableData = [];
|
|
127
|
+
for (let article of articles) {
|
|
128
|
+
tableData.push({
|
|
129
|
+
Title: article.title,
|
|
130
|
+
Date: article.createdAt,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
console.table(tableData);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function log(filename, message) {
|
|
137
|
+
if (filename) {
|
|
138
|
+
console.log(`${filename}: ${message}`);
|
|
139
|
+
} else {
|
|
140
|
+
console.log(`Unknown: ${message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|