summaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
authoraltaf-creator <dev@altafcreator.com>2025-11-29 12:48:14 +0700
committeraltaf-creator <dev@altafcreator.com>2025-11-29 12:48:14 +0700
commitb107add5afb23a710e3297249e314eeee6bf0563 (patch)
treeaa37a07ce05ef24ebbb73728871d9ae33b94079c /frontend
parent3e0e77713e73b03e98bdfeb9bf8bd9cfa1b6c88e (diff)
a lot of progress, frontend, a lot of new backend methods
Diffstat (limited to 'frontend')
-rw-r--r--frontend/dryer_clothes.pngbin0 -> 36448 bytes
-rw-r--r--frontend/dryer_off.pngbin0 -> 31935 bytes
-rw-r--r--frontend/dryer_on.pngbin0 -> 41171 bytes
-rw-r--r--frontend/index.html28
-rw-r--r--frontend/main.js235
-rw-r--r--frontend/start/index.html84
-rw-r--r--frontend/status/index.html60
-rw-r--r--frontend/style.css233
-rw-r--r--frontend/timer/index.html36
-rw-r--r--frontend/washer_clothes.pngbin0 -> 37298 bytes
-rw-r--r--frontend/washer_off.pngbin0 -> 32719 bytes
-rw-r--r--frontend/washer_on.pngbin0 -> 40206 bytes
12 files changed, 655 insertions, 21 deletions
diff --git a/frontend/dryer_clothes.png b/frontend/dryer_clothes.png
new file mode 100644
index 0000000..babfc36
--- /dev/null
+++ b/frontend/dryer_clothes.png
Binary files differ
diff --git a/frontend/dryer_off.png b/frontend/dryer_off.png
new file mode 100644
index 0000000..9919a01
--- /dev/null
+++ b/frontend/dryer_off.png
Binary files differ
diff --git a/frontend/dryer_on.png b/frontend/dryer_on.png
new file mode 100644
index 0000000..7851dd5
--- /dev/null
+++ b/frontend/dryer_on.png
Binary files differ
diff --git a/frontend/index.html b/frontend/index.html
index deceae5..0eeb0b0 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+ <link rel="stylesheet" href="./style.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>frontend</title>
+ <title>Victoria Hall LaundryWeb</title>
<script src="https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js" defer></script>
<script>
window.OneSignalDeferred = window.OneSignalDeferred || [];
@@ -19,7 +19,27 @@
</script>
</head>
<body>
- <button id="notbtn"></button>
- <script src="main.js"></script>
+ <div class="section-container row bg-1" style="height: 164px;">
+ <span id="logo">Victoria Hall<br>LaundryWeb</span>
+ <span id="logo-id">H1</span>
+ </div>
+ <div class="section-container row bg-2" style="padding: 8px; gap: 8px;">
+ <button class="button button-tab bg-3" disabled>Timer</button>
+ <button class="button button-tab bg-3" onclick="window.location.href = '/status/'">Status</button>
+ </div>
+ <div class="section-container no-pad">
+ <span>Loading...</span>
+ </div>
+ <script src="/main.js"></script>
+ <script>
+ (async () => {
+ const result = await checkUserStatus();
+ if (result != "you got laundry") {
+ window.location.href = './start/';
+ } else {
+ window.location.href = './timer/';
+ }
+ })();
+ </script>
</body>
</html>
diff --git a/frontend/main.js b/frontend/main.js
index 51041fe..4f47473 100644
--- a/frontend/main.js
+++ b/frontend/main.js
@@ -1,23 +1,224 @@
+// --- begin, important constants
+const data = {
+ duration: 1, // will be multiplied by 30
+ block: 1,
+ machine: 2,
+}
+
+const API_URL = "http://localhost:8000"
+
+// --- permission request
const btn = document.getElementById("notbtn")
-btn.addEventListener("click", () => requestPermission())
-function requestPermission() {
- console.log("Requesting permission...");
- OneSignal.Notifications.requestPermission();
+if (btn) {
+ btn.addEventListener("click", () => requestPermission())
+ function requestPermission() {
+ console.log("Requesting permission...");
+ OneSignal.Notifications.requestPermission();
+ }
}
-document.cookie = "session_key=0"
+// --- check user status
+// returns true if there exists a timer, and returns false otherwise.
+// cookie need to be included.
+async function checkUserStatus() {
+ const response = await fetch(`${API_URL}/check`, {
+ credentials: "include",
+ method: "POST",
+ });
+ return await response.text();
+}
-const data = {
- duration: 2,
- block: 1,
- machine: 2,
+// --- check machine status
+// returns a 2d array representing machines
+// []: root array
+// []: block
+// enum: machine status
+async function checkMachineStatus() {
+ const response = await fetch(`${API_URL}/status`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({block: 1}),
+ });
+ return await response.json();
}
-//fetch("http://localhost:8000/start", {
-// credentials: "include",
-// method: "POST",
-// headers: {
-// "Content-Type": "application/json"
-// },
-// body: JSON.stringify(data)
-//});
+// --- timer duration
+const minField = document.getElementById("min-field");
+const decBtn = document.getElementById("decTime");
+function increaseTime() {
+ data.duration += 1;
+ minField.innerText = (30 * data.duration).toString() + " minutes";
+ decBtn.disabled = false;
+}
+
+function decreaseTime() {
+ data.duration -= 1;
+ data.duration = Math.max(data.duration, 1);
+ if (data.duration == 1) {
+ decBtn.disabled = true;
+ }
+ minField.innerText = (30 * data.duration).toString() + " minutes";
+}
+
+// --- starting a timer
+function start() {
+ fetch(`${API_URL}/start`, {
+ credentials: "include",
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify(data)
+ }).then(response => response.text())
+ .then(data => {
+ console.log(data);
+ if (data == "all good bro timer started") {
+ window.location.href = "/timer/";
+ }
+ });
+}
+
+
+// ------ page specific -----
+
+// ---- machine status page
+
+// --- machines visual
+async function startUpdateMachines() {
+ updateMachines();
+
+ while (true) {
+ await delay(60 * 1000);
+
+ await updateMachines();
+ }
+}
+
+async function updateMachines() {
+ const dryer1 = document.getElementById("dryer1-img");
+ const washer1 = document.getElementById("washer1-img");
+ const dryer2 = document.getElementById("dryer2-img");
+ const washer2 = document.getElementById("washer2-img");
+
+ const machine_imgs = [dryer1, washer1, dryer2, washer2];
+
+ const dryer1txt = document.getElementById("dryer1-span");
+ const washer1txt = document.getElementById("washer1-span");
+ const dryer2txt = document.getElementById("dryer2-span");
+ const washer2txt = document.getElementById("washer2-span");
+
+ const machine_txts = [dryer1txt, washer1txt, dryer2txt, washer2txt];
+
+ const status = await checkMachineStatus();
+ console.log(status);
+
+ for (let i = 0; i < status[0].length; i++) {
+ if (status[0][i] == "RUNNING") {
+ if ((i + 1) % 2 == 0) {
+ machine_imgs[i].src = "/washer_on.png";
+ } else {
+ machine_imgs[i].src = "/dryer_on.png";
+ }
+ const now = Date.now();
+ const start = Date.parse(status[1][i]);
+ machine_txts[i].innerHTML = Math.ceil(((start + (status[2][i] * 60000)) - now) / 60000).toString() + " min(s) left";
+ } else if (status[0][i] == "FINISHED") {
+ if ((i + 1) % 2 == 0) {
+ machine_imgs[i].src = "/washer_clothes.png";
+ } else {
+ machine_imgs[i].src = "/dryer_clothes.png";
+ }
+ machine_txts[i].innerHTML = "Idle"
+ } else {
+ if ((i + 1) % 2 == 0) {
+ machine_imgs[i].src = "/washer_off.png";
+ } else {
+ machine_imgs[i].src = "/dryer_off.png";
+ }
+ machine_txts[i].innerHTML = "Idle"
+ }
+ }
+}
+
+// --- current timers
+async function startLoadTimers() {
+ const timers = await fetchTimers();
+
+ const container = document.getElementById("timer-container")
+
+ const textList = []
+ const progList = []
+ const endTimestamps = []
+
+ for (let i = 0; i < timers.length; i++) {
+ container.innerHTML += `
+ <div class="section-container no-pad">
+ <h1>Timer #${(i + 1).toString()}</h1>
+ <div class="machine-container">
+ <div class="txtcol-dryer ${timers[i]["machine"] == 1 ? "machine-selected" : ""}">
+ <span>Dryer 1</span>
+ <img src="/dryer_${timers[i]["machine"] == 1 ? "on" : "off"}.png" alt="">
+ </div>
+ <div class="txtcol-washer ${timers[i]["machine"] == 2 ? "machine-selected" : ""}">
+ <span>Washer 1</span>
+ <img src="/washer_${timers[i]["machine"] == 2 ? "on" : "off"}.png" alt="">
+ </div>
+ <div class="txtcol-dryer ${timers[i]["machine"] == 3 ? "machine-selected" : ""}">
+ <span>Dryer 2</span>
+ <img src="/dryer_${timers[i]["machine"] == 3 ? "on" : "off"}.png" alt="">
+ </div>
+ <div class="txtcol-washer ${timers[i]["machine"] == 4 ? "machine-selected" : ""}">
+ <span>Washer 2</span>
+ <img src="/washer_${timers[i]["machine"] == 4 ? "on" : "off"}.png" alt="">
+ </div>
+ </div>
+ <div class="timer-container">
+ <span id="timer-txt-${i}">15:00</span>
+ <div class="prog-container">
+ <div class="prog" id="timer-prog-${i}"></div>
+ </div>
+ </div>
+ <button class="button bg-1" disabled>I have collected my laundry!</button>
+ </div>
+ `
+
+ textList.push(`timer-txt-${i}`);
+ progList.push(`timer-prog-${i}`);
+ endTimestamps.push(Date.parse(timers[i]["start_time"]) + timers[i]["duration"] * 60000);
+ }
+
+ console.log(textList);
+ console.log(endTimestamps);
+
+ while (true) {
+ for (let i = 0; i < textList.length; i++) {
+ const text = document.getElementById(textList[i]);
+ text.innerText = "";
+ const timeRemaining = Math.max(Math.round((endTimestamps[i] - Date.now()) / 1000), 0);
+ const hours = Math.floor(timeRemaining / 3600);
+ const minutes = Math.floor(timeRemaining / 60);
+ const seconds = timeRemaining % 60;
+ if (hours > 0) text.innerText += hours.toString().padStart(2, '0') + ':';
+ text.innerText += minutes.toString().padStart(2, '0') + ':';
+ text.innerText += seconds.toString().padStart(2, '0');
+
+ const prog = document.getElementById(progList[i]);
+ prog.style.width = ((timeRemaining / (timers[i]["duration"] * 60)) * 100).toString() + "%";
+ }
+ await delay(1000);
+ }
+}
+
+async function fetchTimers() {
+ const response = await fetch(`${API_URL}/laundry`, {
+ method: "POST",
+ credentials: "include",
+ });
+ return await response.json();
+}
+
+const delay = (durationMs) => {
+ return new Promise(resolve => setTimeout(resolve, durationMs));
+}
diff --git a/frontend/start/index.html b/frontend/start/index.html
new file mode 100644
index 0000000..7567ad5
--- /dev/null
+++ b/frontend/start/index.html
@@ -0,0 +1,84 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <link rel="stylesheet" href="/style.css">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Victoria Hall LaundryWeb</title>
+ <script src="https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js" defer></script>
+ <script>
+ window.OneSignalDeferred = window.OneSignalDeferred || [];
+ OneSignalDeferred.push(async function(OneSignal) {
+ await OneSignal.init({
+ appId: "83901cc7-d964-475a-90ec-9f854df4ba52",
+ welcomeNotification: {
+ disable: true,
+ },
+ });
+ });
+ </script>
+ </head>
+ <body>
+ <div class="section-container row bg-1" style="height: 164px;">
+ <span id="logo">Victoria Hall<br>LaundryWeb</span>
+ <span id="logo-id">H1</span>
+ </div>
+ <div class="section-container row bg-2" style="padding: 8px; gap: 8px;">
+ <button class="button button-tab bg-3" disabled>Timer</button>
+ <button class="button button-tab bg-3" onclick="window.location.href = '/status/'">Status</button>
+ </div>
+ <div class="section-container no-pad" style="opacity: .5;">
+ <h1>Selected Machine</h1>
+ <div class="machine-container">
+ <div class="txtcol-dryer" id="dryer1">
+ <span>Dryer 1</span>
+ <img src="/dryer_off.png" alt="" id="dryer1-img">
+ <span id="dryer1-span"></span>
+ </div>
+ <div class="txtcol-washer machine-selected" id="washer1">
+ <span>Washer 1</span>
+ <img src="/washer_off.png" alt="" id="washer1-img">
+ <span id="washer1-span"></span>
+ </div>
+ <div class="txtcol-dryer" id="dryer2">
+ <span>Dryer 2</span>
+ <img src="/dryer_off.png" alt="" id="dryer2-img">
+ <span id="dryer2-span"></span>
+ </div>
+ <div class="txtcol-washer" id="washer2">
+ <span>Washer 2</span>
+ <img src="/washer_off.png" alt="" id="washer2-img">
+ <span id="washer2-span"></span>
+ </div>
+ </div>
+ </div>
+ <div class="section-container no-pad">
+ <h1>Duration</h1>
+ <div class="input-field">
+ <button class="button bg-3" onclick="decreaseTime()" id="decTime" disabled>-</button>
+ <span id="min-field">30 minutes</span>
+ <button class="button bg-3" onclick="increaseTime()" id="incTime">+</button>
+ </div>
+ </div>
+ <div class="section-container no-pad">
+ <h1>Start</h1>
+ <div class="section-container row bg-red">
+ <div class="flex-center-container">
+ <span class="icon">🔔</span>
+ </div>
+ <div>
+ <span>Please allow this website to send you notifications to remind you about your laundry. That's like, the whole point of this website.</span>
+ <button id="notbtn" class="button bg-redder">Enable Required Permissions</button>
+ </div>
+ </div>
+ <button class="button bg-1" id="startbtn">Start</button>
+ </div>
+ <script src="/main.js"></script>
+ <script>
+ document.getElementById("startbtn").addEventListener("click", () => {
+ start();
+ });
+ startUpdateMachines();
+ </script>
+ </body>
+</html>
diff --git a/frontend/status/index.html b/frontend/status/index.html
new file mode 100644
index 0000000..61a2e06
--- /dev/null
+++ b/frontend/status/index.html
@@ -0,0 +1,60 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <link rel="stylesheet" href="/style.css">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Victoria Hall LaundryWeb</title>
+ <script src="https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js" defer></script>
+ <script>
+ window.OneSignalDeferred = window.OneSignalDeferred || [];
+ OneSignalDeferred.push(async function(OneSignal) {
+ await OneSignal.init({
+ appId: "83901cc7-d964-475a-90ec-9f854df4ba52",
+ welcomeNotification: {
+ disable: true,
+ },
+ });
+ });
+ </script>
+ </head>
+ <body>
+ <style>
+ .machine-container > div > img {
+ padding: 6px;
+ }
+ </style>
+ <div class="section-container row bg-1" style="height: 164px;">
+ <span id="logo">Victoria Hall<br>LaundryWeb</span>
+ <span id="logo-id">H1</span>
+ </div>
+ <div class="section-container no-pad">
+ <div class="machine-container">
+ <div class="txtcol-dryer" id="dryer1">
+ <span>Dryer 1</span>
+ <img src="/dryer_off.png" alt="" id="dryer1-img">
+ <span id="dryer1-span"></span>
+ </div>
+ <div class="txtcol-washer" id="washer1">
+ <span>Washer 1</span>
+ <img src="/washer_off.png" alt="" id="washer1-img">
+ <span id="washer1-span"></span>
+ </div>
+ <div class="txtcol-dryer" id="dryer2">
+ <span>Dryer 2</span>
+ <img src="/dryer_off.png" alt="" id="dryer2-img">
+ <span id="dryer2-span"></span>
+ </div>
+ <div class="txtcol-washer" id="washer2">
+ <span>Washer 2</span>
+ <img src="/washer_off.png" alt="" id="washer2-img">
+ <span id="washer2-span"></span>
+ </div>
+ </div>
+ </div>
+ <script src="/main.js"></script>
+ <script>
+ startUpdateMachines();
+ </script>
+ </body>
+</html>
diff --git a/frontend/style.css b/frontend/style.css
new file mode 100644
index 0000000..5713576
--- /dev/null
+++ b/frontend/style.css
@@ -0,0 +1,233 @@
+:root {
+ --col-1: #93B6B1;
+ --col-2: #E8DEB6;
+ --col-3: #B3C9AA;
+ --col-red-bg: #FFD0D0;
+ --col-red-fg: #A33939;
+ --col-washer: #666a83;
+ --col-dryer: #7c89a0;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ gap: 16px;
+ padding: 16px;
+ font-family: sans-serif;
+}
+
+.section-container {
+ border-radius: 32px;
+ padding: 24px;
+ max-width: 512px;
+ width: 100%;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 16px;
+}
+
+.section-container > span {
+ align-self: center;
+}
+
+.section-container > div {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.section-container > h1 {
+ margin: 0;
+}
+
+.no-pad {
+ padding: 0;
+}
+
+.row {
+ flex-direction: row !important;
+}
+
+.flex-center-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.button {
+ width: 100%;
+ height: 48px;
+ border-radius: 48px;
+ border: none;
+ cursor: pointer;
+}
+
+.button:hover:not(:disabled) {
+ box-shadow: inset 0 0 0 3px #00000033;
+}
+
+.button:active:not(:disabled) {
+ opacity: .5;
+}
+
+.button:disabled {
+ cursor: default !important;
+ color: black !important;
+ opacity: .5;
+}
+
+.button-tab:not(:disabled) {
+ background-color: transparent;
+}
+
+.button-tab:disabled {
+ opacity: 1 !important;
+}
+
+.button-tab:hover {
+ background-color: var(--col-3);
+}
+
+.bg-1 {
+ background-color: var(--col-1);
+}
+
+.bg-2 {
+ background-color: var(--col-2);
+}
+
+.bg-3 {
+ background-color: var(--col-3);
+}
+
+.bg-red {
+ background-color: var(--col-red-bg);
+}
+
+.bg-redder {
+ background-color: var(--col-red-fg);
+ color: white;
+}
+
+.txtcol-washer {
+ color: var(--col-washer);
+}
+
+.txtcol-dryer {
+ color: var(--col-dryer);
+}
+
+.icon {
+ margin: 16px;
+ margin-left: 0;
+ font-size: 3rem;
+}
+
+.machine-container {
+ display: flex;
+ flex-direction: row !important;
+ width: 100%;
+}
+
+.machine-container > div {
+ flex: 1;
+}
+
+.machine-container > div > span {
+ text-align: center;
+ width: 100%;
+ display: block;
+ height: 1rem;
+}
+
+.machine-container > div > img {
+ width: 100%;
+ display: block;
+ padding: 11px;
+ box-sizing: border-box;
+ margin-top: 6px;
+ margin-bottom: 6px;
+ border-radius: 16px;
+ overflow: visible;
+}
+
+.machine-selected {
+ font-weight: bold;
+}
+
+.machine-selected > img {
+ background-color: var(--col-2);
+}
+
+.input-field {
+ display: flex;
+ flex-direction: row !important;
+ flex-wrap: nowrap;
+ background-color: var(--col-2);
+ width: 272px;
+ margin-left: auto;
+ margin-right: auto;
+ padding: 8px;
+ box-sizing: border-box;
+ align-items: center;
+ border-radius: 36px;
+}
+
+.input-field > span {
+ flex: 1;
+ text-align: center;
+}
+
+.input-field > button {
+ height: 48px;
+ width: 56px;
+ border-radius: 64px;
+}
+
+.timer-container {
+ height: 124px;
+ background-color: var(--col-2);
+ position: relative;
+ border-radius: 32px;
+ overflow: hidden;
+}
+
+.timer-container > span {
+ font-weight: bolder;
+ font-size: 4.1rem;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, calc(-50% - 8px));
+}
+
+.timer-container > .prog-container {
+ position: absolute;
+ bottom: 0;
+}
+
+.prog-container {
+ width: 100%;
+ height: 16px;
+ background: #00000011;
+}
+
+.prog {
+ height: 100%;
+ width: 50%;
+ background-color: var(--col-1);
+}
+
+#logo-id {
+ font-size: 4rem;
+ margin: 0;
+ font-weight: 900;
+}
+
+#logo {
+ font-size: 2rem;
+}
diff --git a/frontend/timer/index.html b/frontend/timer/index.html
new file mode 100644
index 0000000..6405843
--- /dev/null
+++ b/frontend/timer/index.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <link rel="stylesheet" href="/style.css">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Victoria Hall LaundryWeb</title>
+ <script src="https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js" defer></script>
+ <script>
+ window.OneSignalDeferred = window.OneSignalDeferred || [];
+ OneSignalDeferred.push(async function(OneSignal) {
+ await OneSignal.init({
+ appId: "83901cc7-d964-475a-90ec-9f854df4ba52",
+ welcomeNotification: {
+ disable: true,
+ },
+ });
+ });
+ </script>
+ </head>
+ <body>
+ <div class="section-container row bg-1" style="height: 164px;">
+ <span id="logo">Victoria Hall<br>LaundryWeb</span>
+ <span id="logo-id">H1</span>
+ </div>
+ <div class="section-container row bg-2" style="padding: 8px; gap: 8px;">
+ <button class="button button-tab bg-3" disabled>Timer</button>
+ <button class="button button-tab bg-3" onclick="window.location.href = '/status/'">Status</button>
+ </div>
+ <div id="timer-container" class="section-container no-pad"></div>
+ <script src="/main.js"></script>
+ <script>
+ startLoadTimers();
+ </script>
+ </body>
+</html>
diff --git a/frontend/washer_clothes.png b/frontend/washer_clothes.png
new file mode 100644
index 0000000..b16688a
--- /dev/null
+++ b/frontend/washer_clothes.png
Binary files differ
diff --git a/frontend/washer_off.png b/frontend/washer_off.png
new file mode 100644
index 0000000..0be7233
--- /dev/null
+++ b/frontend/washer_off.png
Binary files differ
diff --git a/frontend/washer_on.png b/frontend/washer_on.png
new file mode 100644
index 0000000..7b811a4
--- /dev/null
+++ b/frontend/washer_on.png
Binary files differ