summaryrefslogtreecommitdiff
path: root/Blog/Components/Pages
diff options
context:
space:
mode:
authorMarijn Besseling <njirambem@gmail.com>2025-09-07 20:56:09 +0200
committerMarijn Besseling <njirambem@gmail.com>2025-09-07 20:56:09 +0200
commit9ab322751a732d8cbc1ddf4f2ecf5022d7242baa (patch)
tree49abc49c7d148b2f575aa5daef32875d44729561 /Blog/Components/Pages
WIP migration
Diffstat (limited to 'Blog/Components/Pages')
-rw-r--r--Blog/Components/Pages/Calc.razor56
-rw-r--r--Blog/Components/Pages/Calc.razor.js140
-rw-r--r--Blog/Components/Pages/Concat.razor138
-rw-r--r--Blog/Components/Pages/Concat.razor.js261
-rw-r--r--Blog/Components/Pages/Error.razor37
-rw-r--r--Blog/Components/Pages/Home.razor10
-rw-r--r--Blog/Components/Pages/Letterflixd.razor30
-rw-r--r--Blog/Components/Pages/Letterflixd.razor.js99
-rw-r--r--Blog/Components/Pages/NotFound.razor4
-rw-r--r--Blog/Components/Pages/Note.razor8
-rw-r--r--Blog/Components/Pages/Note.razor.js33
11 files changed, 816 insertions, 0 deletions
diff --git a/Blog/Components/Pages/Calc.razor b/Blog/Components/Pages/Calc.razor
new file mode 100644
index 0000000..942d9a9
--- /dev/null
+++ b/Blog/Components/Pages/Calc.razor
@@ -0,0 +1,56 @@
1@page "/Calc"
2<PageTitle>Calculator</PageTitle>
3<PageScript Src="./Components/Pages/Calc.razor.js"/>
4
5<main>
6 <p>A rpn calculator</p>
7 <form id="form" class="flex-row">
8 <input id="input" type="text" placeholder="1 2 +" />
9 <input type="submit" value="Evaluate">
10 </form>
11 <div>
12 <div id="log"></div>
13 </div>
14
15 <fieldset class="docs">
16 <legend>Available stack operations</legend>
17 <details>
18 <summary>dup</summary>
19 <p>Duplicates the top value on the stack</p>
20 <div class="flex-spread">
21 <span>[1, 2, 3]</span>
22 <span>dup</span>
23 </div>
24 <div class="flex-spread">
25 <span>[1, 1, 2, 3]</span>
26 <span></span>
27 </div>
28 </details>
29
30 <details>
31 <summary>drop</summary>
32 <p>Drops the top value on the stack</p>
33 <div class="flex-spread">
34 <span>[1, 2, 3]</span>
35 <span>drop</span>
36 </div>
37 <div class="flex-spread">
38 <span>[2, 3]</span>
39 <span></span>
40 </div>
41 </details>
42
43 <details>
44 <summary>swap</summary>
45 <p>Swaps the top two values on the stack</p>
46 <div class="flex-spread">
47 <span>[1, 2, 3]</span>
48 <span>swap</span>
49 </div>
50 <div class="flex-spread">
51 <span>[2, 1, 3]</span>
52 <span></span>
53 </div>
54 </details>
55 </fieldset>
56</main> \ No newline at end of file
diff --git a/Blog/Components/Pages/Calc.razor.js b/Blog/Components/Pages/Calc.razor.js
new file mode 100644
index 0000000..d18634e
--- /dev/null
+++ b/Blog/Components/Pages/Calc.razor.js
@@ -0,0 +1,140 @@
1import { getById, div, span, h, writeError } from "/common.module.js"
2
3export function onLoad() {
4 console.log('Loaded');
5 const form = getById("form");
6 const input = getById("input");
7 const log = getById("log");
8
9 form.addEventListener("submit", submitForm);
10
11 const urlParams = new URLSearchParams(window.location.search);
12 const queryInput = urlParams.get('in');
13
14 if (input.value.length === 0) {
15 input.value = queryInput;
16 }
17}
18
19export function onUpdate() {
20 console.log('Updated');
21}
22
23export function onDispose() {
24 console.log('Disposed');
25}
26
27/** @param {SubmitEvent} event */
28function submitForm(event) {
29 console.log(input.value);
30 resetLog();
31 try {
32 const stack = evalString(input.value);
33 console.log(stack);
34
35 const path = window.location.pathname;
36 const params = new URLSearchParams(window.location.search);
37 const hash = window.location.hash;
38
39 params.set("in", input.value);
40 window.history.replaceState({}, '', `${path}?${params.toString()}${hash}`);
41 }
42 catch (error) {
43 writeError(error);
44 }
45 event.preventDefault();
46}
47
48/** @param {string} input */
49function evalString(input) {
50 let words = input.trim().split(' ').filter(i => i);
51 return evalWords([], words);
52}
53
54function effect2(f) {
55 return (stack) => {
56 if (stack.length <= 1) throw "stack underflow";
57 let [x, y, ...rest] = stack;
58 return [f(y, x), ...rest];
59 }
60}
61
62const plus = effect2((a, b) => a + b);
63const subtract = effect2((a, b) => a - b);
64const multiply = effect2((a, b) => a * b);
65const divide = effect2((a, b) => a / b);
66
67
68function evalWords(stack, words) {
69 writeLog(stack, words);
70 if (words.length === 0) return stack;
71
72 let [word, ...rest] = words;
73 return evalWords(evalWord(word, stack), rest);
74}
75
76function evalWord(word, stack, rest) {
77 switch (word) {
78 case "+": return plus(stack);
79 case "-": return subtract(stack);
80 case "*": return multiply(stack);
81 case "/": return divide(stack);
82 case "dup": return dup(stack);
83 case "drop": return drop(stack);
84 case "swap": return swap(stack);
85 default: return parse(word, stack);
86 }
87}
88
89function dup(stack) {
90 let [x, ...rest] = stack;
91 return [x, x, ...rest];
92}
93
94function drop(stack) {
95 let [_, ...rest] = stack;
96 return rest;
97}
98
99function swap(stack) {
100 let [x, y, ...rest] = stack;
101 return [y, x, ...rest];
102}
103
104function parse(word, stack) {
105 let num = Number(word);
106 if (isNaN(num)) {
107 throw `word '${word}' not recognised`;
108 }
109 return [num, ...stack];
110}
111
112function writeLog(stack, words) {
113 let log_left = span(`[${stack.join(', ')}]`);
114 let log_right = span(words.join(' '));
115
116 if (words.length === 0) {
117 log_left.textContent += " <=="
118 }
119
120 let log_row = div(log_left, log_right);
121 log_row.className = "flex-spread";
122
123 log.appendChild(log_row);
124}
125
126function resetLog() {
127 if (!log.hasChildNodes()) return;
128
129 let summary = h("summary");
130 summary.textContent = log.firstChild.lastChild.textContent;
131
132 let old_log = log.cloneNode(true);
133 old_log.id = '';
134
135 let details = h("details", summary, old_log);
136 details.className = "history"
137
138 log.insertAdjacentElement("afterend", details);
139 log.replaceChildren(); //remove children, clear log
140} \ No newline at end of file
diff --git a/Blog/Components/Pages/Concat.razor b/Blog/Components/Pages/Concat.razor
new file mode 100644
index 0000000..856d4e0
--- /dev/null
+++ b/Blog/Components/Pages/Concat.razor
@@ -0,0 +1,138 @@
1@page "/Concat"
2<PageTitle>Concatenator</PageTitle>
3<PageScript Src="./Components/Pages/Concat.razor.js"/>
4<main>
5 <p>A "concatenative language"</p>
6 <form id="form">
7 <textarea id="input" placeholder="1 2 +" rows="5"></textarea>
8 <input type="submit" value="Run program">
9 </form>
10 <div>
11 <div id="log"></div>
12 </div>
13
14 <fieldset class="docs">
15 <legend>Predefined operations</legend>
16 <details>
17 <summary>dup</summary>
18 <p>Duplicates the top value on the stack</p>
19 <div class="flex-spread">
20 <span>[1, 2, 3]</span>
21 <span>dup</span>
22 </div>
23 <div class="flex-spread">
24 <span>[1, 1, 2, 3]</span>
25 <span></span>
26 </div>
27 </details>
28
29 <details>
30 <summary>drop</summary>
31 <p>Drops the top value on the stack</p>
32 <div class="flex-spread">
33 <span>[1, 2, 3]</span>
34 <span>drop</span>
35 </div>
36 <div class="flex-spread">
37 <span>[2, 3]</span>
38 <span></span>
39 </div>
40 </details>
41
42 <details>
43 <summary>swap</summary>
44 <p>Swaps the top two values on the stack</p>
45 <div class="flex-spread">
46 <span>[1, 2, 3]</span>
47 <span>swap</span>
48 </div>
49 <div class="flex-spread">
50 <span>[2, 1, 3]</span>
51 <span></span>
52 </div>
53 </details>
54
55 <details>
56 <summary>skip</summary>
57 <p>Skips forward in the program by the amount on the top of the stack.
58 If the value on the top of the stack is 0 or negative,
59 the value is dropped and the program does not skip ahead.
60 </p>
61 <div class="flex-spread">
62 <span>[1, 2, 3]</span>
63 <span>skip drop dup</span>
64 </div>
65 <div class="flex-spread">
66 <span>[2, 3]</span>
67 <span>dup</span>
68 </div>
69 <div class="flex-spread">
70 <span>[2, 2, 3]</span>
71 <span></span>
72 </div>
73 Or
74 <div class="flex-spread">
75 <span>[-1, 2, 3]</span>
76 <span>skip drop dup</span>
77 </div>
78 <div class="flex-spread">
79 <span>[2, 3]</span>
80 <span>drop dup</span>
81 </div>
82 <div class="flex-spread">
83 <span>[3]</span>
84 <span>dup</span>
85 </div>
86 <div class="flex-spread">
87 <span>[3, 3]</span>
88 <span></span>
89 </div>
90 </details>
91
92 <details>
93 <summary>: word ... ;</summary>
94 <p>Creates a new definition.</p>
95 <p>Example: ": double 2 * ;" defines a new word called double. Any subsequent mention of double will
96 replace the word with its definition.</p>
97 <div class="flex-spread">
98 <span>[]</span>
99 <span>: double 2 * ; 3 double</span>
100 </div>
101 <div class="flex-spread">
102 <span>[]</span>
103 <span>3 double</span>
104 </div>
105 <div class="flex-spread">
106 <span>[3]</span>
107 <span>double</span>
108 </div>
109 <div class="flex-spread">
110 <span>[3]</span>
111 <span>2 *</span>
112 </div>
113 <div class="flex-spread">
114 <span>[2, 3]</span>
115 <span>*</span>
116 </div>
117 <div class="flex-spread">
118 <span>[6] &lt;==</span>
119 <span></span>
120 </div>
121 </details>
122 </fieldset>
123 <fieldset class="docs">
124 <legend>Example programs</legend>
125 <details>
126 <summary>Factorial</summary>
127 <NavLink
128 href="/concat?in=FwAgxg9grgdgLgUwE4lQEygBxAJhAZwHcBDbAWhAG4AoUAQlRA2wEYQLJZEV8BrAS2wMAVFWoB2EHWpA">
129 Replace program
130 </NavLink>
131 <pre>
132 : counter dup 2 swap - ;
133 : ! dup 1 - counter skip ! * ;
134 7 !
135 </pre>
136 </details>
137 </fieldset>
138</main> \ No newline at end of file
diff --git a/Blog/Components/Pages/Concat.razor.js b/Blog/Components/Pages/Concat.razor.js
new file mode 100644
index 0000000..bd476e9
--- /dev/null
+++ b/Blog/Components/Pages/Concat.razor.js
@@ -0,0 +1,261 @@
1import { getById, div, span, h, writeError } from "/common.module.js";
2import lzString from "/lz-string.module.js";
3
4let definitions = {};
5let log = undefined;
6let input = undefined;
7
8export function onLoad() {
9 console.log('Loaded');
10 const form = getById("form");
11 input = getById("input");
12 log = getById("log");
13
14 form.onsubmit = async (event) => {
15 event.preventDefault();
16 await submitForm();
17 };
18
19 const urlParams = new URLSearchParams(window.location.search);
20 const queryInput = urlParams.get("in");
21
22 if (input.value.length === 0) {
23 input.value = lzString.decompressFromEncodedURIComponent(queryInput);
24 }
25}
26
27export function onUpdate() {
28 const urlParams = new URLSearchParams(window.location.search);
29 const queryInput = urlParams.get("in");
30
31 if (input.value.length === 0) {
32 input.value = lzString.decompressFromEncodedURIComponent(queryInput);
33 }
34}
35
36async function submitForm() {
37 console.log(input.value);
38 resetLog();
39 try {
40 definitions = {};
41 const stack = await evalString(input.value);
42 console.log(stack);
43
44 const path = window.location.pathname;
45 const params = new URLSearchParams(window.location.search);
46 const hash = window.location.hash;
47
48 params.set("in", lzString.compressToEncodedURIComponent(input.value));
49 window.history.replaceState(
50 {},
51 "",
52 `${path}?${params.toString()}${hash}`,
53 );
54 } catch (error) {
55 writeError(error);
56 console.log(error);
57 }
58}
59
60function splitWords(input) {
61 return input
62 .trim()
63 .split(/\s+/)
64 .filter((i) => i);
65}
66
67/** @param {string} inputString */
68function evalString(inputString) {
69 let words = splitWords(inputString);
70 return evalWords(words);
71}
72
73function effect2(f) {
74 return (stack) => {
75 if (stack.length <= 1) throw "stack underflow, need 2 numbers";
76 let [x, y, ...rest] = stack;
77 return [f(y, x), ...rest];
78 };
79}
80
81const plus = effect2((a, b) => a + b);
82const subtract = effect2((a, b) => a - b);
83const multiply = effect2((a, b) => a * b);
84const divide = effect2((a, b) => a / b);
85
86async function evalWords(inputWords) {
87 let words = inputWords;
88 let stack = [];
89
90 while (words.length > 0) {
91 await writeLog(stack, words);
92 if (words.length === 0) return stack;
93
94 let [word, ...rest] = words;
95 [stack, words] = evalWord(word, stack, rest);
96
97 await new Promise((r) => setTimeout(r, 100));
98 }
99 await writeLog(stack, words);
100 return stack;
101}
102
103function evalWord(word, stack, rest) {
104 switch (word) {
105 case "+":
106 return [plus(stack), rest];
107 case "-":
108 return [subtract(stack), rest];
109 case "*":
110 return [multiply(stack), rest];
111 case "/":
112 return [divide(stack), rest];
113 case "dup":
114 return [dup(stack), rest];
115 case "drop":
116 return [drop(stack), rest];
117 case "swap":
118 return [swap(stack), rest];
119 case "skip":
120 return skip(stack, rest);
121 case ",,":
122 return unquote(stack, rest);
123 case ":":
124 return [stack, define(rest)];
125 default:
126 return parse(word, stack, rest);
127 }
128}
129
130function unquote(stack, rest) {
131 let [quote, ...restStack] = stack;
132 if (typeof quote === "string" || quote instanceof String) {
133 return [restStack, [...splitWords(quote), ...rest]];
134 } else {
135 throw "not a string, only strings are unquoteable";
136 }
137}
138
139function skip(stack, words) {
140 if (stack.length === 0) throw "stack underflow, dont know how much to skip";
141 let [amount, ...restStack] = stack;
142
143 if (amount > words.length)
144 throw `program underflow, cant skip ${amount} words`;
145 if (amount <= 0) return [stack.slice(1), words]; // no skipping on <= 0
146
147 let restWords = words.slice(amount);
148
149 return [restStack, restWords];
150}
151
152function define(words) {
153 if (words.length < 2) throw "missing definition after ':'";
154 let [ident, ...rest] = words;
155 let index = 0;
156 let definition = [];
157
158 for (; index < rest.length; index++) {
159 const word = rest[index];
160 if (word === ";") {
161 break;
162 } else if (rest.length - 1 === index) {
163 throw "expected ';', found end of program";
164 }
165 definition.push(word);
166 }
167
168 definitions[ident] = definition;
169
170 return rest.slice(index + 1);
171}
172
173function dup(stack) {
174 if (stack.length === 0) throw "stack underflow, nothing to duplicate";
175 let [x, ...rest] = stack;
176 return [x, x, ...rest];
177}
178
179function drop(stack) {
180 if (stack.length === 0) throw "stack underflow, nothing to drop";
181 let [_, ...rest] = stack;
182 return rest;
183}
184
185function swap(stack) {
186 if (stack.length < 2) throw "stack underflow, not enough to swap";
187 let [x, y, ...rest] = stack;
188 return [y, x, ...rest];
189}
190
191function parse(word, stack, rest) {
192 if (word.startsWith('"')) {
193 return parseString(word, stack, rest);
194 }
195
196 if (word in definitions) {
197 return [stack, [...definitions[word], ...rest]];
198 }
199
200 let num = Number(word);
201 if (isNaN(num)) {
202 throw `word '${word}' not recognised`;
203 }
204
205 return [[num, ...stack], rest];
206}
207
208function parseString(word, stack, rest) {
209 if (word.length > 1 && word.endsWith('"')) {
210 return [[word.slice(1, -1), ...stack], rest];
211 }
212
213 let string = word.slice(1);
214 let index = 0;
215
216 for (; index < rest.length; index++) {
217 const word = rest[index];
218 if (word.endsWith('"')) {
219 string = string.concat(" ", word.slice(0, -1));
220 break;
221 } else if (rest.length - 1 === index) {
222 throw "expected word ending with '\"', found end of program";
223 }
224 string = string.concat(" ", word);
225 }
226
227 return [[string, ...stack], rest.slice(index + 1)];
228}
229
230function writeLog(stack, words) {
231 return new Promise((resolve, _reject) => {
232 let log_left = span(`[${stack.join(", ")}]`);
233 let log_right = span(words.join(" "));
234
235 if (words.length === 0) {
236 log_left.textContent += " <==";
237 }
238
239 let log_row = div(log_left, log_right);
240 log_row.className = "flex-spread";
241
242 log.appendChild(log_row);
243 resolve();
244 });
245}
246
247function resetLog() {
248 if (!log.hasChildNodes()) return;
249
250 let summary = h("summary");
251 summary.textContent = log.firstChild.lastChild.textContent;
252
253 let old_log = log.cloneNode(true);
254 old_log.id = "";
255
256 let details = h("details", summary, old_log);
257 details.className = "history";
258
259 log.insertAdjacentElement("afterend", details);
260 log.replaceChildren(); //remove children, clear log
261}
diff --git a/Blog/Components/Pages/Error.razor b/Blog/Components/Pages/Error.razor
new file mode 100644
index 0000000..6567978
--- /dev/null
+++ b/Blog/Components/Pages/Error.razor
@@ -0,0 +1,37 @@
1@page "/Error"
2@using System.Diagnostics
3@using Microsoft.AspNetCore.Http
4
5<PageTitle>Error</PageTitle>
6
7<h1 class="text-danger">Error.</h1>
8<h2 class="text-danger">An error occurred while processing your request.</h2>
9
10@if (ShowRequestId)
11{
12 <p>
13 <strong>Request ID:</strong> <code>@RequestId</code>
14 </p>
15}
16
17<h3>Development Mode</h3>
18<p>
19 Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
20</p>
21<p>
22 <strong>The Development environment shouldn't be enabled for deployed applications.</strong>
23 It can result in displaying sensitive information from exceptions to end users.
24 For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
25 and restarting the app.
26</p>
27
28@code{
29 [CascadingParameter] private HttpContext? HttpContext { get; set; }
30
31 private string? RequestId { get; set; }
32 private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
33
34 protected override void OnInitialized() =>
35 RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
36
37} \ No newline at end of file
diff --git a/Blog/Components/Pages/Home.razor b/Blog/Components/Pages/Home.razor
new file mode 100644
index 0000000..be924d5
--- /dev/null
+++ b/Blog/Components/Pages/Home.razor
@@ -0,0 +1,10 @@
1@page "/"
2
3<PageTitle>Home</PageTitle>
4
5<main>
6 <p>A couple of JS/HTML experiments.</p>
7 <div>
8 <div id="log"></div>
9 </div>
10</main> \ No newline at end of file
diff --git a/Blog/Components/Pages/Letterflixd.razor b/Blog/Components/Pages/Letterflixd.razor
new file mode 100644
index 0000000..b6a088a
--- /dev/null
+++ b/Blog/Components/Pages/Letterflixd.razor
@@ -0,0 +1,30 @@
1@page "/Letterflixd"
2<PageTitle>Netflix to Letterboxd converter</PageTitle>
3<PageScript Src="./Components/Pages/Letterflixd.razor.js"/>
4<main>
5 <p>Convert Netflix viewing history to a Letterboxd import file.</p>
6 <p>Download your viewing history from Netflix at <a href="https://www.netflix.com/settings/viewed/"
7 target="_blank">https://www.netflix.com/settings/viewed/</a></p>
8 <p>Import the resulting Letterboxd file at <a href="https://letterboxd.com/import/"
9 target="_blank">https://letterboxd.com/import/</a></p>
10 <form id="form">
11 <fieldset class="docs">
12 <div class="row">
13 <label for="netflix-file">
14 Netflix viewing history file:
15 <input id="netflix-file" type="file">
16 </label>
17 </div>
18 <div class="row">
19 <input type="submit" value="Convert">
20 </div>
21 </fieldset>
22 </form>
23 <div>
24 <label for="debug">
25 Show debug log
26 <input type="checkbox" id="debug">
27 </label>
28 <div id="log"></div>
29 </div>
30</main> \ No newline at end of file
diff --git a/Blog/Components/Pages/Letterflixd.razor.js b/Blog/Components/Pages/Letterflixd.razor.js
new file mode 100644
index 0000000..a3903e2
--- /dev/null
+++ b/Blog/Components/Pages/Letterflixd.razor.js
@@ -0,0 +1,99 @@
1import { getById, resetLog, writeError, writeInfo, writeDebug, a, span, div, addClass } from "/common.module.js"
2
3let netflix_file_input = undefined;
4
5export function onLoad() {
6
7 const form = getById("form");
8 netflix_file_input = getById("netflix-file");
9
10 form.addEventListener("submit", submitForm);
11}
12
13
14/** @param {SubmitEvent} event */
15function submitForm(event) {
16 event.preventDefault();
17 event.stopPropagation();
18
19 resetLog();
20 const files = netflix_file_input.files;
21 if (files.length !== 1) {
22 writeError("First select your viewing history file.");
23 return;
24 }
25
26 const netflix_file = files[0];
27
28 writeInfo("Checking metadata...");
29
30 if (!netflix_file.type.match('text/csv')) {
31 let detected_filetype = "";
32 if (netflix_file.type) {
33 detected_filetype = ", detected filetype: " + netflix_file.type;
34 }
35 writeError("Select a CSV file" + detected_filetype);
36 return;
37 }
38
39 handleFile(netflix_file);
40}
41
42/** @param {File} file */
43function handleFile(file) {
44 writeInfo("Reading file...");
45 file.text()
46 .then(convertText);
47}
48
49const viewingHistoryPattern = /"(.*)","(\d{1,2})\/(\d{1,2})\/(\d+)"/;
50
51/** @param {string} data */
52function convertText(data) {
53 writeInfo("Checking header...");
54 const [header, ...lines] = data.split(/\r?\n|\r|\n/g);
55 if (header !== "Title,Date") {
56 writeError("Invalid Netflix viewing history file, expected header \"Title,Date\"");
57 return;
58 }
59 writeInfo("History count: " + lines.length);
60 let letterboxd_output = "Title,WatchedDate,Rewatch\n";
61 let watched_movies = new Set();
62
63 for (let line of lines) {
64 if (!line) continue;
65 if (line.includes(": Season")
66 || line.includes(": Limited Series:")
67 || line.includes(": Part")
68 || line.includes(": Chapter ")
69 || line.includes(": Volume ")
70 || line.includes(": Series ")
71 || line.includes(": Book ")) {
72 writeDebug("Skipping show episode: " + line);
73 continue;
74 }
75 if (line.startsWith(": ")) {
76 writeDebug("Skipping empty title entry")
77 }
78 const [_, title, day, month, year] = viewingHistoryPattern.exec(line);
79 if (title && day && month && year) {
80 const rewatch = watched_movies.has(title);
81 letterboxd_output += `"${title}",${year}-${month}-${day},${rewatch}\n`
82 if (rewatch) {
83 writeDebug("Rewatch of: " + title);
84 }
85 watched_movies.add(title);
86 }
87 else {
88 writeError("could not parse line: " + line);
89 }
90 }
91
92 let letterboxd_import = new Blob([letterboxd_output], { type: 'text/csv' });
93
94 const filename = "netflix-letterboxd-import.csv"
95 let download_button = a(window.URL.createObjectURL(letterboxd_import), filename)
96 let download_text = span("Download import file ==> ")
97 download_button.download = filename;
98 log.appendChild(div(addClass(download_text, "info"), download_button));
99} \ No newline at end of file
diff --git a/Blog/Components/Pages/NotFound.razor b/Blog/Components/Pages/NotFound.razor
new file mode 100644
index 0000000..5278494
--- /dev/null
+++ b/Blog/Components/Pages/NotFound.razor
@@ -0,0 +1,4 @@
1@page "/not-found"
2
3<h3>Not Found</h3>
4<p>Sorry, the content you are looking for does not exist.</p> \ No newline at end of file
diff --git a/Blog/Components/Pages/Note.razor b/Blog/Components/Pages/Note.razor
new file mode 100644
index 0000000..531adcf
--- /dev/null
+++ b/Blog/Components/Pages/Note.razor
@@ -0,0 +1,8 @@
1@page "/Note"
2<PageTitle>Note</PageTitle>
3<PageScript Src="./Components/Pages/Note.razor.js"></PageScript>
4
5<main>
6 <textarea id="input" style="height:60vh" autofocus></textarea>
7 <div id="log"></div>
8</main> \ No newline at end of file
diff --git a/Blog/Components/Pages/Note.razor.js b/Blog/Components/Pages/Note.razor.js
new file mode 100644
index 0000000..3d47d23
--- /dev/null
+++ b/Blog/Components/Pages/Note.razor.js
@@ -0,0 +1,33 @@
1import { getById, debounce, writeError, resetLog } from "/common.module.js";
2import lzString from "/lz-string.module.js";
3
4let input = undefined;
5export function onLoad() {
6 input = getById("input");
7 input.addEventListener("input", debounce(() => {
8 if (input.value === '') {
9 window.location.hash = ''
10 }
11 else {
12 window.location.hash = '#' + lzString.compressToEncodedURIComponent(input.value);
13 }
14 resetLog();
15 }, 10))
16
17 window.addEventListener('hashchange', loadState);
18 loadState();
19}
20
21export function onUpdate() {
22 loadState();
23}
24
25function loadState() {
26 if (window.location.hash !== '') {
27 input.value = lzString.decompressFromEncodedURIComponent(window.location.hash.substring(1));
28 if (input.value === '') {
29 //Hash but no content?
30 writeError("Failed to load note from url.")
31 }
32 }
33} \ No newline at end of file