From 2082e0c7bcf25dafca794d28e47f5188b74d287c Mon Sep 17 00:00:00 2001 From: matiasc18 Date: Wed, 18 Mar 2026 17:36:03 -0400 Subject: [PATCH] Signup/Login + styling adjustments - Modified SearchBar styling - Modified TopBar to display blank circle if the user has no networks (previously missing image icon) - Refactored Login into AuthPage.js - AuthPage contains a tab selector for switching between Signup and Login - Both Login/Signup send the request and either receive an auth_token or an error message - If auth_token, user will be logged in as usual, in both cases - Signup validates user input before sending request - Added /signup target in vite config file --- src/Home/AuthPage/AuthPage.js | 83 ++++++++++++++++ src/Home/AuthPage/Login.js | 87 ++++++++++++++++ src/Home/AuthPage/Signup.js | 182 ++++++++++++++++++++++++++++++++++ src/Home/Login.js | 82 --------------- src/components/SearchBar.js | 4 +- src/components/TopBar.js | 35 ++++--- src/index.js | 4 +- vite.config.ts | 4 + 8 files changed, 382 insertions(+), 99 deletions(-) create mode 100644 src/Home/AuthPage/AuthPage.js create mode 100644 src/Home/AuthPage/Login.js create mode 100644 src/Home/AuthPage/Signup.js delete mode 100644 src/Home/Login.js diff --git a/src/Home/AuthPage/AuthPage.js b/src/Home/AuthPage/AuthPage.js new file mode 100644 index 0000000..d845a99 --- /dev/null +++ b/src/Home/AuthPage/AuthPage.js @@ -0,0 +1,83 @@ +import { Preferences } from '@capacitor/preferences'; +import util from "../../util.js" +import "./Login.js"; +import "./Signup.js" + +class AuthPage extends Shadow { + inputStyles(el) { + return el + .background("var(--main)") + .color("var(--text)") + .border("1px solid var(--accent)") + .fontSize(0.9, rem) + .backgroundColor("var(--searchbackground)") + .borderRadius(12, px) + .outline("none") + .onTouch((start) => { + if(start) { + this.style.backgroundColor = "var(--accent)" + } else { + this.style.backgroundColor = "var(--searchbackground)" + } + }) + } + + selectedPage = 2 // 1 == login, 2 == signup + + render() { + VStack(() => { + img(window.matchMedia('(prefers-color-scheme: dark)').matches ? "/_/icons/columnwhite.svg" : "/_/icons/logo.svg", window.isMobile() ? "5vmax" : "3vmax") + .marginTop(5, em) + .marginLeft(2, em) + .onClick((done) => { + window.navigateTo("/") + }) + + HStack(() => { + p("Login") + .fontWeight(this.selectedPage === 1 ? "bold" : "normal") + .padding(0.75, em) + .borderRadius(12, px) + .background(this.selectedPage === 1 ? "var(--darkaccent)" : "transparent") + .onTap(() => { + this.selectedPage = 1; + this.rerender(); + }) + + p("Signup") + .fontWeight(this.selectedPage === 2 ? "bold" : "normal") + .padding(0.75, em) + .borderRadius(12, px) + .background(this.selectedPage === 2 ? "var(--darkaccent)" : "transparent") + .onTap(() => { + this.selectedPage = 2; + this.rerender(); + }) + }) + .padding(0.25, em) + .borderRadius(12, px) + .background("var(--accent)") + .color("var(--text)") + .horizontalAlign("center") + .margin("auto") + .marginTop(1, em) + .marginBottom(0, em) + .gap(0.5, em) + + switch (this.selectedPage) { + case 1: + Login() + break; + case 2: + Signup() + break; + } + + }) + .width(100, vw) + .height(100, vh) + .margin(0) + } +} + +register(AuthPage) \ No newline at end of file diff --git a/src/Home/AuthPage/Login.js b/src/Home/AuthPage/Login.js new file mode 100644 index 0000000..57738a0 --- /dev/null +++ b/src/Home/AuthPage/Login.js @@ -0,0 +1,87 @@ +import { Preferences } from '@capacitor/preferences'; +import util from "../../util.js" + +class Login extends Shadow { + inputStyles(el) { + return el + .background("var(--main)") + .color("var(--text)") + .border("1px solid var(--accent)") + .fontSize(0.9, rem) + .backgroundColor("var(--searchbackground)") + .borderRadius(12, px) + .outline("none") + .onTouch((start) => { + if (start) { + this.style.backgroundColor = "var(--accent)" + } else { + this.style.backgroundColor = "var(--searchbackground)" + } + }) + } + + render() { + form(() => { + VStack(() => { + input("Email", "70vw") + .attr({ name: "email", type: "email" }) + .margin("auto") + .marginVertical(1, em) + .padding(1, em) + .styles(this.inputStyles) + input("Password", "70vw") + .attr({ name: "password", type: "password" }) + .margin("auto") + .marginVertical(1, em) + .padding(1, em) + .styles(this.inputStyles) + HStack(() => { + button("==>") + .padding(1, em) + .fontSize(0.9, rem) + .borderRadius(12, px) + .background("var(--searchbackground)") + .color("var(--text)") + .border("1px solid var(--accent)") + .boxSizing("border-box") + }) + .width(70, vw) + .margin("auto") + .fontSize(0.9, rem) + .paddingLeft(0, em) + .paddingRight(2, em) + .marginVertical(1, em) + .border("1px solid transparent") + }) + }) + .height(100, pct) + .onSubmit(async (e) => { + e.preventDefault(); + const data = new FormData(e.target); + await this.requestLogin(data); + }) + } + + async requestLogin(data) { + const res = await fetch(`${util.HOST}/login`, { + method: "POST", + headers: { "Content-Type": "application/json", "X-Client": "mobile" }, + body: JSON.stringify({ + email: data.get("email"), + password: data.get("password") + }) + }); + + if (res.ok) { + const { token } = await res.json(); + await Preferences.set({ key: 'auth_token', value: token }); + global.onLogin(); + } else { + const { error } = await res.json(); + this.errorMessage = error; + console.error(error) + } + } +} + +register(Login) \ No newline at end of file diff --git a/src/Home/AuthPage/Signup.js b/src/Home/AuthPage/Signup.js new file mode 100644 index 0000000..e79a9d9 --- /dev/null +++ b/src/Home/AuthPage/Signup.js @@ -0,0 +1,182 @@ +import { Preferences } from '@capacitor/preferences'; +import util from "../../util.js" + +class Signup extends Shadow { + inputStyles(el) { + return el + .background("var(--main)") + .color("var(--text)") + .border("1px solid var(--accent)") + .fontSize(0.9, rem) + .backgroundColor("var(--searchbackground)") + .borderRadius(12, px) + .outline("none") + .onTouch((start) => { + if (start) { + this.style.backgroundColor = "var(--accent)" + } else { + this.style.backgroundColor = "var(--searchbackground)" + } + }) + } + + errorMessage = "" + + render() { + form(() => { + VStack(() => { + input("Email", "70vw") + .attr({ name: "email", type: "email" }) + .margin("auto") + .marginVertical(1, em) + .padding(1, em) + .styles(this.inputStyles) + input("First Name", "70vw") + .attr({ name: "firstName", type: "text" }) + .margin("auto") + .marginVertical(1, em) + .padding(1, em) + .styles(this.inputStyles) + input("Last Name", "70vw") + .attr({ name: "lastName", type: "text" }) + .margin("auto") + .marginVertical(1, em) + .padding(1, em) + .styles(this.inputStyles) + input("Password", "70vw") + .attr({ name: "password", type: "password" }) + .margin("auto") + .marginVertical(1, em) + .padding(1, em) + .styles(this.inputStyles) + input("Confirm Password", "70vw") + .attr({ name: "confirmPassword", type: "password" }) + .margin("auto") + .marginVertical(1, em) + .padding(1, em) + .styles(this.inputStyles) + HStack(() => { + button("==>") + .padding(1, em) + .fontSize(0.9, rem) + .borderRadius(12, px) + .background("var(--searchbackground)") + .color("var(--text)") + .border("1px solid var(--accent)") + .boxSizing("border-box") + }) + .width(70, vw) + .margin("auto") + .fontSize(0.9, rem) + .paddingLeft(0, em) + .paddingRight(2, em) + .marginVertical(1, em) + .border("1px solid transparent") + }) + }) + .height(100, pct) + .onSubmit(async (e) => { + e.preventDefault(); + const data = new FormData(e.target); + if (this.verifyInput(data)) { + this.errorMessage = ""; + await this.requestSignup(data); + } else { + console.log(this.errorMessage) + } + }) + } + + async requestSignup(data) { + const res = await fetch(`${util.HOST}/signup`, { + method: "POST", + headers: { "Content-Type": "application/json", "X-Client": "mobile" }, + body: JSON.stringify({ + firstName: data.get("firstName"), + lastName: data.get("lastName"), + email: data.get("email"), + password: data.get("password") + }) + }); + + if (res.ok) { + const { token } = await res.json(); + await Preferences.set({ key: 'auth_token', value: token }); + global.onLogin(); + } else { + const { error } = await res.json(); + this.errorMessage = error; + console.error(this.errorMessage) + } + } + + verifyInput(data) { + const firstName = data.get("firstName"); + const lastName = data.get("lastName"); + const email = data.get("email"); + const password = data.get("password"); + const confirmPassword = data.get("confirmPassword"); + + if (!firstName || firstName.trim() === "") { + this.errorMessage = "First name is required."; + return false + } else if (firstName.trim().length < 2) { + this.errorMessage = "First name must be at least 2 characters."; + return false + } else if (!/^[a-zA-Z\s'-]+$/.test(firstName.trim())) { + this.errorMessage = "First name contains invalid characters."; + return false + } + + if (!lastName || lastName.trim() === "") { + this.errorMessage = "Last name is required."; + return false + } else if (lastName.trim().length < 2) { + this.errorMessage = "Last name must be at least 2 characters."; + return false + } else if (!/^[a-zA-Z\s'-]+$/.test(lastName.trim())) { + this.errorMessage = "Last name contains invalid characters."; + return false + } + + if (!email || email.trim() === "") { + this.errorMessage = "Email is required."; + return false + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) { + this.errorMessage = "Please enter a valid email address."; + return false + } + + if (!password) { + this.errorMessage = "Password is required."; + return false + } else if (password.length < 8) { + this.errorMessage = "Password must be at least 8 characters."; + return false + } else if (!/[A-Z]/.test(password)) { + this.errorMessage = "Password must contain at least one uppercase letter."; + return false + } else if (!/[a-z]/.test(password)) { + this.errorMessage = "Password must contain at least one lowercase letter."; + return false + } else if (!/[0-9]/.test(password)) { + this.errorMessage = "Password must contain at least one number."; + return false + } else if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + this.errorMessage = "Password must contain at least one special character."; + return false + } + + if (!confirmPassword) { + this.errorMessage = "Please confirm your password."; + return false + } else if (confirmPassword !== password) { + this.errorMessage = "Passwords do not match."; + return false + } + + return true; + } +} + +register(Signup) \ No newline at end of file diff --git a/src/Home/Login.js b/src/Home/Login.js deleted file mode 100644 index 7d2d412..0000000 --- a/src/Home/Login.js +++ /dev/null @@ -1,82 +0,0 @@ -import { Preferences } from '@capacitor/preferences'; -import util from "../util.js" - -class Login extends Shadow { - inputStyles(el) { - return el - .background("var(--main)") - .color("var(--text)") - .border("1px solid var(--accent)") - .fontSize(0.9, rem) - .backgroundColor("var(--searchbackground)") - .borderRadius(12, px) - .outline("none") - .onTouch((start) => { - if(start) { - this.style.backgroundColor = "var(--accent)" - } else { - this.style.backgroundColor = "var(--searchbackground)" - } - }) - } - - render() { - ZStack(() => { - img(window.matchMedia('(prefers-color-scheme: dark)').matches ? "/_/icons/columnwhite.svg" : "/_/icons/logo.svg", window.isMobile() ? "5vmax" : "3vmax") - .position("absolute") - .top(5, em) - .left(2, em) - .onClick((done) => { - window.navigateTo("/") - }) - - form(() => { - input("Email", "70vw") - .attr({name: "email", type: "email"}) - .margin(1, em).padding(1, em) - .styles(this.inputStyles) - input("Password", "70vw") - .attr({name: "password", type: "password"}) - .margin(1, em).padding(1, em) - .styles(this.inputStyles) - button("==>") - .margin(1, em).padding(1, em) - .fontSize(0.9, rem) - .borderRadius(12, px) - .background("var(--searchbackground)") - .color("var(--text)") - .border("1px solid var(--accent)") - }) - .x(50, vw).y(50, vh) - .center() - .onSubmit(async (e) => { - e.preventDefault(); - const data = new FormData(e.target); - - const res = await fetch(`${util.HOST}/login`, { - method: "POST", - headers: { "Content-Type": "application/json", "X-Client": "mobile" }, - body: JSON.stringify({ - email: data.get("email"), - password: data.get("password") - }) - }); - - if (res.ok) { - const { token } = await res.json(); - await Preferences.set({ key: 'auth_token', value: token }); - global.onLogin(); - } else { - const { error } = await res.json(); - console.error(error) - } - }) - }) - .background("rgb(69, 20, 13)") - .width(100, vw) - .height(100, pct) - .margin(0) - } -} - -register(Login) \ No newline at end of file diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index cf1f433..e25595a 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -1,6 +1,6 @@ css(` searchbar- input::placeholder { - color: #5C504D + color: #5C504D; } `) @@ -36,7 +36,7 @@ class SearchBar extends Shadow { .paddingVertical(0.75, em) .boxSizing("border-box") .paddingHorizontal(1, em) - .background("var(--darkaccent)") + .background("var(--searchbackground)") .color("var(--accent)") .marginBottom(1, em) .border("1px solid var(--accent)") diff --git a/src/components/TopBar.js b/src/components/TopBar.js index 8021f8b..93584d1 100644 --- a/src/components/TopBar.js +++ b/src/components/TopBar.js @@ -3,19 +3,28 @@ import util from "../util.js" class TopBar extends Shadow { render() { HStack(() => { - img(`${util.HOST}/db/images/${global.currentNetwork.logo}`, "2.5em", "2.5em") - .borderRadius("50", pct) - .objectFit("cover") - .padding(0.3, em) - .background("var(--accent)") - .onTouch(function (start) { - if(start) { - this.style.scale = "0.8" - } else if(start === false) { - this.style.scale = "" - $("sidebar-").toggle() - } - }) + if (global.currentNetwork.logo) { + img(`${util.HOST}/db/images/${global.currentNetwork.logo}`, "2.5em", "2.5em") + .borderRadius("50", pct) + .objectFit("cover") + .padding(0.3, em) + .background("var(--accent)") + .onTouch(function (start) { + if(start) { + this.style.scale = "0.8" + } else if(start === false) { + this.style.scale = "" + $("sidebar-").toggle() + } + }) + } else { + HStack(() => { }) + .height(2.5, em) + .width(2.5, em) + .padding(0.3, em) + .background("var(--accent)") + .borderRadius("50", pct) + } p(global.currentApp()) .color("var(--headertext)") diff --git a/src/index.js b/src/index.js index 01b1016..9cf2834 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ import Socket from "/_/code/ws/Socket.js" import "./Home/Home.js" -import "./Home/Login.js" +import "./Home/AuthPage/AuthPage.js" import "./Home/ConnectionError.js" import util from "./util.js" @@ -155,7 +155,7 @@ let Global = class { this.getProfile().then(async (status) => { if (status === 401) { - Login() + AuthPage() } else if(status === 500) { ConnectionError() } else { diff --git a/vite.config.ts b/vite.config.ts index 50cd158..74905fb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -20,6 +20,10 @@ export default defineConfig({ target: "http://localhost:10002", changeOrigin: true }, + "/signup": { + target: "http://localhost:10002", + changeOrigin: true + }, "/profile": { target: "http://localhost:10002", changeOrigin: true