diff options
Diffstat (limited to 'docker/nginx/sites/www/src')
| -rw-r--r-- | docker/nginx/sites/www/src/main.ts | 97 | ||||
| -rw-r--r-- | docker/nginx/sites/www/src/mock-todos.ts | 126 | ||||
| -rw-r--r-- | docker/nginx/sites/www/src/style.css | 185 | ||||
| -rw-r--r-- | docker/nginx/sites/www/src/todos.ts | 29 | 
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; +  } +} | 
