aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2023-07-19 01:37:02 +0800
committercrupest <crupest@outlook.com>2023-07-19 01:37:02 +0800
commitbf3f4fcef32940991bf8f7a1d896d10653012339 (patch)
tree4b48a597d23efd757623ae415d658a134ff75a45
parent2197b07482b3500fdb87db579e7d538be17942c6 (diff)
downloadcrupest-bf3f4fcef32940991bf8f7a1d896d10653012339.tar.gz
crupest-bf3f4fcef32940991bf8f7a1d896d10653012339.tar.bz2
crupest-bf3f4fcef32940991bf8f7a1d896d10653012339.zip
A lot prettier.
-rw-r--r--docker/crupest-nginx/sites/www/index.html101
-rw-r--r--docker/crupest-nginx/sites/www/package.json8
-rw-r--r--docker/crupest-nginx/sites/www/pnpm-lock.yaml32
-rw-r--r--docker/crupest-nginx/sites/www/src/main.js89
-rw-r--r--docker/crupest-nginx/sites/www/src/main.ts96
-rw-r--r--docker/crupest-nginx/sites/www/src/mock-todos.ts126
-rw-r--r--docker/crupest-nginx/sites/www/src/style.css82
-rw-r--r--docker/crupest-nginx/sites/www/src/todos.ts29
-rw-r--r--docker/crupest-nginx/sites/www/tsconfig.json19
9 files changed, 439 insertions, 143 deletions
diff --git a/docker/crupest-nginx/sites/www/index.html b/docker/crupest-nginx/sites/www/index.html
index 0a7a99b..339d3c2 100644
--- a/docker/crupest-nginx/sites/www/index.html
+++ b/docker/crupest-nginx/sites/www/index.html
@@ -20,50 +20,61 @@
</div>
<article id="main-article">
<h1 id="title">Hello! This is crupest.</h1>
- <p>Welcome to my home page! I'm very glad to meet you here.</p>
- <p>If you have something interesting to share with me, feel free to reach me via email at <a
- href="mailto:crupest@outlook.com">crupest@outlook.com</a> or, of course, <a
- href="mailto:I@crupest.life">I@crupest.life</a>. You can also fire an issue in
- any repo on GitHub. Here is the link to my GitHub profile, <a
- href="https://github.com/crupest">https://github.com/crupest</a>.</p>
- <p>Currently this page is hosted on my own server and my own apex domain (crupest.life). You can also share this
- website link, aka, <a href="https://crupest.life">https://crupest.life</a>.</p>
- <p>This is just the home page. I also run some other services on my server under this domain:</p>
- <p>Public:</p>
- <ul>
- <li>
- https://crupest.life/api: Some APIs written in <a
- href="https://learn.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core" target="_blank">ASP.NET
- Core</a>.
- </li>
- <li><a href="https://blog.crupest.life" target="_blank">https://blog.crupest.life</a>: My blogs built with <a
- href="https://gohugo.io" target="_blank">hugo</a>.</li>
- <li><a href="https://timeline.crupest.life" target="_blank">https://timeline.crupest.life</a>: My micro-blogs with
- my own web app <a href="https://github.com/crupest/Timeline">Timeline</a>.</li>
- <li><a href="https://github.crupest.life" target="_blank">https://github.crupest.life</a>: Redirect to my github
- profile, aka, <a href="https://github.com/crupest">https://github.com/crupest</a>.</li>
- </ul>
- <p>Private:</p>
- <ul>
- <li><a href="https://code.crupest.life" target="_blank">https://code.crupest.life</a>: Browser based vscode with
- <a href="https://github.com/coder/code-server" target="_blank">code-server</a>.
- </li>
- </ul>
- <p>If you wish to deploy similar services like me, you are in the right place. Take a look at <a
- href="https://github.com/crupest/crupest" target="_blank">https://github.com/crupest/crupest</a> and there is
- all you need to start with. Also, contact me if you run into some problem.</p>
- <h2 id="friends">Friends of me:</h2>
- <div id="friends-container">
- <a class="friend-link" href="https://wsmcs.cn" target="_blank">
- <img class="friend-img"
- src="https://wsmcs.cn/wp-content/uploads/2023/02/BifengxiaPanda_ZH-CN8879969527_UHD-scaled.jpg">
- <span class="friend-name">wsm</span>
- </a>
- </div>
- <h2 id="todos">TODOs of me:</h2>
- <p id="todo-message">Fetching...</p>
- <ul id="todo-container">
- </ul>
+ <section>
+ <p>Welcome to my home page! I'm very glad to meet you here.</p>
+ <p>If you have something interesting to share with me, feel free to reach me via email at <a
+ href="mailto:crupest@outlook.com">crupest@outlook.com</a> or, of course, <a
+ href="mailto:I@crupest.life">I@crupest.life</a>. You can also fire an issue in
+ any repo on GitHub. Here is the link to my GitHub profile, <a
+ href="https://github.com/crupest">https://github.com/crupest</a>.</p>
+ <p>Currently this page is hosted on my own server and my own apex domain (crupest.life). You can also share this
+ website link, aka, <a href="https://crupest.life">https://crupest.life</a>.</p>
+ </section>
+ <section>
+ <h2 id="todos">TODOs of me <span class="h-note">grabbed from <a
+ href="https://github.com/users/crupest/projects/2">my Github
+ project</a></span></h2>
+ <p id="todo-message">Fetching...</p>
+ <ul id="todo-container">
+ </ul>
+ </section>
+ <section>
+ <h2 id="friends">Friends of me:</h2>
+ <div id="friends-container">
+ <a class="friend-link" href="https://wsmcs.cn" target="_blank">
+ <img class="friend-img"
+ src="https://wsmcs.cn/wp-content/uploads/2023/02/BifengxiaPanda_ZH-CN8879969527_UHD-scaled.jpg">
+ <span class="friend-name">wsm</span>
+ </a>
+ </div>
+ </section>
+ <section>
+ <p>This is just the home page. I also run some other services on my server under this domain:</p>
+ <p>Public:</p>
+ <ul>
+ <li>
+ https://crupest.life/api: Some APIs written in <a
+ href="https://learn.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core" target="_blank">ASP.NET
+ Core</a>.
+ </li>
+ <li><a href="https://blog.crupest.life" target="_blank">https://blog.crupest.life</a>: My blogs built with <a
+ href="https://gohugo.io" target="_blank">hugo</a>.</li>
+ <li><a href="https://timeline.crupest.life" target="_blank">https://timeline.crupest.life</a>: My micro-blogs
+ with
+ my own web app <a href="https://github.com/crupest/Timeline">Timeline</a>.</li>
+ <li><a href="https://github.crupest.life" target="_blank">https://github.crupest.life</a>: Redirect to my github
+ profile, aka, <a href="https://github.com/crupest">https://github.com/crupest</a>.</li>
+ </ul>
+ <p>Private:</p>
+ <ul>
+ <li><a href="https://code.crupest.life" target="_blank">https://code.crupest.life</a>: Browser based vscode with
+ <a href="https://github.com/coder/code-server" target="_blank">code-server</a>.
+ </li>
+ </ul>
+ <p>If you wish to deploy similar services like me, you are in the right place. Take a look at <a
+ href="https://github.com/crupest/crupest" target="_blank">https://github.com/crupest/crupest</a> and there is
+ all you need to start with. Also, contact me if you run into some problem.</p>
+ </section>
<footer>
<p>That's all of me!</p>
<p>© 2022 crupest</p>
@@ -75,7 +86,7 @@
</p>
</footer>
</article>
- <script type="module" src="./src/main.js"></script>
+ <script type="module" src="./src/main.ts"></script>
</body>
</html> \ No newline at end of file
diff --git a/docker/crupest-nginx/sites/www/package.json b/docker/crupest-nginx/sites/www/package.json
index 29d2d78..6927dfa 100644
--- a/docker/crupest-nginx/sites/www/package.json
+++ b/docker/crupest-nginx/sites/www/package.json
@@ -5,9 +5,13 @@
"source": "index.html",
"scripts": {
"start": "parcel",
- "build": "parcel build"
+ "build": "tsc && parcel build"
},
"devDependencies": {
- "parcel": "latest"
+ "@tsconfig/recommended": "^1.0.2",
+ "@types/parcel-env": "^0.0.1",
+ "parcel": "latest",
+ "prettier": "3.0.0",
+ "typescript": "^5.1.6"
}
} \ No newline at end of file
diff --git a/docker/crupest-nginx/sites/www/pnpm-lock.yaml b/docker/crupest-nginx/sites/www/pnpm-lock.yaml
index a868dab..6de8ce2 100644
--- a/docker/crupest-nginx/sites/www/pnpm-lock.yaml
+++ b/docker/crupest-nginx/sites/www/pnpm-lock.yaml
@@ -5,9 +5,21 @@ settings:
excludeLinksFromLockfile: false
devDependencies:
+ '@tsconfig/recommended':
+ specifier: ^1.0.2
+ version: 1.0.2
+ '@types/parcel-env':
+ specifier: ^0.0.1
+ version: 0.0.1
parcel:
specifier: latest
version: 2.9.3
+ prettier:
+ specifier: 3.0.0
+ version: 3.0.0
+ typescript:
+ specifier: ^5.1.6
+ version: 5.1.6
packages:
@@ -1057,6 +1069,14 @@ packages:
engines: {node: '>=10.13.0'}
dev: true
+ /@tsconfig/recommended@1.0.2:
+ resolution: {integrity: sha512-dbHBtbWBOjq0/otpopAE02NT2Cm05Qe2JsEKeCf/wjSYbI2hz8nCqnpnOJWHATgjDz4fd3dchs3Wy1gQGjfN6w==}
+ dev: true
+
+ /@types/parcel-env@0.0.1:
+ resolution: {integrity: sha512-8WmdiJ1uEBcW6AOWzQH7i0141ZXZr7B03YfTpguUDrTHXJHwYU9eEOckBRCZzYGrzb4pdoyBlaIMiTee04uqPQ==}
+ dev: true
+
/abortcontroller-polyfill@1.7.5:
resolution: {integrity: sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==}
dev: true
@@ -1699,6 +1719,12 @@ packages:
posthtml-render: 3.0.0
dev: true
+ /prettier@3.0.0:
+ resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==}
+ engines: {node: '>=14'}
+ hasBin: true
+ dev: true
+
/react-error-overlay@6.0.9:
resolution: {integrity: sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==}
dev: true
@@ -1797,6 +1823,12 @@ packages:
engines: {node: '>=10'}
dev: true
+ /typescript@5.1.6:
+ resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+ dev: true
+
/update-browserslist-db@1.0.11(browserslist@4.21.9):
resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
hasBin: true
diff --git a/docker/crupest-nginx/sites/www/src/main.js b/docker/crupest-nginx/sites/www/src/main.js
deleted file mode 100644
index 39d2ba8..0000000
--- a/docker/crupest-nginx/sites/www/src/main.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import "./style.css";
-
-const happy = "happy";
-const angry = "angry";
-
-function emotionOpposite(emotion) {
- if (emotion === happy) {
- return angry;
- } else if (emotion === angry) {
- return happy;
- }
-}
-
-function emotionElement(emotion) {
- return document.querySelector(`.slogan.${emotion}`);
-}
-
-function emotionElementHeight(emotion) {
- return emotionElement(emotion).clientHeight;
-}
-
-function setBodyPaddingForEmotion(emotion) {
- document.body.style.paddingTop = `${emotionElementHeight(emotion)}px`;
-}
-
-const sloganEmotionKey = "sloganEmotion";
-
-const savedEmotion = localStorage.getItem(sloganEmotionKey) ?? happy;
-if (savedEmotion !== happy && savedEmotion !== angry) {
- console.error(`Invalid saved emotion: ${savedEmotion}`);
-}
-
-setBodyPaddingForEmotion(savedEmotion);
-setTimeout(() => {
- document.body.style.transition = "padding-top 1s";
-})
-
-/** @type {HTMLDivElement} */
-const sloganContainer = document.querySelector(".slogan-container")
-
-setTimeout(() => {
- sloganContainer.dataset.sloganEmotion = savedEmotion;
- setBodyPaddingForEmotion(savedEmotion);
-}, 500);
-
-for (const emotion of [happy, angry]) {
- emotionElement(emotion).addEventListener("click", () => {
- const opposite = emotionOpposite(emotion);
- localStorage.setItem(sloganEmotionKey, opposite);
- sloganContainer.dataset.sloganEmotion = opposite;
- setBodyPaddingForEmotion(opposite);
- });
-}
-
-
-document.addEventListener("DOMContentLoaded", async () => {
- console.log("Try to fetch GitHub project(number: 2) of crupest.");
-
- const todoMessage = document.getElementById("todo-message");
- const todoContainer = document.getElementById("todo-container");
-
- const res = await fetch("/api/todos");
- const body = await res.json();
-
- if (res.status !== 200) {
- todoMessage.style.color = "red";
- todoMessage.textContent =
- "Failed to fetch TODOs. (Maybe due to rate limit. Please try later.)";
- console.log(
- `Failed to get GitHub project info. Status: ${res.status}. Body: ${body}`
- );
- } else {
- body.forEach((item) => {
- const { status, title, color } = item;
- const li = document.createElement("li");
- const statusSpan = document.createElement("span");
- const titleSpan = document.createElement("span");
- statusSpan.textContent = status;
- statusSpan.style.color = color;
- titleSpan.textContent = title;
- li.appendChild(statusSpan);
- li.append(" : ");
- li.append(titleSpan);
- todoContainer.appendChild(li);
- });
-
- todoMessage.parentElement.removeChild(todoMessage);
- }
-});
diff --git a/docker/crupest-nginx/sites/www/src/main.ts b/docker/crupest-nginx/sites/www/src/main.ts
new file mode 100644
index 0000000..17d7e12
--- /dev/null
+++ b/docker/crupest-nginx/sites/www/src/main.ts
@@ -0,0 +1,96 @@
+import "./style.css";
+
+import { fetchTodos } from "./todos";
+
+const happy = "happy" as const;
+const angry = "angry" as const;
+type Emotion = typeof happy | typeof angry;
+
+function emotionOpposite(emotion: Emotion): Emotion {
+ if (emotion === happy) {
+ return angry;
+ } else {
+ return happy;
+ }
+}
+
+function emotionElement(emotion: Emotion): HTMLDivElement {
+ return document.querySelector<HTMLDivElement>(`.slogan.${emotion}`)!;
+}
+
+function emotionElementHeight(emotion: Emotion): number {
+ return emotionElement(emotion).clientHeight;
+}
+
+function updateBodyTopPadding(emotion: Emotion): void {
+ document.body.style.paddingTop = `${emotionElementHeight(emotion)}px`;
+}
+
+const sloganEmotionKey = "sloganEmotion";
+
+const savedEmotion =
+ (localStorage.getItem(sloganEmotionKey) as Emotion | null) ?? happy;
+if (savedEmotion !== happy && savedEmotion !== angry) {
+ console.error(`Invalid saved emotion: ${savedEmotion}`);
+}
+
+updateBodyTopPadding(savedEmotion);
+// Then we add transition animation.
+setTimeout(() => {
+ document.body.style.transition = "padding-top 1s";
+});
+
+const sloganContainer = document.querySelector(
+ ".slogan-container"
+) as HTMLDivElement;
+
+setTimeout(() => {
+ sloganContainer.dataset.sloganEmotion = savedEmotion;
+}, 500);
+
+const sloganLoadedPromise = new Promise<void>((resolve) => {
+ setTimeout(() => {
+ resolve();
+ }, 1500);
+});
+
+for (const emotion of [happy, angry]) {
+ emotionElement(emotion).addEventListener("click", () => {
+ const opposite = emotionOpposite(emotion);
+ localStorage.setItem(sloganEmotionKey, opposite);
+ sloganContainer.dataset.sloganEmotion = opposite;
+ updateBodyTopPadding(opposite);
+ });
+}
+
+async function loadTodos(syncWith: Promise<unknown>): Promise<void> {
+ const todoMessage = document.getElementById("todo-message")!;
+ const todoContainer = document.getElementById("todo-container")!;
+
+ try {
+ const todosPromise = fetchTodos();
+ await syncWith; // Let's wait this first.
+ const todos = await todosPromise;
+ todos.forEach((item) => {
+ const { status, title, closed } = item;
+ const li = document.createElement("li");
+ li.dataset.status = closed ? "closed" : "open";
+ // The color from api server is kind of ugly at present.
+ // li.style.background = color;
+ const statusSpan = document.createElement("span");
+ const titleSpan = document.createElement("span");
+ statusSpan.textContent = status;
+ titleSpan.textContent = title;
+ li.appendChild(statusSpan);
+ li.append(" : ");
+ li.append(titleSpan);
+ todoContainer.appendChild(li);
+ });
+ todoMessage.parentElement!.removeChild(todoMessage);
+ } catch (e) {
+ todoMessage.style.color = "red";
+ todoMessage.textContent = (e as Error).message;
+ }
+}
+
+loadTodos(sloganLoadedPromise);
diff --git a/docker/crupest-nginx/sites/www/src/mock-todos.ts b/docker/crupest-nginx/sites/www/src/mock-todos.ts
new file mode 100644
index 0000000..aacb40e
--- /dev/null
+++ b/docker/crupest-nginx/sites/www/src/mock-todos.ts
@@ -0,0 +1,126 @@
+/** Grabbed at Tue, 18 Jul 2023 15:30:05 GMT, used as mock data. 🍻 */
+
+const todos = [
+ {
+ status: "Done",
+ title: "All BLOCKed by graduate paper.",
+ closed: true,
+ color: "green",
+ },
+ {
+ status: "Done",
+ title: "Slogan is not completely visible on phone.",
+ closed: true,
+ color: "green",
+ },
+ {
+ status: "Todo",
+ title: "Users api.",
+ closed: false,
+ color: "blue",
+ },
+ {
+ status: "Todo",
+ title: "Secrets api.",
+ closed: false,
+ color: "blue",
+ },
+ {
+ status: "Todo",
+ title: "Refactor aio python scripts.",
+ closed: false,
+ color: "blue",
+ },
+ {
+ status: "Todo",
+ title: "Nginx path redirection.",
+ closed: false,
+ color: "blue",
+ },
+ {
+ status: "Todo",
+ title: "Make services optional.",
+ closed: false,
+ color: "blue",
+ },
+ {
+ status: "Done",
+ title: "Optimize code-server.",
+ closed: true,
+ color: "green",
+ },
+ {
+ status: "Todo",
+ title: "No more alpine.",
+ closed: false,
+ color: "blue",
+ },
+ {
+ status: "Done",
+ title: "No netease music.",
+ closed: true,
+ color: "green",
+ },
+ {
+ status: "Done",
+ title: "Draft issue status in www TODOs.",
+ closed: true,
+ color: "green",
+ },
+ {
+ status: "Done",
+ title: "Re-bootstrap front end.",
+ closed: true,
+ color: "green",
+ },
+ {
+ status: "Todo",
+ title: "Clean react imports for new jsx usage.",
+ closed: false,
+ color: "blue",
+ },
+ {
+ status: "Done",
+ title: "i18next backend bug.",
+ closed: true,
+ color: "green",
+ },
+ {
+ status: "Done",
+ title: "Organize buttons.",
+ closed: true,
+ color: "green",
+ },
+ {
+ status: "Done",
+ title: "Fix dialog typo.",
+ closed: true,
+ color: "green",
+ },
+ {
+ status: "Todo",
+ title: "Organize OperationDialog.",
+ closed: false,
+ color: "blue",
+ },
+ {
+ status: "Todo",
+ title: "New palette api.",
+ closed: false,
+ color: "blue",
+ },
+ {
+ status: "Todo",
+ title: "No Docker!!!",
+ closed: false,
+ color: "blue",
+ },
+ {
+ status: "Done",
+ title: "Improve animation of slogan.",
+ closed: true,
+ color: "green",
+ },
+];
+
+export default todos;
diff --git a/docker/crupest-nginx/sites/www/src/style.css b/docker/crupest-nginx/sites/www/src/style.css
index f23bbc0..6937051 100644
--- a/docker/crupest-nginx/sites/www/src/style.css
+++ b/docker/crupest-nginx/sites/www/src/style.css
@@ -8,6 +8,11 @@ body {
box-sizing: border-box;
}
+.h-note {
+ font-size: 0.6em;
+ color: gray;
+}
+
@keyframes article-enter {
from {
opacity: 0;
@@ -20,17 +25,28 @@ body {
}
}
-#main-article {
- padding: 0 1em;
- animation: article-enter 1s;
+:root {
+ --main-article-horizontal-padding: 1em;
+ --main-article-horizontal-margin-shrink: -1em;
+
+ --im-happy: dodgerblue;
+ --im-angry: orangered;
+ --im-good: green;
+ --im-active: blue;
}
@media (min-width: 576px) {
- #main-article {
- padding: 0 2em;
+ :root {
+ --main-article-horizontal-padding: 2em;
+ --main-article-horizontal-margin-shrink: -2em;
}
}
+#main-article {
+ padding: 0 var(--main-article-horizontal-padding);
+ animation: article-enter 1s;
+}
+
.slogan-container {
width: 100%;
position: fixed;
@@ -48,11 +64,11 @@ body {
}
.slogan.happy {
- background-color: dodgerblue;
+ background-color: var(--im-happy);
}
.slogan.angry {
- background-color: orangered;
+ background-color: var(--im-angry);
}
.slogan-container[data-slogan-emotion="happy"] .slogan.happy {
@@ -70,6 +86,58 @@ body {
font-size: 1.2em;
}
+#todo-container {
+ list-style: none;
+ margin-block: 0;
+ padding-inline: 0;
+}
+
+#todo-container li {
+ margin: 0 var(--main-article-horizontal-margin-shrink);
+ padding: 0.6em 3em;
+ color: white;
+}
+
+@keyframes todo-enter-left {
+ from {
+ opacity: 0;
+ transform: translateX(-100%);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+@keyframes todo-enter-right {
+ from {
+ opacity: 0;
+ transform: translateX(100%);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+#todo-container li[data-status="closed"] {
+ background-color: var(--im-good);
+}
+
+#todo-container li[data-status="open"] {
+ background-color: var(--im-active);
+}
+
+#todo-container li:nth-child(odd) {
+ animation: todo-enter-left 1s;
+}
+
+#todo-container li:nth-child(even) {
+ animation: todo-enter-right 1s;
+}
+
.friend-link {
display: inline-block;
}
diff --git a/docker/crupest-nginx/sites/www/src/todos.ts b/docker/crupest-nginx/sites/www/src/todos.ts
new file mode 100644
index 0000000..5e36875
--- /dev/null
+++ b/docker/crupest-nginx/sites/www/src/todos.ts
@@ -0,0 +1,29 @@
+export interface Todo {
+ status: string;
+ title: string;
+ closed: boolean;
+ color: string;
+}
+
+export async function fetchTodos(): Promise<Todo[]> {
+ console.log("Try to fetch TODOs from server.");
+
+ if (process.env.NODE_ENV !== "production") {
+ console.log("YaY! We are developers. 🍻 Use mock TODOs. (After 2s)");
+ // await new Promise((resolve) => setTimeout(resolve, 2000));
+ return (await import("./mock-todos")).default;
+ } else {
+ const res = await fetch("/api/todos");
+ const body: Todo[] = await res.json();
+
+ if (res.status !== 200) {
+ console.error(
+ `Failed to get TODOs. Status: ${res.status}. Body: ${body}`
+ );
+ throw new Error(
+ "Failed to fetch TODOs. (Maybe due to rate limit. Please try later.)"
+ );
+ }
+ return body;
+ }
+}
diff --git a/docker/crupest-nginx/sites/www/tsconfig.json b/docker/crupest-nginx/sites/www/tsconfig.json
new file mode 100644
index 0000000..9d1434c
--- /dev/null
+++ b/docker/crupest-nginx/sites/www/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "@tsconfig/recommended/tsconfig.json",
+ "compilerOptions": {
+ "lib": [
+ "ESNext",
+ "DOM",
+ "DOM.Iterable"
+ ],
+ "types": [
+ "parcel-env"
+ ],
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true
+ }
+} \ No newline at end of file