This commit is contained in:
metacryst
2025-12-26 19:55:31 -06:00
27 changed files with 435 additions and 1080 deletions

View File

@@ -36,26 +36,18 @@ export default class AuthHandler {
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 });
res.send({ email: "sam@hyperia.so", name: "Sam Russell" });
} catch (err) {
res.status(401).send({ error: "Invalid token" });
}
}
async login(req, res) {
const { email, password } = req.body;
let foundUser = global.db.members.getByEmail(email)
if(!foundUser) {
res.status(400).json({ error: 'Incorrect email.' });
return;
}
const storedHash = foundUser.password
const valid = await argon2.verify(storedHash, password);
if (!valid) {
const { password } = req.body;
if (!(password === process.env.PASSWORD)) {
res.status(400).json({ error: 'Incorrect password.' });
} else {
const payload = { email: foundUser.email };
const payload = { email: "sam@hyperia.so" };
console.log(payload)
const secret = process.env.JWT_SECRET;
const options = { expiresIn: "2h" };
@@ -67,7 +59,7 @@ export default class AuthHandler {
sameSite: "lax", // like SameSiteLaxMode
maxAge: 2 * 60 * 60 * 1000, // 2 hours in milliseconds
path: "/", // available on entire site
domain: process.env.ENV === "production" ? "." + process.env.BASE_URL : undefined
domain: process.env.ENV === "production" ? process.env.BASE_URL : undefined
});
res.redirect("/")

View File

@@ -20,7 +20,6 @@ export default class Database {
this.logs = dbJson
setInterval(() => {
console.log("saving db")
global.db.saveData()
}, 5000)
}

View File

@@ -7,13 +7,15 @@ import chalk from 'chalk'
import moment from 'moment'
import path from 'path'
import * as useragent from "express-useragent";
import forms from 'forms'
import "./util.js"
import Socket from './ws/ws.js'
import Clients from './ws/Clients.js'
import Database from "./db/db.js"
import AuthHandler from './auth.js';
export default class Server {
store;
db;
auth;
UIPath = path.join(global.__dirname, '../ui')
@@ -30,6 +32,10 @@ export default class Server {
return router
}
handleNewLog(log) {
global.Clients.broadcast(log)
}
verifyToken = (req, res, next) => {
const { token } = req.query;
if (!token) {
@@ -131,6 +137,10 @@ export default class Server {
this.db = new Database()
global.db = this.db
this.auth = new AuthHandler()
this.store = new forms.client()
this.store.connect()
this.store.watch("log", this.handleNewLog)
const app = express();
app.use(cors({ origin: '*' }));
app.use(express.json());
@@ -146,11 +156,11 @@ export default class Server {
app.use('/', router);
const server = http.createServer(app);
global.Socket = new Socket(server);
global.Clients = new Clients(server);
const PORT = 4001;
server.listen(PORT, () => {
console.log("\n")
console.log(chalk.yellow("*************** Hyperia ***************"))
console.log(chalk.yellow("*************** Console ***************"))
console.log(chalk.yellowBright(`Server is running on port ${PORT}: http://localhost`));
console.log(chalk.yellow("***************************************"))
console.log("\n")

View File

@@ -1,22 +1,21 @@
import { WebSocket, WebSocketServer } from 'ws'
import { z } from "zod"
import jwt from 'jsonwebtoken'
import ForumHandler from "./handlers/ForumHandler.js"
import MessagesHandler from "./handlers/MessagesHandler.js"
import handler from "./handler.js"
import chalk from 'chalk'
export default class Socket {
export default class Clients {
wss;
messageSchema = z.object({
id: z.string(),
app: z.string(),
operation: z.string().optional(),
op: 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) {
if (data.op !== "GET" && data.msg === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["msg"],
@@ -43,12 +42,17 @@ export default class Socket {
const cookies = parseCookies(req.headers.cookie);
const token = cookies.auth_token;
if (!token) throw new Error("No auth token");
if (!token) {
console.error("No auth token");
return
}
const payload = jwt.verify(token, process.env.JWT_SECRET);
ws.userEmail = payload.email;
ws.on('message', (msg) => {
this.handleMessage(msg, ws);
const text = msg.toString("utf8");
const data = JSON.parse(text);
this.handleMessage(data, ws);
});
ws.on('close', () => {
@@ -62,7 +66,12 @@ export default class Socket {
// 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) => {
if(!this.messageSchema.safeParse(msg).success) {
console.log(chalk.red("Socket.handleMessage: Incoming ws message has incorrect format! ", this.messageSchema.safeParse(msg).error))
return
}
console.log("websocket message received: ", msg)
handler.handle(msg)
}
broadcast(event) {

14
server/ws/handler.js Normal file
View File

@@ -0,0 +1,14 @@
export default class handler {
static handleGet(msg) {
// let data = forms.get("log", 10)
// return data
}
static handle(msg, ws) {
switch(msg.op) {
case "GET":
return this.handleGet(msg)
}
}
}

View File

@@ -1,43 +0,0 @@
import { z } from "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)
}
}
}

View File

@@ -1,40 +0,0 @@
import { z } from "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)
}
}
}

View File

@@ -1,6 +1,9 @@
/*
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
@@ -68,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 */
@@ -163,6 +172,7 @@ Object.defineProperty(Array.prototype, 'last', {
window.quill = {
rendering: [],
lastState: null,
render: (el) => {
if(el instanceof Shadow) {
@@ -193,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")
},
@@ -216,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)
@@ -230,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
}
@@ -314,6 +381,7 @@ function extendHTMLElementWithStyleSetters() {
case "marginRight":
case "textUnderlineOffset":
case "letterSpacing":
return "unit-number"
@@ -333,24 +401,27 @@ function extendHTMLElementWithStyleSetters() {
switch (type) {
case "unit-number":
HTMLElement.prototype[prop] = function(value, unit = "px") {
if ((typeof value !== "number" || isNaN(value)) && value !== "auto") {
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;
}
});
@@ -358,46 +429,86 @@ function extendHTMLElementWithStyleSetters() {
extendHTMLElementWithStyleSetters();
HTMLElement.prototype.addStateWatcher = function(field, cb) {
let parent = this
while(!(parent instanceof Shadow)) {
parent = parent.parentNode
}
parent.stateWatchers[field].push(cb)
}
// 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]}
// 1. Run the callback to get the style argument and also update lastState
let styleArgs = format(cb())
// 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 {
func.call(this, value, unit); // ".call" ensures that "this" is correct
return this
}
}
return styleFunction
}
HTMLElement.prototype.styles = function(cb) {
cb.call(this, 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.`);
/* Type 1 */
HTMLElement.prototype.paddingVertical = StyleFunction(function(value, unit = "px") {
this.style.paddingTop = value + unit
this.style.paddingBottom = value + unit
return this
}
})
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.`);
HTMLElement.prototype.paddingHorizontal = StyleFunction(function(value, unit = "px") {
this.style.paddingRight = value + unit
this.style.paddingLeft = value + unit
return this
}
})
HTMLElement.prototype.marginVertical = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
HTMLElement.prototype.marginVertical = StyleFunction(function(value, unit = "px") {
this.style.marginTop = value + unit
this.style.marginBottom = value + unit
return this
}
})
HTMLElement.prototype.marginHorizontal = 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 = function(value, unit = "px") {
if ((typeof value !== 'number' && value !== "auto") || Number.isNaN(value))
throw new Error(`Invalid value: ${value}. Expected a number.`);
HTMLElement.prototype.fontSize = StyleFunction(function(value, unit = "px") {
switch(value) {
case "6xl":
@@ -441,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) {
@@ -661,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
@@ -670,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)
@@ -687,61 +823,48 @@ 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)
quill.lastState = null
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 */
@@ -845,8 +968,8 @@ HTMLElement.prototype.onAppear = function(func) {
};
HTMLElement.prototype.onClick = function(func) {
const onMouseDown = () => func.call(this, false);
const onMouseUp = () => func.call(this, true);
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;
@@ -929,34 +1052,38 @@ 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 = []
HTMLElement.prototype.onNavigate = function(cb) {
this._storeListener("navigate", cb);
let found = false
for(entry of navigateListeners) {
if(entry.cb.toString() === cb.toString() &&
this.nodeName === entry.el.nodeName) {
found = true
break;
}
}
if(found === false) {
navigateListeners.push({el: this, cb: cb})
}
return this;
};
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

View File

@@ -43,10 +43,10 @@
}
body {
background-color: #2B311A;
color: var(--accent);
font-family: 'Bona Nova', sans-serif;
font-size: 16px;
background-color: var(--main);
color: var(--accent);
}
#title {

8
ui/_/code/zod_4.2.1.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,133 +0,0 @@
css(`
app-menu {
color: var(--tan);
transform: translateX(-50%);
transition: transform .3s;
display: flex; gap: 2em; position: fixed; left: 50vw; bottom: 2em;
}
app-menu.minimized {
color: var(--accent);
transform: translate(-50%, 65%);
border: 0.2px solid var(--accent);
padding-top: 0.5em;
padding-left: 2em;
padding-right: 2em;
padding-bottom: 4em;
bottom: 1em;
border-radius: 12px;
}
app-menu p {
cursor: default;
transition: transform .3s, text-decoration .3s;
padding: 0.5em;
border-radius: 5px;
text-underline-offset: 5px;
}
app-menu p:hover {
text-decoration: underline;
transform: translateY(-5%)
}
app-menu p.touched {
text-decoration: underline;
transform: translateY(0%)
}
app-menu p.selected {
text-decoration: underline;
transform: translateY(-10%)
}
#divider.minimized {
display: none;
}
`)
register(
class AppMenu extends Shadow {
selected;
constructor(selected) {
super()
this.selected = selected
}
render() {
VStack(() => {
HStack(() => {
p("Forum")
p("Messages")
p("Market")
p("Jobs")
})
.justifyContent("center")
.gap(1.5, em)
.paddingRight(2, em)
img("/_/images/divider.svg", "40vw")
.attr({
"id": "divider",
})
})
.gap(0.5, em)
.onNavigate(() => {
if(window.location.pathname === "/") {
this.styleMaximized()
$("app-window").close()
} else {
this.styleMinimized()
$("app-window").open(this.selected)
}
})
.onAppear(() => {
Array.from(this.querySelectorAll("p")).forEach((el) => {
el.addEventListener("mousedown", (e) => {
el.classList.add("touched")
})
})
window.addEventListener("mouseup", (e) => {
let target = e.target
if(!target.matches("app-menu p")) {
return
}
target.classList.remove("touched")
if(target.classList.contains("selected")) {
this.selected = ""
window.navigateTo("/")
} else {
this.selected = target.innerText
window.navigateTo("/app/" + target.innerText.toLowerCase())
}
})
})
if(this.selected) {
this.styleMinimized()
}
}
styleMaximized() {
$$("app-menu p").forEach((el) => {
el.classList.remove("selected")
})
this.classList.remove("minimized")
$("#divider").style.display = ""
}
styleMinimized() {
$$("app-menu p").forEach((el) => {
if(el.innerText !== this.selected) {
el.classList.remove("selected")
} else {
el.classList.add("selected")
}
})
this.classList.add("minimized")
$("#divider").style.display = "none"
}
}
, "app-menu")

View File

@@ -1,54 +0,0 @@
import "../apps/Forum/Forum.js"
import "../apps/Tasks/Tasks.js"
import "../apps/Messages/Messages.js"
import "../apps/Market/Market.js"
import "../apps/Jobs/Jobs.js"
class AppWindow extends Shadow {
app;
constructor(app) {
super()
this.app = app
}
render() {
ZStack(() => {
switch(this.app) {
case "Forum":
Forum()
break;
case "Messages":
Messages()
break;
case "Market":
Market()
break;
case "Jobs":
Jobs()
break;
}
})
.position("fixed")
.display(this.app ? 'block' : 'none')
.width(100, "vw")
.height(100, "vh")
.background("#591d10")
.x(0)
.y(0)
// .backgroundImage("/_/images/fabric.png")
// .backgroundSize("33vw auto")
}
open(app) {
this.app = app
this.rerender()
}
close() {
this.style.display = "none"
}
}
register(AppWindow, "app-window")

View File

@@ -1,61 +1,20 @@
import "./AppWindow.js"
import "./AppMenu.js"
import "./ProfileButton.js"
import "./InputBox.js"
import "./Sidebar.js"
import "./LogTable.js"
class Home extends Shadow {
render() {
ZStack(() => {
img("/_/icons/logo.svg", "2.5em")
.position("fixed")
.left(3, em)
.top(3, vh)
.zIndex(3)
.onClick(() => {
window.navigateTo("/")
})
div()
.width(100, vw)
.height(100, vh)
.margin(0)
.backgroundImage("/_/images/the_return.webp")
.backgroundSize("cover")
.backgroundPosition("48% 65%")
.backgroundRepeat("no-repeat")
switch(window.location.pathname) {
case "/":
AppWindow()
AppMenu()
LogTable()
break
case "/app/jobs":
AppWindow("Jobs")
AppMenu("Jobs")
break;
case "/app/messages":
AppWindow("Messages")
AppMenu("Messages")
break;
case "/app/market":
AppWindow("Market")
AppMenu("Market")
break;
case "/app/forum":
AppWindow("Forum")
AppMenu("Forum")
break;
default:
throw new Error("Unknown route!")
}
HStack(() => {
ProfileButton()
.zIndex(1)
.cursor("default")
a("/signout", "Sign Out")
.background("transparent")

View File

@@ -0,0 +1,31 @@
class LogTable extends Shadow {
state = {
logs: []
}
render() {
VStack(() => {
this.state.logs.forEach((log) => {
HStack(() => {
p(log.time)
p(log.ip)
p(log.host)
p(log.path)
.maxWidth(50, vw)
})
.gap(1, em)
})
})
.onAppear(async () => {
let logs = await ServerClient.send("GET")
this.state.logs = logs
})
.onEvent("log", (e) => {
console.log(e.detail)
this.state.logs.push(e.detail)
console.log(this.state.logs)
})
}
}
register(LogTable)

View File

@@ -1,12 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hyperia</title>
<title>Console</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/_/icons/logo.svg">
<link rel="stylesheet" href="/_/code/shared.css">
<script src="/_/code/quill.js"></script>
<script src="/_/code/zod.js"></script>
<script type="module" src="75820185/index.js"></script>
</head>
<body style="margin: 0px">

View File

@@ -1,8 +1,8 @@
import Socket from "./ws/Socket.js"
import ServerClient from "./ws/ServerClient.js"
import "./components/Home.js"
import util from "./util.js"
window.util = util
window.Socket = new Socket()
window.ServerClient = new ServerClient()
Home()

View File

@@ -44,7 +44,7 @@ class Connection {
}
send = (msg) => {
console.log("sending")
console.log("sending: ", msg)
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(msg);
}

View File

@@ -0,0 +1,78 @@
import Connection from "./Connection.js";
import { z } from '/_/code/zod_4.2.1.js';
export default class ServerClient {
connection;
disabled = true;
requestID = 1;
pending = new Map();
messageSchema = z.object({
id: z.string(),
op: 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.op !== "GET" && data.msg === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["msg"],
message: "msg is required when operation is not GET"
})
}
})
.strict()
constructor() {
this.connection = new Connection(this.receive);
}
isOpen() {
if(this.connection.checkOpen()) {
return true;
} else {
return false;
}
}
send(op, msg) {
const id = (++this.requestID).toString();
let toSend = {
id: (++this.requestID).toString(),
op: op,
msg: msg
}
console.log(this.messageSchema.safeParse(toSend).error)
if(!this.messageSchema.safeParse(toSend).success) throw new Error("ServerClient.send: ws message has incorrect format!")
return new Promise(resolve => {
this.pending.set(id, resolve);
this.connection.send(JSON.stringify(toSend));
});
}
receive = async (event) => {
let msg = event.data;
if(msg instanceof Blob) {
msg = await msg.text()
}
msg = JSON.parse(msg);
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("log", {
detail: msg
}));
}
}

View File

@@ -1,45 +0,0 @@
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
}));
}
}

View File

@@ -1,17 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hyperia</title>
<title>Console</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">
<style>
body {
font-size: 16px;
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="75820185/index.js"></script>

View File

@@ -1,325 +0,0 @@
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(`January 23, 2025`)
p(`Hyperia Winter Ball`)
.fontSize(1.2, em)
p(`Austin, TX`)
})
p(`Join us in Austin, Texas for a great dance, with free drinks and live music. <br><br>Admission: $35 for men, women are free.`)
.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("Buy Ticket")
.color("var(--darkbrown")
.border("1px solid #ab2f007d")
.background('var(--green)')
.marginLeft("auto")
.onClick(async function() {
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" })
}
}
register(Events)

View File

@@ -1,79 +1,37 @@
import "../components/NavBar.js"
import "../components/SignupForm.js"
import "./Why.js"
import "./Events.js"
import "./Join.js"
import "./SignIn.js"
import "./Success.js"
class Home extends Shadow {
inputStyles(el) {
return el
.color("var(--accent)")
.border("1px solid var(--accent)")
}
render() {
ZStack(() => {
ZStack(() => {
if(window.location.search.includes("new")) {
p("Welcome to Hyperia! You may now log in.")
.x(50, vw).y(40, vh)
.center()
}
NavBar()
img("/_/icons/logo.svg", "2.5em")
.onClick((done) => {
if(!done) return
window.navigateTo("/")
form(() => {
input("Password")
.attr({name: "password", type: "password"})
.margin(1, em)
.styles(this.inputStyles)
button("Submit")
.margin(1, em)
})
.position("absolute")
.left(50, vw).top(4, em)
.attr({action: "/login", method: "POST"})
.x(50, vw).y(50, vh)
.center()
.transform(`translate(${window.isMobile() ? "-50%" : "-2em"}, -50%)`)
switch(window.location.pathname) {
case "/":
img("/_/images/knight.png", "29vmax")
.position("absolute")
.left(50, vw).top(isMobile() ? 50 : 53, vh)
.center()
p("H &nbsp; Y &nbsp; P &nbsp; E &nbsp; R &nbsp; I &nbsp; A &nbsp;")
.x(50, vw).y(isMobile() ? 50 : 53, vh)
.textAlign("center")
.center()
.color("var(--gold)")
.fontSize(isMobile() ? 6 : 5, vw)
.maxWidth(isMobile() ? 0.8 : 100, 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.3, em)
.width(isMobile() ? 80 : 100, vw)
.fontSize(isMobile() ? 0.8 : 1, em)
.textAlign("center")
}
break;
case "/why":
Why()
break;
case "/events":
Events()
break;
case "/join":
Join()
break;
case "/success":
Success()
break;
default:
if(window.location.pathname.startsWith("/signup")) {
SignupForm()
} else if(window.location.pathname.startsWith("/signin")) {
SignIn()
}
}
})
.onNavigate(() => {
this.rerender()
})
})
.width(98, vw).height(98, vh)
.background("#7B413A")
.x(1, vw).y(1, vh)
}
}

View File

@@ -1,29 +0,0 @@
class Join extends Shadow {
render() {
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()
}
}
register(Join)

View File

@@ -1,9 +0,0 @@
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)

View File

@@ -1,21 +0,0 @@
class Why extends Shadow {
render() {
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)
}
}
register(Why)

View File

@@ -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);

View File

@@ -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;
}
}