diff --git a/package.json b/package.json index ca950a4..1761750 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^4.18.2", + "express-useragent": "^2.0.2", "jsonwebtoken": "^9.0.2", "moment": "^2.30.1", "ws": "^8.18.3", diff --git a/server/index.js b/server/index.js index e14544b..f703edc 100644 --- a/server/index.js +++ b/server/index.js @@ -6,6 +6,7 @@ const fs = require('fs'); const chalk = require('chalk'); const moment = require('moment'); const path = require('path'); +const useragent = require("express-useragent"); import "./util.js" import Socket from './ws/ws.js' @@ -112,12 +113,13 @@ class Server { let privateSite = () => { let filePath; + let platformFolder = req.useragent.isMobile ? "mobile" : "desktop" if(url.startsWith("/_")) { filePath = path.join(this.UIPath, url); } else if(url.includes("75820185")) { - filePath = path.join(this.UIPath, "site", url.split("75820185")[1]); + filePath = path.join(this.UIPath, platformFolder, url.split("75820185")[1]); } else { - filePath = path.join(this.UIPath, "site", "index.html"); + filePath = path.join(this.UIPath, platformFolder, "index.html"); } res.sendFile(filePath); @@ -134,10 +136,10 @@ class Server { const formattedDate = moment().format('M.D'); const formattedTime = moment().format('h:mma'); if(req.url.includes("/api/")) { - console.logclean(chalk.blue(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`)); + console.log(chalk.blue(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`)); } else { if(req.url === "/") - console.logclean(chalk.gray(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`)); + console.log(chalk.gray(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`)); } next(); } @@ -146,9 +148,9 @@ class Server { const originalSend = res.send; res.send = function (body) { if(res.statusCode >= 400) { - console.logclean(chalk.blue( `<-${chalk.red(res.statusCode)}- ${req.method} ${req.url} | ${chalk.red(body)}`)); + console.log(chalk.blue( `<-${chalk.red(res.statusCode)}- ${req.method} ${req.url} | ${chalk.red(body)}`)); } else { - console.logclean(chalk.blue(`<-${res.statusCode}- ${req.method} ${req.url}`)); + console.log(chalk.blue(`<-${res.statusCode}- ${req.method} ${req.url}`)); } originalSend.call(this, body); }; @@ -164,6 +166,7 @@ class Server { app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); + app.use(useragent.express()); app.use(this.logRequest); app.use(this.logResponse); @@ -176,16 +179,16 @@ class Server { global.Socket = new Socket(server); const PORT = 3003; server.listen(PORT, () => { - console.logclean("\n") - console.logclean(chalk.yellow("*************** Hyperia ***************")) - console.logclean(chalk.yellowBright(`Server is running on port ${PORT}: http://localhost`)); - console.logclean(chalk.yellow("***************************************")) - console.logclean("\n") + console.log("\n") + console.log(chalk.yellow("*************** Hyperia ***************")) + console.log(chalk.yellowBright(`Server is running on port ${PORT}: http://localhost`)); + console.log(chalk.yellow("***************************************")) + console.log("\n") }); process.on('SIGINT', async () => { - console.logclean(chalk.red('Closing server...')); - console.logclean(chalk.green('Database connection closed.')); + console.log(chalk.red('Closing server...')); + console.log(chalk.green('Database connection closed.')); process.exit(0); }); @@ -193,31 +196,4 @@ class Server { } } -const _log = console.log; - -console.logclean = function (...args) { - _log.call(console, ...args); -} - -// console.log = function (...args) { -// // Get the caller location -// const stack = new Error().stack.split("\n")[2]; -// const match = stack.match(/(\/.*:\d+:\d+)/); -// let location = match ? match[1] : "unknown"; - -// // Remove CWD prefix -// while (location.startsWith("/")) { -// location = location.slice(1); -// } -// location = "/" + location - -// let cwd = process.cwd(); -// if (location.startsWith(cwd)) { -// location = location.slice(cwd.length); -// if (location.startsWith("/")) location = location.slice(1); -// } - -// _log.call(console, `[${location}]`, ...args); -// }; - const server = new Server() \ No newline at end of file diff --git a/ui/_/icons/Column.svg b/ui/_/icons/Column.svg new file mode 100644 index 0000000..ab79e11 --- /dev/null +++ b/ui/_/icons/Column.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/site/apps/Forum/Forum.js b/ui/desktop/apps/Forum/Forum.js similarity index 100% rename from ui/site/apps/Forum/Forum.js rename to ui/desktop/apps/Forum/Forum.js diff --git a/ui/site/apps/Forum/ForumPanel.js b/ui/desktop/apps/Forum/ForumPanel.js similarity index 100% rename from ui/site/apps/Forum/ForumPanel.js rename to ui/desktop/apps/Forum/ForumPanel.js diff --git a/ui/site/apps/Jobs/Jobs.js b/ui/desktop/apps/Jobs/Jobs.js similarity index 100% rename from ui/site/apps/Jobs/Jobs.js rename to ui/desktop/apps/Jobs/Jobs.js diff --git a/ui/site/apps/Jobs/JobsGrid.js b/ui/desktop/apps/Jobs/JobsGrid.js similarity index 100% rename from ui/site/apps/Jobs/JobsGrid.js rename to ui/desktop/apps/Jobs/JobsGrid.js diff --git a/ui/site/apps/Jobs/JobsSidebar.js b/ui/desktop/apps/Jobs/JobsSidebar.js similarity index 100% rename from ui/site/apps/Jobs/JobsSidebar.js rename to ui/desktop/apps/Jobs/JobsSidebar.js diff --git a/ui/site/apps/Market/Market.js b/ui/desktop/apps/Market/Market.js similarity index 100% rename from ui/site/apps/Market/Market.js rename to ui/desktop/apps/Market/Market.js diff --git a/ui/site/apps/Market/MarketGrid.js b/ui/desktop/apps/Market/MarketGrid.js similarity index 100% rename from ui/site/apps/Market/MarketGrid.js rename to ui/desktop/apps/Market/MarketGrid.js diff --git a/ui/site/apps/Market/MarketSidebar.js b/ui/desktop/apps/Market/MarketSidebar.js similarity index 100% rename from ui/site/apps/Market/MarketSidebar.js rename to ui/desktop/apps/Market/MarketSidebar.js diff --git a/ui/site/apps/Messages/Messages.js b/ui/desktop/apps/Messages/Messages.js similarity index 100% rename from ui/site/apps/Messages/Messages.js rename to ui/desktop/apps/Messages/Messages.js diff --git a/ui/site/apps/Messages/MessagesPanel.js b/ui/desktop/apps/Messages/MessagesPanel.js similarity index 100% rename from ui/site/apps/Messages/MessagesPanel.js rename to ui/desktop/apps/Messages/MessagesPanel.js diff --git a/ui/site/apps/Messages/MessagesSidebar.js b/ui/desktop/apps/Messages/MessagesSidebar.js similarity index 100% rename from ui/site/apps/Messages/MessagesSidebar.js rename to ui/desktop/apps/Messages/MessagesSidebar.js diff --git a/ui/site/apps/Tasks/Tasks.js b/ui/desktop/apps/Tasks/Tasks.js similarity index 100% rename from ui/site/apps/Tasks/Tasks.js rename to ui/desktop/apps/Tasks/Tasks.js diff --git a/ui/site/components/AppMenu.js b/ui/desktop/components/AppMenu.js similarity index 100% rename from ui/site/components/AppMenu.js rename to ui/desktop/components/AppMenu.js diff --git a/ui/site/components/AppWindow.js b/ui/desktop/components/AppWindow.js similarity index 100% rename from ui/site/components/AppWindow.js rename to ui/desktop/components/AppWindow.js diff --git a/ui/site/components/Home.js b/ui/desktop/components/Home.js similarity index 100% rename from ui/site/components/Home.js rename to ui/desktop/components/Home.js diff --git a/ui/site/components/InputBox.js b/ui/desktop/components/InputBox.js similarity index 100% rename from ui/site/components/InputBox.js rename to ui/desktop/components/InputBox.js diff --git a/ui/site/components/LoadingCircle.js b/ui/desktop/components/LoadingCircle.js similarity index 100% rename from ui/site/components/LoadingCircle.js rename to ui/desktop/components/LoadingCircle.js diff --git a/ui/site/components/ProfileButton.js b/ui/desktop/components/ProfileButton.js similarity index 100% rename from ui/site/components/ProfileButton.js rename to ui/desktop/components/ProfileButton.js diff --git a/ui/site/components/ProfileMenu.js b/ui/desktop/components/ProfileMenu.js similarity index 100% rename from ui/site/components/ProfileMenu.js rename to ui/desktop/components/ProfileMenu.js diff --git a/ui/site/components/Sidebar.js b/ui/desktop/components/Sidebar.js similarity index 100% rename from ui/site/components/Sidebar.js rename to ui/desktop/components/Sidebar.js diff --git a/ui/site/index.html b/ui/desktop/index.html similarity index 100% rename from ui/site/index.html rename to ui/desktop/index.html diff --git a/ui/site/index.js b/ui/desktop/index.js similarity index 100% rename from ui/site/index.js rename to ui/desktop/index.js diff --git a/ui/site/util.js b/ui/desktop/util.js similarity index 100% rename from ui/site/util.js rename to ui/desktop/util.js diff --git a/ui/site/ws/Connection.js b/ui/desktop/ws/Connection.js similarity index 100% rename from ui/site/ws/Connection.js rename to ui/desktop/ws/Connection.js diff --git a/ui/site/ws/Socket.js b/ui/desktop/ws/Socket.js similarity index 100% rename from ui/site/ws/Socket.js rename to ui/desktop/ws/Socket.js diff --git a/ui/mobile/apps/Forum/Forum.js b/ui/mobile/apps/Forum/Forum.js new file mode 100644 index 0000000..0a26881 --- /dev/null +++ b/ui/mobile/apps/Forum/Forum.js @@ -0,0 +1,104 @@ +import './ForumPanel.js' + +css(` + forum- { + font-family: 'Bona'; + } + + forum- input::placeholder { + font-family: 'Bona Nova'; + font-size: 0.9em; + color: var(--accent); + } + + input[type="checkbox"] { + appearance: none; /* remove default style */ + -webkit-appearance: none; + width: 1em; + height: 1em; + border: 1px solid var(--accent); + } + + input[type="checkbox"]:checked { + background-color: var(--red); + } +`) + +class Forum extends Shadow { + + selectedForum = "HY" + + render() { + ZStack(() => { + HStack(() => { + VStack(() => { + img("/_/icons/logo.svg", "2em") + .padding(0.8, em) + .borderRadius(12, px) + .marginHorizontal(1, em) + .onHover(function (hovering) { + if(hovering) { + this.style.background = "var(--darkbrown)" + } else { + this.style.background = "" + } + }) + .opacity(0) + + img("/_/icons/place/austin.svg", "2em") + .padding(0.8, em) + .borderRadius(12, px) + .marginHorizontal(1, em) + .onHover(function (hovering) { + if(hovering) { + this.style.background = "var(--darkbrown)" + } else { + this.style.background = "" + } + }) + .opacity(0) + + }) + .height(100, vh) + .paddingLeft(2, em) + .paddingRight(2, em) + .gap(1, em) + .marginTop(20, vh) + + VStack(() => { + + ForumPanel() + + input("Message Hyperia", "98%") + .paddingVertical(1, em) + .paddingLeft(2, pct) + .color("var(--accent)") + .background("var(--darkbrown)") + .marginBottom(6, em) + .border("none") + .fontSize(1, em) + .onKeyDown(function (e) { + if (e.key === "Enter") { + window.Socket.send({app: "FORUM", operation: "SEND", msg: {forum: "HY", text: this.value }}) + this.value = "" + } + }) + }) + .gap(0.5, em) + .width(100, pct) + .height(100, vh) + .alignHorizontal("center") + .alignVertical("end") + }) + .width(100, "%") + .height(87, vh) + .x(0).y(0, vh) + }) + .width(100, pct) + .height(100, pct) + } + + +} + +register(Forum) \ No newline at end of file diff --git a/ui/mobile/apps/Forum/ForumPanel.js b/ui/mobile/apps/Forum/ForumPanel.js new file mode 100644 index 0000000..d2a6fa4 --- /dev/null +++ b/ui/mobile/apps/Forum/ForumPanel.js @@ -0,0 +1,90 @@ +import "../../components/LoadingCircle.js" + +class ForumPanel extends Shadow { + forums = [ + "HY" + ] + messages = [] + + render() { + VStack(() => { + if(this.messages.length > 0) { + + let previousDate = null + + for(let i=0; i { + HStack(() => { + p(message.sentBy) + .fontWeight("bold") + .marginBottom(0.3, em) + + p(util.formatTime(message.time)) + .opacity(0.2) + .marginLeft(1, em) + }) + p(message.text) + }) + } + } else { + LoadingCircle() + } + }) + .gap(1, em) + .position("relative") + .overflow("scroll") + .height(100, pct) + .width(96, pct) + .paddingTop(5, em) + .paddingBottom(2, em) + .paddingLeft(4, pct) + .backgroundColor("var(--darkbrown)") + .onAppear(async () => { + console.log("appear") + requestAnimationFrame(() => { + this.scrollTop = this.scrollHeight + }); + let res = await Socket.send({app: "FORUM", operation: "GET", msg: {forum: "HY", number: 100}}) + if(!res) console.error("failed to get messages") + if(res.msg.length > 0 && this.messages.length === 0) { + console.log("rerendering", res.msg) + this.messages = res.msg + this.rerender() + } + window.addEventListener("new-post", (e) => { + this.messages = e.detail + if(e.detail.length !== this.messages || e.detail.last.time !== this.messages.last.time || e.detail.first.time !== this.messages.first.time) { + this.rerender() + } + }) + }) + } + + parseDate(str) { + // Format: MM.DD.YYYY-HH:MM:SSxxxxxx(am|pm) + const match = str.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})-(\d{1,2}):(\d{2}).*(am|pm)$/i); + if (!match) return null; + + const [, mm, dd, yyyy, hh, min, ampm] = match; + const date = `${mm}/${dd}/${yyyy}`; + const time = `${hh}:${min}${ampm.toLowerCase()}`; + + return { date, time }; + } +} + +register(ForumPanel) \ No newline at end of file diff --git a/ui/mobile/apps/Jobs/Jobs.js b/ui/mobile/apps/Jobs/Jobs.js new file mode 100644 index 0000000..bb72598 --- /dev/null +++ b/ui/mobile/apps/Jobs/Jobs.js @@ -0,0 +1,101 @@ +import "./JobsSidebar.js" +import "./JobsGrid.js" + +css(` + jobs- { + font-family: 'Bona'; + } + + jobs- input::placeholder { + font-family: 'Bona Nova'; + font-size: 0.9em; + color: var(--accent); + } + + input[type="checkbox"] { + appearance: none; /* remove default style */ + -webkit-appearance: none; + width: 1em; + height: 1em; + border: 1px solid var(--accent); + } + + input[type="checkbox"]:checked { + background-color: var(--red); + } +`) + +class Jobs extends Shadow { + jobs = [ + { + title: "Austin Chapter Lead", + salary: "1% of Local Revenue", + company: "Hyperia", + city: "Austin", + state: "TX" + } + ] + + render() { + ZStack(() => { + HStack(() => { + JobsSidebar() + + JobsGrid(this.jobs) + }) + .width(100, "%") + .x(0).y(13, vh) + + HStack(() => { + input("Search jobs... (Coming Soon!)", "45vw") + .attr({ + "type": "text", + "disabled": "true" + }) + .fontSize(1.1, em) + .paddingLeft(1.3, em) + .background("transparent") + .border("0.5px solid var(--divider)") + .outline("none") + .color("var(--accent)") + .opacity(0.5) + .borderRadius(10, px) + .background("grey") + .cursor("not-allowed") + + button("+ Add Job") + .width(7, em) + .marginLeft(1, em) + .borderRadius(10, px) + .background("transparent") + .border("0.3px solid var(--accent2)") + .color("var(--accent)") + .fontFamily("Bona Nova") + .onHover(function (hovering) { + if(hovering) { + this.style.background = "var(--green)" + + } else { + this.style.background = "transparent" + + } + }) + .onClick((clicking) => { + console.log(this, "clicked") + }) + + }) + .x(55, vw).y(4, vh) + .position("absolute") + .transform("translateX(-50%)") + }) + .width(100, "%") + .height(100, "%") + } + + connectedCallback() { + // Optional additional logic + } +} + +register(Jobs) \ No newline at end of file diff --git a/ui/mobile/apps/Jobs/JobsGrid.js b/ui/mobile/apps/Jobs/JobsGrid.js new file mode 100644 index 0000000..2af5d4f --- /dev/null +++ b/ui/mobile/apps/Jobs/JobsGrid.js @@ -0,0 +1,60 @@ +class JobsGrid extends Shadow { + jobs; + + constructor(jobs) { + super() + this.jobs = jobs + } + + boldUntilFirstSpace(text) { + const index = text.indexOf(' '); + if (index === -1) { + // No spaces — bold the whole thing + return `${text}`; + } + return `${text.slice(0, index)}${text.slice(index)}`; + } + + render() { + VStack(() => { + h3("Results") + .marginTop(0.1, em) + .marginBottom(1, em) + .marginLeft(0.4, em) + .color("var(--accent2)") + + if (this.jobs.length > 0) { + ZStack(() => { + for (let i = 0; i < this.jobs.length; i++) { + VStack(() => { + p(this.jobs[i].title) + .fontSize(1.2, em) + .fontWeight("bold") + .marginBottom(0.5, em) + p(this.jobs[i].company) + p(this.jobs[i].city + ", " + this.jobs[i].state) + .marginBottom(0.5, em) + p(this.boldUntilFirstSpace(this.jobs[i].salary)) + }) + .padding(1, em) + .borderRadius(5, "px") + .background("var(--darkbrown)") + } + }) + .display("grid") + .gridTemplateColumns("repeat(auto-fill, minmax(250px, 1fr))") + .gap(1, em) + } else { + p("No Jobs!") + } + }) + .height(100, vh) + .paddingLeft(2, em) + .paddingRight(2, em) + .paddingTop(2, em) + .gap(0, em) + .width(100, "%") + } +} + +register(JobsGrid) diff --git a/ui/mobile/apps/Jobs/JobsSidebar.js b/ui/mobile/apps/Jobs/JobsSidebar.js new file mode 100644 index 0000000..1546cec --- /dev/null +++ b/ui/mobile/apps/Jobs/JobsSidebar.js @@ -0,0 +1,26 @@ +class JobsSidebar extends Shadow { + render() { + VStack(() => { + h3("Location") + .color("var(--accent2)") + .marginBottom(0, em) + + HStack(() => { + input("Location", "100%") + .paddingLeft(3, em) + .paddingVertical(0.75, em) + .backgroundImage("/_/icons/locationPin.svg") + .backgroundRepeat("no-repeat") + .backgroundSize("18px 18px") + .backgroundPosition("10px center") + }) + }) + .paddingTop(1, em) + .paddingLeft(3, em) + .paddingRight(3, em) + .gap(1, em) + .minWidth(10, vw) + } +} + +register(JobsSidebar) \ No newline at end of file diff --git a/ui/mobile/apps/Market/Market.js b/ui/mobile/apps/Market/Market.js new file mode 100644 index 0000000..e3bceeb --- /dev/null +++ b/ui/mobile/apps/Market/Market.js @@ -0,0 +1,105 @@ +import "./MarketSidebar.js" +import "./MarketGrid.js" + +css(` + market- { + font-family: 'Bona'; + } + + market- input::placeholder { + font-family: 'Bona Nova'; + font-size: 0.9em; + color: var(--accent); + } + + input[type="checkbox"] { + appearance: none; /* remove default style */ + -webkit-appearance: none; + width: 1em; + height: 1em; + border: 1px solid var(--accent); + } + + input[type="checkbox"]:checked { + background-color: var(--red); + } +`) + +class Market extends Shadow { + + listings = [ + { + title: "Shield Lapel Pin", + stars: "5", + reviews: 1, + price: "$12", + company: "Hyperia", + type: "new", + image: "/db/images/1", + madeIn: "America" + } + ] + + render() { + ZStack(() => { + HStack(() => { + MarketSidebar() + + MarketGrid(this.listings) + }) + .width(100, "%") + .x(0).y(13, vh) + + HStack(() => { + input("Search for products... (Coming Soon!)", "45vw") + .attr({ + "type": "text", + "disabled": "true" + }) + .fontSize(1.1, em) + .paddingLeft(1.3, em) + .background("transparent") + .border("0.5px solid var(--divider)") + .outline("none") + .color("var(--accent)") + .opacity(0.5) + .borderRadius(10, px) + .background("grey") + .cursor("not-allowed") + + button("+ Add Item") + .width(7, em) + .marginLeft(1, em) + .borderRadius(10, px) + .background("transparent") + .border("0.5px solid var(--accent2)") + .color("var(--accent)") + .fontFamily("Bona Nova") + .onHover(function (hovering) { + if(hovering) { + this.style.background = "var(--green)" + + } else { + this.style.background = "transparent" + + } + }) + .onClick((clicking) => { + console.log(this, "clicked") + }) + + }) + .x(55, vw).y(4, vh) + .position("absolute") + .transform("translateX(-50%)") + }) + .width(100, "%") + .height(100, "%") + } + + connectedCallback() { + // Optional additional logic + } +} + +register(Market) diff --git a/ui/mobile/apps/Market/MarketGrid.js b/ui/mobile/apps/Market/MarketGrid.js new file mode 100644 index 0000000..8740f9e --- /dev/null +++ b/ui/mobile/apps/Market/MarketGrid.js @@ -0,0 +1,140 @@ +class MarketGrid extends Shadow { + listings; + + constructor(listings) { + super() + this.listings = listings + } + + boldUntilFirstSpace(text) { + if(!text) return + const index = text.indexOf(' '); + if (index === -1) { + // No spaces — bold the whole thing + return `${text}`; + } + return `${text.slice(0, index)}${text.slice(index)}`; + } + + render() { + VStack(() => { + h3("Results") + .marginTop(0.1, em) + .marginBottom(1, em) + .marginLeft(0.4, em) + .color("var(--accent)") + .opacity(0.7) + + if (this.listings.length > 0) { + ZStack(() => { + // BuyModal() + + let params = new URLSearchParams(window.location.search); + + const hyperiaMade = params.get("hyperia-made") === "true"; + const americaMade = params.get("america-made") === "true"; + const newItem = params.get("new") === "true"; + const usedItem = params.get("used") === "true"; + + + let filtered = this.listings; + if (hyperiaMade) { + filtered = filtered.filter(item => item.madeIn === "Hyperia"); + } + if (americaMade) { + filtered = filtered.filter(item => item.madeIn === "America"); + } + if (newItem) { + filtered = filtered.filter(item => item.type === "new"); + } + if (usedItem) { + filtered = filtered.filter(item => item.type === "used"); + } + + for (let i = 0; i < filtered.length; i++) { + const rating = filtered[i].stars + const percent = (rating / 5) + + VStack(() => { + img(filtered[i].image) + .marginBottom(0.5, em) + + p(filtered[i].company) + .marginBottom(0.5, em) + + p(filtered[i].title) + .fontSize(1.2, em) + .fontWeight("bold") + .marginBottom(0.5, em) + + HStack(() => { + p(filtered[i].stars) + .marginRight(0.2, em) + + ZStack(() => { + div("★★★★★") // Empty stars (background) + .color("#ccc") + + div("★★★★★") // Filled stars (foreground, clipped by width) + .color("#ffa500") + .position("absolute") + .top(0) + .left(0) + .whiteSpace("nowrap") + .overflow("hidden") + .width(percent * 5, em) + }) + .display("inline-block") + .position("relative") + .fontSize(1.2, em) + .lineHeight(1) + + p(filtered[i].reviews) + .marginLeft(0.2, em) + }) + .marginBottom(0.5, em) + + p(filtered[i].price) + .fontSize(1.75, em) + .marginBottom(0.5, em) + + button("Coming Soon!") + .onClick((finished) => { + if(finished) { + + } + }) + .onHover(function (hovering) { + if(hovering) { + this.style.backgroundColor = "var(--green)" + } else { + this.style.backgroundColor = "" + } + }) + + }) + .padding(1, em) + .border("1px solid var(--accent2)") + .borderRadius(5, "px") + } + }) + .display("grid") + .gridTemplateColumns("repeat(auto-fill, minmax(250px, 1fr))") + .gap(1, em) + } else { + p("No Listings!") + } + }) + .onQueryChanged(() => { + console.log("query did change yup") + this.rerender() + }) + .height(100, vh) + .paddingLeft(2, em) + .paddingRight(2, em) + .gap(0, em) + .width(100, "%") + } +} + +register(MarketGrid) \ No newline at end of file diff --git a/ui/mobile/apps/Market/MarketSidebar.js b/ui/mobile/apps/Market/MarketSidebar.js new file mode 100644 index 0000000..9324618 --- /dev/null +++ b/ui/mobile/apps/Market/MarketSidebar.js @@ -0,0 +1,85 @@ +class MarketSidebar extends Shadow { + + handleChecked(e) { + let checked = e.target.checked + let label = $(`label[for="${e.target.id}"]`).innerText + if(checked) { + window.setQuery(label.toLowerCase(), true) + } else { + window.setQuery(label.toLowerCase(), null) + } + } + + render() { + VStack(() => { + + p("Make") + + HStack(() => { + input() + .attr({ + "type": "checkbox", + "id": "hyperia-check" + }) + .onChange(this.handleChecked) + label("Hyperia-Made") + .attr({ + "for": "hyperia-check" + }) + .marginLeft(0.5, em) + }) + + HStack(() => { + input() + .attr({ + "type": "checkbox", + "id": "america-check" + }) + .onChange(this.handleChecked) + label("America-Made") + .attr({ + "for": "america-check" + }) + .marginLeft(0.5, em) + }) + + p("Condition") + + HStack(() => { + input() + .attr({ + "type": "checkbox", + "id": "new-check" + }) + .onChange(this.handleChecked) + label("New") + .attr({ + "for": "new-check" + }) + .marginLeft(0.5, em) + }) + + HStack(() => { + input() + .attr({ + "type": "checkbox", + "id": "used-check" + }) + .onChange(this.handleChecked) + label("Used") + .attr({ + "for": "used-check" + }) + .marginLeft(0.5, em) + }) + }) + .paddingTop(12, vh) + .paddingLeft(3, em) + .paddingRight(3, em) + .gap(1, em) + .minWidth(10, vw) + .userSelect('none') + } +} + +register(MarketSidebar) \ No newline at end of file diff --git a/ui/mobile/apps/Messages/Messages.js b/ui/mobile/apps/Messages/Messages.js new file mode 100644 index 0000000..dd288a6 --- /dev/null +++ b/ui/mobile/apps/Messages/Messages.js @@ -0,0 +1,188 @@ +import "./MessagesSidebar.js" +import "./MessagesPanel.js" + +css(` + messages- { + font-family: 'Bona'; + } + + messages- input::placeholder { + font-family: 'Bona Nova'; + font-size: 0.9em; + color: var(--accent); + } + + input[type="checkbox"] { + appearance: none; /* remove default style */ + -webkit-appearance: none; + width: 1em; + height: 1em; + border: 1px solid var(--accent); + } + + input[type="checkbox"]:checked { + background-color: var(--red); + } +`) + +class Messages extends Shadow { + conversations = [] + selectedConvoID = null + onConversationSelect(i) { + console.log("convo selected: ", i) + this.selectedConvoID = i + this.$("messagessidebar-").rerender() + this.$("messagespanel-").rerender() + } + + getConvoFromID(id) { + for(let i=0; i { + HStack(() => { + MessagesSidebar(this.conversations, this.selectedConvoID, this.onConversationSelect) + + VStack(() => { + if(this.getConvoFromID(this.selectedConvoID)) { + MessagesPanel(this.getConvoFromID(this.selectedConvoID).messages) + } else { + MessagesPanel() + } + + input("Send Message", "93%") + .paddingVertical(1, em) + .paddingHorizontal(2, em) + .color("var(--accent)") + .background("var(--darkbrown)") + .marginBottom(6, em) + .border("none") + .fontSize(1, em) + .onKeyDown((e) => { + if (e.key === "Enter") { + window.Socket.send({app: "MESSAGES", operation: "SEND", msg: { conversation: `CONVERSATION-${this.selectedConvoID}`, text: e.target.value }}) + e.target.value = "" + } + }) + }) + .gap(1, em) + .width(100, pct) + .alignHorizontal("center") + .alignVertical("end") + }) + .onAppear(async () => { + let res = await Socket.send({app: "MESSAGES", operation: "GET"}) + if(!res) console.error("failed to get messages") + + if(res.msg.length > 0 && this.conversations.length === 0) { + this.conversations = res.msg + this.selectedConvoID = this.conversations[0].id + this.rerender() + } + + window.addEventListener("new-message", (e) => { + let convoID = e.detail.conversationID + let messages = e.detail.messages + let convo = this.getConvoFromID(convoID) + convo.messages = messages + this.rerender() + }) + }) + .width(100, "%") + .height(87, vh) + .x(0).y(13, vh) + + VStack(() => { + p("Add Message") + + input("enter email...") + .color("var(--accent)") + .onKeyDown(function (e) { + if (e.key === "Enter") { + window.Socket.send({app: "MESSAGES", operation: "ADDCONVERSATION", msg: {email: this.value }}) + this.value = "" + } + }) + + p("x") + .onClick(function (done) { + if(done) { + this.parentElement.style.display = "none" + } + }) + .xRight(2, em).y(2, em) + .fontSize(1.4, em) + .cursor("pointer") + + }) + .gap(1, em) + .alignVertical("center") + .alignHorizontal("center") + .backgroundColor("black") + .border("1px solid var(--accent)") + .position("fixed") + .x(50, vw).y(50, vh) + .center() + .width(60, vw) + .height(60, vh) + .display("none") + .attr({id: "addPanel"}) + + HStack(() => { + input("Search messages... (Coming Soon!)", "45vw") + .attr({ + "type": "text", + "disabled": "true" + }) + .fontSize(1.1, em) + .paddingLeft(1.3, em) + .background("transparent") + .border("0.5px solid var(--divider)") + .outline("none") + .color("var(--accent)") + .opacity(0.5) + .borderRadius(10, px) + .background("grey") + .cursor("not-allowed") + + button("+ New Message") + .width(13, em) + .marginLeft(1, em) + .borderRadius(10, px) + .background("transparent") + .border("0.5px solid var(--divider)") + .color("var(--accent)") + .fontFamily("Bona Nova") + .onHover(function (hovering) { + if(hovering) { + this.style.background = "var(--green)" + + } else { + this.style.background = "transparent" + + } + }) + .onClick((done) => { + console.log("click") + if(done) { + this.$("#addPanel").style.display = "flex" + } + console.log(this, "clicked") + }) + + }) + .x(55, vw).y(4, vh) + .position("absolute") + .transform("translateX(-50%)") + }) + .width(100, "%") + .height(100, "%") + } +} + +register(Messages) \ No newline at end of file diff --git a/ui/mobile/apps/Messages/MessagesPanel.js b/ui/mobile/apps/Messages/MessagesPanel.js new file mode 100644 index 0000000..b608212 --- /dev/null +++ b/ui/mobile/apps/Messages/MessagesPanel.js @@ -0,0 +1,56 @@ +import "../../components/LoadingCircle.js" + +class MessagesPanel extends Shadow { + messages + + constructor(messages) { + super() + this.messages = messages + } + + render() { + VStack(() => { + if(this.messages) { + for(let i=0; i { + HStack(() => { + p(message.from.firstName + " " + message.from.lastName) + .fontWeight("bold") + .marginBottom(0.3, em) + + p(util.formatTime(message.time)) + .opacity(0.2) + .marginLeft(1, em) + }) + p(message.text) + }) + .paddingVertical(0.5, em) + .marginLeft(fromMe ? 70 : 0, pct) + .paddingRight(fromMe ? 10 : 0, pct) + .marginRight(fromMe ? 0 : 70, pct) + .paddingLeft(fromMe ? 5 : 10, pct) + .background(fromMe ? "var(--brown)" : "var(--green)") + } + } else { + LoadingCircle() + } + }) + .onAppear(async () => { + requestAnimationFrame(() => { + this.scrollTop = this.scrollHeight + }); + }) + .gap(1, em) + .position("relative") + .overflow("scroll") + .height(95, pct) + .width(100, pct) + .paddingTop(2, em) + .paddingBottom(2, em) + .backgroundColor("var(--darkbrown)") + } +} + +register(MessagesPanel) \ No newline at end of file diff --git a/ui/mobile/apps/Messages/MessagesSidebar.js b/ui/mobile/apps/Messages/MessagesSidebar.js new file mode 100644 index 0000000..453a91d --- /dev/null +++ b/ui/mobile/apps/Messages/MessagesSidebar.js @@ -0,0 +1,73 @@ +class MessagesSidebar extends Shadow { + conversations = [] + selectedConvoID + onSelect + + constructor(conversations, selectedConvoID, onSelect) { + super() + this.conversations = conversations + this.selectedConvoID = selectedConvoID + this.onSelect = onSelect + } + + render() { + VStack(() => { + this.conversations.forEach((convo, i) => { + + VStack(() => { + HStack(() => { + + p(this.makeConvoTitle(convo.between)) + .textAlign("left") + .marginLeft(0.5, inches) + .paddingTop(0.2, inches) + .width(100, pct) + .marginTop(0) + .fontSize(1, em) + .fontWeight("bold") + + p(util.formatTime(convo.messages.last.time)) + .paddingTop(0.2, inches) + .fontSize(0.8, em) + .marginRight(0.1, inches) + .color("var(--divider") + }) + .justifyContent("space-between") + .marginBottom(0) + + p(convo.messages.last.text) + .fontSize(0.8, em) + .textAlign("left") + .marginLeft(0.5, inches) + .marginBottom(2, em) + .color("var(--divider)") + }) + .background(convo.id === this.selectedConvoID ? "var(--darkbrown)" : "") + .onClick(() => { + this.onSelect(i) + }) + }) + }) + .minWidth(15, vw) + .height(100, vh) + .gap(0, em) + } + + makeConvoTitle(members) { + let membersString = "" + for(let i=0; i 2) { + membersString += member.firstName + } else { + membersString += member.firstName + " " + member.lastName + } + } + return membersString + } +} + +register(MessagesSidebar) \ No newline at end of file diff --git a/ui/mobile/apps/Tasks/Tasks.js b/ui/mobile/apps/Tasks/Tasks.js new file mode 100644 index 0000000..4b0e733 --- /dev/null +++ b/ui/mobile/apps/Tasks/Tasks.js @@ -0,0 +1,153 @@ +css(` + tasks- { + font-family: 'Bona'; + } + + tasks- input::placeholder { + font-family: 'Bona Nova'; + font-size: 0.9em; + color: var(--accent); + } + + input[type="checkbox"] { + appearance: none; /* remove default style */ + -webkit-appearance: none; + width: 1em; + height: 1em; + border: 1px solid var(--accent); + } + + input[type="checkbox"]:checked { + background-color: var(--red); + } +`) + +class Tasks extends Shadow { + projects = [ + { + "title": "Blockcatcher", + "tasks": {} + } + ] + columns = [ + { + "title": "backlog", + "tasks": {} + } + ] + + render() { + ZStack(() => { + HStack(() => { + VStack(() => { + h3("Projects") + .marginTop(0) + .marginBottom(1, em) + .marginLeft(0.4, em) + + if (this.projects.length >= 1) { + for(let i = 0; i < this.projects.length; i++) { + p(this.projects[i].title) + } + } else { + p("No Projects!") + } + }) + .height(100, vh) + .paddingLeft(2, em) + .paddingRight(2, em) + .paddingTop(2, em) + .gap(0, em) + .borderRight("0.5px solid var(--accent2)") + + HStack(() => { + if (this.columns.length >= 1) { + for(let i = 0; i < this.columns.length; i++) { + p(this.columns[i].name) + } + } else { + p("No Conversations!") + } + }) + .height(100, vh) + .paddingLeft(2, em) + .paddingRight(2, em) + .paddingTop(2, em) + .gap(0, em) + .borderRight("0.5px solid var(--accent2)") + }) + .width(100, "%") + .x(0).y(13, vh) + .borderTop("0.5px solid var(--accent2)") + + p("0 Items") + .position("absolute") + .x(50, vw).y(50, vh) + .transform("translate(-50%, -50%)") + + HStack(() => { + input("Search tasks...", "45vw") + .attr({ + "type": "text" + }) + .fontSize(1.1, em) + .paddingLeft(1.3, em) + .background("transparent") + .border("0.5px solid var(--accent2)") + .outline("none") + .color("var(--accent)") + .borderRadius(10, px) + + button("Search") + .marginLeft(2, em) + .borderRadius(10, px) + .background("transparent") + .border("0.5px solid var(--accent2)") + .color("var(--accent)") + .fontFamily("Bona Nova") + .onHover(function (hovering) { + if(hovering) { + this.style.background = "var(--green)" + + } else { + this.style.background = "transparent" + + } + }) + + button("+ New Task") + .width(9, em) + .marginLeft(1, em) + .borderRadius(10, px) + .background("transparent") + .border("0.5px solid var(--accent2)") + .color("var(--accent)") + .fontFamily("Bona Nova") + .onHover(function (hovering) { + if(hovering) { + this.style.background = "var(--green)" + + } else { + this.style.background = "transparent" + + } + }) + .onClick((clicking) => { + console.log(this, "clicked") + }) + + }) + .x(55, vw).y(4, vh) + .position("absolute") + .transform("translateX(-50%)") + }) + .width(100, "%") + .height(100, "%") + } + + connectedCallback() { + // Optional additional logic + } +} + +register(Tasks) \ No newline at end of file diff --git a/ui/mobile/components/AppMenu.js b/ui/mobile/components/AppMenu.js new file mode 100644 index 0000000..7b28205 --- /dev/null +++ b/ui/mobile/components/AppMenu.js @@ -0,0 +1,22 @@ +class AppMenu extends Shadow { + render() { + HStack(() => { + img("/_/icons/mail.png", "2em", "2em") + img("/_/icons/Column.svg", "2em", "2em") + p("S") + p("S") + p("S") + }) + .borderTop("1px solid black") + .height(3, em) + .x(0).yBottom(0) + .justifyContent("space-between") + .paddingHorizontal(2, em) + .paddingVertical(1, em) + .transform("translateY(-50%)") + .width(100, vw) + .boxSizing("border-box") + } +} + +register(AppMenu) \ No newline at end of file diff --git a/ui/mobile/components/Home.js b/ui/mobile/components/Home.js new file mode 100644 index 0000000..0e97d8e --- /dev/null +++ b/ui/mobile/components/Home.js @@ -0,0 +1,12 @@ +import "./AppMenu.js" + +class Home extends Shadow { + + render() { + ZStack(() => { + AppMenu() + }) + } +} + +register(Home) \ No newline at end of file diff --git a/ui/mobile/index.html b/ui/mobile/index.html new file mode 100644 index 0000000..ca92388 --- /dev/null +++ b/ui/mobile/index.html @@ -0,0 +1,14 @@ + + + + Hyperia + + + + + + + + + + \ No newline at end of file diff --git a/ui/mobile/index.js b/ui/mobile/index.js new file mode 100644 index 0000000..d0acae8 --- /dev/null +++ b/ui/mobile/index.js @@ -0,0 +1,8 @@ +import Socket from "./ws/Socket.js" +import "./components/Home.js" + +import util from "./util.js" +window.util = util + +window.Socket = new Socket() +Home() \ No newline at end of file diff --git a/ui/mobile/util.js b/ui/mobile/util.js new file mode 100644 index 0000000..43c3bd1 --- /dev/null +++ b/ui/mobile/util.js @@ -0,0 +1,9 @@ +export default class util { + static formatTime(str) { + const match = str.match(/-(\d+:\d+):\d+.*(am|pm)/i); + if (!match) return null; + + const [_, hourMin, ampm] = match; + return hourMin + ampm.toLowerCase(); + } +} \ No newline at end of file diff --git a/ui/mobile/ws/Connection.js b/ui/mobile/ws/Connection.js new file mode 100644 index 0000000..69232ff --- /dev/null +++ b/ui/mobile/ws/Connection.js @@ -0,0 +1,62 @@ +class Connection { + connectionTries = 0 + ws; + linkCreated; + wsStatus; + + constructor(receiveCB) { + this.init() + this.receiveCB = receiveCB + } + + init() { + if(window.location.hostname === "localhost") { + this.ws = new WebSocket("ws://" + "localhost:3003") + } else { + this.ws = new WebSocket("wss://" + window.location.hostname + window.location.pathname) + } + this.ws.addEventListener('open', () => { + this.connectionTries = 0 + console.log("Websocket connection established."); + this.ws.addEventListener('message', this.receiveCB) + }); + this.ws.addEventListener("close", () => { + this.checkOpen(); + console.log('Websocket Closed') + }) + } + + async checkOpen() { + if (this.ws.readyState === WebSocket.OPEN) { + return true + } else { + await this.sleep(this.connectionTries < 20 ? 5000 : 60000) + this.connectionTries++ + console.log('Reestablishing connection') + this.init() + } + } + + sleep = (time) => { + return new Promise(resolve => { + setTimeout(resolve, time); + }); + } + + send = (msg) => { + console.log("sending") + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.send(msg); + } + else if(this.connectionTries === 0) { + setTimeout(() => { + this.send(msg) + }, 100) + } + else { + console.error('No websocket connection: Cannot send message'); + } + } +} + +export default Connection \ No newline at end of file diff --git a/ui/mobile/ws/Socket.js b/ui/mobile/ws/Socket.js new file mode 100644 index 0000000..49357ef --- /dev/null +++ b/ui/mobile/ws/Socket.js @@ -0,0 +1,45 @@ +import Connection from "./Connection.js"; + +export default class Socket { + connection; + disabled = true; + requestID = 1; + pending = new Map(); + + constructor() { + this.connection = new Connection(this.receive); + } + + isOpen() { + if(this.connection.checkOpen()) { + return true; + } else { + return false; + } + } + + send(msg) { + return new Promise(resolve => { + const id = (++this.requestID).toString(); + this.pending.set(id, resolve); + this.connection.send(JSON.stringify({ id, ...msg })); + }); + } + + receive = (event) => { + const msg = JSON.parse(event.data); + if (msg.id && this.pending.has(msg.id)) { + this.pending.get(msg.id)(msg); + this.pending.delete(msg.id); + return; + } else { + this.onBroadcast(msg) + } + } + + onBroadcast(msg) { + window.dispatchEvent(new CustomEvent(msg.event, { + detail: msg.msg + })); + } +} \ No newline at end of file diff --git a/ui/public/pages/Events.js b/ui/public/pages/Events.js index 143aa64..5772e28 100644 --- a/ui/public/pages/Events.js +++ b/ui/public/pages/Events.js @@ -29,7 +29,7 @@ class Events extends Shadow { }) - p(`Join us in Austin, Texas for a great dance, with free drinks and live music by Boerne's own Noah Kurtis.

Admission: $50 for men, women are free.`) + p(`Join us in Austin, Texas for a great dance, with free drinks and live music by Boerne's own Noah Kurtis.

Admission: $30 for men, women are free.`) .marginRight(4, em) button("Tickets Available Soon") diff --git a/ui/public/pages/SIgnIn.js b/ui/public/pages/SIgnIn.js index ec93dad..c766caf 100644 --- a/ui/public/pages/SIgnIn.js +++ b/ui/public/pages/SIgnIn.js @@ -1,9 +1,9 @@ class SignIn extends Shadow { inputStyles(el) { - console.log(el) return el - .border("1px solid var(--accent)") + .color("var(--accent)") + .border("1px solid var(--accent)") } render() { diff --git a/ui/site/index.css b/ui/site/index.css deleted file mode 100644 index e69de29..0000000