beginning of mobile site
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"express-useragent": "^2.0.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.18.3",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const fs = require('fs');
|
|||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const useragent = require("express-useragent");
|
||||||
|
|
||||||
import "./util.js"
|
import "./util.js"
|
||||||
import Socket from './ws/ws.js'
|
import Socket from './ws/ws.js'
|
||||||
@@ -112,12 +113,13 @@ class Server {
|
|||||||
|
|
||||||
let privateSite = () => {
|
let privateSite = () => {
|
||||||
let filePath;
|
let filePath;
|
||||||
|
let platformFolder = req.useragent.isMobile ? "mobile" : "desktop"
|
||||||
if(url.startsWith("/_")) {
|
if(url.startsWith("/_")) {
|
||||||
filePath = path.join(this.UIPath, url);
|
filePath = path.join(this.UIPath, url);
|
||||||
} else if(url.includes("75820185")) {
|
} 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 {
|
} else {
|
||||||
filePath = path.join(this.UIPath, "site", "index.html");
|
filePath = path.join(this.UIPath, platformFolder, "index.html");
|
||||||
}
|
}
|
||||||
|
|
||||||
res.sendFile(filePath);
|
res.sendFile(filePath);
|
||||||
@@ -134,10 +136,10 @@ class Server {
|
|||||||
const formattedDate = moment().format('M.D');
|
const formattedDate = moment().format('M.D');
|
||||||
const formattedTime = moment().format('h:mma');
|
const formattedTime = moment().format('h:mma');
|
||||||
if(req.url.includes("/api/")) {
|
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 {
|
} else {
|
||||||
if(req.url === "/")
|
if(req.url === "/")
|
||||||
console.logclean(chalk.gray(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`));
|
console.log(chalk.gray(` ${req.method} ${req.url} | ${formattedDate} ${formattedTime}`));
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
@@ -146,9 +148,9 @@ class Server {
|
|||||||
const originalSend = res.send;
|
const originalSend = res.send;
|
||||||
res.send = function (body) {
|
res.send = function (body) {
|
||||||
if(res.statusCode >= 400) {
|
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 {
|
} 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);
|
originalSend.call(this, body);
|
||||||
};
|
};
|
||||||
@@ -164,6 +166,7 @@ class Server {
|
|||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
app.use(useragent.express());
|
||||||
|
|
||||||
app.use(this.logRequest);
|
app.use(this.logRequest);
|
||||||
app.use(this.logResponse);
|
app.use(this.logResponse);
|
||||||
@@ -176,16 +179,16 @@ class Server {
|
|||||||
global.Socket = new Socket(server);
|
global.Socket = new Socket(server);
|
||||||
const PORT = 3003;
|
const PORT = 3003;
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
console.logclean("\n")
|
console.log("\n")
|
||||||
console.logclean(chalk.yellow("*************** Hyperia ***************"))
|
console.log(chalk.yellow("*************** Hyperia ***************"))
|
||||||
console.logclean(chalk.yellowBright(`Server is running on port ${PORT}: http://localhost`));
|
console.log(chalk.yellowBright(`Server is running on port ${PORT}: http://localhost`));
|
||||||
console.logclean(chalk.yellow("***************************************"))
|
console.log(chalk.yellow("***************************************"))
|
||||||
console.logclean("\n")
|
console.log("\n")
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', async () => {
|
||||||
console.logclean(chalk.red('Closing server...'));
|
console.log(chalk.red('Closing server...'));
|
||||||
console.logclean(chalk.green('Database connection closed.'));
|
console.log(chalk.green('Database connection closed.'));
|
||||||
process.exit(0);
|
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()
|
const server = new Server()
|
||||||
4
ui/_/icons/Column.svg
Normal file
4
ui/_/icons/Column.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="28" height="32" viewBox="0 0 28 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9.52366 0.0493342C4.88415 0.328928 1.12711 1.09781 0.253382 1.93659L0 2.18124V3.45688V4.73253L0.244645 4.9597C0.541713 5.23929 0.917417 5.43152 1.69504 5.69363C2.42023 5.94702 2.60372 5.96449 2.44645 5.77227C1.99211 5.22182 3.27649 4.584 5.7142 4.16461C8.0558 3.75395 9.35765 3.67532 13.5428 3.67532C17.728 3.67532 19.0299 3.75395 21.3715 4.16461C23.8354 4.584 25.0935 5.22182 24.6305 5.78974C24.5169 5.9208 24.5344 5.92954 24.7877 5.87712C25.3382 5.77227 26.4915 5.26551 26.7886 5.01212L27.0856 4.75001V3.45688V2.16376L26.7886 1.90164C25.9498 1.16771 22.8743 0.4862 18.7852 0.136707C17.3523 0.00564766 11.1401 -0.0467762 9.52366 0.0493342Z" fill="black"/>
|
||||||
|
<path d="M10.6246 5.30045C8.06453 5.44899 5.65304 5.82469 4.49971 6.26156C3.80073 6.52367 3.49492 6.83822 3.49492 7.27508V7.62458L4.0978 7.61584C4.63077 7.6071 4.73562 7.63331 4.93658 7.82553C5.06764 7.94786 5.20743 8.11386 5.25986 8.20997C5.31228 8.31482 5.33849 11.3292 5.32976 16.79L5.32102 25.2128H5.76662H6.20349V16.423C6.20349 6.60231 6.16854 7.15276 6.79762 6.89064C7.18207 6.73337 7.75873 6.80327 8.06453 7.03918C8.58877 7.45857 8.56256 6.82948 8.56256 18.1268V28.4456H9.17417H9.78578V17.8734C9.78578 11.4428 9.81199 7.24013 9.86442 7.14402C10.0741 6.75958 10.3974 6.56736 10.9216 6.53241C11.5158 6.48873 11.9526 6.68968 12.1361 7.0916C12.2148 7.26635 12.241 10.1671 12.2322 19.4549V31.591H13.5865H14.9408V19.4636C14.9408 7.59836 14.9408 7.33624 15.1155 7.06539C15.6136 6.24408 16.9853 6.34893 17.3436 7.24013C17.4571 7.52846 17.4746 8.89148 17.4746 18.0132V28.4543L18.0687 28.4281L18.6541 28.4019L18.6279 18.2229C18.6017 11.2069 18.6279 7.94786 18.6891 7.7469C18.9774 6.82948 20.2443 6.48873 20.7861 7.18771C20.9695 7.41488 20.9695 7.4673 20.9695 16.3095V25.2128H21.4064H21.8433V16.8424C21.8433 8.708 21.852 8.47209 22.018 8.20124C22.2714 7.77311 22.5597 7.63331 23.1189 7.64205H23.6169L23.5645 7.2314C23.5296 6.94307 23.4597 6.76832 23.2937 6.63726C22.1403 5.63247 16.0155 4.99465 10.6246 5.30045Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
104
ui/mobile/apps/Forum/Forum.js
Normal file
104
ui/mobile/apps/Forum/Forum.js
Normal file
@@ -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)
|
||||||
90
ui/mobile/apps/Forum/ForumPanel.js
Normal file
90
ui/mobile/apps/Forum/ForumPanel.js
Normal file
@@ -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<this.messages.length; i++) {
|
||||||
|
let message = this.messages[i]
|
||||||
|
const dateParts = this.parseDate(message.time);
|
||||||
|
const { date, time } = dateParts;
|
||||||
|
|
||||||
|
if (previousDate !== date) {
|
||||||
|
previousDate = date;
|
||||||
|
|
||||||
|
p(date)
|
||||||
|
.textAlign("center")
|
||||||
|
.opacity(0.5)
|
||||||
|
.marginVertical(1, em)
|
||||||
|
.color("var(--divider)")
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(() => {
|
||||||
|
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)
|
||||||
101
ui/mobile/apps/Jobs/Jobs.js
Normal file
101
ui/mobile/apps/Jobs/Jobs.js
Normal file
@@ -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)
|
||||||
60
ui/mobile/apps/Jobs/JobsGrid.js
Normal file
60
ui/mobile/apps/Jobs/JobsGrid.js
Normal file
@@ -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 `<b>${text}</b>`;
|
||||||
|
}
|
||||||
|
return `<b>${text.slice(0, index)}</b>${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)
|
||||||
26
ui/mobile/apps/Jobs/JobsSidebar.js
Normal file
26
ui/mobile/apps/Jobs/JobsSidebar.js
Normal file
@@ -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)
|
||||||
105
ui/mobile/apps/Market/Market.js
Normal file
105
ui/mobile/apps/Market/Market.js
Normal file
@@ -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)
|
||||||
140
ui/mobile/apps/Market/MarketGrid.js
Normal file
140
ui/mobile/apps/Market/MarketGrid.js
Normal file
@@ -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 `<b>${text}</b>`;
|
||||||
|
}
|
||||||
|
return `<b>${text.slice(0, index)}</b>${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)
|
||||||
85
ui/mobile/apps/Market/MarketSidebar.js
Normal file
85
ui/mobile/apps/Market/MarketSidebar.js
Normal file
@@ -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)
|
||||||
188
ui/mobile/apps/Messages/Messages.js
Normal file
188
ui/mobile/apps/Messages/Messages.js
Normal file
@@ -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<this.conversations.length; i++) {
|
||||||
|
if(this.conversations[i].id === id) {
|
||||||
|
return this.conversations[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
ZStack(() => {
|
||||||
|
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)
|
||||||
56
ui/mobile/apps/Messages/MessagesPanel.js
Normal file
56
ui/mobile/apps/Messages/MessagesPanel.js
Normal file
@@ -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<this.messages.length; i++) {
|
||||||
|
let message = this.messages[i]
|
||||||
|
let fromMe = window.profile.email === message.from.email
|
||||||
|
VStack(() => {
|
||||||
|
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)
|
||||||
73
ui/mobile/apps/Messages/MessagesSidebar.js
Normal file
73
ui/mobile/apps/Messages/MessagesSidebar.js
Normal file
@@ -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<members.length; i++) {
|
||||||
|
let member = members[i]
|
||||||
|
if(member.email === window.profile.email) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(members.length > 2) {
|
||||||
|
membersString += member.firstName
|
||||||
|
} else {
|
||||||
|
membersString += member.firstName + " " + member.lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return membersString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
register(MessagesSidebar)
|
||||||
153
ui/mobile/apps/Tasks/Tasks.js
Normal file
153
ui/mobile/apps/Tasks/Tasks.js
Normal file
@@ -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)
|
||||||
22
ui/mobile/components/AppMenu.js
Normal file
22
ui/mobile/components/AppMenu.js
Normal file
@@ -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)
|
||||||
12
ui/mobile/components/Home.js
Normal file
12
ui/mobile/components/Home.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import "./AppMenu.js"
|
||||||
|
|
||||||
|
class Home extends Shadow {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
ZStack(() => {
|
||||||
|
AppMenu()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
register(Home)
|
||||||
14
ui/mobile/index.html
Normal file
14
ui/mobile/index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Hyperia</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="/_/icons/logo.svg">
|
||||||
|
<link rel="stylesheet" href="/_/code/shared.css">
|
||||||
|
<script src="/_/code/quill.js"></script>
|
||||||
|
<script src="/_/code/zod.js"></script>
|
||||||
|
<script type="module" src="75820185/index.js"></script>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0px">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
ui/mobile/index.js
Normal file
8
ui/mobile/index.js
Normal file
@@ -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()
|
||||||
9
ui/mobile/util.js
Normal file
9
ui/mobile/util.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
62
ui/mobile/ws/Connection.js
Normal file
62
ui/mobile/ws/Connection.js
Normal file
@@ -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
|
||||||
45
ui/mobile/ws/Socket.js
Normal file
45
ui/mobile/ws/Socket.js
Normal file
@@ -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
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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. <br><br>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. <br><br>Admission: $30 for men, women are free.`)
|
||||||
.marginRight(4, em)
|
.marginRight(4, em)
|
||||||
|
|
||||||
button("Tickets Available Soon")
|
button("Tickets Available Soon")
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
class SignIn extends Shadow {
|
class SignIn extends Shadow {
|
||||||
|
|
||||||
inputStyles(el) {
|
inputStyles(el) {
|
||||||
console.log(el)
|
|
||||||
return el
|
return el
|
||||||
.border("1px solid var(--accent)")
|
.color("var(--accent)")
|
||||||
|
.border("1px solid var(--accent)")
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
Reference in New Issue
Block a user