Compare commits
34 Commits
1cc94f4d5f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07b42ccbc3 | ||
|
|
f1c7c340f9 | ||
|
|
f8861666dd | ||
|
|
cd5f92b55f | ||
|
|
8d57a507d1 | ||
|
|
9567e4e284 | ||
|
|
5b564fbfb3 | ||
|
|
279be987a4 | ||
|
|
9a1aa55297 | ||
|
|
be155ae913 | ||
|
|
9d43362cae | ||
|
|
5d59cf8e50 | ||
|
|
bc1a3ccf75 | ||
|
|
daa4182778 | ||
|
|
fcf0b4b08a | ||
|
|
eca1354e94 | ||
|
|
9e87364147 | ||
|
|
dc9b106439 | ||
|
|
cf9edc066a | ||
|
|
89702efa3a | ||
|
|
7f4a9a8b18 | ||
|
|
6c2b183f8e | ||
|
|
c7ca5c01c4 | ||
|
|
d63a7d02ec | ||
|
|
fd08d13bee | ||
|
|
6299e80268 | ||
|
|
40e7987ca2 | ||
|
|
9d62dbad86 | ||
|
|
8c7ed68975 | ||
|
|
c3ed5ac5ac | ||
|
|
525642d04e | ||
|
|
30faf12b20 | ||
|
|
2bc5c00830 | ||
|
|
3588a68bee |
11
.gitignore
vendored
@@ -1,11 +1,8 @@
|
||||
.DS_Store
|
||||
package-lock.json
|
||||
node_modules
|
||||
_/build
|
||||
.env
|
||||
|
||||
content
|
||||
db/*
|
||||
server/db/users.json
|
||||
server/.env
|
||||
abetaccredited.html
|
||||
qrCodes/qr_codes
|
||||
.env
|
||||
db/db.json
|
||||
qrCodes/qr_codes
|
||||
27
_test/OrderedObject.test.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import OrderedObject from "../server/db/model/OrderedObject.js"
|
||||
|
||||
window.testSuites.push(
|
||||
|
||||
class testOrderedObject {
|
||||
|
||||
async addShouldFailIfKeyIsDuplicate() {
|
||||
class Test extends OrderedObject {
|
||||
|
||||
}
|
||||
|
||||
let test = new Test()
|
||||
|
||||
test.add("1", {name: "hello"})
|
||||
|
||||
try {
|
||||
test.add("1", {name: "bye"})
|
||||
} catch(e){
|
||||
return
|
||||
}
|
||||
|
||||
return "Received no error!"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
)
|
||||
57
_test/test.js
Normal file
@@ -0,0 +1,57 @@
|
||||
let scriptToPaste = `
|
||||
<script type="module" src="./_test/test.js"></script>
|
||||
`;
|
||||
console.log("Tests initializing.")
|
||||
window.testSuites = [];
|
||||
|
||||
/* Server - DB */
|
||||
import ("./OrderedObject.test.js")
|
||||
|
||||
window.test = async function() {
|
||||
// window.testSuites.sort();
|
||||
window.alert = () => true;
|
||||
window.confirm = () => true;
|
||||
|
||||
console.clear();
|
||||
|
||||
let failed = 0;
|
||||
let success = 0;
|
||||
|
||||
var start = new Date();
|
||||
for(let j=0; j<window.testSuites.length; j++) {
|
||||
let testSuite = window.testSuites[j];
|
||||
console.log(`%c ➽ ${j+1} ${testSuite.name.replace("test", "")}`, 'color: #ffffff; font-size: 17px; padding-left: -20px; padding-top: 10px; padding-bottom: 10px; text-align: right;')
|
||||
let suite = new testSuite();
|
||||
let testNum = 0;
|
||||
let suiteContents = Object.getOwnPropertyNames(testSuite.prototype)
|
||||
for(let i=0; i<suiteContents.length; i++) {
|
||||
let test = suiteContents[i];
|
||||
if(typeof suite[test] === 'function' && test !== "constructor") {
|
||||
testNum++;
|
||||
let fail = await suite[test]();
|
||||
if(fail) {
|
||||
failed++;
|
||||
console.log(`%c ${testNum}. ${test}: ${fail}`, 'background: #222; color: rgb(254, 62, 43)');
|
||||
} else {
|
||||
success++;
|
||||
console.log(`%c ${testNum}. ${test}`, 'background: #222; color: #00FF00');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("")
|
||||
console.log("")
|
||||
let elapsed = new Date() - start;
|
||||
if(failed === 0) {
|
||||
console.log(`%cRan ${failed+success} tests in ${elapsed}ms`, 'background: #222; color: #00FF00');
|
||||
} else {
|
||||
console.log(`%cRan ${failed+success} tests in ${elapsed}ms`, 'background: #222; color: rgb(254, 62, 43)');
|
||||
}
|
||||
console.log(`%c ${success} passed`, 'background: #222; color: #00FF00');
|
||||
console.log(`%c ${failed} failed`, 'background: #222; color: rgb(254, 62, 43)');
|
||||
}
|
||||
|
||||
window.wait = ms => new Promise(res => setTimeout(res, ms));
|
||||
|
||||
window.__defineGetter__("test", test);
|
||||
BIN
db/images/1.jpg
Normal file
|
After Width: | Height: | Size: 179 KiB |
37
db/processJSON.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
async function processJSON() {
|
||||
// 1. Read original JSON
|
||||
const inputPath = path.join(process.cwd(), "db/tokens.json");
|
||||
const raw = await fs.readFile(inputPath, "utf8");
|
||||
const data = JSON.parse(raw);
|
||||
|
||||
// 2. Create a new result object
|
||||
const result = {};
|
||||
|
||||
// 3. Loop through all entries and modify as needed
|
||||
const entries = Object.entries(data);
|
||||
|
||||
for (const [i, [key, value]] of entries.entries()) {
|
||||
console.log(i);
|
||||
// ==== CHANGE THINGS HERE ====
|
||||
const newValue = {
|
||||
"labels": value.labels,
|
||||
"index": i+1,
|
||||
"url": value.url,
|
||||
"used": false
|
||||
};
|
||||
// =============================
|
||||
|
||||
// 4. Put modified entry into result
|
||||
result[key] = newValue;
|
||||
}
|
||||
|
||||
// 5. Write output JSON
|
||||
const outputPath = path.join(process.cwd(), "db/output.json");
|
||||
const jsonString = JSON.stringify(result, null, 2);
|
||||
await fs.writeFile(outputPath, jsonString, "utf8");
|
||||
}
|
||||
|
||||
processJSON();
|
||||
11
index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Node Terminal</title>
|
||||
<!-- <script type="module" src="./_test/test.js"></script> -->
|
||||
</head>
|
||||
<body style="background-color: rgb(32, 33, 36)">
|
||||
<script type="module" src="./server/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
25
main.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// main.js
|
||||
const { app, BrowserWindow, nativeImage } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
|
||||
|
||||
function createWindow() {
|
||||
const win = new BrowserWindow({
|
||||
show: false, // window is hidden
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
});
|
||||
win.loadFile(path.join(__dirname, 'index.html'));
|
||||
win.webContents.openDevTools({ mode: 'undocked' });
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
20
package.json
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"name": "Hyperia",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "node server/index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"start": "electron ."
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"argon2": "^0.44.0",
|
||||
"chalk": "^5.6.2",
|
||||
"chalk": "^4.1.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.18.2",
|
||||
"http-proxy": "^1.18.1",
|
||||
"express-useragent": "^2.0.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"ws": "^8.18.3"
|
||||
"stripe": "^20.0.0",
|
||||
"ws": "^8.18.3",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^25.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import chalk from 'chalk'
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import { pathToFileURL } from 'url';
|
||||
const chalk = require('chalk');
|
||||
const path = require('path');
|
||||
const fs = require('fs/promises');
|
||||
const { pathToFileURL } = require('url');
|
||||
|
||||
function Node(node) {
|
||||
let traits = [
|
||||
@@ -23,37 +23,6 @@ export default class QuillDB {
|
||||
this.loadData()
|
||||
}
|
||||
|
||||
#checkLabelSchemas(id, entry, labelModels) {
|
||||
entry.labels.forEach(label => {
|
||||
const model = labelModels[label];
|
||||
if (!model) {
|
||||
throw new Error("Data has unknown label or missing model: " + label)
|
||||
}
|
||||
model(entry);
|
||||
this.#labels[label].push(id);
|
||||
});
|
||||
}
|
||||
|
||||
async getLabelModels() {
|
||||
const labelHandlers = {};
|
||||
const labelDir = path.join(process.cwd(), 'server/db/model');
|
||||
const files = await fs.readdir(labelDir);
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.js')) continue;
|
||||
|
||||
const label = path.basename(file, '.js');
|
||||
const modulePath = path.join(labelDir, file);
|
||||
const module = await import(pathToFileURL(modulePath).href);
|
||||
labelHandlers[label] = module.default;
|
||||
|
||||
if (!this.#labels[label]) {
|
||||
this.#labels[label] = [];
|
||||
}
|
||||
}
|
||||
return labelHandlers
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
const dbData = await fs.readFile(path.join(process.cwd(), 'db/db.json'), 'utf8');
|
||||
let dbJson;
|
||||
@@ -81,27 +50,35 @@ export default class QuillDB {
|
||||
Object.preventExtensions(this);
|
||||
}
|
||||
|
||||
// superKey = "nodes" || "edges"
|
||||
async writeData(superKey, key, value) {
|
||||
const dbData = await fs.readFile(path.join(process.cwd(), 'db/db.json'), 'utf8');
|
||||
let dbJson;
|
||||
try {
|
||||
dbJson = JSON.parse(dbData);
|
||||
} catch {
|
||||
dbJson = []
|
||||
async getLabelModels() {
|
||||
const labelHandlers = {};
|
||||
const labelDir = path.join(process.cwd(), 'server/db/model');
|
||||
const files = await fs.readdir(labelDir);
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.js')) continue;
|
||||
|
||||
const label = path.basename(file, '.js');
|
||||
const modulePath = path.join(labelDir, file);
|
||||
const module = await import(pathToFileURL(modulePath).href);
|
||||
labelHandlers[label] = module.default;
|
||||
|
||||
if (!this.#labels[label]) {
|
||||
this.#labels[label] = [];
|
||||
}
|
||||
}
|
||||
|
||||
dbJson[superKey][key] = value;
|
||||
|
||||
await fs.writeFile(path.join(process.cwd(), 'db/db.json'), JSON.stringify(dbJson, null, 2), 'utf8')
|
||||
return labelHandlers
|
||||
}
|
||||
|
||||
generateUserID() {
|
||||
let id = this.#labels["User"].length + 1;
|
||||
while (this.get.user(`user-${id}`)) {
|
||||
id++;
|
||||
}
|
||||
return `user-${id}`; // O(1) most of the time
|
||||
#checkLabelSchemas(id, entry, labelModels) {
|
||||
entry.labels.forEach(label => {
|
||||
const model = labelModels[label];
|
||||
if (!model) {
|
||||
throw new Error("Data has unknown label or missing model: " + label)
|
||||
}
|
||||
model(entry);
|
||||
this.#labels[label].push(id);
|
||||
});
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import dotenv from 'dotenv';
|
||||
import chalk from 'chalk';
|
||||
import jwt from 'jsonwebtoken'
|
||||
import argon2 from 'argon2'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
const dotenv = require("dotenv")
|
||||
const jwt = require('jsonwebtoken');
|
||||
const argon2 = require('argon2');
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export default class AuthHandler {
|
||||
@@ -29,9 +28,24 @@ export default class AuthHandler {
|
||||
}
|
||||
}
|
||||
|
||||
getProfile(req, res) {
|
||||
const token = req.cookies.auth_token;
|
||||
if (!token) return res.status(401).send({ error: "No auth token" });
|
||||
|
||||
try {
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const email = payload.email;
|
||||
|
||||
const user = db.members.getByEmail(email);
|
||||
res.send({ email: user.email, name: user.firstName + " " + user.lastName });
|
||||
} catch (err) {
|
||||
res.status(401).send({ error: "Invalid token" });
|
||||
}
|
||||
}
|
||||
|
||||
async login(req, res) {
|
||||
const { email, password } = req.body;
|
||||
let foundUser = global.db.get.userByEmail(email)
|
||||
let foundUser = global.db.members.getByEmail(email)
|
||||
if(!foundUser) {
|
||||
res.status(400).json({ error: 'Incorrect email.' });
|
||||
return;
|
||||
@@ -41,7 +55,8 @@ export default class AuthHandler {
|
||||
if (!valid) {
|
||||
res.status(400).json({ error: 'Incorrect password.' });
|
||||
} else {
|
||||
const payload = { id: foundUser.id };
|
||||
const payload = { email: foundUser.email };
|
||||
console.log(payload)
|
||||
const secret = process.env.JWT_SECRET;
|
||||
const options = { expiresIn: "2h" };
|
||||
const token = jwt.sign(payload, secret, options);
|
||||
|
||||
127
server/db/db.js
@@ -1,48 +1,107 @@
|
||||
const fs = require('fs/promises');
|
||||
const chalk = require('chalk');
|
||||
const path = require('path');
|
||||
import QuillDB from "../_/quilldb.js"
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import Titles from "./model/Titles.js"
|
||||
import Members from './model/Members.js'
|
||||
import Tokens from './model/Tokens.js'
|
||||
import Payments from "./model/Payments.js"
|
||||
import Posts from "./model/Forum/Posts.js"
|
||||
import Conversations from "./model/Messages/Conversations.js"
|
||||
import Messages from "./model/Messages/Messages.js"
|
||||
|
||||
export default class Database extends QuillDB {
|
||||
tokens;
|
||||
export default class Database {
|
||||
titles = new Titles()
|
||||
members = new Members()
|
||||
tokens = new Tokens()
|
||||
payments = new Payments()
|
||||
posts = new Posts()
|
||||
conversations = new Conversations()
|
||||
messages = new Messages()
|
||||
|
||||
fromID = {
|
||||
"HY": this.titles,
|
||||
"MEMBER": this.members,
|
||||
"TOKEN": this.tokens,
|
||||
"PAYMENT": this.payments,
|
||||
"POST": this.posts,
|
||||
"CONVERSATION": this.conversations,
|
||||
"DM": this.messages
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.loadTokens()
|
||||
this.loadData()
|
||||
}
|
||||
|
||||
async loadTokens() {
|
||||
const tokenData = await fs.readFile(path.join(process.cwd(), 'db/tokens.json'), 'utf8');
|
||||
let tokenJSON = JSON.parse(tokenData);
|
||||
this.tokens = tokenJSON
|
||||
}
|
||||
|
||||
get = {
|
||||
user: (id) => {
|
||||
return this.nodes[id]
|
||||
},
|
||||
userByEmail: (email) => {
|
||||
for (const id of this.labels["User"]) {
|
||||
const user = this.get.user(id);
|
||||
if (user.email === email) {
|
||||
return { id, ...user }
|
||||
async loadData() {
|
||||
const dbData = await fs.readFile(path.join(process.cwd(), 'db/db.json'), 'utf8');
|
||||
let dbJson;
|
||||
try {
|
||||
dbJson = JSON.parse(dbData);
|
||||
} catch {
|
||||
dbJson = []
|
||||
}
|
||||
let nodes = dbJson["nodes"];
|
||||
let entries = Object.entries(nodes)
|
||||
|
||||
for(let i=0; i<entries.length; i++) {
|
||||
let entry = entries[i]
|
||||
let id = entry[0]; let node = entry[1];
|
||||
let type = id.split("-")[0]
|
||||
try {
|
||||
let collection = this.fromID[type]
|
||||
if(collection) {
|
||||
collection.save(node, id)
|
||||
} else {
|
||||
throw new Error("Type does not exist for node: ", id)
|
||||
}
|
||||
} catch(e) {
|
||||
throw e
|
||||
}
|
||||
return null;
|
||||
},
|
||||
token: (id) => {
|
||||
return this.tokens[id]
|
||||
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
console.log("saving db")
|
||||
global.db.saveData()
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
generateUserID() {
|
||||
let id = this.labels["User"].length + 1;
|
||||
while (this.get.user(`user-${id}`)) {
|
||||
id++;
|
||||
}
|
||||
return `user-${id}`; // O(1) most of the time
|
||||
}
|
||||
async saveData() {
|
||||
let data = {
|
||||
"nodes": {
|
||||
|
||||
async getAll() {
|
||||
return { nodes: this.nodes }
|
||||
},
|
||||
"edges": {
|
||||
|
||||
}
|
||||
}
|
||||
let arrs = [
|
||||
this.titles.entries,
|
||||
this.members.entries,
|
||||
this.tokens.entries,
|
||||
this.posts.entries,
|
||||
this.conversations.entries,
|
||||
this.messages.entries,
|
||||
this.payments.entries,
|
||||
]
|
||||
let ids = [
|
||||
Object.entries(this.titles.ids),
|
||||
Object.entries(this.members.ids),
|
||||
Object.entries(this.tokens.ids),
|
||||
Object.entries(this.posts.ids),
|
||||
Object.entries(this.conversations.ids),
|
||||
Object.entries(this.messages.ids),
|
||||
Object.entries(this.payments.ids),
|
||||
]
|
||||
for(let i=0; i<arrs.length; i++) {
|
||||
let arr = arrs[i]
|
||||
for(let j=0; j<arr.length; j++) {
|
||||
data.nodes[ids[i][j][0]] = arr[j]
|
||||
}
|
||||
}
|
||||
|
||||
let string = JSON.stringify(data, null, 4)
|
||||
await fs.writeFile(path.join(process.cwd(), 'db/db.json'), string, "utf8");
|
||||
}
|
||||
}
|
||||
56
server/db/model/Forum/Posts.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import OrderedObject from "../OrderedObject.js"
|
||||
const { z } = require("zod")
|
||||
|
||||
export default class Posts extends OrderedObject {
|
||||
schema = z.object({
|
||||
text: z.string(),
|
||||
time: z.string(),
|
||||
sentBy: z.string()
|
||||
})
|
||||
|
||||
makeID(forum, number) {
|
||||
return `POST-${forum}-${number}`
|
||||
}
|
||||
|
||||
save(post, id) {
|
||||
let result = this.schema.safeParse(post)
|
||||
if(result.success) {
|
||||
try {
|
||||
super.add(id, post)
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
console.error("Failed parsing member: ", result.error)
|
||||
throw new global.ServerError(400, "Invalid Member Data!: ");
|
||||
}
|
||||
}
|
||||
|
||||
get(forum, number) {
|
||||
let result = []
|
||||
let limit = Math.min(number, this.entries.length)
|
||||
for(let i=1; i<=limit; i++) {
|
||||
let id = this.makeID(forum, i)
|
||||
let post = this.entries[this.ids[id]]
|
||||
let {firstName, lastName} = global.db.members.get(post.sentBy)
|
||||
let seededObj = {
|
||||
...post
|
||||
}
|
||||
seededObj.sentByID = post.sentBy
|
||||
seededObj.sentBy = firstName + " " + lastName
|
||||
result.push(seededObj)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async add(text, forum, userEmail) {
|
||||
let newPost = {}
|
||||
newPost.text = text
|
||||
newPost.sentBy = db.members.getIDFromEmail(userEmail)
|
||||
newPost.time = global.currentTime()
|
||||
|
||||
let idNumber = this.entries.length+1
|
||||
super.add(this.makeID(forum, idNumber), newPost)
|
||||
}
|
||||
}
|
||||
77
server/db/model/Members.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import OrderedObject from "./OrderedObject.js"
|
||||
const argon2 = require("argon2")
|
||||
const { z } = require("zod")
|
||||
|
||||
export default class Members extends OrderedObject {
|
||||
addressSchema = z.object({
|
||||
address1: z.string(),
|
||||
address2: z.string().optional(),
|
||||
zip: z.string().regex(/^\d{5}(-\d{4})?$/),
|
||||
state: z.string(),
|
||||
city: z.string()
|
||||
})
|
||||
schema = z.object({
|
||||
id: z.number(),
|
||||
email: z.string().email(),
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
password: z.string(),
|
||||
tokenUsed: z.string().regex(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||
"Invalid UUID"
|
||||
),
|
||||
joined: z.string(),
|
||||
address: this.addressSchema,
|
||||
})
|
||||
|
||||
isHashed = (s) => {return s.startsWith("$argon2")}
|
||||
|
||||
save(member) {
|
||||
let id = `MEMBER-${member.id}`
|
||||
let result = this.schema.safeParse(member)
|
||||
if(result.success) {
|
||||
try {
|
||||
super.add(id, member)
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
console.error("Failed parsing member: ", result.error)
|
||||
throw new global.ServerError(400, "Invalid Member Data!: ");
|
||||
}
|
||||
}
|
||||
|
||||
async add(newMember, tokenID) {
|
||||
newMember.tokenUsed = tokenID
|
||||
const hash = await argon2.hash(newMember.password);
|
||||
newMember.password = hash
|
||||
newMember.joined = global.currentTime()
|
||||
newMember.id = this.entries.length+1
|
||||
this.save(newMember)
|
||||
}
|
||||
|
||||
get(id) {
|
||||
return this.entries[this.ids[id]]
|
||||
}
|
||||
|
||||
getByEmail(email) {
|
||||
for(let i=0; i<this.entries.length; i++) {
|
||||
if(this.entries[i].email === email) {
|
||||
return this.entries[i]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
getIDFromEmail(email) {
|
||||
let index = 0
|
||||
for(let i=0; i<this.entries.length; i++) {
|
||||
if(this.entries[i].email === email) {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
return Object.entries(this.ids)[index][0]
|
||||
}
|
||||
}
|
||||
62
server/db/model/Messages/Conversations.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import OrderedObject from "../OrderedObject.js"
|
||||
const { z } = require("zod")
|
||||
|
||||
export default class Conversations extends OrderedObject {
|
||||
|
||||
schema = z.object({
|
||||
id: z.number(),
|
||||
between: z.array(z.string()),
|
||||
lastUpdated: z.string()
|
||||
}).strict()
|
||||
|
||||
save(convo) {
|
||||
let id = `CONVERSATION-${convo.id}`
|
||||
let result = this.schema.safeParse(convo)
|
||||
if(result.success) {
|
||||
try {
|
||||
super.add(id, convo)
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
console.error(result.error)
|
||||
throw new global.ServerError(400, "Invalid Conversation Data!");
|
||||
}
|
||||
}
|
||||
|
||||
get(convoID) {
|
||||
console.log("convo getting, ", convoID)
|
||||
return this.entries[this.ids[convoID]]
|
||||
}
|
||||
|
||||
getByMember(userID) {
|
||||
let convos = []
|
||||
|
||||
function populateMemberProfilesFromIDs(ids) {
|
||||
let result = []
|
||||
for(let i=0; i<ids.length; i++) {
|
||||
result[i] = global.db.members.get(ids[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
for(let i=0; i<this.entries.length; i++) {
|
||||
let convo = this.entries[i]
|
||||
console.log(convo, userID)
|
||||
if(convo.between.includes(userID)) {
|
||||
|
||||
console.log("found user convo: ", convo.id)
|
||||
let messages = global.db.messages.getByConversation(`CONVERSATION-${convo.id}`)
|
||||
let result = {
|
||||
...convo,
|
||||
messages,
|
||||
}
|
||||
result.between = populateMemberProfilesFromIDs(convo.between)
|
||||
|
||||
convos.push(result)
|
||||
}
|
||||
}
|
||||
return convos
|
||||
}
|
||||
}
|
||||
57
server/db/model/Messages/Messages.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import OrderedObject from "../OrderedObject.js"
|
||||
const { z } = require("zod")
|
||||
|
||||
export default class Messages extends OrderedObject {
|
||||
|
||||
schema = z.object({
|
||||
id: z.number(),
|
||||
conversation: z.string(),
|
||||
from: z.string(),
|
||||
text: z.string(),
|
||||
time: z.string()
|
||||
}).strict()
|
||||
|
||||
save(msg) {
|
||||
let id = `DM-${msg.id}`
|
||||
let result = this.schema.safeParse(msg)
|
||||
if(result.success) {
|
||||
try {
|
||||
super.add(id, msg)
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
console.error(result.error)
|
||||
throw new global.ServerError(400, "Invalid Conversation Data!");
|
||||
}
|
||||
}
|
||||
|
||||
add(convo, text, userID) {
|
||||
let newMessage = {}
|
||||
newMessage.time = global.currentTime()
|
||||
newMessage.from = userID
|
||||
newMessage.conversation = convo
|
||||
newMessage.text = text
|
||||
newMessage.id = this.entries.length+1
|
||||
console.log(newMessage)
|
||||
this.save(newMessage)
|
||||
}
|
||||
|
||||
getByConversation(convoID) {
|
||||
let result = []
|
||||
for(let i=0; i<this.entries.length; i++) {
|
||||
let entry = this.entries[i]
|
||||
if(entry.conversation = convoID) {
|
||||
let userID = entry.from
|
||||
let fromUser = global.db.members.get(userID)
|
||||
let newObj = {
|
||||
...entry
|
||||
}
|
||||
newObj.from = fromUser
|
||||
result.push(newObj)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
27
server/db/model/OrderedObject.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export default class OrderedObject {
|
||||
entries = []
|
||||
ids = {}
|
||||
indexes = []
|
||||
|
||||
add(id, data) {
|
||||
if(this.ids[id]) {
|
||||
console.error(`Can't add item ${id}: id already exists`)
|
||||
throw new global.ServerError(400, `Member with this email already exists`)
|
||||
}
|
||||
this.entries.push(data)
|
||||
this.ids[id] = this.entries.length - 1
|
||||
}
|
||||
|
||||
update(id, data) {
|
||||
let index = this.ids[id]
|
||||
this.entries[index] = data
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
if (typeof key === "number") {
|
||||
return this.entries[key]
|
||||
} else {
|
||||
return this.entries[this.ids[key]]
|
||||
}
|
||||
}
|
||||
}
|
||||
42
server/db/model/Payments.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import OrderedObject from "./OrderedObject.js"
|
||||
const { z } = require("zod")
|
||||
|
||||
export default class Payments extends OrderedObject {
|
||||
|
||||
schema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
time: z.string(),
|
||||
amount: z.number(),
|
||||
product: z.string(),
|
||||
})
|
||||
|
||||
save(payment) {
|
||||
let id = `PAYMENT-${payment.id}`
|
||||
let result = this.schema.safeParse(payment)
|
||||
if(result.success) {
|
||||
try {
|
||||
super.add(id, payment)
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
console.error(result.error)
|
||||
throw new global.ServerError(400, "Invalid Member Data!");
|
||||
}
|
||||
}
|
||||
|
||||
add(paymentObj) {
|
||||
let toSave = {
|
||||
id: this.entries.length+1,
|
||||
...paymentObj
|
||||
}
|
||||
this.save(toSave)
|
||||
}
|
||||
|
||||
get(id) {
|
||||
return this.entries[this.ids[`PAYMENT-${id}`]]
|
||||
}
|
||||
}
|
||||
52
server/db/model/Titles.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import OrderedObject from "./OrderedObject.js"
|
||||
|
||||
export default class Titles extends OrderedObject {
|
||||
|
||||
save(newTitle) {
|
||||
let id = `HY-${this.entries.length+1}`
|
||||
if(this.validate(id, newTitle)) {
|
||||
try {
|
||||
super.add(id, newTitle)
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
throw new global.ServerError(400, "Invalid Member Data!");
|
||||
}
|
||||
}
|
||||
|
||||
validate(id, node) {
|
||||
let checkID = () => {
|
||||
let split = id.split("-")
|
||||
return (
|
||||
split.length === 2
|
||||
&& split[0] === "HY"
|
||||
&& !isNaN(Number(split[1]))
|
||||
)
|
||||
}
|
||||
let idres = checkID()
|
||||
if(!idres) {
|
||||
return false
|
||||
}
|
||||
|
||||
let checkFields = () => {
|
||||
let fields = [
|
||||
"fullName",
|
||||
]
|
||||
for(let i = 0; i < fields.length; i++) {
|
||||
if(!node[fields[i]]) {
|
||||
throw new Error(`Title ${id} is missing trait ${fields[i]}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
let fieldres = checkFields()
|
||||
if(!fieldres) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
41
server/db/model/Tokens.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import OrderedObject from "./OrderedObject.js"
|
||||
const { z } = require("zod")
|
||||
|
||||
export default class Tokens extends OrderedObject {
|
||||
|
||||
schema = z.object({
|
||||
index: z.number(),
|
||||
url: z.string(),
|
||||
uuid: z.string().regex(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||
"Invalid UUID"
|
||||
),
|
||||
used: z.boolean(),
|
||||
})
|
||||
|
||||
markUsed(uuid) {
|
||||
let token = this.get(uuid)
|
||||
token.used = true
|
||||
super.update(`TOKEN-${uuid}`, token)
|
||||
}
|
||||
|
||||
save(token) {
|
||||
let id = `TOKEN-${token.uuid}`
|
||||
let result = this.schema.safeParse(token)
|
||||
if(result.success) {
|
||||
try {
|
||||
super.add(id, token)
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
console.error(result.error)
|
||||
throw new global.ServerError(400, "Invalid Member Data!");
|
||||
}
|
||||
}
|
||||
|
||||
get(uuid) {
|
||||
return this.entries[this.ids[`TOKEN-${uuid}`]]
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
export default function User(node) {
|
||||
let traits = [
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"password"
|
||||
]
|
||||
for(let i = 0; i < traits.length; i++) {
|
||||
if(!node[traits[i]]) {
|
||||
if (traits[i] == "lastName") { continue; } // Ignores optional Last Name
|
||||
throw new Error(`User ${node.email} is missing trait ${traits[i]}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { broadcast } from './ws.js';
|
||||
|
||||
const handlers = {
|
||||
updateLocation(req, res) {
|
||||
const { name, latitude, longitude, timestamp } = req.body;
|
||||
console.log(`Received location: (${name}, ${latitude}, ${longitude}) at ${timestamp}`);
|
||||
broadcast("update-location", { name, latitude, longitude, timestamp });
|
||||
res.json({ success: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default handlers;
|
||||
@@ -1,32 +1,36 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import http from 'http'
|
||||
import fs from 'fs'
|
||||
import chalk from 'chalk'
|
||||
import moment from 'moment'
|
||||
import path from 'path';
|
||||
import { initWebSocket } from './ws.js'
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const http = require('http');
|
||||
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'
|
||||
import Database from "./db/db.js"
|
||||
import AuthHandler from './auth.js';
|
||||
import handlers from "./handlers.js";
|
||||
|
||||
// Get __dirname in ES6 environment
|
||||
import { fileURLToPath } from 'url';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
import PaymentsHandler from "./payments.js"
|
||||
|
||||
class Server {
|
||||
db;
|
||||
auth;
|
||||
UIPath = path.join(__dirname, '../ui')
|
||||
DBPath = path.join(__dirname, '../db')
|
||||
UIPath = path.join(__dirname, './ui')
|
||||
DBPath = path.join(__dirname, './db')
|
||||
|
||||
registerRoutes(router) {
|
||||
// router.post('/api/location', handlers.updateLocation)
|
||||
/* Stripe */
|
||||
router.post("/create-checkout-session", PaymentsHandler.danceTicket)
|
||||
router.post("/webhook", express.raw({ type: "application/json" }), PaymentsHandler.webhook)
|
||||
|
||||
/* Auth */
|
||||
router.post('/login', this.auth.login)
|
||||
router.get('/profile', this.auth.getProfile)
|
||||
router.get('/signout', this.auth.logout)
|
||||
|
||||
/* Site */
|
||||
router.get('/signup', this.verifyToken, this.get)
|
||||
router.post('/signup', this.verifyToken, this.newUserSubmission)
|
||||
router.get('/db/images/*', this.getUserImage)
|
||||
@@ -39,7 +43,7 @@ class Server {
|
||||
if (!token) {
|
||||
return res.status(400).json({ error: 'Token is required' });
|
||||
}
|
||||
let fromDB = this.db.get.token(token)
|
||||
let fromDB = this.db.tokens.get(token)
|
||||
if (!fromDB) {
|
||||
return res.status(403).json({ error: 'Invalid or expired token' });
|
||||
} else if(fromDB.used) {
|
||||
@@ -48,8 +52,19 @@ class Server {
|
||||
next()
|
||||
}
|
||||
|
||||
newUserSubmission = (req, res) => {
|
||||
return res.status(400).json({ error: 'Haven\t finished this bruh' });
|
||||
newUserSubmission = async (req, res) => {
|
||||
const { token } = req.query;
|
||||
try {
|
||||
let tokenData = db.tokens.get(token)
|
||||
if(tokenData.used) throw new global.ServerError(400, "Token alredy used!")
|
||||
await db.members.add(req.body, tokenData.uuid)
|
||||
db.tokens.markUsed(token)
|
||||
global.db.saveData()
|
||||
return res.status(200).json({});
|
||||
} catch(e) {
|
||||
console.log(e)
|
||||
return res.status(e.status).json({ error: 'Error adding new member' });
|
||||
}
|
||||
}
|
||||
|
||||
authMiddleware = (req, res, next) => {
|
||||
@@ -90,23 +105,14 @@ class Server {
|
||||
|
||||
let url = req.url
|
||||
|
||||
let publicPage = () => {
|
||||
url = "/index.html"
|
||||
let filePath = path.join(this.UIPath, "public", url);
|
||||
res.sendFile(filePath, (err) => {
|
||||
if (err) {
|
||||
console.log("File not found, sending fallback:", filePath);
|
||||
res.redirect("/");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let publicFile = () => {
|
||||
let publicSite = () => {
|
||||
let filePath;
|
||||
if(url.startsWith("/_")) {
|
||||
filePath = path.join(this.UIPath, url);
|
||||
} else if(url.includes("75820185")) {
|
||||
filePath = path.join(this.UIPath, "public", url.split("75820185")[1]);
|
||||
} else {
|
||||
filePath = path.join(this.UIPath, "public", url);
|
||||
filePath = path.join(this.UIPath, "public", "index.html");
|
||||
}
|
||||
|
||||
res.sendFile(filePath);
|
||||
@@ -114,23 +120,20 @@ 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);
|
||||
}
|
||||
|
||||
if(!this.auth.isLoggedInUser(req, res)) {
|
||||
if(!url.includes(".")) {
|
||||
publicPage()
|
||||
} else {
|
||||
publicFile()
|
||||
}
|
||||
publicSite()
|
||||
} else {
|
||||
privateSite()
|
||||
}
|
||||
@@ -166,10 +169,12 @@ class Server {
|
||||
global.db = this.db
|
||||
this.auth = new AuthHandler()
|
||||
const app = express();
|
||||
app.post("/webhook", express.raw({ type: "application/json" }), PaymentsHandler.webhook)
|
||||
app.use(cors({ origin: '*' }));
|
||||
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);
|
||||
@@ -179,7 +184,7 @@ class Server {
|
||||
app.use('/', router);
|
||||
|
||||
const server = http.createServer(app);
|
||||
initWebSocket(server);
|
||||
global.Socket = new Socket(server);
|
||||
const PORT = 3003;
|
||||
server.listen(PORT, () => {
|
||||
console.log("\n")
|
||||
|
||||
68
server/payments.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const Stripe = require("stripe")
|
||||
const dotenv = require("dotenv")
|
||||
dotenv.config();
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET);
|
||||
|
||||
export default class PaymentsHandler {
|
||||
|
||||
static async danceTicket(req, res) {
|
||||
try {
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
mode: "payment",
|
||||
payment_method_types: ["card"],
|
||||
metadata: {
|
||||
productId: "austin_winter_ball_2025_ticket"
|
||||
},
|
||||
line_items: [
|
||||
{
|
||||
price_data: {
|
||||
currency: "usd",
|
||||
product_data: {
|
||||
name: "Hyperia Winter Ball"
|
||||
},
|
||||
unit_amount: 3500
|
||||
},
|
||||
quantity: 1
|
||||
}
|
||||
],
|
||||
success_url: `${process.env.PROTOCOL + process.env.BASE_URL}/success`,
|
||||
cancel_url: `${process.env.PROTOCOL + process.env.BASE_URL}/events`
|
||||
});
|
||||
|
||||
res.json({ url: session.url });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Something went wrong." });
|
||||
}
|
||||
}
|
||||
|
||||
static webhook = (req, res) => {
|
||||
const sig = req.headers["stripe-signature"];
|
||||
|
||||
try {
|
||||
const event = stripe.webhooks.constructEvent(
|
||||
req.body,
|
||||
sig,
|
||||
process.env.WEBHOOK_SECRET
|
||||
);
|
||||
|
||||
if (event.type === "checkout.session.completed") {
|
||||
const session = event.data.object;
|
||||
let toStore = {
|
||||
"product": session.metadata.productId,
|
||||
"email": session.customer_details.email,
|
||||
"name": session.customer_details.name,
|
||||
"time": global.currentTime(),
|
||||
"amount": session.amount_total,
|
||||
}
|
||||
global.db.payments.add(toStore)
|
||||
}
|
||||
|
||||
res.sendStatus(200);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.sendStatus(400);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
server/util.js
Normal file
@@ -0,0 +1,24 @@
|
||||
global.ServerError = class extends Error {
|
||||
constructor(status, msg) {
|
||||
super(msg);
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
global.currentTime = function () {
|
||||
const now = new Date();
|
||||
|
||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(now.getDate()).padStart(2, "0");
|
||||
const year = now.getFullYear();
|
||||
|
||||
let hours = now.getHours();
|
||||
const ampm = hours >= 12 ? "pm" : "am";
|
||||
hours = hours % 12 || 12; // convert to 12-hour format
|
||||
|
||||
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||||
const seconds = String(now.getSeconds()).padStart(2, "0");
|
||||
const ms = String(now.getMilliseconds()).padStart(4, "0"); // 4-digit like "5838"
|
||||
|
||||
return `${month}.${day}.${year}-${hours}:${minutes}:${seconds}${ms}${ampm}`;
|
||||
}
|
||||
31
server/ws.js
@@ -1,31 +0,0 @@
|
||||
import { WebSocket, WebSocketServer } from 'ws';
|
||||
|
||||
let wss;
|
||||
|
||||
export function initWebSocket(server) {
|
||||
wss = new WebSocketServer({ server });
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
console.log('✅ New WebSocket client connected');
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log('Client disconnected');
|
||||
});
|
||||
});
|
||||
|
||||
console.log('WebSocket server initialized');
|
||||
}
|
||||
|
||||
// Broadcast a message to all connected clients
|
||||
export function broadcast(reqType, data) {
|
||||
if (!wss) return;
|
||||
|
||||
const payload = typeof data === 'string' ? data : JSON.stringify(data);
|
||||
const message = `${reqType}|${payload}`;
|
||||
|
||||
wss.clients.forEach(client => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
43
server/ws/handlers/ForumHandler.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const { z } = require("zod")
|
||||
|
||||
const sendSchema = z.object({
|
||||
forum: z.string(),
|
||||
text: z.string(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
const getSchema = z.object({
|
||||
forum: z.string(),
|
||||
number: z.number()
|
||||
})
|
||||
.strict()
|
||||
|
||||
|
||||
export default class ForumHandler {
|
||||
static handleSend(msg, ws) {
|
||||
try {
|
||||
global.db.posts.add(msg.text, msg.forum, ws.userEmail)
|
||||
global.Socket.broadcast({event: "new-post", app: "FORUM", forum: msg.forum, msg: this.handleGet({forum: msg.forum, number: 100})})
|
||||
return {success: true}
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
static handleGet(msg) {
|
||||
let data = global.db.posts.get(msg.forum, msg.number)
|
||||
return data
|
||||
}
|
||||
|
||||
static handle(operation, msg, ws) {
|
||||
switch(operation) {
|
||||
case "SEND":
|
||||
if(!sendSchema.safeParse(msg).success) throw new Error("Incorrectly formatted Forum ws message!")
|
||||
return this.handleSend(msg, ws)
|
||||
case "GET":
|
||||
if(!getSchema.safeParse(msg).success) throw new Error("Incorrectly formatted Forum ws message!")
|
||||
return this.handleGet(msg)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
40
server/ws/handlers/MessagesHandler.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const { z } = require("zod")
|
||||
|
||||
const sendSchema = z.object({
|
||||
conversation: z.string(),
|
||||
text: z.string(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
export default class MessagesHandler {
|
||||
|
||||
static handleSend(msg, ws) {
|
||||
let user = global.db.members.getByEmail(ws.userEmail)
|
||||
let convo = global.db.conversations.get(msg.conversation)
|
||||
if(convo.between.includes(`MEMBER-${user.id}`)) {
|
||||
global.db.messages.add(msg.conversation, msg.text, `MEMBER-${user.id}`)
|
||||
global.Socket.broadcast({event: "new-message", app: "MESSAGES", msg: {conversationID: convo.id, messages: global.db.messages.getByConversation(`CONVERSATION-${msg.conversation}`)}})
|
||||
|
||||
} else {
|
||||
throw new Error("Can't add to a conversation user is not a part of!")
|
||||
}
|
||||
return {success: true}
|
||||
}
|
||||
|
||||
static handleGet(ws) {
|
||||
let user = global.db.members.getByEmail(ws.userEmail)
|
||||
let data = global.db.conversations.getByMember(`MEMBER-${user.id}`)
|
||||
return data
|
||||
}
|
||||
|
||||
static handle(operation, msg, ws) {
|
||||
switch(operation) {
|
||||
case "GET":
|
||||
return this.handleGet(ws)
|
||||
case "SEND":
|
||||
if(!sendSchema.safeParse(msg).success) throw new Error("Incorrectly formatted Forum ws message!")
|
||||
return this.handleSend(msg, ws)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
108
server/ws/ws.js
Normal file
@@ -0,0 +1,108 @@
|
||||
const { WebSocket, WebSocketServer } = require('ws');
|
||||
const { z } = require("zod")
|
||||
const jwt = require('jsonwebtoken');
|
||||
import ForumHandler from "./handlers/ForumHandler.js"
|
||||
import MessagesHandler from "./handlers/MessagesHandler.js"
|
||||
|
||||
export default class Socket {
|
||||
wss;
|
||||
messageSchema = z.object({
|
||||
id: z.string(),
|
||||
app: z.string(),
|
||||
operation: z.string().optional(),
|
||||
msg: z.union([
|
||||
z.object({}).passthrough(), // allows any object
|
||||
z.array(z.any()) // allows any array
|
||||
]).optional()
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.operation !== "GET" && data.msg === undefined) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["msg"],
|
||||
message: "msg is required when operation is not GET"
|
||||
})
|
||||
}
|
||||
})
|
||||
.strict()
|
||||
|
||||
constructor(server) {
|
||||
this.wss = new WebSocketServer({ server });
|
||||
|
||||
this.wss.on('connection', (ws, req) => {
|
||||
console.log('✅ New WebSocket client connected');
|
||||
|
||||
function parseCookies(cookieHeader = "") {
|
||||
return Object.fromEntries(
|
||||
cookieHeader.split(";").map(c => {
|
||||
const [key, ...v] = c.trim().split("=");
|
||||
return [key, decodeURIComponent(v.join("="))];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const cookies = parseCookies(req.headers.cookie);
|
||||
const token = cookies.auth_token;
|
||||
if (!token) throw new Error("No auth token");
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET);
|
||||
ws.userEmail = payload.email;
|
||||
|
||||
ws.on('message', (msg) => {
|
||||
this.handleMessage(msg, ws);
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log('Client disconnected');
|
||||
});
|
||||
});
|
||||
|
||||
console.log('WebSocket server initialized');
|
||||
}
|
||||
|
||||
// Build a system where the ws obj is updated every time on navigate, so it already has context
|
||||
// this way, we can only send broadcast messages to clients that actually have that app / subapp open
|
||||
handleMessage = (msg, ws) => {
|
||||
try {
|
||||
const text = msg.toString();
|
||||
const req = JSON.parse(text);
|
||||
if(!this.messageSchema.safeParse(req).success) throw new Error("Socket.handleMessage: Incoming ws message has incorrect format!")
|
||||
|
||||
let responseData;
|
||||
switch (req.app) {
|
||||
case "FORUM":
|
||||
responseData = ForumHandler.handle(req.operation, req.msg, ws)
|
||||
break;
|
||||
|
||||
case "MESSAGES":
|
||||
responseData = MessagesHandler.handle(req.operation, req.msg, ws)
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error("unknown ws message")
|
||||
}
|
||||
|
||||
let response = {
|
||||
...req
|
||||
}
|
||||
response.msg = responseData
|
||||
|
||||
if(!this.messageSchema.safeParse(response).success) throw new Error("Socket.handleMessage: Outgoing ws message has incorrect format!")
|
||||
ws.send(JSON.stringify(response))
|
||||
|
||||
} catch (e) {
|
||||
console.error("Invalid WS message:", e);
|
||||
}
|
||||
}
|
||||
|
||||
broadcast(event) {
|
||||
if (!this.wss) return;
|
||||
|
||||
let message = JSON.stringify(event)
|
||||
|
||||
this.wss.clients.forEach(ws => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,15 @@
|
||||
/*
|
||||
Sam Russell
|
||||
Captured Sun
|
||||
12.26.25 - State for arrays, nested objects. State for stacks (Shadow-only)
|
||||
12.17.25 - [Hyperia] - adding width, height functions. adding "e" to onClick. adding the non-window $$ funcs.
|
||||
12.16.25 - [comalyr] - State
|
||||
11.25.25.1 - Added minHeight and minWidth to be counted as numerical styles
|
||||
11.25.25 - Added onChange for check boxes, added setQuery / onQueryChanged for easy filtering
|
||||
11.24.25 - Fixing onClick because it was reversed, adding event to onHover params
|
||||
11.23.25 - Added onSubmit() event for form submission, added marginHorizontal() and marginVertical()
|
||||
11.20.25 - Added "pct" style unit, added alignVertical and alignHorizontal for flex boxes
|
||||
11.19.25 - Allowing for "auto" values in otherwise numeric styles, adding vmin and vmax units
|
||||
11.17.25.3 - Adding styles() and fixing dynamic function from earlier
|
||||
11.17.25.2 - Fixing onNavigate() and onAppear()
|
||||
11.17.25 - Added dynamic function to have units in style func parameters.
|
||||
@@ -24,6 +33,23 @@ history.pushState = function pushState() {
|
||||
window.addEventListener('popstate', () => {
|
||||
window.dispatchEvent(new Event('navigate'));
|
||||
});
|
||||
|
||||
window.setQuery = function(key, value) {
|
||||
const url = new URL(window.location.href);
|
||||
const params = url.searchParams;
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
params.delete(key);
|
||||
} else {
|
||||
params.set(key, value);
|
||||
}
|
||||
|
||||
const newUrl = url.toString();
|
||||
history.replaceState(null, "", newUrl);
|
||||
window.dispatchEvent(new Event('query-changed'));
|
||||
|
||||
return newUrl;
|
||||
};
|
||||
|
||||
window.navigateTo = function(url) {
|
||||
window.dispatchEvent(new Event('navigate'));
|
||||
@@ -45,6 +71,12 @@ window.$ = function(selector, el = document) {
|
||||
window.$$ = function(selector, el = document) {
|
||||
return Array.from(el.querySelectorAll(selector))
|
||||
}
|
||||
HTMLElement.prototype.$$ = function(selector) {
|
||||
return window.$$(selector, this)
|
||||
}
|
||||
DocumentFragment.prototype.$$ = function(selector) {
|
||||
return window.$$(selector, this)
|
||||
}
|
||||
|
||||
/* CONSOLE */
|
||||
|
||||
@@ -140,6 +172,7 @@ Object.defineProperty(Array.prototype, 'last', {
|
||||
|
||||
window.quill = {
|
||||
rendering: [],
|
||||
lastState: null,
|
||||
|
||||
render: (el) => {
|
||||
if(el instanceof Shadow) {
|
||||
@@ -170,19 +203,6 @@ window.quill = {
|
||||
quill.rendering.pop()
|
||||
},
|
||||
|
||||
loadPage: () => {
|
||||
let URL = window.location.pathname
|
||||
if(!window.routes[URL]) {
|
||||
throw new Error("No URL for this route: ", URL)
|
||||
}
|
||||
|
||||
let pageClass = window[routes[URL]]
|
||||
document.title = pageClass.title ?? document.title
|
||||
document.body.innerHTML = ""
|
||||
let page = new pageClass()
|
||||
quill.render(page)
|
||||
},
|
||||
|
||||
isStack: (el) => {
|
||||
return el.classList.contains("HStack") || el.classList.contains("ZStack") || el.classList.contains("VStack")
|
||||
},
|
||||
@@ -193,6 +213,7 @@ window.Shadow = class extends HTMLElement {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
window.register = (el, tagname) => {
|
||||
if (typeof el.prototype.render !== 'function') {
|
||||
throw new Error("Element must have a render: " + el.prototype.constructor.name)
|
||||
@@ -207,6 +228,75 @@ window.register = (el, tagname) => {
|
||||
|
||||
window[el.prototype.constructor.name] = function (...params) {
|
||||
let instance = new el(...params)
|
||||
if(instance.state) {
|
||||
const proxyCache = new WeakMap();
|
||||
|
||||
function reactive(value, path=[]) {
|
||||
if (value && typeof value === "object") {
|
||||
if (proxyCache.has(value)) return proxyCache.get(value);
|
||||
|
||||
const p = new Proxy(value, createHandlers(path));
|
||||
proxyCache.set(value, p);
|
||||
return p;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function isNumericKey(prop) {
|
||||
return typeof prop === "string" && prop !== "" && String(+prop) === prop;
|
||||
}
|
||||
|
||||
function createHandlers(path) {
|
||||
return {
|
||||
get(target, prop, receiver) {
|
||||
if (typeof prop === "symbol") {
|
||||
return Reflect.get(target, prop, receiver);
|
||||
}
|
||||
|
||||
let nextPath = (Array.isArray(target) && !isNumericKey(prop)) ? path : path.concat(prop) // To filter out arr.length, arr.map, arr.forEach, etc.
|
||||
quill.lastState = nextPath.join(".");
|
||||
|
||||
const v = Reflect.get(target, prop, receiver);
|
||||
return reactive(v, nextPath);
|
||||
},
|
||||
|
||||
set(target, prop, value, receiver) {
|
||||
const oldLength = Array.isArray(target) ? target.length : undefined;
|
||||
const oldValue = target[prop];
|
||||
if (oldValue === value) return true;
|
||||
|
||||
const result = Reflect.set(target, prop, value, receiver);
|
||||
|
||||
let changedPath = (Array.isArray(target) && (!isNumericKey(prop) || target.length !== oldLength)) ? path : path.concat(prop).join("."); // To filter out arr.length, arr.map, arr.forEach, and also a push/pop/unshift.
|
||||
const watchers = instance.stateWatchers[changedPath];
|
||||
|
||||
if (watchers) {
|
||||
watchers.forEach(cb => cb());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let proxy = reactive(instance.state)
|
||||
|
||||
Object.defineProperty(instance, "state", {
|
||||
value: proxy,
|
||||
writable: false,
|
||||
configurable: false,
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
let stateWatchers = {}
|
||||
Object.keys(instance.state).forEach((key) => stateWatchers[key] = [])
|
||||
Object.defineProperty(instance, "stateWatchers", {
|
||||
value: stateWatchers,
|
||||
writable: false,
|
||||
configurable: false,
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
quill.render(instance)
|
||||
return instance
|
||||
}
|
||||
@@ -218,6 +308,9 @@ HTMLElement.prototype.rerender = function() {
|
||||
|
||||
/* Styling */
|
||||
|
||||
window.pct = "%"
|
||||
window.vmin = "vmin"
|
||||
window.vmax = "vmax"
|
||||
window.vh = "vh"
|
||||
window.vw = "vw"
|
||||
window.px = "px"
|
||||
@@ -267,23 +360,28 @@ function extendHTMLElementWithStyleSetters() {
|
||||
case "height":
|
||||
case "maxWidth":
|
||||
case "maxHeight":
|
||||
case "minWidth":
|
||||
case "minHeight":
|
||||
|
||||
case "left":
|
||||
case "top":
|
||||
case "bottom":
|
||||
case "right":
|
||||
|
||||
case "padding":
|
||||
case "paddingLeft":
|
||||
case "paddingTop":
|
||||
case "paddingBottom":
|
||||
case "paddingRight":
|
||||
|
||||
case "margin":
|
||||
case "marginLeft":
|
||||
case "marginTop":
|
||||
case "marginBottom":
|
||||
case "marginRight":
|
||||
|
||||
case "textUnderlineOffset":
|
||||
case "letterSpacing":
|
||||
|
||||
return "unit-number"
|
||||
|
||||
@@ -303,20 +401,27 @@ function extendHTMLElementWithStyleSetters() {
|
||||
|
||||
switch (type) {
|
||||
case "unit-number":
|
||||
HTMLElement.prototype[prop] = function(value, unit = "px") {
|
||||
if (typeof value !== "number" || isNaN(value)) {
|
||||
throw new Error(`Invalid value for ${prop}: ${value}. Expected a number.`);
|
||||
HTMLElement.prototype[prop] = StyleFunction(function(value, unit = "px") {
|
||||
if(value === "auto") {
|
||||
this.style[prop] = value
|
||||
return this
|
||||
}
|
||||
this.style[prop] = value + unit;
|
||||
if (value !== "" && this.style[prop] === "") {
|
||||
throw new Error(`Invalid CSS value for ${prop}: ` + value + unit);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
});
|
||||
break;
|
||||
|
||||
case "string":
|
||||
HTMLElement.prototype[prop] = function(value) {
|
||||
HTMLElement.prototype[prop] = StyleFunction(function(value) {
|
||||
this.style[prop] = value;
|
||||
if (value !== "" && this.style[prop] === "") {
|
||||
throw new Error(`Invalid CSS value for ${prop}: ` + value);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -324,79 +429,86 @@ function extendHTMLElementWithStyleSetters() {
|
||||
|
||||
extendHTMLElementWithStyleSetters();
|
||||
|
||||
HTMLElement.prototype.styles = function(cb) {
|
||||
cb.call(this, this)
|
||||
HTMLElement.prototype.addStateWatcher = function(field, cb) {
|
||||
let parent = this
|
||||
while(!(parent instanceof Shadow)) {
|
||||
parent = parent.parentNode
|
||||
}
|
||||
parent.stateWatchers[field].push(cb)
|
||||
}
|
||||
|
||||
HTMLElement.prototype.padding = function(one, two, three = "px") {
|
||||
// Currently only works for one state variable in the function
|
||||
// Could probably be fixed by just making lastState an array and clearing it out every function call?
|
||||
HTMLElement.prototype.setUpState = function(styleFunc, cb) {
|
||||
let format = (value) => {return Array.isArray(value) ? value : [value]}
|
||||
|
||||
const setPadding = (side, val) => {
|
||||
const directionName = `padding${side.charAt(0).toUpperCase()}${side.slice(1)}`;
|
||||
this.style[directionName] = (typeof val === 'number') ? `${val}${three}` : val;
|
||||
};
|
||||
// 1. Run the callback to get the style argument and also update lastState
|
||||
let styleArgs = format(cb())
|
||||
|
||||
if(one === "horizontal" || one === "vertical") { // is one a direction
|
||||
if (one === "horizontal") {
|
||||
setPadding("left", two);
|
||||
setPadding("right", two);
|
||||
} else if (one === "vertical") {
|
||||
setPadding("top", two);
|
||||
setPadding("bottom", two);
|
||||
}
|
||||
} else { // is two a value
|
||||
if(typeof one !== 'number' || isNaN(one)) {
|
||||
this.style.padding = one
|
||||
// 2. Check if lastState has really been updated. If not, the user-provided cb did not access valid state
|
||||
if(!quill.lastState) {
|
||||
throw new Error("Quill: style state function does not access valid state")
|
||||
}
|
||||
|
||||
// 3. Construct function to run when state changes
|
||||
let onStateChange = () => {
|
||||
styleFunc.call(this, ...format(cb()))
|
||||
}
|
||||
|
||||
// 4. Now listen for the state to change
|
||||
this.addStateWatcher(quill.lastState, onStateChange)
|
||||
|
||||
// 5. Run the original function again, this time with the actual arguments
|
||||
quill.lastState = null
|
||||
styleFunc.call(this, ...styleArgs)
|
||||
}
|
||||
|
||||
function StyleFunction(func) {
|
||||
let styleFunction = function(value, unit = "px") {
|
||||
if(typeof value === 'function') {
|
||||
this.setUpState(styleFunction, value)
|
||||
return this
|
||||
} else {
|
||||
this.style.padding = one + two
|
||||
func.call(this, value, unit); // ".call" ensures that "this" is correct
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
HTMLElement.prototype.paddingVertical = function(value, unit = "px") {
|
||||
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||
this.style.paddingTop = value + unit
|
||||
this.style.paddingBottom = value + unit
|
||||
return this
|
||||
}
|
||||
return styleFunction
|
||||
}
|
||||
|
||||
HTMLElement.prototype.paddingHorizontal = function(value, unit = "px") {
|
||||
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||
this.style.paddingRight = value + unit
|
||||
this.style.paddingLeft = value + unit
|
||||
return this
|
||||
}
|
||||
HTMLElement.prototype.styles = function(cb) {
|
||||
cb.call(this, this)
|
||||
return this
|
||||
}
|
||||
|
||||
HTMLElement.prototype.margin = function(direction, value, unit = "px") {
|
||||
if (!value) {
|
||||
this.style.margin = direction;
|
||||
return this;
|
||||
}
|
||||
/* Type 1 */
|
||||
|
||||
const setMargin = (side, val) => {
|
||||
const directionName = `margin${side.charAt(0).toUpperCase()}${side.slice(1)}`;
|
||||
this.style[directionName] = (typeof val === 'number') ? `${val}${unit}` : val;
|
||||
};
|
||||
HTMLElement.prototype.paddingVertical = StyleFunction(function(value, unit = "px") {
|
||||
this.style.paddingTop = value + unit
|
||||
this.style.paddingBottom = value + unit
|
||||
return this
|
||||
})
|
||||
|
||||
if (direction === "horizontal") {
|
||||
setMargin("left", value);
|
||||
setMargin("right", value);
|
||||
} else if (direction === "vertical") {
|
||||
setMargin("top", value);
|
||||
setMargin("bottom", value);
|
||||
} else {
|
||||
setMargin(direction, value);
|
||||
}
|
||||
HTMLElement.prototype.paddingHorizontal = StyleFunction(function(value, unit = "px") {
|
||||
this.style.paddingRight = value + unit
|
||||
this.style.paddingLeft = value + unit
|
||||
return this
|
||||
})
|
||||
|
||||
return this;
|
||||
};
|
||||
HTMLElement.prototype.marginVertical = StyleFunction(function(value, unit = "px") {
|
||||
this.style.marginTop = value + unit
|
||||
this.style.marginBottom = value + unit
|
||||
return this
|
||||
})
|
||||
|
||||
HTMLElement.prototype.fontSize = function(value, unit = "px") {
|
||||
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||
HTMLElement.prototype.marginHorizontal = StyleFunction(function(value, unit = "px") {
|
||||
this.style.marginRight = value + unit
|
||||
this.style.marginLeft = value + unit
|
||||
return this
|
||||
})
|
||||
|
||||
HTMLElement.prototype.fontSize = StyleFunction(function(value, unit = "px") {
|
||||
|
||||
switch(value) {
|
||||
case "6xl":
|
||||
@@ -440,6 +552,27 @@ HTMLElement.prototype.fontSize = function(value, unit = "px") {
|
||||
}
|
||||
this.style.fontSize = value + unit
|
||||
return this
|
||||
})
|
||||
|
||||
|
||||
HTMLElement.prototype.width = function(value, unit = "px") {
|
||||
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||
this.style.width = value + unit
|
||||
if(window.getComputedStyle(this).display === "inline") {
|
||||
this.style.display = "block"
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
HTMLElement.prototype.height = function(value, unit = "px") {
|
||||
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
|
||||
throw new Error(`Invalid value: ${value}. Expected a number.`);
|
||||
this.style.height = value + unit
|
||||
if(window.getComputedStyle(this).display === "inline") {
|
||||
this.style.display = "block"
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
function checkPositionType(el) {
|
||||
@@ -510,6 +643,35 @@ HTMLElement.prototype.centerY = function () {
|
||||
return this;
|
||||
};
|
||||
|
||||
HTMLElement.prototype.alignVertical = function (value) {
|
||||
const direction = getComputedStyle(this).flexDirection;
|
||||
if(!direction) {
|
||||
throw new Error("alignVertical can be only be used on HStacks or VStacks!")
|
||||
}
|
||||
|
||||
if (direction === "column" || direction === "column-reverse") {
|
||||
this.style.justifyContent = value;
|
||||
} else {
|
||||
this.style.alignItems = value;
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
HTMLElement.prototype.alignHorizontal = function (value) {
|
||||
const direction = getComputedStyle(this).flexDirection;
|
||||
if(!direction) {
|
||||
throw new Error("alignHorizontal can be only be used on HStacks or VStacks!")
|
||||
}
|
||||
|
||||
if (direction === "column" || direction === "column-reverse") {
|
||||
this.style.alignItems = value;
|
||||
} else {
|
||||
this.style.justifyContent = value;
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
/* Elements */
|
||||
|
||||
quill.setChildren = function(el, innerContent) {
|
||||
@@ -631,7 +793,7 @@ window.form = function(cb) {
|
||||
return el
|
||||
}
|
||||
|
||||
window.input = function(placeholder, width, height) {
|
||||
window.input = function(placeholder = "", width, height) {
|
||||
let el = document.createElement("input")
|
||||
el.placeholder = placeholder
|
||||
el.style.width = width
|
||||
@@ -640,14 +802,18 @@ window.input = function(placeholder, width, height) {
|
||||
return el
|
||||
}
|
||||
|
||||
window.label = function(text) {
|
||||
window.label = function(inside) {
|
||||
let el = document.createElement("label")
|
||||
el.innerText = text
|
||||
if(typeof inside === "string") {
|
||||
el.innerText = inside
|
||||
} else {
|
||||
el.render = inside
|
||||
}
|
||||
quill.render(el)
|
||||
return el
|
||||
}
|
||||
|
||||
window.textarea = function(placeholder) {
|
||||
window.textarea = function(placeholder = "") {
|
||||
let el = document.createElement("textarea")
|
||||
el.placeholder = placeholder
|
||||
quill.render(el)
|
||||
@@ -657,61 +823,47 @@ window.textarea = function(placeholder) {
|
||||
|
||||
/* STACKS */
|
||||
|
||||
window.VStack = function (cb = () => {}) {
|
||||
let styles = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`
|
||||
handleStack = function(cb, name, styles="") {
|
||||
let nowRendering = quill.rendering[quill.rendering.length-1]
|
||||
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
|
||||
nowRendering.style.cssText += styles
|
||||
nowRendering.classList.add("VStack")
|
||||
nowRendering.classList.add(name)
|
||||
cb()
|
||||
if(quill.lastState) {
|
||||
nowRendering.addStateWatcher(quill.lastState, () => {
|
||||
nowRendering.innerHTML = ""
|
||||
cb()
|
||||
})
|
||||
}
|
||||
return nowRendering
|
||||
} else {
|
||||
let div = document.createElement("div")
|
||||
div.classList.add(name)
|
||||
div.style.cssText += styles
|
||||
div.render = cb
|
||||
quill.render(div)
|
||||
return div
|
||||
}
|
||||
}
|
||||
|
||||
let div = document.createElement("div")
|
||||
div.classList.add("VStack")
|
||||
div.style.cssText += styles
|
||||
div.render = cb
|
||||
quill.render(div)
|
||||
return div
|
||||
window.VStack = function (cb = () => {}) {
|
||||
let styles = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`
|
||||
return handleStack(cb, "VStack", styles)
|
||||
}
|
||||
|
||||
window.HStack = function (cb = () => {}) {
|
||||
let styles = `
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
let nowRendering = quill.rendering[quill.rendering.length - 1];
|
||||
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
|
||||
nowRendering.style.cssText += styles;
|
||||
nowRendering.classList.add("HStack")
|
||||
cb();
|
||||
return nowRendering;
|
||||
}
|
||||
|
||||
let div = document.createElement("div");
|
||||
div.classList.add("HStack");
|
||||
div.style.cssText += styles;
|
||||
div.render = cb;
|
||||
quill.render(div);
|
||||
return div;
|
||||
return handleStack(cb, "HStack", styles)
|
||||
};
|
||||
|
||||
window.ZStack = function (cb = () => {}) {
|
||||
let nowRendering = quill.rendering[quill.rendering.length - 1];
|
||||
if (nowRendering.innerHTML.trim() === "" && !quill.isStack(nowRendering)) {
|
||||
nowRendering.classList.add("ZStack")
|
||||
cb();
|
||||
return nowRendering;
|
||||
}
|
||||
|
||||
let div = document.createElement("div");
|
||||
div.classList.add("ZStack");
|
||||
div.render = cb;
|
||||
quill.render(div);
|
||||
return div;
|
||||
return handleStack(cb, "ZStack")
|
||||
};
|
||||
|
||||
/* SHAPES */
|
||||
@@ -815,8 +967,8 @@ HTMLElement.prototype.onAppear = function(func) {
|
||||
};
|
||||
|
||||
HTMLElement.prototype.onClick = function(func) {
|
||||
const onMouseDown = () => func.call(this, true);
|
||||
const onMouseUp = () => func.call(this, false);
|
||||
const onMouseDown = (e) => func.call(this, false, e);
|
||||
const onMouseUp = (e) => func.call(this, true, e);
|
||||
this._storeListener("mousedown", onMouseDown);
|
||||
this._storeListener("mouseup", onMouseUp);
|
||||
return this;
|
||||
@@ -838,8 +990,8 @@ HTMLElement.prototype.onRightClick = function(func) {
|
||||
};
|
||||
|
||||
HTMLElement.prototype.onHover = function(cb) {
|
||||
const onEnter = () => cb.call(this, true);
|
||||
const onLeave = () => cb.call(this, false);
|
||||
const onEnter = (e) => cb.call(this, true, e);
|
||||
const onLeave = (e) => cb.call(this, false, e);
|
||||
this._storeListener("mouseover", onEnter);
|
||||
this._storeListener("mouseleave", onLeave);
|
||||
return this;
|
||||
@@ -868,6 +1020,21 @@ HTMLElement.prototype.onInput = function(cb) {
|
||||
return this;
|
||||
};
|
||||
|
||||
HTMLElement.prototype.onChange = function(cb) {
|
||||
if(!this.matches('input, textarea, [contenteditable=""], [contenteditable="true"]'))
|
||||
throw new Error("Can't put input event on non-input element!")
|
||||
this._storeListener("change", cb);
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
HTMLElement.prototype.onSubmit = function(cb) {
|
||||
if(!this.matches('form'))
|
||||
throw new Error("Can't put form event on non-form element!")
|
||||
this._storeListener("submit", cb);
|
||||
return this;
|
||||
};
|
||||
|
||||
HTMLElement.prototype.onTouch = function(cb) {
|
||||
const onStart = () => cb.call(this, true);
|
||||
const onEnd = () => cb.call(this, false);
|
||||
@@ -884,17 +1051,48 @@ HTMLElement.prototype.onTap = function(cb) {
|
||||
};
|
||||
|
||||
/* WHY THIS LISTENER IS THE WAY IT IS:
|
||||
- If we dispatch the "navigate" event on the window (as one would expect for a "navigate" event), a listener placed on the element will not pick it up.
|
||||
- However, if we add the listener on the window, it won't have the "this" scope that a callback normally would. Which makes it much less useful.
|
||||
- Then, if we try to add that scope using bind(), it makes the function.toString() unreadable, which means we cannot detect duplicate listeners.
|
||||
- Therefore, we just have to attach the navigate event to the element, and manually trigger that when the window listener fires.
|
||||
- We can't just put a listener on the element, because a window "navigate" event won't trigger it
|
||||
- We can't just put a listener on the window, because the "this" variable will only refer to the window
|
||||
- And, if we try to re-add that scope using bind(), it makes the return value of .toString() unreadable, which means we cannot detect duplicate listeners.
|
||||
- Therefore, we attach a global navigate event to the window, and each navigate event in this array, and manually trigger each event when the global one fires.
|
||||
*/
|
||||
navigateListeners = []
|
||||
window.addEventListener("navigate", () => {
|
||||
for(entry of navigateListeners) {
|
||||
entry.el.dispatchEvent(new CustomEvent("navigate"))
|
||||
}
|
||||
})
|
||||
HTMLElement.prototype.onNavigate = function(cb) {
|
||||
this._storeListener("navigate", cb);
|
||||
|
||||
let found = false
|
||||
let elementIndex = Array.from(this.parentNode.children).indexOf(this)
|
||||
for(entry of navigateListeners) {
|
||||
if(
|
||||
entry.cb.toString() === cb.toString()
|
||||
&& entry.index === elementIndex
|
||||
&& this.nodeName === entry.el.nodeName
|
||||
) {
|
||||
found = true
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(found === false) {
|
||||
navigateListeners.push({el: this, cb: cb, index: elementIndex})
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Same principle applies
|
||||
*/
|
||||
queryListeners = []
|
||||
HTMLElement.prototype.onQueryChanged = function(cb) {
|
||||
this._storeListener("query-changed", cb);
|
||||
|
||||
let found = false
|
||||
for(entry of queryListeners) {
|
||||
if(entry.cb.toString() === cb.toString() &&
|
||||
this.nodeName === entry.el.nodeName) {
|
||||
found = true
|
||||
@@ -902,14 +1100,14 @@ HTMLElement.prototype.onNavigate = function(cb) {
|
||||
}
|
||||
}
|
||||
if(found === false) {
|
||||
navigateListeners.push({el: this, cb: cb})
|
||||
queryListeners.push({el: this, cb: cb})
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
window.addEventListener("navigate", () => {
|
||||
for(entry of navigateListeners) {
|
||||
entry.el.dispatchEvent(new CustomEvent("navigate"))
|
||||
window.addEventListener("query-changed", () => {
|
||||
for(entry of queryListeners) {
|
||||
entry.el.dispatchEvent(new CustomEvent("query-changed"))
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
:root {
|
||||
--main: #FFDFB4;
|
||||
--accent: var(--darkbrown);
|
||||
--main: var(--brown);
|
||||
--accent: var(--gold);
|
||||
|
||||
--tan: #FFDFB4;
|
||||
--gold: #F2B36F;
|
||||
--purple: #251D44;
|
||||
--divider: #bb7c36;
|
||||
--green: #0857265c;
|
||||
--red: #BC1C02;
|
||||
--brown: #c6a476;
|
||||
--darkbrown: #60320c;
|
||||
--orange: #FE9201;
|
||||
--periwinkle: #655BAF;
|
||||
--brown: #812A18;
|
||||
--darkbrown: #3f0808;
|
||||
|
||||
--accent2: var(--green);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--main: #251D44;
|
||||
--main: var(--brown);
|
||||
--accent: var(--gold);
|
||||
--dividerColor: var(--gold);
|
||||
--accent2: var(--gold);
|
||||
}
|
||||
}
|
||||
@@ -46,12 +43,19 @@
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
font-family: 'Bona Nova', sans-serif;
|
||||
font-size: 16px;
|
||||
background-color: var(--main);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
body, html{
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
#title {
|
||||
padding: 5px 10px;
|
||||
font-size: 1.7rem;
|
||||
@@ -98,6 +102,10 @@ input {
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--accent)
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: 1px solid var(--red);
|
||||
}
|
||||
|
||||
62
ui/_/code/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.includes("local")) {
|
||||
this.ws = new WebSocket("ws://" + window.location.host)
|
||||
} 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/_/code/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
|
||||
}));
|
||||
}
|
||||
}
|
||||
8
ui/_/code/zod.js
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 |
2
ui/_/icons/creditcards/amex.svg
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
2
ui/_/icons/creditcards/discover.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 -140 780 780" enable-background="new 0 0 780 500" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect width="780" height="500" fill="#FFF" stroke-width="15px" stroke="#000000"/><path d="m409.41 197.26c30.938 0 56.02 23.58 56.02 52.709v0.033c0 29.129-25.082 52.742-56.02 52.742-30.941 0-56.022-23.613-56.022-52.742v-0.033c0-29.129 25.081-52.709 56.022-52.709z" fill="#F47216"/><path d="m321.43 197.94c8.836 0 16.247 1.785 25.27 6.09v22.753c-8.544-7.863-15.955-11.154-25.757-11.154-19.265 0-34.413 15.015-34.413 34.051 0 20.074 14.681 34.195 35.368 34.195 9.312 0 16.586-3.12 24.802-10.856v22.764c-9.343 4.142-16.912 5.775-25.757 5.775-31.277 0-55.581-22.597-55.581-51.736-2e-3 -28.83 24.949-51.882 56.068-51.882zm-97.113 0.626c11.546 0 22.109 3.721 30.942 10.994l-10.748 13.248c-5.351-5.646-10.411-8.027-16.563-8.027-8.854 0-15.301 4.744-15.301 10.988 0 5.354 3.618 8.188 15.944 12.481 23.364 8.043 30.289 15.176 30.289 30.926 0 19.193-14.976 32.554-36.319 32.554-15.631 0-26.993-5.795-36.457-18.871l13.268-12.03c4.73 8.608 12.622 13.223 22.42 13.223 9.163 0 15.947-5.95 15.947-13.983 0-4.164-2.056-7.733-6.158-10.258-2.066-1.195-6.158-2.978-14.199-5.646-19.292-6.538-25.91-13.527-25.91-27.186-1e-3 -16.227 14.213-28.413 32.845-28.413zm234.72 1.729h22.436l28.084 66.592 28.447-66.592h22.267l-45.493 101.69h-11.054l-44.687-101.69zm-301.21 0.152h20.541v99.143h-20.541v-99.143zm411.73 0h58.253v16.799h-37.726v22.006h36.336v16.791h-36.336v26.762h37.726v16.785h-58.253v-99.143zm115.59 57.377c15.471-2.965 23.983-12.926 23.983-28.105 0-18.562-13.575-29.271-37.266-29.271h-30.454v99.144h20.516v-39.83h2.681l28.43 39.828h25.26l-33.15-41.766zm-17.218-11.736h-6.002v-30.025h6.326c12.791 0 19.744 5.049 19.744 14.697 2e-3 9.967-6.951 15.328-20.068 15.328zm-576.09-45.641h-30.149v99.143h29.992c15.946 0 27.465-3.543 37.573-11.445 12.014-9.359 19.117-23.467 19.117-38.057 1e-3 -29.259-23.221-49.641-56.533-49.641zm23.997 74.479c-6.454 5.484-14.837 7.879-28.108 7.879h-5.514v-65.559h5.513c13.271 0 21.323 2.238 28.108 8.018 7.104 5.956 11.377 15.184 11.377 24.682 1e-3 9.513-4.273 19.024-11.376 24.98z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
7
ui/_/icons/creditcards/mastercard.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 -9 58 58" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="57" height="39" rx="3.5" fill="white" stroke="#F3F3F3"/>
|
||||
<path d="M34.3102 28.9765H23.9591V10.5122H34.3102V28.9765Z" fill="#FF5F00"/>
|
||||
<path d="M24.6223 19.7429C24.6223 15.9973 26.3891 12.6608 29.1406 10.5107C27.1285 8.93843 24.5892 7.99998 21.8294 7.99998C15.2961 7.99998 10 13.2574 10 19.7429C10 26.2283 15.2961 31.4857 21.8294 31.4857C24.5892 31.4857 27.1285 30.5473 29.1406 28.975C26.3891 26.8249 24.6223 23.4884 24.6223 19.7429" fill="#EB001B"/>
|
||||
<path d="M48.2706 19.7429C48.2706 26.2283 42.9745 31.4857 36.4412 31.4857C33.6814 31.4857 31.1421 30.5473 29.1293 28.975C31.8815 26.8249 33.6483 23.4884 33.6483 19.7429C33.6483 15.9973 31.8815 12.6608 29.1293 10.5107C31.1421 8.93843 33.6814 7.99998 36.4412 7.99998C42.9745 7.99998 48.2706 13.2574 48.2706 19.7429" fill="#F79E1B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
2
ui/_/icons/creditcards/visa.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 -140 780 780" enable-background="new 0 0 780 500" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M40,0h700c22.092,0,40,17.909,40,40v420c0,22.092-17.908,40-40,40H40c-22.091,0-40-17.908-40-40V40 C0,17.909,17.909,0,40,0z" fill="#0E4595"/><path d="m293.2 348.73l33.361-195.76h53.36l-33.385 195.76h-53.336zm246.11-191.54c-10.57-3.966-27.137-8.222-47.822-8.222-52.725 0-89.865 26.55-90.18 64.603-0.299 28.13 26.514 43.822 46.752 53.186 20.771 9.595 27.752 15.714 27.654 24.283-0.131 13.121-16.586 19.116-31.922 19.116-21.357 0-32.703-2.967-50.227-10.276l-6.876-3.11-7.489 43.823c12.463 5.464 35.51 10.198 59.438 10.443 56.09 0 92.5-26.246 92.916-66.882 0.199-22.269-14.016-39.216-44.801-53.188-18.65-9.055-30.072-15.099-29.951-24.268 0-8.137 9.668-16.839 30.557-16.839 17.449-0.27 30.09 3.535 39.938 7.5l4.781 2.26 7.232-42.429m137.31-4.223h-41.232c-12.773 0-22.332 3.487-27.941 16.234l-79.244 179.4h56.031s9.16-24.123 11.232-29.418c6.125 0 60.555 0.084 68.338 0.084 1.596 6.853 6.49 29.334 6.49 29.334h49.514l-43.188-195.64zm-65.418 126.41c4.412-11.279 21.26-54.723 21.26-54.723-0.316 0.522 4.379-11.334 7.074-18.684l3.605 16.879s10.219 46.729 12.354 56.528h-44.293zm-363.3-126.41l-52.24 133.5-5.567-27.13c-9.725-31.273-40.025-65.155-73.898-82.118l47.766 171.2 56.456-0.064 84.004-195.39h-56.521" fill="#ffffff"/><path d="m146.92 152.96h-86.041l-0.681 4.073c66.938 16.204 111.23 55.363 129.62 102.41l-18.71-89.96c-3.23-12.395-12.597-16.094-24.186-16.527" fill="#F2AE14"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
5
ui/_/icons/jobs.svg
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
4
ui/_/icons/letter.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
|
||||
<path d="M0 0 C33 0 66 0 100 0 C100 23.76 100 47.52 100 72 C67 72 34 72 0 72 C0 48.24 0 24.48 0 0 Z M4 4 C3.46383745 8.70053019 3.46383745 8.70053019 5.30664062 10.5546875 C5.91701172 10.99039062 6.52738281 11.42609375 7.15625 11.875 C7.84114502 12.37870117 8.52604004 12.88240234 9.23168945 13.40136719 C9.98023193 13.92891602 10.72877441 14.45646484 11.5 15 C13.06813799 16.14868129 14.63450237 17.29978719 16.19921875 18.453125 C17.40070557 19.33194336 17.40070557 19.33194336 18.62646484 20.22851562 C22.28176453 22.95664302 25.79456149 25.84835522 29.3125 28.75 C31.2591282 30.33484221 33.20813973 31.91671841 35.15722656 33.49853516 C36.36876709 34.48568066 37.57657813 35.4774244 38.78027344 36.47412109 C40.40268905 37.81286849 42.04179632 39.1275644 43.6875 40.4375 C44.59886719 41.17871094 45.51023438 41.91992188 46.44921875 42.68359375 C48.88630843 44.26660079 48.88630843 44.26660079 51.37890625 43.5859375 C54.95082046 41.42468986 58.06328957 38.87595616 61.25 36.1875 C67.05660886 31.37582668 72.93368091 26.68843648 78.9375 22.125 C80.0061731 21.30745239 80.0061731 21.30745239 81.09643555 20.47338867 C84.06177854 18.20933035 87.03509444 15.97490703 90.0703125 13.8046875 C91.30394531 12.91136719 91.30394531 12.91136719 92.5625 12 C93.24441406 11.525625 93.92632812 11.05125 94.62890625 10.5625 C96.54964019 8.72009187 96.54964019 8.72009187 96 4 C65.64 4 35.28 4 4 4 Z M4 15 C4 32.49 4 49.98 4 68 C34.36 68 64.72 68 96 68 C96 50.51 96 33.02 96 15 C91.10723979 18.66957016 86.23777639 22.33985116 81.4375 26.125 C80.5556604 26.81843506 80.5556604 26.81843506 79.65600586 27.52587891 C76.42735841 30.06945531 73.21156659 32.62826175 70.0078125 35.203125 C69.01890015 35.99759033 69.01890015 35.99759033 68.01000977 36.80810547 C66.75119369 37.82090246 65.49376977 38.83543328 64.23803711 39.85205078 C63.07629198 40.7874235 61.90867537 41.71555794 60.73486328 42.63574219 C59.54616335 43.57050931 58.37318371 44.52551784 57.21435547 45.49707031 C54.26370419 47.84496996 52.75924576 48.97890984 48.90625 49.0859375 C45.41094599 47.77989648 43.28715117 46.10681719 40.5 43.625 C39.44697905 42.70771873 38.39222174 41.79242782 37.3359375 40.87890625 C36.7903418 40.40340332 36.24474609 39.92790039 35.68261719 39.43798828 C32.89861809 37.05874355 30.01440497 34.81161154 27.125 32.5625 C26.54911133 32.11293945 25.97322266 31.66337891 25.37988281 31.20019531 C24.19857561 30.27805578 23.01693136 29.35634787 21.83496094 28.43505859 C19.90812651 26.92814897 17.98846374 25.41247563 16.0703125 23.89453125 C15.43077637 23.38913818 14.79124023 22.88374512 14.13232422 22.36303711 C12.90121746 21.38980809 11.671035 20.41540842 10.44189453 19.43969727 C7.37045638 16.99238729 7.37045638 16.99238729 4 15 Z " fill="#000000" transform="translate(0,14)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
6
ui/_/icons/place/austin.svg
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
ui/_/images/fabric.webp
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
ui/_/images/knight.webp
Normal file
|
After Width: | Height: | Size: 177 KiB |
104
ui/desktop/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/desktop/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)
|
||||
@@ -33,13 +33,6 @@ class Jobs extends Shadow {
|
||||
company: "Hyperia",
|
||||
city: "Austin",
|
||||
state: "TX"
|
||||
},
|
||||
{
|
||||
title: "San Marcos Chapter Lead",
|
||||
salary: "1% of Local Revenue",
|
||||
company: "Hyperia",
|
||||
city: "San Marcos",
|
||||
state: "TX"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -54,41 +47,28 @@ class Jobs extends Shadow {
|
||||
.x(0).y(13, vh)
|
||||
|
||||
HStack(() => {
|
||||
input("Search jobs...", "45vw")
|
||||
input("Search jobs... (Coming Soon!)", "45vw")
|
||||
.attr({
|
||||
"type": "text"
|
||||
"type": "text",
|
||||
"disabled": "true"
|
||||
})
|
||||
.fontSize(1.1, em)
|
||||
.paddingLeft(1.3, em)
|
||||
.background("transparent")
|
||||
.border("1px solid var(--accent2)")
|
||||
.border("0.5px solid var(--divider)")
|
||||
.outline("none")
|
||||
.color("var(--accent)")
|
||||
.opacity(0.5)
|
||||
.borderRadius(10, px)
|
||||
|
||||
button("Search")
|
||||
.marginLeft(2, em)
|
||||
.borderRadius(10, px)
|
||||
.background("transparent")
|
||||
.border("1px solid var(--accent2)")
|
||||
.color("var(--accent)")
|
||||
.fontFamily("Bona Nova")
|
||||
.onHover(function (hovering) {
|
||||
if(hovering) {
|
||||
this.style.background = "var(--green)"
|
||||
|
||||
} else {
|
||||
this.style.background = "transparent"
|
||||
|
||||
}
|
||||
})
|
||||
.background("grey")
|
||||
.cursor("not-allowed")
|
||||
|
||||
button("+ Add Job")
|
||||
.width(7, em)
|
||||
.marginLeft(1, em)
|
||||
.borderRadius(10, px)
|
||||
.background("transparent")
|
||||
.border("1px solid var(--accent2)")
|
||||
.border("0.3px solid var(--accent2)")
|
||||
.color("var(--accent)")
|
||||
.fontFamily("Bona Nova")
|
||||
.onHover(function (hovering) {
|
||||
@@ -37,8 +37,8 @@ class JobsGrid extends Shadow {
|
||||
p(this.boldUntilFirstSpace(this.jobs[i].salary))
|
||||
})
|
||||
.padding(1, em)
|
||||
.border("1px solid var(--accent2)")
|
||||
.borderRadius(5, "px")
|
||||
.background("var(--darkbrown)")
|
||||
}
|
||||
})
|
||||
.display("grid")
|
||||
@@ -54,7 +54,6 @@ class JobsGrid extends Shadow {
|
||||
.paddingTop(2, em)
|
||||
.gap(0, em)
|
||||
.width(100, "%")
|
||||
.borderTop("1px solid var(--accent2)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ class JobsSidebar extends Shadow {
|
||||
.paddingLeft(3, em)
|
||||
.paddingRight(3, em)
|
||||
.gap(1, em)
|
||||
.borderTop("1px solid var(--accent2)")
|
||||
.minWidth(10, vw)
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,8 @@ class Market extends Shadow {
|
||||
price: "$12",
|
||||
company: "Hyperia",
|
||||
type: "new",
|
||||
image: "/db/images/1"
|
||||
image: "/db/images/1",
|
||||
madeIn: "America"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -50,41 +51,28 @@ class Market extends Shadow {
|
||||
.x(0).y(13, vh)
|
||||
|
||||
HStack(() => {
|
||||
input("Search for products...", "45vw")
|
||||
input("Search for products... (Coming Soon!)", "45vw")
|
||||
.attr({
|
||||
"type": "text"
|
||||
"type": "text",
|
||||
"disabled": "true"
|
||||
})
|
||||
.fontSize(1.1, em)
|
||||
.paddingLeft(1.3, em)
|
||||
.background("transparent")
|
||||
.border("1px solid var(--accent2)")
|
||||
.border("0.5px solid var(--divider)")
|
||||
.outline("none")
|
||||
.color("var(--accent)")
|
||||
.opacity(0.5)
|
||||
.borderRadius(10, px)
|
||||
|
||||
button("Search")
|
||||
.marginLeft(2, em)
|
||||
.borderRadius(10, px)
|
||||
.background("transparent")
|
||||
.border("1px solid var(--accent2)")
|
||||
.color("var(--accent)")
|
||||
.fontFamily("Bona Nova")
|
||||
.onHover(function (hovering) {
|
||||
if(hovering) {
|
||||
this.style.background = "var(--green)"
|
||||
|
||||
} else {
|
||||
this.style.background = "transparent"
|
||||
|
||||
}
|
||||
})
|
||||
.background("grey")
|
||||
.cursor("not-allowed")
|
||||
|
||||
button("+ Add Item")
|
||||
.width(7, em)
|
||||
.marginLeft(1, em)
|
||||
.borderRadius(10, px)
|
||||
.background("transparent")
|
||||
.border("1px solid var(--accent2)")
|
||||
.border("0.5px solid var(--accent2)")
|
||||
.color("var(--accent)")
|
||||
.fontFamily("Bona Nova")
|
||||
.onHover(function (hovering) {
|
||||
@@ -29,24 +29,46 @@ class MarketGrid extends Shadow {
|
||||
ZStack(() => {
|
||||
// BuyModal()
|
||||
|
||||
for (let i = 0; i < this.listings.length; i++) {
|
||||
const rating = this.listings[i].stars
|
||||
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(this.listings[i].image)
|
||||
img(filtered[i].image)
|
||||
.marginBottom(0.5, em)
|
||||
|
||||
p(this.listings[i].company)
|
||||
p(filtered[i].company)
|
||||
.marginBottom(0.5, em)
|
||||
|
||||
p(this.listings[i].title)
|
||||
p(filtered[i].title)
|
||||
.fontSize(1.2, em)
|
||||
.fontWeight("bold")
|
||||
.marginBottom(0.5, em)
|
||||
|
||||
HStack(() => {
|
||||
p(this.listings[i].stars)
|
||||
p(filtered[i].stars)
|
||||
.marginRight(0.2, em)
|
||||
|
||||
ZStack(() => {
|
||||
@@ -67,21 +89,28 @@ class MarketGrid extends Shadow {
|
||||
.fontSize(1.2, em)
|
||||
.lineHeight(1)
|
||||
|
||||
p(this.listings[i].reviews)
|
||||
p(filtered[i].reviews)
|
||||
.marginLeft(0.2, em)
|
||||
})
|
||||
.marginBottom(0.5, em)
|
||||
|
||||
p(this.listings[i].price)
|
||||
p(filtered[i].price)
|
||||
.fontSize(1.75, em)
|
||||
.marginBottom(0.5, em)
|
||||
|
||||
button("Buy Now")
|
||||
button("Coming Soon!")
|
||||
.onClick((finished) => {
|
||||
if(finished) {
|
||||
|
||||
}
|
||||
})
|
||||
.onHover(function (hovering) {
|
||||
if(hovering) {
|
||||
this.style.backgroundColor = "var(--green)"
|
||||
} else {
|
||||
this.style.backgroundColor = ""
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
.padding(1, em)
|
||||
@@ -96,6 +125,10 @@ class MarketGrid extends Shadow {
|
||||
p("No Listings!")
|
||||
}
|
||||
})
|
||||
.onQueryChanged(() => {
|
||||
console.log("query did change yup")
|
||||
this.rerender()
|
||||
})
|
||||
.height(100, vh)
|
||||
.paddingLeft(2, em)
|
||||
.paddingRight(2, em)
|
||||
@@ -1,12 +1,27 @@
|
||||
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"
|
||||
@@ -20,6 +35,7 @@ class MarketSidebar extends Shadow {
|
||||
"type": "checkbox",
|
||||
"id": "america-check"
|
||||
})
|
||||
.onChange(this.handleChecked)
|
||||
label("America-Made")
|
||||
.attr({
|
||||
"for": "america-check"
|
||||
@@ -27,12 +43,15 @@ class MarketSidebar extends Shadow {
|
||||
.marginLeft(0.5, em)
|
||||
})
|
||||
|
||||
p("Condition")
|
||||
|
||||
HStack(() => {
|
||||
input()
|
||||
.attr({
|
||||
"type": "checkbox",
|
||||
"id": "new-check"
|
||||
})
|
||||
.onChange(this.handleChecked)
|
||||
label("New")
|
||||
.attr({
|
||||
"for": "new-check"
|
||||
@@ -46,6 +65,7 @@ class MarketSidebar extends Shadow {
|
||||
"type": "checkbox",
|
||||
"id": "used-check"
|
||||
})
|
||||
.onChange(this.handleChecked)
|
||||
label("Used")
|
||||
.attr({
|
||||
"for": "used-check"
|
||||
188
ui/desktop/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/desktop/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/desktop/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)
|
||||
@@ -9,14 +9,13 @@ css(`
|
||||
app-menu.minimized {
|
||||
color: var(--accent);
|
||||
transform: translate(-50%, 65%);
|
||||
border: 1px solid var(--accent);
|
||||
border: 0.2px solid var(--accent);
|
||||
padding-top: 0.5em;
|
||||
padding-left: 2em;
|
||||
padding-right: 2em;
|
||||
padding-bottom: 4em;
|
||||
bottom: 1em;
|
||||
border-radius: 12px;
|
||||
background-color: var(--green);
|
||||
}
|
||||
|
||||
app-menu p {
|
||||
@@ -58,7 +57,6 @@ class AppMenu extends Shadow {
|
||||
VStack(() => {
|
||||
HStack(() => {
|
||||
p("Forum")
|
||||
p("Tasks")
|
||||
p("Messages")
|
||||
p("Market")
|
||||
p("Jobs")
|
||||
@@ -18,9 +18,6 @@ class AppWindow extends Shadow {
|
||||
case "Forum":
|
||||
Forum()
|
||||
break;
|
||||
case "Tasks":
|
||||
Tasks()
|
||||
break;
|
||||
case "Messages":
|
||||
Messages()
|
||||
break;
|
||||
@@ -32,14 +29,15 @@ class AppWindow extends Shadow {
|
||||
break;
|
||||
}
|
||||
})
|
||||
.display(this.app ? '' : 'none')
|
||||
.position("fixed")
|
||||
.display(this.app ? 'block' : 'none')
|
||||
.width(100, "vw")
|
||||
.height(100, "vh")
|
||||
.backgroundImage("/_/images/fabric.png")
|
||||
.backgroundSize("33vw auto")
|
||||
.position("fixed")
|
||||
.top(0)
|
||||
.left(0)
|
||||
.background("#591d10")
|
||||
.x(0)
|
||||
.y(0)
|
||||
// .backgroundImage("/_/images/fabric.png")
|
||||
// .backgroundSize("33vw auto")
|
||||
}
|
||||
|
||||
open(app) {
|
||||
@@ -20,7 +20,7 @@ class Home extends Shadow {
|
||||
div()
|
||||
.width(100, vw)
|
||||
.height(100, vh)
|
||||
.margin("0px")
|
||||
.margin(0)
|
||||
.backgroundImage("/_/images/the_return.webp")
|
||||
.backgroundSize("cover")
|
||||
.backgroundPosition("48% 65%")
|
||||
@@ -43,10 +43,6 @@ class Home extends Shadow {
|
||||
AppWindow("Market")
|
||||
AppMenu("Market")
|
||||
break;
|
||||
case "/app/tasks":
|
||||
AppWindow("Tasks")
|
||||
AppMenu("Tasks")
|
||||
break;
|
||||
case "/app/forum":
|
||||
AppWindow("Forum")
|
||||
AppMenu("Forum")
|
||||
@@ -55,37 +51,39 @@ class Home extends Shadow {
|
||||
throw new Error("Unknown route!")
|
||||
}
|
||||
|
||||
ProfileButton()
|
||||
.zIndex(1)
|
||||
.cursor("default")
|
||||
.position("fixed")
|
||||
.top(5.5, vh)
|
||||
.right(4.5, vw)
|
||||
|
||||
a("/signout", "Sign Out")
|
||||
.position("fixed")
|
||||
.top(5, vh)
|
||||
.right(2, em)
|
||||
.background("transparent")
|
||||
.border(window.location.pathname === "/" ? "1px solid var(--tan)" : "1px solid var(--accent2)")
|
||||
.color(window.location.pathname === "/" ? "var(--tan)" : "var(--accent)")
|
||||
.borderRadius(5, px)
|
||||
.onHover(function (hovering) {
|
||||
if(hovering) {
|
||||
this.style.background = "var(--green)"
|
||||
} else {
|
||||
this.style.background = ""
|
||||
}
|
||||
})
|
||||
.onNavigate(function () {
|
||||
if(window.location.pathname === "/") {
|
||||
this.style.border = "1px solid var(--tan)"
|
||||
this.style.color = "var(--tan)"
|
||||
} else {
|
||||
this.style.border = "1px solid var(--accent2)"
|
||||
this.style.color = "var(--accent)"
|
||||
}
|
||||
})
|
||||
HStack(() => {
|
||||
ProfileButton()
|
||||
.zIndex(1)
|
||||
.cursor("default")
|
||||
|
||||
a("/signout", "Sign Out")
|
||||
.background("transparent")
|
||||
.border(window.location.pathname === "/" ? "1px solid var(--tan)" : "0.5px solid #bb7c36")
|
||||
.color(window.location.pathname === "/" ? "var(--tan)" : "var(--accent)")
|
||||
.borderRadius(5, px)
|
||||
.onHover(function (hovering) {
|
||||
if(hovering) {
|
||||
this.style.background = "var(--green)"
|
||||
} else {
|
||||
this.style.background = ""
|
||||
}
|
||||
})
|
||||
.onNavigate(function () {
|
||||
if(window.location.pathname === "/") {
|
||||
this.style.border = "1px solid var(--tan)"
|
||||
this.style.color = "var(--tan)"
|
||||
} else {
|
||||
this.style.border = "0.5px solid #bb7c36"
|
||||
this.style.color = "var(--accent)"
|
||||
}
|
||||
})
|
||||
})
|
||||
.gap(1, em)
|
||||
.xRight(2, em).y(2.3, em)
|
||||
.position("fixed")
|
||||
.alignVertical("center")
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
25
ui/desktop/components/LoadingCircle.js
Normal file
@@ -0,0 +1,25 @@
|
||||
class LoadingCircle extends Shadow {
|
||||
render() {
|
||||
div()
|
||||
.borderRadius(100, pct)
|
||||
.width(2, em).height(2, em)
|
||||
.x(45, pct).y(50, pct)
|
||||
.center()
|
||||
.backgroundColor("var(--accent")
|
||||
.transition("transform 1.75s ease-in-out")
|
||||
.onAppear(function () {
|
||||
let growing = true;
|
||||
|
||||
setInterval(() => {
|
||||
if (growing) {
|
||||
this.style.transform = "scale(1.5)";
|
||||
} else {
|
||||
this.style.transform = "scale(0.7)";
|
||||
}
|
||||
growing = !growing;
|
||||
}, 750);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
register(LoadingCircle)
|
||||
43
ui/desktop/components/ProfileButton.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import "./ProfileMenu.js"
|
||||
|
||||
class ProfileButton extends Shadow {
|
||||
|
||||
async render() {
|
||||
ZStack(async () => {
|
||||
img("/_/icons/profile.svg", "1.5em", "1.5em")
|
||||
.backgroundColor("var(--accent)")
|
||||
.padding(0.2, em)
|
||||
.borderRadius(5, px)
|
||||
|
||||
ProfileMenu()
|
||||
|
||||
})
|
||||
.display("block")
|
||||
.onAppear(() => {
|
||||
window.addEventListener("mousedown", (e) => { // bad - adding every time it renders
|
||||
if(!e.target.closest("profilebutton-")) {
|
||||
this.$("profile-menu").style.display = "none"
|
||||
}
|
||||
})
|
||||
})
|
||||
.onHover((hovering, e) => {
|
||||
console.log(hovering)
|
||||
console.log(e.target)
|
||||
if(hovering && !e.target.closest("profile-menu")) {
|
||||
this.$("img").backgroundColor("var(--accent)")
|
||||
this.$("img").style.outline = "1px solid black"
|
||||
} else if(!e.target.closest("profile-menu")) {
|
||||
this.$("img").backgroundColor("")
|
||||
this.$("img").style.outline = ""
|
||||
}
|
||||
})
|
||||
.onClick((done) => {
|
||||
console.log(done)
|
||||
if(done) {
|
||||
this.$("profile-menu").style.display = ""
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
register(ProfileButton)
|
||||
68
ui/desktop/components/ProfileMenu.js
Normal file
@@ -0,0 +1,68 @@
|
||||
class ProfileMenu extends Shadow {
|
||||
|
||||
render() {
|
||||
VStack(() => {
|
||||
h2("Profile")
|
||||
|
||||
HStack(() => {
|
||||
p("Email: ")
|
||||
.fontWeight("bold")
|
||||
|
||||
p(window.profile?.email)
|
||||
})
|
||||
.gap(1, em)
|
||||
|
||||
HStack(() => {
|
||||
p("Name: ")
|
||||
.fontWeight("bold")
|
||||
|
||||
p(window.profile?.name)
|
||||
})
|
||||
.gap(1, em)
|
||||
|
||||
p("X")
|
||||
.onClick(() => {
|
||||
this.style.display = "none"
|
||||
})
|
||||
.xRight(2, em).y(1, em)
|
||||
})
|
||||
.paddingLeft(1, em)
|
||||
.color("var(--accent)")
|
||||
.position("fixed")
|
||||
.border("1px solid var(--accent)")
|
||||
.x(50, vw).y(47, vh)
|
||||
.width(70, vw)
|
||||
.height(70, vh)
|
||||
.backgroundColor("black")
|
||||
.center()
|
||||
.display("none")
|
||||
.onAppear(async () => {
|
||||
if(!window.profile) {
|
||||
window.profile = await this.fetchProfile()
|
||||
this.rerender()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fetchProfile() {
|
||||
try {
|
||||
const res = await fetch("/profile", {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Failed to fetch profile");
|
||||
|
||||
const profile = await res.json();
|
||||
console.log(profile);
|
||||
return profile
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
register(ProfileMenu, "profile-menu")
|
||||
@@ -6,8 +6,9 @@
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
8
ui/desktop/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/desktop/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/desktop/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.includes("local")) {
|
||||
this.ws = new WebSocket("ws://" + window.location.host)
|
||||
} 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/desktop/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
|
||||
}));
|
||||
}
|
||||
}
|
||||
66
ui/mobile/apps/Forum/Forum.js
Normal file
@@ -0,0 +1,66 @@
|
||||
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(() => {
|
||||
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")
|
||||
})
|
||||
.onAppear(() => document.body.style.backgroundColor = "var(--darkbrown)")
|
||||
.width(100, pct)
|
||||
.height(100, pct)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
register(Forum)
|
||||
88
ui/mobile/apps/Forum/ForumPanel.js
Normal file
@@ -0,0 +1,88 @@
|
||||
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 () => {
|
||||
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) {
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
@@ -1,9 +1,9 @@
|
||||
css(`
|
||||
forum- {
|
||||
tasks- {
|
||||
font-family: 'Bona';
|
||||
}
|
||||
|
||||
forum- input::placeholder {
|
||||
tasks- input::placeholder {
|
||||
font-family: 'Bona Nova';
|
||||
font-size: 0.9em;
|
||||
color: var(--accent);
|
||||
@@ -22,25 +22,35 @@ css(`
|
||||
}
|
||||
`)
|
||||
|
||||
class Forum extends Shadow {
|
||||
friends = []
|
||||
conversations = []
|
||||
class Tasks extends Shadow {
|
||||
projects = [
|
||||
{
|
||||
"title": "Blockcatcher",
|
||||
"tasks": {}
|
||||
}
|
||||
]
|
||||
columns = [
|
||||
{
|
||||
"title": "backlog",
|
||||
"tasks": {}
|
||||
}
|
||||
]
|
||||
|
||||
render() {
|
||||
ZStack(() => {
|
||||
HStack(() => {
|
||||
VStack(() => {
|
||||
h3("Friends")
|
||||
h3("Projects")
|
||||
.marginTop(0)
|
||||
.marginBottom(1, em)
|
||||
.marginLeft(0.4, em)
|
||||
|
||||
if (this.friends.length > 1) {
|
||||
for(let i = 0; i < this.friends.length; i++) {
|
||||
p(this.friends[i].name)
|
||||
if (this.projects.length >= 1) {
|
||||
for(let i = 0; i < this.projects.length; i++) {
|
||||
p(this.projects[i].title)
|
||||
}
|
||||
} else {
|
||||
p("No Friends!")
|
||||
p("No Projects!")
|
||||
}
|
||||
})
|
||||
.height(100, vh)
|
||||
@@ -48,11 +58,27 @@ class Forum extends Shadow {
|
||||
.paddingRight(2, em)
|
||||
.paddingTop(2, em)
|
||||
.gap(0, em)
|
||||
.borderRight("1px solid var(--accent2)")
|
||||
.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("1px solid var(--accent2)")
|
||||
.borderTop("0.5px solid var(--accent2)")
|
||||
|
||||
p("0 Items")
|
||||
.position("absolute")
|
||||
@@ -60,14 +86,14 @@ class Forum extends Shadow {
|
||||
.transform("translate(-50%, -50%)")
|
||||
|
||||
HStack(() => {
|
||||
input("Search messages...", "45vw")
|
||||
input("Search tasks...", "45vw")
|
||||
.attr({
|
||||
"type": "text"
|
||||
})
|
||||
.fontSize(1.1, em)
|
||||
.paddingLeft(1.3, em)
|
||||
.background("transparent")
|
||||
.border("1px solid var(--accent2)")
|
||||
.border("0.5px solid var(--accent2)")
|
||||
.outline("none")
|
||||
.color("var(--accent)")
|
||||
.borderRadius(10, px)
|
||||
@@ -76,7 +102,7 @@ class Forum extends Shadow {
|
||||
.marginLeft(2, em)
|
||||
.borderRadius(10, px)
|
||||
.background("transparent")
|
||||
.border("1px solid var(--accent2)")
|
||||
.border("0.5px solid var(--accent2)")
|
||||
.color("var(--accent)")
|
||||
.fontFamily("Bona Nova")
|
||||
.onHover(function (hovering) {
|
||||
@@ -89,12 +115,12 @@ class Forum extends Shadow {
|
||||
}
|
||||
})
|
||||
|
||||
button("+ New Message")
|
||||
.width(13, em)
|
||||
button("+ New Task")
|
||||
.width(9, em)
|
||||
.marginLeft(1, em)
|
||||
.borderRadius(10, px)
|
||||
.background("transparent")
|
||||
.border("1px solid var(--accent2)")
|
||||
.border("0.5px solid var(--accent2)")
|
||||
.color("var(--accent)")
|
||||
.fontFamily("Bona Nova")
|
||||
.onHover(function (hovering) {
|
||||
@@ -119,39 +145,9 @@ class Forum extends Shadow {
|
||||
.height(100, "%")
|
||||
}
|
||||
|
||||
SidebarName(name) {
|
||||
let firstLetter = name[0]
|
||||
|
||||
HStack(() => {
|
||||
div(firstLetter)
|
||||
.display("flex")
|
||||
.justifyContent("center")
|
||||
.alignItems("center")
|
||||
.width(1.5, em)
|
||||
.height(1.5, em)
|
||||
.border("1px solid var(--accent2)")
|
||||
.borderRadius(100, "%")
|
||||
p(name)
|
||||
.marginLeft(1, em)
|
||||
})
|
||||
.alignItems("center")
|
||||
.padding(5, px)
|
||||
.borderRadius(0.5, em)
|
||||
.cursor("default")
|
||||
.onHover(function (hovering) {
|
||||
if(hovering) {
|
||||
this.style.background = "var(--green)"
|
||||
|
||||
} else {
|
||||
this.style.background = "transparent"
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// Optional additional logic
|
||||
}
|
||||
}
|
||||
|
||||
register(Forum)
|
||||
register(Tasks)
|
||||
77
ui/mobile/components/AppMenu.js
Normal file
@@ -0,0 +1,77 @@
|
||||
class AppMenu extends Shadow {
|
||||
selected = ""
|
||||
|
||||
onNewSelection() {
|
||||
this.$$("img").forEach((image) => {
|
||||
image.style.background = ""
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log("rendering")
|
||||
HStack(() => {
|
||||
img("/_/icons/Column.svg", "1.5em", "1.5em")
|
||||
.attr({app: "forum"})
|
||||
.padding(0.5, em)
|
||||
.borderRadius(10, px)
|
||||
.onAppear(function () {
|
||||
this.style.border = "1px solid black"
|
||||
})
|
||||
.onClick((finished, e) => {
|
||||
if(finished) {
|
||||
this.onNewSelection()
|
||||
}
|
||||
e.target.style.background = "var(--accent)"
|
||||
console.log(e.target, e.target.style.background)
|
||||
if(finished) {
|
||||
window.navigateTo("/")
|
||||
}
|
||||
})
|
||||
img("/_/icons/letter.svg", "1.5em", "1.5em")
|
||||
.attr({app: "messages"})
|
||||
.padding(0.5, em)
|
||||
.borderRadius(10, px)
|
||||
.onAppear(function () {
|
||||
this.style.border = "1px solid black"
|
||||
})
|
||||
.onClick((finished, e) => {
|
||||
if(finished) {
|
||||
this.onNewSelection()
|
||||
}
|
||||
e.target.style.background = "rgb(112 150 114)"
|
||||
if(finished) {
|
||||
window.navigateTo("/messages")
|
||||
}
|
||||
})
|
||||
img("/_/icons/jobs.svg", "1.5em", "1.5em")
|
||||
.attr({app: "jobs"})
|
||||
.padding(0.5, em)
|
||||
.borderRadius(10, px)
|
||||
.onAppear(function () {
|
||||
this.style.border = "1px solid black"
|
||||
})
|
||||
.onClick((finished, e) => {
|
||||
if(finished) {
|
||||
this.onNewSelection()
|
||||
}
|
||||
e.target.style.background = "#9392bb"
|
||||
if(finished) {
|
||||
window.navigateTo("/jobs")
|
||||
}
|
||||
})
|
||||
})
|
||||
.borderTop("1px solid black")
|
||||
.height("auto")
|
||||
.position('fixed')
|
||||
.background("var(--main)")
|
||||
.zIndex(1)
|
||||
.x(0).yBottom(0)
|
||||
.justifyContent("space-between")
|
||||
.paddingHorizontal(4, em)
|
||||
.paddingVertical(1, em)
|
||||
.width(100, vw)
|
||||
.boxSizing("border-box")
|
||||
}
|
||||
}
|
||||
|
||||
register(AppMenu)
|
||||
37
ui/mobile/components/Home.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import "./AppMenu.js"
|
||||
import "../apps/Forum/Forum.js"
|
||||
import "../apps/Messages/Messages.js"
|
||||
import "../apps/Jobs/Jobs.js"
|
||||
|
||||
class Home extends Shadow {
|
||||
|
||||
render() {
|
||||
ZStack(() => {
|
||||
|
||||
ZStack(() => {
|
||||
switch(window.location.pathname) {
|
||||
case "/":
|
||||
Forum()
|
||||
break;
|
||||
|
||||
case "/messages":
|
||||
Messages()
|
||||
break;
|
||||
|
||||
case "/jobs":
|
||||
Jobs()
|
||||
break;
|
||||
}
|
||||
})
|
||||
.onNavigate(function () {
|
||||
console.log("navigate")
|
||||
this.rerender()
|
||||
})
|
||||
|
||||
AppMenu()
|
||||
})
|
||||
.overflowX("hidden")
|
||||
}
|
||||
}
|
||||
|
||||
register(Home)
|
||||
25
ui/mobile/components/LoadingCircle.js
Normal file
@@ -0,0 +1,25 @@
|
||||
class LoadingCircle extends Shadow {
|
||||
render() {
|
||||
div()
|
||||
.borderRadius(100, pct)
|
||||
.width(2, em).height(2, em)
|
||||
.x(45, pct).y(50, pct)
|
||||
.center()
|
||||
.backgroundColor("var(--accent")
|
||||
.transition("transform 1.75s ease-in-out")
|
||||
.onAppear(function () {
|
||||
let growing = true;
|
||||
|
||||
setInterval(() => {
|
||||
if (growing) {
|
||||
this.style.transform = "scale(1.5)";
|
||||
} else {
|
||||
this.style.transform = "scale(0.7)";
|
||||
}
|
||||
growing = !growing;
|
||||
}, 750);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
register(LoadingCircle)
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
8
ui/mobile/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import Socket from "/_/code/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
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
css(`
|
||||
joinform- input::placeholder {
|
||||
color: var(--accent)
|
||||
}
|
||||
`)
|
||||
|
||||
class JoinForm extends Shadow {
|
||||
|
||||
inputStyles(el) {
|
||||
return el
|
||||
.border("1px solid var(--accent)")
|
||||
}
|
||||
|
||||
render() {
|
||||
ZStack(() => {
|
||||
form(() => {
|
||||
|
||||
VStack(() => {
|
||||
|
||||
HStack(() => {
|
||||
|
||||
VStack(() => {
|
||||
input("First Name")
|
||||
.attr({name: "firstname", type: "name"})
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("Last Name")
|
||||
.attr({name: "lastname", type: "name"})
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("Email")
|
||||
.attr({name: "email", type: "email"})
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("Password")
|
||||
.attr({name: "password", type: "password"})
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("Confirm Password")
|
||||
.attr({name: "password", type: "password"})
|
||||
.styles(this.inputStyles)
|
||||
})
|
||||
.width(50, "%")
|
||||
.gap(1, em)
|
||||
|
||||
VStack(() => {
|
||||
input("Street Address")
|
||||
.attr({ name: "address1", type: "text", autocomplete: "address-line1" })
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("Apt, Suite, Unit (optional)")
|
||||
.attr({ name: "address2", type: "text", autocomplete: "address-line2" })
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("City")
|
||||
.attr({ name: "city", type: "text", autocomplete: "address-level2" })
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("State")
|
||||
.attr({ name: "state", type: "text", autocomplete: "address-level1" })
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("ZIP Code")
|
||||
.attr({ name: "zip", type: "text", autocomplete: "postal-code" })
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("Country")
|
||||
.attr({ name: "country", type: "text", autocomplete: "country-name" })
|
||||
.styles(this.inputStyles)
|
||||
})
|
||||
.width(50, "%")
|
||||
.gap(1, em)
|
||||
|
||||
})
|
||||
.gap(2, em)
|
||||
|
||||
button("Submit")
|
||||
})
|
||||
.gap(2, em)
|
||||
|
||||
console.log(window.location.pathname)
|
||||
})
|
||||
.attr({action: window.location.pathname + window.location.search, method: "POST"})
|
||||
.x(50, vw).y(53, vh)
|
||||
.width(60, vw)
|
||||
.center()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
register(JoinForm)
|
||||
138
ui/public/components/SignupForm.js
Normal file
@@ -0,0 +1,138 @@
|
||||
class SignupForm extends Shadow {
|
||||
|
||||
errorMessage = "Error signing up. Please try again later or email info@hyperia.so if the problem persists."
|
||||
successMessage = "Success! You may now log in."
|
||||
|
||||
inputStyles(el) {
|
||||
return el
|
||||
.border("1px solid var(--accent)")
|
||||
.color("var(--accent)")
|
||||
}
|
||||
|
||||
render() {
|
||||
ZStack(() => {
|
||||
form(() => {
|
||||
|
||||
VStack(() => {
|
||||
|
||||
p()
|
||||
.attr({id: "signupMessage"})
|
||||
.display("none")
|
||||
.padding(1, em)
|
||||
.color("var(--main)")
|
||||
.background("var(--accent)")
|
||||
|
||||
HStack(() => {
|
||||
|
||||
VStack(() => {
|
||||
input("First Name*")
|
||||
.attr({name: "firstName", type: "name", required: "true"})
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("Last Name*")
|
||||
.attr({name: "lastName", type: "name", required: "true"})
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("Email*")
|
||||
.attr({name: "email", type: "email", required: "true"})
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("Password*")
|
||||
.attr({name: "password", type: "password", required: "true"})
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("Confirm Password*")
|
||||
.attr({name: "password", type: "password", required: "true"})
|
||||
.styles(this.inputStyles)
|
||||
})
|
||||
.width(50, "%")
|
||||
.gap(1, em)
|
||||
|
||||
VStack(() => {
|
||||
input("Street Address*")
|
||||
.attr({ name: "address1", type: "text", autocomplete: "address-line1", required: "true" })
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("Apt, Suite, Unit (optional)")
|
||||
.attr({ name: "address2", type: "text", autocomplete: "address-line2" })
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("City*")
|
||||
.attr({ name: "city", type: "text", autocomplete: "address-level2", required: "true" })
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("State*")
|
||||
.attr({ name: "state", type: "text", autocomplete: "address-level1", required: "true" })
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("ZIP Code*")
|
||||
.attr({ name: "zip", type: "text", autocomplete: "postal-code", required: "true" })
|
||||
.styles(this.inputStyles)
|
||||
|
||||
input("Country*")
|
||||
.attr({ name: "country", type: "text", autocomplete: "country-name", required: "true" })
|
||||
.styles(this.inputStyles)
|
||||
})
|
||||
.width(50, "%")
|
||||
.gap(1, em)
|
||||
|
||||
})
|
||||
.gap(2, em)
|
||||
|
||||
button("Submit")
|
||||
})
|
||||
.gap(2, em)
|
||||
})
|
||||
.color("var(--accent)")
|
||||
.onSubmit(async (e) => {
|
||||
e.preventDefault()
|
||||
console.log("submitting")
|
||||
$("#signupMessage").style.display = "none"
|
||||
|
||||
const formData = new FormData(this.$("form"));
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
let newMember = {
|
||||
"email": data.email,
|
||||
"firstName": data.firstName,
|
||||
"lastName": data.lastName,
|
||||
"password": data.password
|
||||
}
|
||||
let address = {
|
||||
"address1": data.address1,
|
||||
"address2": data.address2,
|
||||
"zip": data.zip,
|
||||
"state": data.state,
|
||||
"city": data.city
|
||||
}
|
||||
newMember.address = address
|
||||
|
||||
try {
|
||||
const response = await fetch(window.location.pathname + window.location.search, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(newMember)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
$("#signupMessage").style.display = "block"
|
||||
$("#signupMessage").innerText = this.errorMessage
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
} else {
|
||||
$("#signupMessage").style.display = "block"
|
||||
$("#signupMessage").innerText = this.successMessage
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error("Fetch error:", err);
|
||||
}
|
||||
})
|
||||
.x(50, vw).y(53, vh)
|
||||
.width(60, vw)
|
||||
.center()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
register(SignupForm)
|
||||
@@ -4,17 +4,31 @@
|
||||
<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">
|
||||
<link rel="stylesheet" href="/_/code/shared.css">
|
||||
<link
|
||||
rel="preload"
|
||||
href="/_/fonts/BonaNova/BonaNova-Regular.woff"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/_/fonts/BonaNova/BonaNova-Bold.woff"
|
||||
as="font"
|
||||
type="font/woff"
|
||||
crossorigin
|
||||
>
|
||||
<style>
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
background-image: url("_/images/fabric.png");
|
||||
background-image: url("/_/images/fabric.webp");
|
||||
background-size: 33vw auto; /* width height of each tile */
|
||||
}
|
||||
</style>
|
||||
<script src="_/code/quill.js"></script>
|
||||
<script type="module" src="index.js"></script>
|
||||
<script src="/_/code/quill.js"></script>
|
||||
<script type="module" src="75820185/index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
||||
@@ -1,5 +1,324 @@
|
||||
class Events extends Shadow {
|
||||
|
||||
events = [
|
||||
{
|
||||
date: `January 23, 2025`,
|
||||
title: `Hyperia Winter Ball`,
|
||||
description: `Join us in Austin, Texas for a dance. Live music and drinks will be included. <br>Admission for men is $50, women are free. Open to the public.`,
|
||||
location: `Austin, TX`
|
||||
}
|
||||
]
|
||||
|
||||
render() {
|
||||
ZStack(() => {
|
||||
VStack(() => {
|
||||
|
||||
h1("HYPERIA")
|
||||
.marginBottom(0, em)
|
||||
|
||||
p("Public Events")
|
||||
.fontSize(1.2, em)
|
||||
.marginBottom(2, em)
|
||||
|
||||
const Stack = window.isMobile() ? VStack : HStack
|
||||
Stack(() => {
|
||||
|
||||
VStack(() => {
|
||||
p(`February 7, 2026`)
|
||||
|
||||
p(`Hyperia Winter Ball`)
|
||||
.fontSize(1.2, em)
|
||||
|
||||
p(`Austin, TX`)
|
||||
|
||||
})
|
||||
|
||||
p(`Join us in Austin, Texas for two-stepping and live music by Noah Kurtis. 21 and up.`)
|
||||
.marginRight(4, em)
|
||||
|
||||
HStack(() => {
|
||||
img("/_/icons/creditcards/visa.svg")
|
||||
img("/_/icons/creditcards/mastercard.svg")
|
||||
img("/_/icons/creditcards/discover.svg")
|
||||
img("/_/icons/creditcards/amex.svg")
|
||||
})
|
||||
.alignSelf("flex-start")
|
||||
.height(2, em)
|
||||
.maxWidth(40, vw)
|
||||
|
||||
button("RSVP")
|
||||
.color("var(--darkbrown")
|
||||
.border("1px solid #ab2f007d")
|
||||
.background('var(--green)')
|
||||
.marginLeft("auto")
|
||||
.onClick(async function() {
|
||||
window.open("https://partiful.com/e/XqnPwBKS9vUMd8GYX3xz?c=HLU81h69", "_blank", "noopener");
|
||||
// this.innerText = "Loading..."
|
||||
// const res = await fetch("/create-checkout-session", { method: "POST" });
|
||||
// const data = await res.json();
|
||||
// window.location = data.url;
|
||||
})
|
||||
})
|
||||
.gap(3, em)
|
||||
.color("var(--darkbrown)")
|
||||
.background(`var(--accent)`)
|
||||
.padding(1, em)
|
||||
.borderRadius(12, px)
|
||||
.border("2px solid #ab2f007d")
|
||||
})
|
||||
.marginLeft(window.isMobile() ? 0 : 15, vmax)
|
||||
.marginRight(window.isMobile() ? 0 : 15, vmax)
|
||||
.marginTop(10, vmax)
|
||||
|
||||
HStack(() => {
|
||||
p("Privacy Policy")
|
||||
.onHover(function (hovering) {
|
||||
if(hovering) {
|
||||
this.style.color = "var(--darkbrown)"
|
||||
} else {
|
||||
this.style.color = ""
|
||||
}
|
||||
})
|
||||
.onClick(() => {
|
||||
this.$("#policyWindow").style.display = "flex"
|
||||
})
|
||||
p("Refund and Return Policy")
|
||||
.onHover(function (hovering) {
|
||||
if(hovering) {
|
||||
this.style.color = "var(--darkbrown)"
|
||||
} else {
|
||||
this.style.color = ""
|
||||
}
|
||||
})
|
||||
.onClick(() => {
|
||||
this.$("#refundWindow").style.display = "flex"
|
||||
})
|
||||
p("Contact Us")
|
||||
.onHover(function (hovering) {
|
||||
if(hovering) {
|
||||
this.style.color = "var(--darkbrown)"
|
||||
} else {
|
||||
this.style.color = ""
|
||||
}
|
||||
})
|
||||
.onClick(() => {
|
||||
this.$("#contactWindow").style.display = "flex"
|
||||
})
|
||||
})
|
||||
.x(50, vw).yBottom(0, vh)
|
||||
.center()
|
||||
.gap(2, em)
|
||||
.opacity(0.5)
|
||||
.cursor("default")
|
||||
})
|
||||
|
||||
VStack(() => {
|
||||
|
||||
p("Privacy Policy")
|
||||
.fontSize(2, em)
|
||||
.fontWeight(600)
|
||||
.marginBottom(1, em)
|
||||
|
||||
p("We value your privacy. This Privacy Policy explains how we collect, use, store, and protect your information when you use our website or services.")
|
||||
|
||||
p("1. Information We Collect")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("• Personal information you provide, such as your name, email address, or other contact details.")
|
||||
p("• Automatically collected data, including IP address, browser type, device information, and usage statistics.")
|
||||
p("• Cookies or similar tracking technologies that help us improve the user experience.")
|
||||
|
||||
p("2. How We Use Your Information")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("• To operate and improve our website and services.")
|
||||
p("• To communicate with you about updates, support requests, or account-related matters.")
|
||||
p("• To maintain security, prevent fraud, and ensure proper functionality.")
|
||||
|
||||
p("3. How We Share Information")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("We do not sell your personal information. We may share data only with trusted service providers who help us operate the platform, or when required by law.")
|
||||
|
||||
p("4. Data Storage & Security")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("We use reasonable technical and administrative safeguards to protect your information. However, no system is completely secure, and we cannot guarantee absolute protection.")
|
||||
|
||||
p("5. Cookies")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("Our site may use cookies to remember preferences, analyze traffic, and enhance usability. You can disable cookies in your browser settings, but some features may stop working.")
|
||||
|
||||
p("6. Your Rights")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("Depending on your location, you may have rights to access, update, delete, or request a copy of your personal data. Contact us if you wish to exercise these rights.")
|
||||
|
||||
p("7. Third-Party Links")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("Our website may contain links to third-party sites. We are not responsible for their content or privacy practices.")
|
||||
|
||||
p("8. Changes to This Policy")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("We may update this Privacy Policy from time to time. Updated versions will be posted on this page with the effective date.")
|
||||
|
||||
p("9. Contact Us")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("If you have any questions about this Privacy Policy, feel free to contact us at info@hyperia.so.")
|
||||
|
||||
p("x")
|
||||
.onClick(function (done) {
|
||||
if(done) {
|
||||
this.parentElement.style.display = "none"
|
||||
}
|
||||
})
|
||||
.color("var(--red)")
|
||||
.xRight(1, em).y(1, em)
|
||||
.fontSize(2, em)
|
||||
.cursor("pointer")
|
||||
|
||||
})
|
||||
.x(50, vw).y(50, vh)
|
||||
.width(70, vw).height(70, vh)
|
||||
.center()
|
||||
.backgroundColor("var(--accent)")
|
||||
.display("none")
|
||||
.overflow("scroll")
|
||||
.padding(1, em)
|
||||
.border("3px solid black")
|
||||
.color("var(--darkbrown)")
|
||||
.attr({ id: "policyWindow" })
|
||||
|
||||
VStack(() => {
|
||||
|
||||
p("Refund & Return Policy")
|
||||
.fontSize(2, em)
|
||||
.fontWeight(600)
|
||||
.marginBottom(1, em)
|
||||
|
||||
p("1. Eligibility for Refunds")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("• Refund requests may be considered when submitted within 14 days of purchase.")
|
||||
p("• To qualify, you must provide proof of purchase and a valid reason for the request.")
|
||||
p("• Certain digital products or services may be non-refundable once accessed or downloaded.")
|
||||
|
||||
p("2. Non-Refundable Items")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("• Products or services that have already been delivered, downloaded, or accessed in full.")
|
||||
p("• Custom work, personalized items, or one-time service fees.")
|
||||
p("• Any promotional or discounted items, unless required by law.")
|
||||
|
||||
p("3. Returns (If Applicable)")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("• Physical items must be returned in their original condition.")
|
||||
p("• You are responsible for return shipping costs unless the item was defective or incorrect.")
|
||||
p("• Items damaged through misuse or neglect cannot be returned.")
|
||||
|
||||
p("4. Processing Refunds")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("• Approved refunds are issued to the original payment method.")
|
||||
p("• Processing times may vary depending on your bank or payment provider.")
|
||||
p("• We will notify you once your refund has been initiated.")
|
||||
|
||||
p("5. Cancellations")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("• Orders or subscriptions may be cancelled before fulfillment or renewal.")
|
||||
p("• If Hyperia declare a cancellation of any product or event, a refund will be issued to all parties.")
|
||||
|
||||
p("6. Contact for Refund Requests")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("If you need to request a refund, return an item, or cancel an order, please contact us at info@hyperia.so. Include your order number and relevant details so we can assist you promptly.")
|
||||
|
||||
p("7. Policy Updates")
|
||||
.fontWeight(600)
|
||||
.marginTop(1, em)
|
||||
|
||||
p("We may update this Refund & Return Policy from time to time. Any changes will be posted on this page with the effective date.")
|
||||
|
||||
p("x")
|
||||
.onClick(function (done) {
|
||||
if(done) {
|
||||
this.parentElement.style.display = "none"
|
||||
}
|
||||
})
|
||||
.color("var(--red)")
|
||||
.xRight(1, em).y(1, em)
|
||||
.fontSize(2, em)
|
||||
.cursor("pointer")
|
||||
|
||||
})
|
||||
.x(50, vw).y(50, vh)
|
||||
.width(70, vw).height(70, vh)
|
||||
.center()
|
||||
.backgroundColor("var(--accent)")
|
||||
.display("none")
|
||||
.overflow("scroll")
|
||||
.padding(1, em)
|
||||
.border("3px solid black")
|
||||
.color("var(--darkbrown)")
|
||||
.attr({ id: "refundWindow" })
|
||||
|
||||
|
||||
VStack(() => {
|
||||
|
||||
p("Contact Us")
|
||||
.fontSize(2, em)
|
||||
.fontWeight(600)
|
||||
.marginBottom(1, em)
|
||||
|
||||
p("Email: info@hyperia.so")
|
||||
p("Phone: 813-373-9100")
|
||||
p("Address: 2014 E 9th St, Unit A, Austin TX")
|
||||
|
||||
p("x")
|
||||
.onClick(function (done) {
|
||||
if(done) {
|
||||
this.parentElement.style.display = "none"
|
||||
}
|
||||
})
|
||||
.color("var(--red)")
|
||||
.xRight(1, em).y(1, em)
|
||||
.fontSize(2, em)
|
||||
.cursor("pointer")
|
||||
|
||||
})
|
||||
.gap(2, em)
|
||||
.x(50, vw).y(50, vh)
|
||||
.width(50, vw).height(50, vh)
|
||||
.center()
|
||||
.backgroundColor("var(--accent)")
|
||||
.display("none")
|
||||
.overflow("scroll")
|
||||
.padding(1, em)
|
||||
.border("3px solid black")
|
||||
.color("var(--darkbrown)")
|
||||
.attr({ id: "contactWindow" })
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import "../components/NavBar.js"
|
||||
import "../components/JoinForm.js"
|
||||
import "../components/SignupForm.js"
|
||||
import "./Why.js"
|
||||
import "./Events.js"
|
||||
import "./Join.js"
|
||||
import "./SignIn.js"
|
||||
import "./Success.js"
|
||||
|
||||
class Home extends Shadow {
|
||||
render() {
|
||||
@@ -12,7 +13,7 @@ class Home extends Shadow {
|
||||
|
||||
NavBar()
|
||||
|
||||
img("_/icons/logo.svg", "2.5em")
|
||||
img("/_/icons/logo.svg", "2.5em")
|
||||
.onClick((done) => {
|
||||
if(!done) return
|
||||
window.navigateTo("/")
|
||||
@@ -20,25 +21,41 @@ class Home extends Shadow {
|
||||
.position("absolute")
|
||||
.left(50, vw).top(4, em)
|
||||
.center()
|
||||
.transform("translate(-2em, -50%)")
|
||||
.transform(`translate(${window.isMobile() ? "-50%" : "-2em"}, -50%)`)
|
||||
|
||||
switch(window.location.pathname) {
|
||||
case "/":
|
||||
img("_/images/knight.png", "29vw")
|
||||
img("/_/images/knight.webp", "29vmax")
|
||||
.position("absolute")
|
||||
.left(50, vw).top(50, vh)
|
||||
.left(50, vw).top(isMobile() ? 50 : 53, vh)
|
||||
.center()
|
||||
|
||||
p("H Y P E R I A")
|
||||
.x(50, vw).y(50, vh)
|
||||
.center()
|
||||
.color("var(--gold)")
|
||||
.fontSize(5, vw)
|
||||
if(!isMobile()) {
|
||||
p("H Y P E R I A ")
|
||||
.x(50, vw).y(53, vh)
|
||||
.textAlign("center")
|
||||
.center()
|
||||
.color("var(--gold)")
|
||||
.fontSize(5, vw)
|
||||
.maxWidth(isMobile() ? 1000 : 100, em)
|
||||
} else {
|
||||
p("H Y P E R I A ")
|
||||
.x(46, vw).y(isMobile() ? 50 : 53, vh)
|
||||
.textAlign("center")
|
||||
.color("var(--gold)")
|
||||
.fontSize(5, vw)
|
||||
}
|
||||
|
||||
p("A CLASSICAL CHRISTIAN ASSOCIATION")
|
||||
.x(50, vw).y(94, vh)
|
||||
.center()
|
||||
.letterSpacing(0.3, em)
|
||||
if(!isMobile()) {
|
||||
let text = "A Classical Christian Network"
|
||||
p(isMobile() ? text : text.toUpperCase())
|
||||
.x(50, vw).yBottom(isMobile() ? 1 : 3, vh)
|
||||
.center()
|
||||
.letterSpacing(0.1, em)
|
||||
.width(isMobile() ? 80 : 100, vw)
|
||||
.fontSize(isMobile() ? 0.8 : 1, em)
|
||||
.textAlign("center")
|
||||
}
|
||||
break;
|
||||
case "/why":
|
||||
Why()
|
||||
@@ -49,13 +66,15 @@ class Home extends Shadow {
|
||||
case "/join":
|
||||
Join()
|
||||
break;
|
||||
case "/signin":
|
||||
SignIn()
|
||||
case "/success":
|
||||
Success()
|
||||
break;
|
||||
|
||||
default:
|
||||
if(window.location.pathname.startsWith("/signup")) {
|
||||
JoinForm()
|
||||
SignupForm()
|
||||
} else if(window.location.pathname.startsWith("/signin")) {
|
||||
SignIn()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
class Join extends Shadow {
|
||||
render() {
|
||||
p("Membership is invitation-only. Look out for us in person, or come to an event!")
|
||||
|
||||
VStack(() => {
|
||||
|
||||
|
||||
p("Membership is invitation-only. Wait to meet one of us, or come to one of our events!")
|
||||
|
||||
|
||||
// p("Membership is invitation-only. But sign up for our newsletter to hear about more events!")
|
||||
|
||||
// HStack(() => {
|
||||
// input("Email", "40vmin")
|
||||
// .attr({name: "email", type: "email"})
|
||||
|
||||
// button("Sign Up")
|
||||
// .width(15, vmin)
|
||||
// })
|
||||
// .gap(1, em)
|
||||
// .marginTop(1, em)
|
||||
})
|
||||
.alignItems("center")
|
||||
.maxWidth(90, vw)
|
||||
.x(50, vw).y(50, vh)
|
||||
.center()
|
||||
}
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
css(`
|
||||
signin- input::placeholder {
|
||||
color: var(--accent)
|
||||
}
|
||||
`)
|
||||
|
||||
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() {
|
||||
ZStack(() => {
|
||||
if(window.location.search.includes("new")) {
|
||||
p("Welcome to Hyperia! You may now log in.")
|
||||
.x(50, vw).y(40, vh)
|
||||
.center()
|
||||
}
|
||||
|
||||
form(() => {
|
||||
input("Email")
|
||||
.attr({name: "email", type: "email"})
|
||||
.margin(1, em)
|
||||
.styles(this.inputStyles)
|
||||
input("Password")
|
||||
.attr({name: "password", type: "password"})
|
||||
.margin(1, em)
|
||||
.styles(this.inputStyles)
|
||||
button("Submit")
|
||||
.margin(1, em)
|
||||
})
|
||||
.attr({action: "/login", method: "POST"})
|
||||
.x(50, vw).y(50, vh)
|
||||
|
||||
9
ui/public/pages/Success.js
Normal file
@@ -0,0 +1,9 @@
|
||||
class Success extends Shadow {
|
||||
render() {
|
||||
p("Thanks for your purchase! You will receive a confirmation email shortly. <br><br> <b>Keep that email; it will be checked at the door.</b>")
|
||||
.x(50, vw).y(50, vh)
|
||||
.center()
|
||||
}
|
||||
}
|
||||
|
||||
register(Success)
|
||||
@@ -1,8 +1,20 @@
|
||||
class Why extends Shadow {
|
||||
render() {
|
||||
p("The West is Falling. Why Not?")
|
||||
.x(50, vw).y(50, vh)
|
||||
.center()
|
||||
p(`I grew up going to Classical Christian schools all my life. Little did I know, this was a very unique experience - we got to learn all about our history, and everyone had a shared moral understanding.
|
||||
|
||||
<br><br>Only when I went out into the world did I realize that most Americans have no idea what this is like. They have never been a part of a shared culture, and the only value they know is multiculturalism.
|
||||
|
||||
<br><br>As adults, that is the world the we are all expected to live in.
|
||||
|
||||
<br><br>Classical Christian schools are great, but what if I want to live a Classical Christian life?
|
||||
|
||||
<br><br>That is what Hyperia is for. It is a Classical Christian space for adults.
|
||||
|
||||
<br><br> -- Sam Russell, Founder
|
||||
`)
|
||||
.marginTop(window.isMobile() ? 20 : 30, vh)
|
||||
.marginHorizontal(window.isMobile() ? 10 : 20, vw)
|
||||
.marginBottom(20, vh)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
// Create the hover display element
|
||||
const hoverBox = document.createElement('div');
|
||||
hoverBox.style.id = "hoverBox"
|
||||
hoverBox.style.position = 'fixed';
|
||||
hoverBox.style.padding = '8px 12px';
|
||||
hoverBox.style.backgroundColor = 'var(--green)';
|
||||
hoverBox.style.border = '1px solid var(--tan)';
|
||||
hoverBox.style.color = 'var(--tan)';
|
||||
hoverBox.style.opacity = '80%';
|
||||
hoverBox.style.pointerEvents = 'none';
|
||||
hoverBox.style.zIndex = '9999';
|
||||
hoverBox.style.fontFamily = 'sans-serif';
|
||||
hoverBox.style.fontSize = '14px';
|
||||
hoverBox.style.display = 'none';
|
||||
document.body.appendChild(hoverBox);
|
||||
let currentTarget = null;
|
||||
|
||||
function capitalizeWords(str) {
|
||||
return str
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function onMouseOver(e) {
|
||||
const target = e.target;
|
||||
let paintingName; let artistName;
|
||||
if(target.id === "back") {
|
||||
paintingName = "The Garden Terrace"
|
||||
artistName = "Caspar David Friedrich"
|
||||
} else if (target.tagName.toLowerCase() === 'img' && target.classList.contains('interactive')) {
|
||||
const match = target.src.match(/([^\/]+)\.([a-z]{3,4})(\?.*)?$/i); // extract filename
|
||||
if (!match) return;
|
||||
|
||||
const filename = match[1];
|
||||
const parts = filename.split('_');
|
||||
if (parts.length !== 2) return;
|
||||
|
||||
paintingName = capitalizeWords(parts[0]);
|
||||
artistName = capitalizeWords(parts[1]);
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
hoverBox.innerHTML = `<strong>${paintingName}</strong><br><span style="font-size: 12px;">${artistName}</span>`;
|
||||
hoverBox.style.display = 'block';
|
||||
currentTarget = target;
|
||||
hoverBox.style.left = `${e.clientX + 15}px`;
|
||||
hoverBox.style.top = `${e.clientY + 15}px`;
|
||||
}
|
||||
|
||||
function onMouseOut(e) {
|
||||
if (e.target === currentTarget) {
|
||||
hoverBox.style.display = 'none';
|
||||
currentTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseMove(e) {
|
||||
if (hoverBox.style.display === 'block') {
|
||||
hoverBox.style.left = `${e.clientX + 15}px`;
|
||||
hoverBox.style.top = `${e.clientY + 15}px`;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mouseover', onMouseOver);
|
||||
document.addEventListener('mouseout', onMouseOut);
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
@@ -1,56 +0,0 @@
|
||||
let treeOriginalTop = null;
|
||||
let currentVelocity = 0;
|
||||
let isAnimating = false;
|
||||
|
||||
window.addEventListener('wheel', (e) => {
|
||||
if(window.innerWidth < 600) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add scroll delta to the velocity
|
||||
currentVelocity += e.deltaY;
|
||||
|
||||
// Start animation loop if not running
|
||||
if (!isAnimating) {
|
||||
isAnimating = true;
|
||||
requestAnimationFrame(animateScroll);
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
function animateScroll() {
|
||||
const tree = document.getElementById("tree");
|
||||
|
||||
if (!treeOriginalTop) {
|
||||
treeOriginalTop = parseInt(getComputedStyle(tree).top);
|
||||
}
|
||||
|
||||
const treeHeightPX = 0.83 * window.innerHeight;
|
||||
let treeTopPX = parseInt(getComputedStyle(tree).top);
|
||||
|
||||
// Limit per-frame speed (but NOT total speed)
|
||||
let multiplier = window.innerHeight / 2000;
|
||||
let delta = Math.max(-100 * multiplier, Math.min(100 * multiplier, currentVelocity));
|
||||
|
||||
// Apply the scroll
|
||||
let newTop = treeTopPX - delta;
|
||||
|
||||
// Clamp top/bottom bounds
|
||||
const maxTop = treeOriginalTop;
|
||||
const minTop = treeOriginalTop - treeHeightPX;
|
||||
|
||||
if (newTop > maxTop) newTop = maxTop;
|
||||
if (newTop < minTop) newTop = minTop;
|
||||
|
||||
tree.style.top = `${newTop}px`;
|
||||
|
||||
// Slowly reduce velocity
|
||||
currentVelocity *= 0.85;
|
||||
|
||||
// If velocity is small, stop
|
||||
if (Math.abs(currentVelocity) > 0.5) {
|
||||
requestAnimationFrame(animateScroll);
|
||||
} else {
|
||||
isAnimating = false;
|
||||
currentVelocity = 0;
|
||||
}
|
||||
}
|
||||
21
ui/readme.md
@@ -1,20 +1,5 @@
|
||||
See https://github.com/return-to-the-land/go-backend for instructions.
|
||||
# Installs
|
||||
|
||||
# Style Guidelines
|
||||
- Font size is defined in the index.html. Only use rem as a unit.
|
||||
Stripe CLI
|
||||
|
||||
# Documentation
|
||||
```go install golang.org/x/tools/cmd/godoc@latest```
|
||||
```$HOME/go/bin/godoc -http=:6060``` (to run on Mac)
|
||||
|
||||
|
||||
Fix Redirect Issues:
|
||||
|
||||
Have the go server determine redirect, generate a random URL from it
|
||||
When random url is accessed by frontend -> redirect to home
|
||||
|
||||
|
||||
# Resources
|
||||
|
||||
Convert image to webp (so it's smaller)
|
||||
https://www.freeconvert.com/jpg-to-webp/download
|
||||
stripe listen --forward-to localhost:3003/webhook
|
||||
@@ -1,178 +0,0 @@
|
||||
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 {
|
||||
friends = []
|
||||
conversations = []
|
||||
|
||||
render() {
|
||||
ZStack(() => {
|
||||
HStack(() => {
|
||||
VStack(() => {
|
||||
h3("Friends")
|
||||
.marginTop(0)
|
||||
.marginBottom(1, em)
|
||||
.marginLeft(0.4, em)
|
||||
|
||||
if (this.friends.length > 1) {
|
||||
for(let i = 0; i < this.friends.length; i++) {
|
||||
p(this.friends[i].name)
|
||||
}
|
||||
} else {
|
||||
p("No Friends!")
|
||||
}
|
||||
})
|
||||
.height(100, vh)
|
||||
.paddingLeft(2, em)
|
||||
.paddingRight(2, em)
|
||||
.paddingTop(2, em)
|
||||
.gap(0, em)
|
||||
.borderRight("1px solid var(--accent2)")
|
||||
|
||||
VStack(() => {
|
||||
h3("Conversations")
|
||||
.marginTop(0)
|
||||
.marginBottom(1, em)
|
||||
.marginLeft(0.4, em)
|
||||
|
||||
if (this.conversations.length > 1) {
|
||||
for(let i = 0; i < this.conversations.length; i++) {
|
||||
p(this.conversations[i].name)
|
||||
}
|
||||
} else {
|
||||
p("No Conversations!")
|
||||
}
|
||||
})
|
||||
.height(100, vh)
|
||||
.paddingLeft(2, em)
|
||||
.paddingRight(2, em)
|
||||
.paddingTop(2, em)
|
||||
.gap(0, em)
|
||||
.borderRight("1px solid var(--accent2)")
|
||||
})
|
||||
.width(100, "%")
|
||||
.x(0).y(13, vh)
|
||||
.borderTop("1px solid var(--accent2)")
|
||||
|
||||
p("0 Items")
|
||||
.position("absolute")
|
||||
.x(50, vw).y(50, vh)
|
||||
.transform("translate(-50%, -50%)")
|
||||
|
||||
HStack(() => {
|
||||
input("Search messages...", "45vw")
|
||||
.attr({
|
||||
"type": "text"
|
||||
})
|
||||
.fontSize(1.1, em)
|
||||
.paddingLeft(1.3, em)
|
||||
.background("transparent")
|
||||
.border("1px solid var(--accent2)")
|
||||
.outline("none")
|
||||
.color("var(--accent)")
|
||||
.borderRadius(10, px)
|
||||
|
||||
button("Search")
|
||||
.marginLeft(2, em)
|
||||
.borderRadius(10, px)
|
||||
.background("transparent")
|
||||
.border("1px 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 Message")
|
||||
.width(13, em)
|
||||
.marginLeft(1, em)
|
||||
.borderRadius(10, px)
|
||||
.background("transparent")
|
||||
.border("1px 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, "%")
|
||||
}
|
||||
|
||||
SidebarName(name) {
|
||||
let firstLetter = name[0]
|
||||
|
||||
HStack(() => {
|
||||
div(firstLetter)
|
||||
.display("flex")
|
||||
.justifyContent("center")
|
||||
.alignItems("center")
|
||||
.width(1.5, em)
|
||||
.height(1.5, em)
|
||||
.border("1px solid var(--accent2)")
|
||||
.borderRadius(100, "%")
|
||||
p(name)
|
||||
.marginLeft(1, em)
|
||||
})
|
||||
.alignItems("center")
|
||||
.padding(5, px)
|
||||
.borderRadius(0.5, em)
|
||||
.cursor("default")
|
||||
.onHover(function (hovering) {
|
||||
if(hovering) {
|
||||
this.style.background = "var(--green)"
|
||||
|
||||
} else {
|
||||
this.style.background = "transparent"
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// Optional additional logic
|
||||
}
|
||||
}
|
||||
|
||||
register(Messages)
|
||||
@@ -1,43 +0,0 @@
|
||||
import "./ProfileMenu.js"
|
||||
|
||||
class ProfileButton extends Shadow {
|
||||
|
||||
render() {
|
||||
ZStack(() => {
|
||||
html(`
|
||||
<svg style="position: absolute;" viewBox="0 0 88 124" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.998" d="M42.4512 2.00195C52.1659 2.22835 61.398 4.45649 70.166 8.69238L70.3818 8.79688L70.6162 8.84668C72.5686 9.26638 74.537 9.47923 76.5195 9.48535C76.5062 11.0178 76.3492 12.5388 76.0479 14.0508C74.4195 16.4705 72.6152 18.7571 70.6279 20.9072L69.8799 21.7168L70.1641 22.7812C71.785 28.8491 72.6458 35.0483 72.7471 41.3818L72.7549 41.8369L72.958 42.2432C74.3564 45.0408 75.7681 47.8651 77.1934 50.7148C76.401 51.7697 75.5014 52.7412 74.4893 53.6279L73.8906 54.1533L73.8164 54.9463C73.2326 61.1851 71.2275 66.786 67.8037 71.7871C66.3409 71.9333 64.8778 72.0802 63.415 72.2266C61.648 72.4034 59.8809 72.5803 58.1143 72.7568L56.9131 72.877L56.46 73.9951C55.1158 77.3095 54.9178 80.7587 55.8496 84.2715L55.9658 84.708L56.2578 85.0527C58.3856 87.5622 60.8538 89.6502 63.6553 91.3105L63.7783 91.3838L63.9102 91.4385C70.6068 94.2094 77.192 97.0352 83.665 99.9141C72.8406 106.409 62.2808 113.347 51.9873 120.731C49.5114 121.96 46.9641 122.264 44.2627 121.648C30.7653 112.891 16.9807 104.63 2.91113 96.8623C2.98649 96.7878 3.06425 96.7148 3.14453 96.6436C9.6346 94.2535 15.6931 91.0299 21.3154 86.9707L21.5225 86.8213L21.6855 86.625C23.5522 84.3704 24.9161 81.8385 25.7686 79.041L25.8184 78.877L25.8398 78.7061C26.5676 72.8871 26.5676 67.0651 25.8398 61.2461L25.7744 60.7236L25.4609 60.3018C16.1326 47.7366 13.0625 33.9256 16.21 18.7246C21.8795 8.29751 30.5529 2.76795 42.4512 2.00195Z"
|
||||
fill="${this.hovered ? window.getColor("orange") : window.getColor("periwinkle")}" fill-opacity="0.36" stroke="${this.hovered ? window.getColor("orange") : window.getColor("periwinkle")}" stroke-width="4"/>
|
||||
</svg>
|
||||
`)
|
||||
|
||||
ProfileMenu()
|
||||
})
|
||||
.display("block")
|
||||
.width(1.5, em)
|
||||
|
||||
document.addEventListener("mousemove", (e) => {
|
||||
this.previousHovered = this.hovered
|
||||
if(e.target.closest("profile-button")) {
|
||||
this.hovered = true
|
||||
} else {
|
||||
this.hovered = false
|
||||
}
|
||||
if(this.hovered !== this.previousHovered) {
|
||||
if(this.hovered === true) {
|
||||
this.rerender()
|
||||
setTimeout(() => {
|
||||
this.querySelector("profile-menu").className = "open"
|
||||
})
|
||||
} else {
|
||||
this.querySelector("profile-menu").className = "closed"
|
||||
setTimeout(() => {
|
||||
this.rerender()
|
||||
}, 140)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
register(ProfileButton)
|
||||