summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMaksymilian Jopek <maks@jopek.eu>2025-04-28 18:03:28 +0200
committerMaksymilian Jopek <maks@jopek.eu>2025-04-28 18:03:28 +0200
commit0539d6963ebedd554cd3cd36b6c94df9659d6a6b (patch)
tree9408c7928d0725b114d0adfa811ba2bb3859f495 /src
downloadrustberry-frontend-master.tar.gz
rustberry-frontend-master.tar.zst
rustberry-frontend-master.zip
Initial commitHEADmaster
Simple frontend version with mock API
Diffstat (limited to 'src')
-rw-r--r--src/components/Button.module.css29
-rw-r--r--src/components/Button.tsx57
-rw-r--r--src/components/Timer.module.css63
-rw-r--r--src/components/Timer.tsx178
-rw-r--r--src/helpers/api.ts103
-rw-r--r--src/index.css6
-rw-r--r--src/index.tsx17
-rw-r--r--src/pages/*.tsx8
-rw-r--r--src/pages/index.module.css14
-rw-r--r--src/pages/index.tsx33
-rw-r--r--src/vite.env.d.ts2
11 files changed, 510 insertions, 0 deletions
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<string>();
+
+ 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 (
+ <button
+ on:click={onClick}
+ class={styles.btn}
+ style={{ "background-color": bgColor() }}
+ >
+ {
+ //btnText()
+ }
+ {props.name}
+ </button>
+ );
+}
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<number | "">("");
+
+ 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 (
+ <div class={styles.cont}>
+ <h2>{props.title}</h2>
+
+ <div class={styles.customOption}>
+ <input
+ type="number"
+ value={customValue()}
+ on:input={(e) =>
+ setCustomValue(e.target.value ? parseFloat(e.target.value) : "")
+ }
+ disabled={disabled()}
+ min="1"
+ />
+ <div class={styles.units}>
+ <For each={TIME_UNITS}>
+ {(u) => (
+ <span
+ on:click={() => (disabled() ? null : setUnit(u))}
+ classList={{
+ [styles.selectedUnit]: unit() === u,
+ [styles.notSelectedUnit]: unit() !== u,
+ [styles.darkgray]: disabled() === true,
+ }}
+ >
+ {u}
+ </span>
+ )}
+ </For>
+ </div>
+ <button
+ class={styles.customOptionBtn}
+ on:click={startStopClick}
+ disabled={
+ disabled()
+ ? false
+ : customValue() === "" || (customValue() as number) <= 0
+ }
+ style={{ "background-color": disabled() ? "red" : "green" }}
+ >
+ {disabled() ? "Wyłącz" : "Włącz"}
+ </button>
+ {/*<Show when={disabled() === true}>*/}
+ <For each={props.extraStopActions}>
+ {(act) => (
+ <button
+ disabled={disabled() === false}
+ on:click={[extraStopClick, act]}
+ class={styles.extraStopBtn}
+ >
+ {act.title}
+ </button>
+ )}
+ </For>
+ {/*</Show>*/}
+ </div>
+
+ <div class={styles.timeLeft}>
+ <Switch>
+ <Match when={onTill() === null}>
+ <span>Wyłączone</span>
+ <h2> </h2>
+ </Match>
+ <Match when={onTill() > 0}>
+ <span>{props.secondsLeftText} </span>
+ <h2>{formattedTime()}</h2>
+ </Match>
+ </Switch>
+ </div>
+
+ <div class={styles.options}>
+ <For each={props.options}>
+ {(option) => (
+ <button
+ class={styles.option}
+ on:click={[optionClick, option]}
+ style={{ "background-color": props.backgroundColor }}
+ disabled={disabled()}
+ >
+ {option.time} {option.unit}
+ </button>
+ )}
+ </For>
+ </div>
+ </div>
+ );
+}
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<Button[]> {
+ 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<Timer[]> {
+ 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(
+ () => (
+ <Router root={(props) => <Suspense>{props.children}</Suspense>}>
+ {routes}
+ </Router>
+ ),
+ 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 (
+ <section>
+ <h1>404: Not Found</h1>
+ <p>It's gone 😞</p>
+ </section>
+ );
+}
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 (
+ <>
+ <div
+ class={styles.btnsCont}
+ style={{ "--rows-count": Math.floor(btns()?.length / 2) }}
+ >
+ <For each={btns()}>{(btn) => <Button {...btn} />}</For>
+ </div>
+ <div class={styles.timersCont}>
+ <For each={timers()}>
+ {(timer) => (
+ <>
+ <hr />
+ <Timer {...timer} />
+ </>
+ )}
+ </For>
+ </div>
+ </>
+ );
+}
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 @@
+/// <reference types="vite/client" />
+/// <reference types="vite-plugin-pages/client-solid" />