From f804d55ce2227e407d005ecaac06c015420523a0 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Thu, 26 Feb 2026 17:55:52 +0100 Subject: [PATCH] [chore] used codex to create a base layout for src --- src/app/dom.ts | 21 +++++ src/app/init.ts | 33 ++++++++ src/features/countdown/api.ts | 26 ++++++ src/features/countdown/controller.ts | 48 +++++++++++ src/features/countdown/types.ts | 21 +++++ src/features/countdown/view.ts | 77 +++++++++++++++++ src/main.ts | 22 +---- src/shared/tauri/invoke.ts | 8 ++ src/shared/time/format.ts | 17 ++++ src/styles.css | 118 +-------------------------- src/styles/base.css | 117 ++++++++++++++++++++++++++ src/styles/countdown.css | 43 ++++++++++ 12 files changed, 416 insertions(+), 135 deletions(-) create mode 100644 src/app/dom.ts create mode 100644 src/app/init.ts create mode 100644 src/features/countdown/api.ts create mode 100644 src/features/countdown/controller.ts create mode 100644 src/features/countdown/types.ts create mode 100644 src/features/countdown/view.ts create mode 100644 src/shared/tauri/invoke.ts create mode 100644 src/shared/time/format.ts create mode 100644 src/styles/base.css create mode 100644 src/styles/countdown.css diff --git a/src/app/dom.ts b/src/app/dom.ts new file mode 100644 index 0000000..045e8b1 --- /dev/null +++ b/src/app/dom.ts @@ -0,0 +1,21 @@ +export type GreetDom = { + form: HTMLFormElement; + input: HTMLInputElement; + message: HTMLElement; +}; + +export function getMainContainer(): HTMLElement | null { + return document.querySelector(".container"); +} + +export function getGreetDom(): GreetDom | null { + const form = document.querySelector("#greet-form"); + const input = document.querySelector("#greet-input"); + const message = document.querySelector("#greet-msg"); + + if (!form || !input || !message) { + return null; + } + + return { form, input, message }; +} diff --git a/src/app/init.ts b/src/app/init.ts new file mode 100644 index 0000000..73c01c4 --- /dev/null +++ b/src/app/init.ts @@ -0,0 +1,33 @@ +import { initCountdownController } from "../features/countdown/controller"; +import { invokeCommand } from "../shared/tauri/invoke"; +import { getGreetDom, getMainContainer } from "./dom"; + +async function greet(name: string, messageEl: HTMLElement): Promise { + try { + const message = await invokeCommand("greet", { name }); + messageEl.textContent = message; + } catch (error) { + messageEl.textContent = `greet error: ${String(error)}`; + } +} + +function bindGreetForm(): void { + const greetDom = getGreetDom(); + if (!greetDom) { + return; + } + + greetDom.form.addEventListener("submit", (event) => { + event.preventDefault(); + void greet(greetDom.input.value, greetDom.message); + }); +} + +export async function initApp(): Promise { + bindGreetForm(); + + const container = getMainContainer(); + if (container) { + await initCountdownController(container); + } +} diff --git a/src/features/countdown/api.ts b/src/features/countdown/api.ts new file mode 100644 index 0000000..3679728 --- /dev/null +++ b/src/features/countdown/api.ts @@ -0,0 +1,26 @@ +import { invokeCommand } from "../../shared/tauri/invoke"; +import type { CountdownCommand, CountdownSnapshotDto } from "./types"; + +export async function fetchCountdownSnapshot(): Promise { + return invokeCommand("countdown_snapshot"); +} + +async function invokeCountdownCommand(command: CountdownCommand): Promise { + await invokeCommand(command); +} + +export async function startCountdown(): Promise { + await invokeCountdownCommand("countdown_start"); +} + +export async function pauseCountdown(): Promise { + await invokeCountdownCommand("countdown_pause"); +} + +export async function resumeCountdown(): Promise { + await invokeCountdownCommand("countdown_resume"); +} + +export async function resetCountdown(): Promise { + await invokeCountdownCommand("countdown_reset"); +} diff --git a/src/features/countdown/controller.ts b/src/features/countdown/controller.ts new file mode 100644 index 0000000..025d1d9 --- /dev/null +++ b/src/features/countdown/controller.ts @@ -0,0 +1,48 @@ +import { + fetchCountdownSnapshot, + pauseCountdown, + resetCountdown, + resumeCountdown, + startCountdown, +} from "./api"; +import { createCountdownView } from "./view"; + +export async function initCountdownController(container: HTMLElement): Promise { + const view = createCountdownView(container); + + const refreshSnapshot = async (): Promise => { + try { + const snapshot = await fetchCountdownSnapshot(); + view.setSnapshot(snapshot); + } catch (error) { + view.setError(`snapshot error: ${String(error)}`); + } + }; + + const runAction = async (action: () => Promise): Promise => { + try { + await action(); + await refreshSnapshot(); + } catch (error) { + view.setError(`command error: ${String(error)}`); + } + }; + + view.onStart(() => { + void runAction(startCountdown); + }); + view.onPause(() => { + void runAction(pauseCountdown); + }); + view.onResume(() => { + void runAction(resumeCountdown); + }); + view.onReset(() => { + void runAction(resetCountdown); + }); + view.onRefresh(() => { + void refreshSnapshot(); + }); + + await refreshSnapshot(); +} diff --git a/src/features/countdown/types.ts b/src/features/countdown/types.ts new file mode 100644 index 0000000..d3edc7c --- /dev/null +++ b/src/features/countdown/types.ts @@ -0,0 +1,21 @@ +export type CountdownState = "Idle" | "Running" | "Paused" | "Finished"; + +export type CountdownDuration = { + secs: number; + nanos: number; +}; + +export type CountdownSnapshotDto = { + id: number; + label: string; + duration: CountdownDuration; + state: CountdownState; + start_epoch_ms: number | null; + target_epoch_ms: number | null; +}; + +export type CountdownCommand = + | "countdown_start" + | "countdown_pause" + | "countdown_resume" + | "countdown_reset"; diff --git a/src/features/countdown/view.ts b/src/features/countdown/view.ts new file mode 100644 index 0000000..38a5903 --- /dev/null +++ b/src/features/countdown/view.ts @@ -0,0 +1,77 @@ +import { formatDuration } from "../../shared/time/format"; +import type { CountdownSnapshotDto } from "./types"; + +export type CountdownView = { + onStart: (handler: () => void) => void; + onPause: (handler: () => void) => void; + onResume: (handler: () => void) => void; + onReset: (handler: () => void) => void; + onRefresh: (handler: () => void) => void; + setSnapshot: (snapshot: CountdownSnapshotDto) => void; + setError: (message: string) => void; +}; + +function makeButton(label: string): HTMLButtonElement { + const button = document.createElement("button"); + button.type = "button"; + button.textContent = label; + return button; +} + +export function createCountdownView(container: HTMLElement): CountdownView { + const panel = document.createElement("section"); + panel.className = "countdown-panel"; + + const title = document.createElement("h2"); + title.textContent = "Countdown Controls"; + + const actions = document.createElement("div"); + actions.className = "countdown-actions"; + + const startButton = makeButton("Start"); + const pauseButton = makeButton("Pause"); + const resumeButton = makeButton("Resume"); + const resetButton = makeButton("Reset"); + const refreshButton = makeButton("Refresh"); + + actions.append(startButton, pauseButton, resumeButton, resetButton, refreshButton); + + const summary = document.createElement("p"); + summary.className = "countdown-summary"; + summary.textContent = "Waiting for snapshot..."; + + const error = document.createElement("p"); + error.className = "countdown-error"; + + const snapshot = document.createElement("pre"); + snapshot.className = "countdown-snapshot"; + + panel.append(title, actions, summary, error, snapshot); + container.appendChild(panel); + + return { + onStart(handler) { + startButton.addEventListener("click", handler); + }, + onPause(handler) { + pauseButton.addEventListener("click", handler); + }, + onResume(handler) { + resumeButton.addEventListener("click", handler); + }, + onReset(handler) { + resetButton.addEventListener("click", handler); + }, + onRefresh(handler) { + refreshButton.addEventListener("click", handler); + }, + setSnapshot(value) { + error.textContent = ""; + summary.textContent = `State: ${value.state} | Remaining: ${formatDuration(value.duration)}`; + snapshot.textContent = JSON.stringify(value, null, 2); + }, + setError(message) { + error.textContent = message; + }, + }; +} diff --git a/src/main.ts b/src/main.ts index 4783341..f321e1a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,22 +1,6 @@ -import { invoke } from "@tauri-apps/api/core"; - -let greetInputEl: HTMLInputElement | null; -let greetMsgEl: HTMLElement | null; - -async function greet() { - if (greetMsgEl && greetInputEl) { - // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ - greetMsgEl.textContent = await invoke("greet", { - name: greetInputEl.value, - }); - } -} +import "./styles.css"; +import { initApp } from "./app/init"; window.addEventListener("DOMContentLoaded", () => { - greetInputEl = document.querySelector("#greet-input"); - greetMsgEl = document.querySelector("#greet-msg"); - document.querySelector("#greet-form")?.addEventListener("submit", (e) => { - e.preventDefault(); - greet(); - }); + void initApp(); }); diff --git a/src/shared/tauri/invoke.ts b/src/shared/tauri/invoke.ts new file mode 100644 index 0000000..980ffab --- /dev/null +++ b/src/shared/tauri/invoke.ts @@ -0,0 +1,8 @@ +import { invoke } from "@tauri-apps/api/core"; + +export async function invokeCommand( + command: string, + payload?: Record +): Promise { + return invoke(command, payload); +} diff --git a/src/shared/time/format.ts b/src/shared/time/format.ts new file mode 100644 index 0000000..a117ef5 --- /dev/null +++ b/src/shared/time/format.ts @@ -0,0 +1,17 @@ +type DurationLike = { + secs?: number; + nanos?: number; +}; + +export function formatDuration(value: DurationLike): string { + const totalSeconds = Math.max(0, Math.floor(value.secs ?? 0)); + const hours = Math.floor(totalSeconds / 3600) + .toString() + .padStart(2, "0"); + const minutes = Math.floor((totalSeconds % 3600) / 60) + .toString() + .padStart(2, "0"); + const seconds = (totalSeconds % 60).toString().padStart(2, "0"); + + return `${hours}:${minutes}:${seconds}`; +} diff --git a/src/styles.css b/src/styles.css index 7011746..05495d8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,116 +1,2 @@ -.logo.vite:hover { - filter: drop-shadow(0 0 2em #747bff); -} - -.logo.typescript:hover { - filter: drop-shadow(0 0 2em #2d79c7); -} -:root { - font-family: Inter, Avenir, Helvetica, Arial, sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 400; - - color: #0f0f0f; - background-color: #f6f6f6; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; -} - -.container { - margin: 0; - padding-top: 10vh; - display: flex; - flex-direction: column; - justify-content: center; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: 0.75s; -} - -.logo.tauri:hover { - filter: drop-shadow(0 0 2em #24c8db); -} - -.row { - display: flex; - justify-content: center; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} - -a:hover { - color: #535bf2; -} - -h1 { - text-align: center; -} - -input, -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - color: #0f0f0f; - background-color: #ffffff; - transition: border-color 0.25s; - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); -} - -button { - cursor: pointer; -} - -button:hover { - border-color: #396cd8; -} -button:active { - border-color: #396cd8; - background-color: #e8e8e8; -} - -input, -button { - outline: none; -} - -#greet-input { - margin-right: 5px; -} - -@media (prefers-color-scheme: dark) { - :root { - color: #f6f6f6; - background-color: #2f2f2f; - } - - a:hover { - color: #24c8db; - } - - input, - button { - color: #ffffff; - background-color: #0f0f0f98; - } - button:active { - background-color: #0f0f0f69; - } -} +@import "./styles/base.css"; +@import "./styles/countdown.css"; diff --git a/src/styles/base.css b/src/styles/base.css new file mode 100644 index 0000000..c64afbf --- /dev/null +++ b/src/styles/base.css @@ -0,0 +1,117 @@ +.logo.vite:hover { + filter: drop-shadow(0 0 2em #747bff); +} + +.logo.typescript:hover { + filter: drop-shadow(0 0 2em #2d79c7); +} + +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + color: #0f0f0f; + background-color: #f6f6f6; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +.container { + margin: 0; + padding-top: 10vh; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: 0.75s; +} + +.logo.tauri:hover { + filter: drop-shadow(0 0 2em #24c8db); +} + +.row { + display: flex; + justify-content: center; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +h1 { + text-align: center; +} + +input, +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + color: #0f0f0f; + background-color: #ffffff; + transition: border-color 0.25s; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); +} + +button { + cursor: pointer; +} + +button:hover { + border-color: #396cd8; +} + +button:active { + border-color: #396cd8; + background-color: #e8e8e8; +} + +input, +button { + outline: none; +} + +#greet-input { + margin-right: 5px; +} + +@media (prefers-color-scheme: dark) { + :root { + color: #f6f6f6; + background-color: #2f2f2f; + } + + a:hover { + color: #24c8db; + } + + input, + button { + color: #ffffff; + background-color: #0f0f0f98; + } + + button:active { + background-color: #0f0f0f69; + } +} diff --git a/src/styles/countdown.css b/src/styles/countdown.css new file mode 100644 index 0000000..d93afba --- /dev/null +++ b/src/styles/countdown.css @@ -0,0 +1,43 @@ +.countdown-panel { + margin: 2rem auto 0; + max-width: 780px; + width: min(90vw, 780px); + text-align: left; +} + +.countdown-actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 0.75rem 0; +} + +.countdown-summary { + margin: 0.5rem 0; +} + +.countdown-error { + min-height: 1.25rem; + color: #b00020; + margin: 0.25rem 0; +} + +.countdown-snapshot { + margin: 0; + padding: 0.75rem; + border-radius: 8px; + border: 1px solid rgba(0, 0, 0, 0.15); + background: rgba(255, 255, 255, 0.65); + overflow: auto; +} + +@media (prefers-color-scheme: dark) { + .countdown-error { + color: #ff8a80; + } + + .countdown-snapshot { + border-color: rgba(255, 255, 255, 0.25); + background: rgba(15, 15, 15, 0.6); + } +}