aboutsummaryrefslogtreecommitdiff
path: root/docker/nginx/sites/www/src
diff options
context:
space:
mode:
Diffstat (limited to 'docker/nginx/sites/www/src')
-rw-r--r--docker/nginx/sites/www/src/main.ts97
-rw-r--r--docker/nginx/sites/www/src/mock-todos.ts126
-rw-r--r--docker/nginx/sites/www/src/style.css185
-rw-r--r--docker/nginx/sites/www/src/todos.ts29
4 files changed, 437 insertions, 0 deletions
diff --git a/docker/nginx/sites/www/src/main.ts b/docker/nginx/sites/www/src/main.ts
new file mode 100644
index 0000000..2f09deb
--- /dev/null
+++ b/docker/nginx/sites/www/src/main.ts
@@ -0,0 +1,97 @@
+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, index) => {
+ const { status, title, closed } = item;
+ const li = document.createElement("li");
+ li.dataset.status = closed ? "closed" : "open";
+ li.style.animationDelay = `${index * 0.04}s`;
+ // 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/nginx/sites/www/src/mock-todos.ts b/docker/nginx/sites/www/src/mock-todos.ts
new file mode 100644
index 0000000..aacb40e
--- /dev/null
+++ b/docker/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/nginx/sites/www/src/style.css b/docker/nginx/sites/www/src/style.css
new file mode 100644
index 0000000..1f9c9ed
--- /dev/null
+++ b/docker/nginx/sites/www/src/style.css
@@ -0,0 +1,185 @@
+html {
+ width: 100%;
+ line-height: 1.5;
+ font-family: ui-sans-serif;
+}
+
+body {
+ width: 100%;
+ margin: 0;
+ box-sizing: border-box;
+}
+
+a.mono {
+ font-family: ui-monospace;
+}
+
+.h-note {
+ font-size: 0.6em;
+ color: gray;
+}
+
+@keyframes article-enter {
+ from {
+ opacity: 0;
+ transform: translateY(100px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+:root {
+ --main-article-horizontal-padding: 1em;
+ --main-article-horizontal-margin-shrink: -1em;
+
+ --im-me: deepskyblue;
+ --im-happy: dodgerblue;
+ --im-angry: orangered;
+ --im-good: hsl(120, 85%, 85%);
+ --im-active: hsl(20, 85%, 85%);
+}
+
+@media (min-width: 576px) {
+ :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;
+}
+
+#title {
+ font-size: 2em;
+}
+
+@keyframes title-name-enter {
+ from {
+ background-color: white;
+ }
+
+ to {
+ background-color: var(--im-me);
+ }
+}
+
+#title-name {
+ font-family: ui-monospace;
+ border-radius: 8px;
+ background-color: white;
+ animation: title-name-enter 3s 2s forwards;
+}
+
+@keyframes avatar-enter {
+ from {
+ opacity: 0;
+ transform: translateX(calc(100% + var(--main-article-horizontal-padding)));
+ }
+
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+#avatar {
+ float: right;
+ transform: translateX(calc(100% + var(--main-article-horizontal-padding)));
+ animation: avatar-enter 0.5s 1s forwards;
+}
+
+.slogan-container {
+ width: 100%;
+ position: fixed;
+ z-index: 1;
+ top: 0;
+}
+
+.slogan {
+ width: 100%;
+ padding: 0.5em 1em;
+ text-align: center;
+ box-sizing: border-box;
+ position: absolute;
+ transform: translateY(-100%);
+ transition: transform 1s;
+}
+
+.slogan.happy {
+ background-color: var(--im-happy);
+}
+
+.slogan.angry {
+ background-color: var(--im-angry);
+}
+
+.slogan-container[data-slogan-emotion="happy"] .slogan.happy {
+ transform: translateY(0);
+}
+
+.slogan-container[data-slogan-emotion="angry"] .slogan.angry {
+ transform: translateY(0);
+}
+
+.slogan-text {
+ display: inline-block;
+ text-align: initial;
+ color: white;
+ font-size: 1.2em;
+}
+
+#todo-container {
+ list-style: none;
+ margin-block: 0;
+ padding-inline: 0;
+}
+
+@keyframes todo-enter {
+ from {
+ opacity: 0;
+ transform: translateX(-100%);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+#todo-container li {
+ margin: 0 var(--main-article-horizontal-margin-shrink);
+ padding: 0.25em 3em;
+ transform: translateX(-100%);
+ animation: todo-enter 1s forwards;
+}
+
+#todo-container li[data-status="closed"] {
+ background-color: var(--im-good);
+}
+
+#todo-container li[data-status="open"] {
+ background-color: var(--im-active);
+}
+
+.friend-link {
+ display: inline-block;
+}
+
+.friend-img {
+ display: block;
+ width: 80px;
+ height: 80px;
+ object-fit: cover;
+ border-radius: 50%;
+}
+
+.friend-name {
+ display: block;
+ text-align: center;
+ font-size: 1.2em;
+}
diff --git a/docker/nginx/sites/www/src/todos.ts b/docker/nginx/sites/www/src/todos.ts
new file mode 100644
index 0000000..b69f524
--- /dev/null
+++ b/docker/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;
+ }
+}