From 0539d6963ebedd554cd3cd36b6c94df9659d6a6b Mon Sep 17 00:00:00 2001 From: Maksymilian Jopek Date: Mon, 28 Apr 2025 18:03:28 +0200 Subject: Initial commit Simple frontend version with mock API --- src/components/Button.module.css | 29 +++++++ src/components/Button.tsx | 57 +++++++++++++ src/components/Timer.module.css | 63 ++++++++++++++ src/components/Timer.tsx | 178 +++++++++++++++++++++++++++++++++++++++ src/helpers/api.ts | 103 ++++++++++++++++++++++ src/index.css | 6 ++ src/index.tsx | 17 ++++ src/pages/*.tsx | 8 ++ src/pages/index.module.css | 14 +++ src/pages/index.tsx | 33 ++++++++ src/vite.env.d.ts | 2 + 11 files changed, 510 insertions(+) create mode 100644 src/components/Button.module.css create mode 100644 src/components/Button.tsx create mode 100644 src/components/Timer.module.css create mode 100644 src/components/Timer.tsx create mode 100644 src/helpers/api.ts create mode 100644 src/index.css create mode 100644 src/index.tsx create mode 100644 src/pages/*.tsx create mode 100644 src/pages/index.module.css create mode 100644 src/pages/index.tsx create mode 100644 src/vite.env.d.ts (limited to 'src') diff --git a/src/components/Button.module.css b/src/components/Button.module.css new file mode 100644 index 0000000..7041eaf --- /dev/null +++ b/src/components/Button.module.css @@ -0,0 +1,29 @@ +.btn:last-child { + margin-bottom: 0; +} +.generic-button { + display: grid; + place-items: center; + border: 3px solid #222; + color: white; + border-radius: 15px; + font-size: 1.6em; + width: 100%; + transition: transform 0.05s; + user-select: none; +} +.generic-button:enabled:active { + transform: scale(0.8); + box-shadow: inset 0px 0px 5px 0px black; +} +.generic-button:disabled { + filter: grayscale(0.8); +} +.btn { + composes: generic-button; + --page-size-without-margins: ( + 100svh - (var(--rows-count) + 1) * var(--btn-gap) + ); + --btn-height: calc(var(--page-size-without-margins) / var(--rows-count)); + height: var(--btn-height); +} diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 0000000..031fc73 --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,57 @@ +import styles from "./Button.module.css"; +import { createEffect, createSignal } from "solid-js"; + +enum BtnType { + click = 0, + switch = 1, +} +export type Button = + | { + type: BtnType.click; + name: string; + color?: string; + } + | { + type: BtnType.switch; + name: string; + }; + +export default function Button(props: Button) { + const [value, setValue] = createSignal(false); + const [bgColor, setBgColor] = createSignal(); + + createEffect(() => { + if (props.type === BtnType.click) { + setBgColor(value() ? "pink" : "blue"); + } else { + setBgColor(value() ? "green" : "red"); + } + }); + + function onClick() { + if (props.type === BtnType.click) { + if (value() === true) { + return; + } + setValue(true); + setTimeout(() => { + setValue(false); + }, 3000); + } else { + setValue((old) => !old); + } + } + + return ( + + ); +} diff --git a/src/components/Timer.module.css b/src/components/Timer.module.css new file mode 100644 index 0000000..b748c9e --- /dev/null +++ b/src/components/Timer.module.css @@ -0,0 +1,63 @@ +.cont { + h2 { + margin-top: 0.4em; + margin-bottom: 1rem; + } + input { + width: 2em; + font-size: 1.5em; + padding: 0.3em; + } +} +.custom-option { + display: grid; + align-items: center; + grid-template-columns: 1fr 2fr 1fr; + margin-bottom: 1em; +} +.units { + display: inline-flex; + gap: 1.5em; + span { + font-size: 2em; + } +} +.not-selected-unit { + font-weight: lighter; +} +.selected-unit { + font-weight: bold; + text-decoration: underline; +} +.darkgray { + color: darkgray; +} +.custom-option-btn { + composes: generic-button from "./Button.module.css"; + background-color: green; + height: 2em; +} +.extra-stop-btn { + composes: generic-button from "./Button.module.css"; + background-color: red; + padding: 0.5em; + margin-top: 0.5em; +} + +.time-left { + margin-bottom: 1em; + h2 { + display: inline; + color: lightblue; + } +} + +.options { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; +} +.option { + composes: generic-button from "./Button.module.css"; + height: calc((100vw - 2rem) / 3 / 1.5); +} diff --git a/src/components/Timer.tsx b/src/components/Timer.tsx new file mode 100644 index 0000000..61a98f5 --- /dev/null +++ b/src/components/Timer.tsx @@ -0,0 +1,178 @@ +import { createSignal, For, Match, Show, Switch } from "solid-js"; +import styles from "./Timer.module.css"; + +export const TIME_UNITS = ["s", "m", "h", "d"] as const; +export type Timer = { + title: string; + options: { + time: number; + unit: (typeof TIME_UNITS)[number]; + }[]; + backgroundColor: string; + onTill: number | null; + secondsLeftText: string; + extraStopActions: { title: string }[]; +}; + +export default function Timer(props: Timer) { + const [onTill, setOnTill] = createSignal(props.onTill); + const [unit, setUnit] = createSignal<(typeof TIME_UNITS)[number]>( + TIME_UNITS[1], + ); + const [formattedTime, setFormattedTime] = createSignal(""); + const [disabled, setDisabled] = createSignal(onTill() !== null); + const [customValue, setCustomValue] = createSignal(""); + + let _intervalId = null; + function toggleAutoUpdate() { + if (_intervalId === null) { + updateTime(); + _intervalId = setInterval(updateTime, 1000); + } else { + clearInterval(_intervalId); + _intervalId = null; + } + } + if (props.onTill !== null) { + toggleAutoUpdate(); + } + function updateTime() { + const s = Math.floor(onTill() - Date.now() / 1000); + const days = Math.floor(s / (24 * 3600)); + const hours = Math.floor((s % (24 * 3600)) / 3600); + const minutes = Math.floor((s % 3600) / 60); + const seconds = s % 60; + + let text = ""; + if (days > 0) { + text += days + "d : "; + } + if (hours > 0 || text.length > 0) { + text += hours + "h : "; + } + if (minutes > 0 || text.length > 0) { + text += minutes + "m : "; + } + if (seconds > 0 || text.length > 0) { + text += seconds + "s"; + } + setFormattedTime(text); + } + function startStopClick() { + const val = customValue(); + if (disabled() === false && val !== "") { + optionClick({ time: val, unit: unit() }); + } else if (disabled() === true) { + setDisabled(false); + setOnTill(null); + toggleAutoUpdate(); + } + } + + function optionClick(option: Timer["options"][number]) { + setDisabled(true); + let newOnTill = Math.floor(Date.now() / 1000); + if (option.unit === "s") { + newOnTill += option.time; + } else if (option.unit === "m") { + newOnTill += option.time * 60; + } else if (option.unit === "h") { + newOnTill += option.time * 3600; + } else { + newOnTill += option.time * 3600 * 24; + } + setOnTill(newOnTill); + toggleAutoUpdate(); + } + + function extraStopClick(act: Timer["extraStopActions"][number]) { + // Some api call for the extra + startStopClick(); + } + return ( +
+

{props.title}

+ +
+ + setCustomValue(e.target.value ? parseFloat(e.target.value) : "") + } + disabled={disabled()} + min="1" + /> +
+ + {(u) => ( + (disabled() ? null : setUnit(u))} + classList={{ + [styles.selectedUnit]: unit() === u, + [styles.notSelectedUnit]: unit() !== u, + [styles.darkgray]: disabled() === true, + }} + > + {u} + + )} + +
+ + {/**/} + + {(act) => ( + + )} + + {/**/} +
+ +
+ + + Wyłączone +

+
+ 0}> + {props.secondsLeftText} +

{formattedTime()}

+
+
+
+ +
+ + {(option) => ( + + )} + +
+
+ ); +} diff --git a/src/helpers/api.ts b/src/helpers/api.ts new file mode 100644 index 0000000..112ba2f --- /dev/null +++ b/src/helpers/api.ts @@ -0,0 +1,103 @@ +import { type Button } from "@/components/Button"; +import { Timer } from "@/components/Timer"; + +export async function getButtons(): Promise { + return [ + { + type: 0, + name: "Brama Wjazdowa", + }, + { + type: 0, + name: "Brama Toyota", + }, + { + type: 0, + name: "Brama Passat", + }, + { + type: 1, + name: "Furtka", + }, + { + type: 0, + name: "Plafon Maks", + }, + { + type: 1, + name: "Ciepła woda 52,8°C", + }, + { + type: 1, + name: "Wentylator łazienka", + }, + { + type: 1, + name: "Wentylator Kuchnia Dolna", + }, + { + type: 1, + name: "Pompa źródło", + }, + { + type: 1, + name: "Nawodnienie", + }, + { + type: 1, + name: "Choinka+Astro", + }, + { + type: 1, + name: "Komputer", + }, + { + type: 1, + name: "Oświetlenie Zewnętrzne", + }, + { + type: 1, + name: "Blok. czujki ośw. zewn.", + }, + { + type: 1, + name: "Ogrzewanie 27,5°C", + }, + { + type: 1, + name: "Termometr - restart", + }, + ]; +} + +export async function getTimers(): Promise { + return [ + { + title: "Otwórz bramę wjazową na:", + options: [ + { time: 15, unit: "m" }, + { time: 45, unit: "m" }, + { time: 1, unit: "h" }, + { time: 3, unit: "h" }, + { time: 24, unit: "h" }, + { time: 48, unit: "h" }, + ], + backgroundColor: "#ab03ab", + onTill: Math.floor(Date.now() / 1000) + 3 * 287721, + secondsLeftText: "Otwarte jeszcze przez:", + extraStopActions: [{ title: "Zamknij i wyłącz" }], + }, + { + title: "Włącz ogrzewanie na:", + secondsLeftText: "Włączone jeszcze na:", + options: [ + { time: 1, unit: "h" }, + { time: 1.5, unit: "h" }, + { time: 2, unit: "h" }, + { time: 3, unit: "h" }, + ], + backgroundColor: "#ab03ab", + onTill: null, + }, + ]; +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..f064787 --- /dev/null +++ b/src/index.css @@ -0,0 +1,6 @@ +body { + margin: 0; + background-color: black; + color: white; + padding: 0.3rem; +} diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..de08c81 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,17 @@ +/* @refresh reload */ +import "./index.css"; + +import { Suspense } from "solid-js"; +import { render } from "solid-js/web"; + +import { Router } from "@solidjs/router"; +import routes from "~solid-pages"; + +render( + () => ( + {props.children}}> + {routes} + + ), + document.getElementById("root"), +); diff --git a/src/pages/*.tsx b/src/pages/*.tsx new file mode 100644 index 0000000..a7021c6 --- /dev/null +++ b/src/pages/*.tsx @@ -0,0 +1,8 @@ +export default function NotFound() { + return ( +
+

404: Not Found

+

It's gone 😞

+
+ ); +} diff --git a/src/pages/index.module.css b/src/pages/index.module.css new file mode 100644 index 0000000..9d6167d --- /dev/null +++ b/src/pages/index.module.css @@ -0,0 +1,14 @@ +.btns-cont { + --btn-gap: 0.3rem; + /* padding: var(--btn-gap); */ + display: grid; + justify-items: center; + align-items: center; + gap: var(--btn-gap); + grid-template-columns: repeat(2, 1fr); +} +.timers-cont { + hr { + margin-top: 2em; + } +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx new file mode 100644 index 0000000..d64f9e1 --- /dev/null +++ b/src/pages/index.tsx @@ -0,0 +1,33 @@ +import { For } from "solid-js"; +import { createAsync } from "@solidjs/router"; + +import Button from "@/components/Button"; +import Timer from "@/components/Timer"; +import { getButtons, getTimers } from "@/helpers/api"; +import styles from "./index.module.css"; + +export default function Home() { + const btns = createAsync(getButtons); + const timers = createAsync(getTimers); + + return ( + <> +
+ {(btn) =>
+
+ + {(timer) => ( + <> +
+ + + )} +
+
+ + ); +} diff --git a/src/vite.env.d.ts b/src/vite.env.d.ts new file mode 100644 index 0000000..50d6f23 --- /dev/null +++ b/src/vite.env.d.ts @@ -0,0 +1,2 @@ +/// +/// -- cgit v1.3.1