This commit is contained in:
metacryst
2026-01-04 07:58:23 -06:00
parent b50468eb5a
commit 6a435ac11a
122 changed files with 13995 additions and 19 deletions

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View File

@@ -0,0 +1,153 @@
css(`
tasks- {
font-family: 'Bona';
}
tasks- input::placeholder {
font-family: 'Bona Nova';
font-size: 0.9em;
color: var(--accent);
}
input[type="checkbox"] {
appearance: none; /* remove default style */
-webkit-appearance: none;
width: 1em;
height: 1em;
border: 1px solid var(--accent);
}
input[type="checkbox"]:checked {
background-color: var(--red);
}
`)
class Tasks extends Shadow {
projects = [
{
"title": "Blockcatcher",
"tasks": {}
}
]
columns = [
{
"title": "backlog",
"tasks": {}
}
]
render() {
ZStack(() => {
HStack(() => {
VStack(() => {
h3("Projects")
.marginTop(0)
.marginBottom(1, em)
.marginLeft(0.4, em)
if (this.projects.length >= 1) {
for(let i = 0; i < this.projects.length; i++) {
p(this.projects[i].title)
}
} else {
p("No Projects!")
}
})
.height(100, vh)
.paddingLeft(2, em)
.paddingRight(2, em)
.paddingTop(2, em)
.gap(0, em)
.borderRight("0.5px solid var(--accent2)")
HStack(() => {
if (this.columns.length >= 1) {
for(let i = 0; i < this.columns.length; i++) {
p(this.columns[i].name)
}
} else {
p("No Conversations!")
}
})
.height(100, vh)
.paddingLeft(2, em)
.paddingRight(2, em)
.paddingTop(2, em)
.gap(0, em)
.borderRight("0.5px solid var(--accent2)")
})
.width(100, "%")
.x(0).y(13, vh)
.borderTop("0.5px solid var(--accent2)")
p("0 Items")
.position("absolute")
.x(50, vw).y(50, vh)
.transform("translate(-50%, -50%)")
HStack(() => {
input("Search tasks...", "45vw")
.attr({
"type": "text"
})
.fontSize(1.1, em)
.paddingLeft(1.3, em)
.background("transparent")
.border("0.5px solid var(--accent2)")
.outline("none")
.color("var(--accent)")
.borderRadius(10, px)
button("Search")
.marginLeft(2, em)
.borderRadius(10, px)
.background("transparent")
.border("0.5px solid var(--accent2)")
.color("var(--accent)")
.fontFamily("Bona Nova")
.onHover(function (hovering) {
if(hovering) {
this.style.background = "var(--green)"
} else {
this.style.background = "transparent"
}
})
button("+ New Task")
.width(9, em)
.marginLeft(1, em)
.borderRadius(10, px)
.background("transparent")
.border("0.5px solid var(--accent2)")
.color("var(--accent)")
.fontFamily("Bona Nova")
.onHover(function (hovering) {
if(hovering) {
this.style.background = "var(--green)"
} else {
this.style.background = "transparent"
}
})
.onClick((clicking) => {
console.log(this, "clicked")
})
})
.x(55, vw).y(4, vh)
.position("absolute")
.transform("translateX(-50%)")
})
.width(100, "%")
.height(100, "%")
}
connectedCallback() {
// Optional additional logic
}
}
register(Tasks)

View File

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

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

@@ -0,0 +1,91 @@
import "./AppWindow.js"
import "./AppMenu.js"
import "./ProfileButton.js"
import "./InputBox.js"
import "./Sidebar.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()
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")
.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")
})
}
}
register(Home)

View File

@@ -0,0 +1,54 @@
css(`
input-box {
display: block;
width: 60vw;
position: fixed;
left: 20vw;
bottom: 2vw;
}
.input {
width: 100%;
background-color: var(--accent2);
opacity: 0.5;
border-radius: 12px;
border: none;
resize: none;
color: var(--orange);
padding: 1em;
height: 1em;
outline: none;
transition: opacity .1s, scale .1s
}
.input:focus {
opacity: 80%;
scale: 1.02
}
`)
export default class InputBox extends HTMLElement {
hovered = false
connectedCallback() {
this.render()
this.addListeners()
}
render() {
this.innerHTML = /* html */`
<textarea class="input"></textarea>
`
}
addListeners() {
this.$("textarea").addEventListener("keydown", (e) => {
if(e.key === "Enter") {
e.preventDefault()
e.target.blur()
}
})
}
}
customElements.define("input-box", InputBox)

View 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)

View 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)

View 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")

View File

@@ -0,0 +1,39 @@
css(`
side-bar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 16vw;
border-right: 0.5px solid var(--accent2);
display: flex;
flex-direction: column;
padding-top: 13vh;
}
side-bar button {
color: var(--darkbrown);
margin: 1.5em;
background-color: color-mix(in srgb, var(--accent2) 35%, var(--orange) 65%);
border: 1px solid var(--orange);
border-radius: 12px;
padding: 0.5em;
font-weight: bold;
}
`)
class Sidebar extends HTMLElement {
connectedCallback() {
this.render()
}
render() {
this.innerHTML = /* html */ `
<span id="title" style="position: absolute; left: 50%; transform: translateX(-50%) " class="link" onclick='window.location.href="/"'>hyperia</span>
<button>Main</button>
`
}
}
customElements.define("side-bar", Sidebar)

14
ui/desktop/index.html Normal file
View 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/desktop/index.js Normal file
View 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
View 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();
}
}

View 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
View 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
}));
}
}