1, 4: Fixing 404 after logged out, adding market entry

This commit is contained in:
metacryst
2025-11-10 04:05:21 -06:00
parent 6f9ed49b2e
commit 41c0bb57a3
16 changed files with 494 additions and 136 deletions

View File

@@ -14,16 +14,18 @@ export default class AuthHandler {
}
isLoggedInUser(req, res) {
const token = req.cookies.auth_token; // read cookie
const token = req.cookies.auth_token;
if (!token) {
return false
return false;
}
try {
return true
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
return true;
} catch (err) {
return false
return false;
}
}

View File

@@ -2,6 +2,7 @@ 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';
@@ -20,11 +21,13 @@ class Server {
db;
auth;
UIPath = path.join(__dirname, '../ui')
DBPath = path.join(__dirname, '../db')
registerRoutes(router) {
// router.post('/api/location', handlers.updateLocation)
router.post('/login', this.auth.login)
router.get('/signout', this.auth.logout)
router.get('/db/images/*', this.getUserImage)
router.get('/*', this.get)
return router
}
@@ -49,24 +52,49 @@ class Server {
}
}
getUserImage = async (req, res) => {
function getFileByNumber(dir, number) {
const files = fs.readdirSync(dir);
const match = files.find(file => {
const base = path.parse(file).name; // filename without extension
return base === String(number);
});
return match ? path.join(dir, match) : null;
}
let filePath = getFileByNumber(path.join(this.DBPath, "images"), path.basename(req.url))
res.sendFile(filePath)
}
get = async (req, res) => {
if(!this.auth.isLoggedInUser(req, res)) {
console.log("Not logged in")
let url = req.url
if(url === "/") {
url = "/index.html"
} else if(!url.includes(".")) { // TODO: Make public app single-page
url = path.join("/pages", url) + ".html"
}
let filePath;
if(url.startsWith("/_")) {
filePath = path.join(this.UIPath, url);
} else {
filePath = path.join(this.UIPath, "public", url);
}
if(!url.includes(".")) { // Page request
if(url === "/") {
url = "/index.html"
} else {
url = path.join("/pages", url) + ".html"
}
res.sendFile(filePath);
let filePath = path.join(this.UIPath, "public", url);
res.sendFile(filePath, (err) => {
if (err) {
console.log("File not found, sending fallback:", filePath);
res.redirect("/");
}
});
} else { // File Request
let filePath;
if(url.startsWith("/_")) {
filePath = path.join(this.UIPath, url);
} else {
filePath = path.join(this.UIPath, "public", url);
}
res.sendFile(filePath);
}
} else {
let url = req.url

View File

@@ -1,6 +1,7 @@
/*
Sam Russell
Captured Sun
11.9.25 - changed p(innerText) to p(innerHTML), adjusted onNavigate to work for multiple elements and with correct "this" scope
11.7.25 - changed registerShadow() to register(), changed onClick() to be like onHover()
11.6.25 - adding default value for "button()" "children" parameter
10.29.25 - adding "gap()" and "label()" functions
@@ -562,12 +563,12 @@ HTMLImageElement.prototype.backgroundColor = function(value) {
return this; // Always returns the element itself
};
window.p = function p(innerText) {
window.p = function p(innerHTML) {
let el = document.createElement("p")
if(typeof innerText === "function") {
el.render = innerText
el.render = innerHTML
} else {
el.innerText = innerText
el.innerHTML = innerHTML
}
el.style.margin = "0";
quill.render(el)
@@ -765,13 +766,15 @@ HTMLElement.prototype.onKeyDown = function(cb) {
return this;
};
/* QUIRK 1:
In all the other callback functions, the user can choose the scope of "this". It can be either the parent shadow or the element itself.
This listener only allows for the latter functionality. This is because the navigate event fires on the window.
Without binding, "this" would refer only to the window. So here we are compromising on one of the two.
/* 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 event as a window event, it won't have the "this" scope that a callback normally would.
- Then, if we try to add that scope using bind(), it makes the function.toString() unreadable, which means we will get false positives for duplicate listeners.
- Therefore, we just have to attach the navigate event to the element, and manually trigger that when the window listener fires.
*/
HTMLElement.prototype.onNavigate = function(cb) {
window._storeListener(window, "navigate", cb.bind(this));
this._storeListener("navigate", cb);
window.addEventListener("navigate", () => this.dispatchEvent(new CustomEvent("navigate")))
return this;
};

View File

@@ -3,7 +3,7 @@
<head>
<title>Hyperia</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="_/icons/logo.svg">
<link rel="icon" href="/_/icons/logo.svg">
<link rel="stylesheet" href="_/code/shared.css">
<style>

178
ui/site/apps/Forum/Forum.js Normal file
View File

@@ -0,0 +1,178 @@
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(--periwinkle)")
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(--periwinkle)")
})
.width(100, "%")
.x(0).y(13, vh)
.borderTop("1px solid var(--periwinkle)")
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(--periwinkle)")
.outline("none")
.color("var(--accent)")
.borderRadius(10, px)
button("Search")
.marginLeft(2, em)
.borderRadius(10, px)
.background("transparent")
.border("1px solid var(--periwinkle)")
.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(--periwinkle)")
.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(--periwinkle)")
.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)

View File

@@ -29,12 +29,17 @@ class Jobs extends Shadow {
jobs = [
{
title: "Austin Chapter Lead",
salary: "1% of Local Tax Revenue",
location: "Austin"
salary: "1% of Local Revenue",
company: "Hyperia",
city: "Austin",
state: "TX"
},
{
title: "San Marcos Chapter Lead",
salary: "1% of Local Tax Revenue"
salary: "1% of Local Revenue",
company: "Hyperia",
city: "San Marcos",
state: "TX"
}
]

View File

@@ -6,10 +6,19 @@ class JobsGrid extends Shadow {
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)
.marginTop(0.1, em)
.marginBottom(1, em)
.marginLeft(0.4, em)
.color("var(--periwinkle)")
@@ -17,14 +26,23 @@ class JobsGrid extends Shadow {
if (this.jobs.length > 0) {
ZStack(() => {
for (let i = 0; i < this.jobs.length; i++) {
p(this.jobs[i].title)
.border("1px solid var(--periwinkle)")
.padding(1, em)
.borderRadius(5, "px")
}
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)
.border("1px solid var(--periwinkle)")
.borderRadius(5, "px")
}
})
.display("grid")
.gridTemplateColumns("repeat(auto-fill, minmax(200px, 1fr))")
.gridTemplateColumns("repeat(auto-fill, minmax(250px, 1fr))")
.gap(1, em)
} else {
p("No Jobs!")

View File

@@ -2,6 +2,7 @@ class JobsSidebar extends Shadow {
render() {
VStack(() => {
h3("Location")
.color("var(--periwinkle)")
})

View File

@@ -1,3 +1,6 @@
import "./MarketSidebar.js"
import "./MarketGrid.js"
css(`
market- {
font-family: 'Bona';
@@ -24,73 +27,27 @@ css(`
class Market extends Shadow {
listings = [
{
title: "Shield Lapel Pin",
stars: "5",
reviews: 1,
price: "$12",
company: "Hyperia",
type: "new",
image: "/db/images/1"
}
]
render() {
ZStack(() => {
HStack(() => {
VStack(() => {
MarketSidebar()
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "hyperia-check"
})
label("Hyperia-Made")
.attr({
"for": "hyperia-check"
})
.marginLeft(0.5, em)
})
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "america-check"
})
label("America-Made")
.attr({
"for": "america-check"
})
.marginLeft(0.5, em)
})
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "new-check"
})
label("New")
.attr({
"for": "new-check"
})
.marginLeft(0.5, em)
})
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "used-check"
})
label("Used")
.attr({
"for": "used-check"
})
.marginLeft(0.5, em)
})
})
.paddingLeft(3, em)
.gap(1, em)
MarketGrid(this.listings)
})
.width(100, "%")
.x(0).y(25, vh)
p("0 Items")
.position("absolute")
.x(50, vw).y(50, vh)
.transform("translate(-50%, -50%)")
.x(0).y(13, vh)
HStack(() => {
input("Search for products...", "45vw")

View File

@@ -0,0 +1,100 @@
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(--periwinkle)")
if (this.listings.length > 0) {
ZStack(() => {
for (let i = 0; i < this.listings.length; i++) {
const rating = this.listings[i].stars
const percent = (rating / 5)
VStack(() => {
img(this.listings[i].image)
.marginBottom(0.5, em)
p(this.listings[i].company)
.marginBottom(0.5, em)
p(this.listings[i].title)
.fontSize(1.2, em)
.fontWeight("bold")
.marginBottom(0.5, em)
HStack(() => {
p(this.listings[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(this.listings[i].reviews)
.marginLeft(0.2, em)
})
.marginBottom(0.5, em)
p(this.listings[i].price)
.fontSize(1.75, em)
.marginBottom(0.5, em)
button("Buy Now")
})
.padding(1, em)
.border("1px solid var(--periwinkle)")
.borderRadius(5, "px")
}
})
.display("grid")
.gridTemplateColumns("repeat(auto-fill, minmax(250px, 1fr))")
.gap(1, em)
} else {
p("No Listings!")
}
})
.height(100, vh)
.paddingLeft(2, em)
.paddingRight(2, em)
.paddingTop(2, em)
.gap(0, em)
.width(100, "%")
}
}
register(MarketGrid)

View File

@@ -0,0 +1,65 @@
class MarketSidebar extends Shadow {
render() {
VStack(() => {
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "hyperia-check"
})
label("Hyperia-Made")
.attr({
"for": "hyperia-check"
})
.marginLeft(0.5, em)
})
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "america-check"
})
label("America-Made")
.attr({
"for": "america-check"
})
.marginLeft(0.5, em)
})
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "new-check"
})
label("New")
.attr({
"for": "new-check"
})
.marginLeft(0.5, em)
})
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "used-check"
})
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)

View File

@@ -111,7 +111,7 @@ class Messages extends Shadow {
})
button("+ New Message")
.width(15, em)
.width(13, em)
.marginLeft(1, em)
.borderRadius(10, px)
.background("transparent")

View File

@@ -64,12 +64,44 @@ class AppMenu extends Shadow {
.gap(1.5, em)
.paddingRight(2, em)
img("_/images/divider.svg", "40vw")
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()
@@ -95,35 +127,6 @@ class AppMenu extends Shadow {
this.classList.add("minimized")
$("#divider").style.display = "none"
}
connectedCallback() {
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 = ""
this.styleMaximized(target)
window.navigateTo("/")
$("app-window").close()
} else {
this.selected = target.innerText
this.styleMinimized(target)
window.navigateTo("/app/" + target.innerText.toLowerCase())
$("app-window").open(target.innerText)
}
})
}
}
, "app-menu")

View File

@@ -1,6 +1,6 @@
import "../apps/Jobs/Jobs.js"
import "../apps/Messages.js"
import "../apps/Market.js"
import "../apps/Market/Market.js"
class AppWindow extends Shadow {
app;

View File

@@ -8,21 +8,20 @@ class Home extends Shadow {
render() {
ZStack(() => {
img("_/icons/logo.svg", "2.5em")
img("/_/icons/logo.svg", "2.5em")
.position("fixed")
.left("3em")
.top("3vh")
.zIndex(3)
// .onClick(() => {
// window.navigateTo("/")
// this.rerender()
// })
.onClick(() => {
window.navigateTo("/")
})
div()
.width(100, vw)
.height(100, vh)
.margin("0px")
.backgroundImage("url('_/images/the_return.webp')")
.backgroundImage("url('/_/images/the_return.webp')")
.backgroundSize("cover")
.backgroundPosition("48% 65%")
.backgroundRepeat("no-repeat")
@@ -69,7 +68,6 @@ class Home extends Shadow {
}
})
.onNavigate(function () {
console.log("navigate")
if(window.location.pathname === "/") {
this.style.border = "1px solid var(--tan)"
this.style.color = "var(--tan)"