Apps load from server

- Deleted /Jobs, /Announcements, /Events, /People
- Commented out Forum and ForumPanel
- Deleted /components/SearchBar, /components/LoadingCircle, /components/AddButton
This commit is contained in:
2026-03-24 14:51:44 -04:00
parent f3aceb69af
commit 124066da59
23 changed files with 254 additions and 2179 deletions

View File

@@ -1 +1 @@
VITE_API_URL=https://frm.so
VITE_API_URL=

View File

@@ -1,90 +0,0 @@
import util from "../../util"
import server from "../../_/code/bridge/server.js"
css(`
announcement- p {
font-size: 0.85em;
color: var(--darktext);
}
`)
class Announcement extends Shadow {
constructor(announcement) {
super()
this.announcement = announcement
}
render() {
VStack(() => {
HStack(() => {
h3(this.announcement.message)
.color("var(--text)")
.fontSize(1.3, em)
.fontWeight("normal")
.margin(0, em)
// Delete button
// if (this.announcement.creator_id === global.profile.id) {
// img(util.cssVariable("trash-src"), "1.5em")
// .marginRight(0.5, em)
// .onTap(() => {
// this.deleteAnnouncement(this.announcement)
// })
// }
})
.justifyContent("space-between")
.verticalAlign("center")
p(this.announcement.author ?? "Unknown author")
.marginTop(0.75, em)
p(this.convertDate(this.announcement.created) ?? "No date included")
.marginTop(0.25, em)
})
.paddingVertical(1.5, em)
.paddingHorizontal(3.5, em)
.marginHorizontal(1, em)
.borderRadius(10, px)
.background("var(--darkaccent)")
.border("1px solid var(--accent)")
.boxSizing("border-box")
}
async deleteAnnouncement(announcement) {
const result = await server.deleteAnnouncement(announcement.id, announcement.network_id, global.profile.id)
if (result.data === null) {
console.log("Failed to delete announcement")
}
}
convertDate(rawDate) {
const parsed = new Date(rawDate);
if (isNaN(parsed.getTime())) return rawDate;
const month = parsed.toLocaleString("en-US", { month: "long", timeZone: "UTC" });
const day = parsed.getUTCDate();
const year = parsed.getUTCFullYear();
const hours24 = parsed.getUTCHours();
const minutes = parsed.getUTCMinutes();
const hours12 = hours24 % 12 || 12;
const ampm = hours24 >= 12 ? "PM" : "AM";
const paddedMinutes = String(minutes).padStart(2, "0");
const ordinal = (n) => {
const mod100 = n % 100;
if (mod100 >= 11 && mod100 <= 13) return `${n}th`;
switch (n % 10) {
case 1: return `${n}st`;
case 2: return `${n}nd`;
case 3: return `${n}rd`;
default: return `${n}th`;
}
};
return `${month} ${ordinal(day)}, ${year} at ${hours12}:${paddedMinutes} ${ampm}`;
}
}
register(Announcement)

View File

@@ -1,271 +0,0 @@
import './Announcement.js'
import server from '../../_/code/bridge/server.js'
import '../../components/SearchBar.js'
css(`
announcements- {
font-family: 'Arial';
scrollbar-width: none;
-ms-overflow-style: none;
}
announcements- h1 {
font-family: 'Bona';
}
announcements- .VStack::-webkit-scrollbar {
display: none;
width: 0px;
height: 0px;
}
announcements- .VStack::-webkit-scrollbar-thumb {
background: transparent;
}
announcements- .VStack::-webkit-scrollbar-track {
background: transparent;
}
`)
class Announcements extends Shadow {
static searchableKeys = ['message', 'author'];
constructor() {
super()
this.announcements = global.currentNetwork.data.announcements.sort((a, b) => new Date(b.created) - new Date(a.created));
this.searchedAnnouncements = [];
this.searchText = "";
}
render() {
ZStack(() => {
VStack(() => {
SearchBar(this.searchText, "90vw")
VStack(() => {
if (!this.announcements || this.announcements == []) {
LoadingCircle()
} else if (this.searchText) {
if (this.searchedAnnouncements.length > 0) {
for (let i = 0; i < this.searchedAnnouncements.length; i++) {
AnnouncementCard(this.searchedAnnouncements[i])
}
} else {
h2("Could not find any announcements with your search criteria.")
.color("var(--divider)")
.fontWeight("bold")
.marginTop(7.5, em)
.marginBottom(0.5, em)
.textAlign("center")
}
} else if (this.announcements.length > 0) {
for (let i = 0; i < this.announcements.length; i++) {
AnnouncementCard(this.announcements[i])
}
} else {
h2("No Announcements")
.color("var(--divider)")
.fontWeight("bold")
.marginTop(7.5, em)
.marginBottom(0.5, em)
.textAlign("center")
}
})
.overflowY("scroll")
.gap(0.75, em)
if(global.currentNetwork.permissions.includes("announcements.add")) {
HStack(() => {
input("Image Upload", "0px", "0px")
.attr({ name: "image-upload", type: "file" })
.display("none")
.visibility("hidden")
.onChange((e) => {
const file = e.target.files[0]
if (!file) return
this.stagedImage = file
this.stagedImageURL = URL.createObjectURL(file)
this.rerender()
})
div("+")
.width(3, rem)
.height(3, rem)
.borderRadius(50, pct)
.border("1px solid color-mix(in srgb, var(--accent) 60%, transparent)")
.fontSize(2, em)
.transform("rotate(180deg)")
.zIndex(1001)
.display("flex")
.alignItems("center")
.justifyContent("center")
.transition("scale .2s")
.state("touched", function (touched) {
if(touched) {
this.scale("1.5")
this.color("var(--darkaccent)")
this.backgroundColor("var(--divider)")
} else {
this.scale("")
this.color("var(--divider)")
this.backgroundColor("var(--searchbackground)")
}
})
.onTouch(function (start) {
if(start) {
this.attr({touched: "true"})
} else {
this.attr({touched: ""})
}
})
.onClick((done) => {
if(done) {
const inputSelector = this.$('[name="image-upload"]');
inputSelector.click()
}
})
input("Add an Announcement")
.flex("1 1 auto")
.minWidth(0)
.color("var(--text)")
.background("var(--searchbackground)")
.paddingVertical(0, rem)
.fontSize(1, rem)
.paddingHorizontal(1, rem)
.borderRadius(100, px)
.border("1px solid color-mix(in srgb, var(--accent) 60%, transparent)")
.onTouch(function (start) {
if (start) {
this.style.backgroundColor = "var(--accent)"
} else {
setTimeout(() => {
$("appmenu-").display("none")
this.focus()
}, 20)
console.log($("appmenu-"))
this.style.backgroundColor = "var(--searchbackground)"
}
})
.addEventListener("blur", () => {
setTimeout(() => {
$("appmenu-").display("grid")
}, 20)
})
})
.width(100, pct)
.boxSizing("border-box")
.position("absolute")
.paddingHorizontal(1, rem)
.bottom(1, vh)
.gap(0.5, rem)
}
})
.boxSizing("border-box")
.height(100, pct)
.width(100, pct)
.onEvent("announcementsearch", this.onAnnouncementSearch)
.onEvent("new-announcement", this.onNewAnnouncement)
.onEvent("deleted-announcement", this.onDeletedAnnouncement)
.onEvent("edited-announcement", this.onEditedAnnouncement)
})
}
async handleUpload(file) {
try {
const body = new FormData();
body.append('image', file);
const res = await util.authFetch(`${util.HOST}/profile/upload-image`, {
method: "POST",
credentials: "include",
headers: {
"Accept": "application/json"
},
body: body
});
if(res.status === 401) {
return res.status
}
if (!res.ok) return res.status;
const data = await res.json()
global.profile = data.member
console.log(global.profile)
} catch (err) { // Network error / Error reaching server
console.error(err);
}
}
addPhoto() {
console.log("hey")
}
onNewAnnouncement = (e) => {
let newAnnouncement = e.detail.announcement;
this.announcements.push(newAnnouncement)
this.announcements.sort((a, b) => new Date(b.created) - new Date(a.created));
this.rerender()
}
onDeletedAnnouncement = (e) => {
let deletedId = e.detail.id
const i = this.announcements.findIndex(ann => ann.id === deletedId)
if (i !== -1) this.announcements.splice(i, 1);
this.rerender()
}
onEditedAnnouncement = (e) => {
let editedAnnouncement = e.detail
const i = this.announcements.findIndex(ann => ann.id === editedAnnouncement.id)
if (i !== -1) {
this.announcements.splice(i, 1)
this.announcements.unshift(editedAnnouncement)
}
this.rerender()
}
onAnnouncementSearch = (e) => {
let searchText = e.detail.searchText.toLowerCase().trim();
if (!searchText) {
this.searchedAnnouncements = [];
} else {
this.searchedAnnouncements = this.announcements.filter(announcement =>
Announcements.searchableKeys.some(key =>
String(announcement[key]).toLowerCase().includes(searchText)
)
);
}
this.searchText = searchText
this.rerender()
}
async getAnnouncements(networkId) {
const fetchedAnnouncements = await server.getAnnouncements(networkId)
if (this.checkForUpdates(this.announcements, fetchedAnnouncements.data)) {
this.announcements = fetchedAnnouncements.data.sort((a, b) => new Date(b.created) - new Date(a.created));
global.currentNetwork.data.announcements = this.announcements
this.rerender()
}
}
connectedCallback() {
this.getAnnouncements(global.currentNetwork.id)
}
checkForUpdates(currentAnnouncements, fetchedAnnouncements) {
if (currentAnnouncements.length !== fetchedAnnouncements.length) return true;
const currentMap = new Map(currentAnnouncements.map(ann => [ann.id, ann]));
for (const fetchedAnn of fetchedAnnouncements) {
const currentAnn = currentMap.get(fetchedAnn.id);
if (!currentAnn) return true;
if (currentAnn.updated_at !== fetchedAnn.updated_at) return true;
}
return false;
}
}
register(Announcements)

View File

@@ -1,150 +0,0 @@
import './Panel.js'
import server from '../../_/code/bridge/serverFunctions.js'
css(`
announcements- {
font-family: 'Bona';
}
announcements- input::placeholder {
font-family: 'Bona Nova';
font-size: 0.9em;
color: var(--accent);
}
input::placeholder {
font-family: Arial;
}
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 Announcements extends Shadow {
announcements;
constructor() {
super()
this.announcements = global.currentNetwork.data.announcements.sort((a, b) => new Date(b.created) - new Date(a.created));
console.log(this.announcements)
}
render() {
ZStack(() => {
VStack(() => {
Panel(this.announcements)
input("Message", "70%")
.paddingVertical(0.75, em)
.boxSizing("border-box")
.paddingHorizontal(2, em)
.color("var(--text)")
.background("var(--searchbackground)")
.marginBottom(1, em)
.border("0.5px solid var(--accent)")
.outline("none")
.borderRadius(100, px)
.fontFamily("Arial")
.fontSize(1, em)
.onKeyDown(async function(e) {
if (e.key === "Enter") {
const result = await server.addAnnouncement(this.value, global.currentNetwork.id, global.profile.id)
if (result.data.status === 200) {
window.dispatchEvent(new CustomEvent('new-announcement', {
detail: { announcement: result.data.announcement }
}));
} else {
// error
}
this.value = ""
}
})
})
.gap(1, em)
.boxSizing("border-box")
.width(100, pct)
.height(100, pct)
.horizontalAlign("center")
.verticalAlign("end")
.minHeight(0)
})
.backgroundColor("var(--main)")
.boxSizing("border-box")
.paddingVertical(1, em)
.width(100, pct)
.height(100, pct)
.flex("1 1 auto")
.onEvent("new-announcement", this.onNewAnnouncement)
.onEvent("deleted-announcement", this.onDeletedAnnouncement)
.onEvent("edited-announcement", this.onEditedAnnouncement)
}
connectedCallback() {
this.getAnnouncements(global.currentNetwork.id)
}
checkForUpdates(currentAnnouncements, fetchedAnnouncements) {
if (currentAnnouncements.length !== fetchedAnnouncements.length) return true;
const currentMap = new Map(currentAnnouncements.map(ann => [ann.id, ann]));
for (const fetchedAnn of fetchedAnnouncements) {
const currentAnn = currentMap.get(fetchedAnn.id);
// new event added
if (!currentAnn) return true;
// existing event changed
if (currentAnn.updated_at !== fetchedAnn.updated_at) {
return true;
}
}
return false;
}
async getAnnouncements(networkId) {
const fetchedAnnouncements = await server.getAnnouncements(networkId)
if (this.checkForUpdates(this.announcements, fetchedAnnouncements.data)) {
console.log("found updates")
this.announcements = fetchedAnnouncements.data.sort((a, b) => new Date(b.created) - new Date(a.created));
global.currentNetwork.data.announcements = this.announcements
this.rerender()
}
}
onNewAnnouncement = (e) => {
let newAnnouncement = e.detail.announcement;
this.announcements.push(newAnnouncement)
this.announcements.sort((a, b) => new Date(b.created) - new Date(a.created));
this.rerender()
}
onDeletedAnnouncement = (e) => {
let deletedId = e.detail.id
const i = this.announcements.findIndex(ann => ann.id === deletedId)
if (i !== -1) this.announcements.splice(i, 1);
this.rerender()
}
onEditedAnnouncement = (e) => {
let editedAnnouncement = e.detail
const i = this.announcements.findIndex(ann => ann.id === editedPost.id)
if (i !== -1) {
this.announcements.splice(i, 1)
this.announcements.unshift(editedAnnouncement)
}
this.rerender()
}
}
register(Announcements)

View File

@@ -1,151 +0,0 @@
import "../../components/LoadingCircle.js"
import server from "../../_/code/bridge/server.js"
css(`
panel- {
scrollbar-width: none;
-ms-overflow-style: none;
}
panel-::-webkit-scrollbar {
display: none;
width: 0px;
height: 0px;
}
panel-::-webkit-scrollbar-thumb {
background: transparent;
}
panel-::-webkit-scrollbar-track {
background: transparent;
}
`)
class Panel extends Shadow {
announcements = []
isSending = false
constructor(announcements) {
super()
this.announcements = announcements
}
render() {
VStack(() => {
if(this.announcements.length > 0) {
let previousDate = null
for(let i=0; i<this.announcements.length; i++) {
let announcement = this.announcements[i]
const isMe = announcement.creator_id === global.profile.id
const dateParts = this.parseDate(announcement.created);
const { date, time } = dateParts;
if (previousDate !== date) {
previousDate = date;
p(date)
.textAlign("center")
.opacity(0.6)
.fontWeight("bold")
.paddingTop(1, em)
.paddingBottom(0.5, em)
.color("var(--quillred)")
.borderTop(`1px solid var(--${i == 0 ? "transparent" : "divider"})`)
}
VStack(() => {
HStack(() => {
h3(isMe ? "Me" : this.getAuthorName(announcement))
.color(isMe ? "var(--quillred)" : "var(--headertext")
.opacity(0.75)
.margin(0)
h3(`${date} ${time}`)
.opacity(0.5)
.color("var(--headertext)")
.margin(0)
.marginLeft(0.5, em)
.fontSize(1, em)
if (announcement.created !== announcement.updated_at) {
p("(edited)")
.color("var(--headertext)")
.letterSpacing(0.8, "px")
.opacity(0.5)
.fontWeight("bold")
.paddingLeft(0.25, em)
.fontSize(0.9, em)
}
})
.verticalAlign("center")
.marginBottom(0.1, em)
p(announcement.text)
.color("var(--text)")
.marginHorizontal(0.2, em)
.paddingVertical(0.2, em)
.boxSizing("border-box")
})
.marginBottom(0.05, em)
}
} else {
LoadingCircle()
}
})
.gap(1, em)
.fontSize(1.1, em)
.boxSizing("border-box")
.flex("1 1 auto")
.minHeight(0)
.overflowY("auto")
.width(100, pct)
.paddingBottom(2, em)
.paddingHorizontal(4, pct)
.backgroundColor("var(--main)")
.onAppear(async () => {
requestAnimationFrame(() => {
this.scrollTo({ top: 0, behavior: "smooth" });
});
})
}
getAuthorName(announcement) {
const members = global.currentNetwork.data.members;
const creator = members.find(m => m.id === announcement.creator_id);
if (creator) {
return `${creator.first_name} ${creator.last_name}`
} else {
return "No name"
}
}
parseDate(str) {
// Format: YYYY-MM-DDTHH:MM:SS.mmmZ
const match = str.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):\d{2}\.\d+Z$/);
if (!match) return null;
const [, yyyy, mm, dd, hh, min] = match;
// Convert 24h to 12h
const hour24 = parseInt(hh, 10);
const ampm = hour24 >= 12 ? 'pm' : 'am';
const hour12 = hour24 % 12 || 12;
const date = `${mm}/${dd}/${yyyy}`;
const time = `${hour12}:${min}${ampm}`;
return { date, time };
}
formatTime(str) {
const match = str.match(/-(\d+:\d+):\d+.*(am|pm)/i);
if (!match) return null;
const [_, hourMin, ampm] = match;
return hourMin + ampm.toLowerCase();
}
}
register(Panel)

View File

@@ -1,92 +0,0 @@
import util from "../../util"
import server from "../../_/code/bridge/server.js"
css(`
eventcard- p {
font-size: 0.85em;
color: var(--darktext);
}
`)
class EventCard extends Shadow {
constructor(event) {
super()
this.event = event
}
render() {
VStack(() => {
HStack(() => {
h3(this.event.title)
.color("var(--text)")
.fontSize(1.3, em)
.fontWeight("normal")
.margin(0, em)
// Delete button
// if (this.event.creator_id === global.profile.id) {
// img(util.cssVariable("trash-src"), "1.5em")
// .marginRight(0.5, em)
// .onTap(() => {
// this.deleteEvent(this.event)
// })
// }
})
.justifyContent("space-between")
.verticalAlign("center")
p(this.event.location ?? "No location added")
.marginTop(0.75, em)
p(this.convertDate(this.event.time_start) ?? "No time included")
.marginTop(0.25, em)
p(this.event.description ?? "No description included")
.marginTop(0.75, em)
})
.paddingVertical(1.5, em)
.paddingHorizontal(3.5, em)
.marginHorizontal(1, em)
.borderRadius(10, px)
.background("var(--darkaccent)")
.border("1px solid var(--accent)")
.boxSizing("border-box")
}
async deleteEvent(event) {
const result = await server.deleteEvent(event.id, event.network_id, global.profile.id)
if (result.data === null) {
console.log("Failed to delete event")
}
}
convertDate(rawDate) {
const parsed = new Date(rawDate);
if (isNaN(parsed.getTime())) return rawDate;
const month = parsed.toLocaleString("en-US", { month: "long", timeZone: "UTC" });
const day = parsed.getUTCDate();
const year = parsed.getUTCFullYear();
const hours24 = parsed.getUTCHours();
const minutes = parsed.getUTCMinutes();
const hours12 = hours24 % 12 || 12;
const ampm = hours24 >= 12 ? "PM" : "AM";
const paddedMinutes = String(minutes).padStart(2, "0");
const ordinal = (n) => {
const mod100 = n % 100;
if (mod100 >= 11 && mod100 <= 13) return `${n}th`;
switch (n % 10) {
case 1: return `${n}st`;
case 2: return `${n}nd`;
case 3: return `${n}rd`;
default: return `${n}th`;
}
};
return `${month} ${ordinal(day)}, ${year} at ${hours12}:${paddedMinutes} ${ampm}`;
}
}
register(EventCard)

View File

@@ -1,175 +0,0 @@
import server from "../../_/code/bridge/server"
class EventForm extends Shadow {
inputStyles(el) {
return el
.background("var(--main)")
.color("var(--text)")
.border("1px solid var(--accent)")
.fontSize(0.9, rem)
.backgroundColor("var(--darkaccent)")
.borderRadius(12, px)
.outline("none")
.onTouch((start) => {
if (start) {
this.style.backgroundColor = "var(--accent)"
} else {
this.style.backgroundColor = "var(--darkaccent)"
}
})
}
errorMessage = ""
render() {
ZStack(() => {
p("X")
.color("var(--darkred)")
.fontSize(2, em)
.position("absolute")
.fontFamily("Arial")
.marginTop(1, rem)
.marginLeft(1, rem)
.onTap(() => {
this.toggle()
})
form(() => {
VStack(() => {
h1("Create an Event")
.color("var(--text)")
.textAlign("center")
.fontFamily("Arial")
.marginTop(1.5, em)
input("Title", "70%")
.attr({ name: "title", type: "text" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Location", "70%")
.attr({ name: "location", type: "text" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Start Time", "70%")
.attr({ name: "time_start", type: "datetime-local" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Description", "70%")
.attr({ name: "description", type: "text" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
HStack(() => {
button("==>")
.padding(1, em)
.fontSize(0.9, rem)
.borderRadius(12, px)
.background("var(--searchbackground)")
.color("var(--text)")
.border("1px solid var(--accent)")
.boxSizing("border-box")
.onTouch(function (start) {
if (start) {
this.style.backgroundColor = "var(--accent)"
} else {
this.style.backgroundColor = "var(--searchbackground)"
}
})
})
.width(70, vw)
.margin("auto")
.fontSize(0.9, rem)
.paddingLeft(0, em)
.paddingRight(2, em)
.marginVertical(1, em)
.border("1px solid transparent")
p("")
.dynamicText("errormessage", "{{}}")
.margin("auto")
.marginTop(1, em)
.color("var(--text)")
.fontFamily("Arial")
.opacity(.7)
.padding(0.5, em)
.backgroundColor("var(--darkred)")
.width(100, pct)
.textAlign("center")
.boxSizing("border-box")
})
.horizontalAlign("center")
})
.onSubmit((e) => {
e.preventDefault()
const data = {
title: e.target.$('[name="title"]').value,
location: e.target.$('[name="location"]').value,
time_start: e.target.$('[name="time_start"]').value,
description: e.target.$('[name="description"]').value,
};
this.handleSend(data)
})
})
.position("fixed")
.height(window.visualViewport.height - 20, px)
.width(100, pct)
.top(100, vh)
.background("var(--main)")
.zIndex(4)
.borderTopLeftRadius("10px")
.borderTopRightRadius("10px")
.boxSizing("border-box")
.border("1px solid var(--accent)")
.transition("top .3s")
}
async handleSend(eventData) {
if (!eventData.title) {
this.$(".VStack > p")
.attr({ errorMessage: 'Events must include a title.' })
.display("")
return;
} else {
this.$(".VStack > p").style.display = "none"
}
const date = new Date(eventData.time_start);
const timestamp = eventData.time_start ? date.toISOString() : null;
const newEvent = {
title: eventData.title,
location: eventData.location ?? null,
time_start: timestamp ?? null,
description: eventData.description ?? null
}
const { data } = await server.addEvent(newEvent, global.currentNetwork.id, global.profile.id)
if (data.status === 200) {
console.log("Added new event: ", data)
this.toggle()
window.dispatchEvent(new CustomEvent('new-event', {
detail: { event: data.event }
}));
} else {
console.log("Failed to add new event: ", data)
this.$(".VStack > p")
.attr({ errorMessage: data.error })
.display("")
}
}
toggle() {
if(this.style.top === "15vh") {
this.style.top = "100vh"
this.pointerEvents = "none"
} else {
this.style.top = "15vh"
this.pointerEvents = "auto"
}
}
}
register(EventForm)

View File

@@ -1,178 +0,0 @@
import "../../components/TopBar.js"
import "../../components/LoadingCircle.js"
import "./EventCard.js"
import "./EventForm.js"
import server from "../../_/code/bridge/server.js"
import "../../components/SearchBar.js"
css(`
events- {
font-family: 'Arial';
scrollbar-width: none;
-ms-overflow-style: none;
}
events- h1 {
font-family: 'Bona';
}
events- .VStack::-webkit-scrollbar {
display: none;
width: 0px;
height: 0px;
}
events- .VStack::-webkit-scrollbar-thumb {
background: transparent;
}
events- .VStack::-webkit-scrollbar-track {
background: transparent;
}
`)
class Events extends Shadow {
static searchableKeys = ['title', 'description', 'location'];
constructor() {
super()
this.events = global.currentNetwork.data.events;
this.searchedEvents = [];
this.searchText = "";
}
render() {
ZStack(() => {
EventForm()
VStack(() => {
HStack(() => {
SearchBar(this.searchText, "75vw")
AddButton()
})
VStack(() => {
if (!this.events || this.events == []) {
LoadingCircle()
} else if (this.searchText) {
if (this.searchedEvents.length > 0) {
for (let i = 0; i < this.searchedEvents.length; i++) {
EventCard(this.searchedEvents[i])
}
} else {
h2("Could not find any events with your search criteria.")
.color("var(--divider)")
.fontWeight("bold")
.marginTop(7.5, em)
.marginBottom(0.5, em)
.textAlign("center")
}
} else if (this.events.length > 0) {
for (let i = 0; i < this.events.length; i++) {
EventCard(this.events[i])
}
} else {
h2("No Events")
.color("var(--divider)")
.fontWeight("bold")
.marginTop(7.5, em)
.marginBottom(0.5, em)
.textAlign("center")
}
})
.overflowY("scroll")
.gap(0.75, em)
})
.boxSizing("border-box")
.height(100, pct)
.width(100, pct)
.onEvent("eventsearch", this.onEventSearch)
.onEvent("new-event", this.onNewEvent)
})
}
onNewEvent = (e) => {
let newEvent = e.detail.event;
this.events.push(newEvent)
this.rerender()
}
onEventSearch = (e) => {
let searchText = e.detail.searchText.toLowerCase().trim();
if (!searchText) {
this.searchedEvents = [];
} else {
this.searchedEvents = this.events.filter(event =>
Events.searchableKeys.some(key =>
String(event[key]).toLowerCase().includes(searchText)
)
);
}
this.searchText = searchText
this.rerender()
}
async getEvents(networkId) {
const fetchedEvents = await server.getEvents(networkId)
if (this.checkForUpdates(this.events, fetchedEvents.data)) {
this.events = fetchedEvents.data
this.rerender()
}
}
connectedCallback() {
this.getEvents(global.currentNetwork.id)
}
checkForUpdates(currentEvents, fetchedEvents) {
if (currentEvents.length !== fetchedEvents.length) return true;
const currentMap = new Map(currentEvents.map(event => [event.id, event]));
for (const fetchedEvent of fetchedEvents) {
const currentEvent = currentMap.get(fetchedEvent.id);
// new event added
if (!currentEvent) return true;
// existing event changed
if (currentEvent.updated_at !== fetchedEvent.updated_at) {
return true;
}
}
return false;
}
convertDate(rawDate) {
const parsed = new Date(rawDate);
if (isNaN(parsed.getTime())) return rawDate;
const month = parsed.toLocaleString("en-US", { month: "long", timeZone: "UTC" });
const day = parsed.getUTCDate();
const year = parsed.getUTCFullYear();
const hours24 = parsed.getUTCHours();
const minutes = parsed.getUTCMinutes();
const hours12 = hours24 % 12 || 12;
const ampm = hours24 >= 12 ? "PM" : "AM";
const paddedMinutes = String(minutes).padStart(2, "0");
const ordinal = (n) => {
const mod100 = n % 100;
if (mod100 >= 11 && mod100 <= 13) return `${n}th`;
switch (n % 10) {
case 1: return `${n}st`;
case 2: return `${n}nd`;
case 3: return `${n}rd`;
default: return `${n}th`;
}
};
return `${month} ${ordinal(day)}, ${year} at ${hours12}:${paddedMinutes} ${ampm}`;
}
}
register(Events)

View File

@@ -1,83 +1,83 @@
import './ForumPanel.js'
// import './ForumPanel.js'
css(`
forum- {
font-family: 'Bona';
}
// css(`
// forum- {
// font-family: 'Bona';
// }
forum- input::placeholder {
font-family: 'Bona Nova';
font-size: 0.9em;
color: var(--accent);
}
// forum- input::placeholder {
// font-family: 'Bona Nova';
// font-size: 0.9em;
// color: var(--accent);
// }
input::placeholder {
font-family: Arial;
}
// input::placeholder {
// font-family: Arial;
// }
input[type="checkbox"] {
appearance: none; /* remove default style */
-webkit-appearance: none;
width: 1em;
height: 1em;
border: 1px solid 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);
}
`)
// input[type="checkbox"]:checked {
// background-color: var(--red);
// }
// `)
class Forum extends Shadow {
render() {
ZStack(() => {
VStack(() => {
// class Forum extends Shadow {
// render() {
// ZStack(() => {
// VStack(() => {
ForumPanel()
// ForumPanel()
input("Message", "70%")
.paddingVertical(0.75, em)
.boxSizing("border-box")
.paddingHorizontal(2, em)
.color("var(--accent)")
.background("black")
.marginBottom(1, em)
.border("0.5px solid #6f5e4e")
.borderRadius(100, px)
.fontFamily("Arial")
.fontSize(1, em)
.onKeyDown(async function(e) {
if (e.key === "Enter") {
let msg = {
forum: global.currentNetwork.abbreviation,
text: this.value
}
await global.Socket.send({
app: "FORUM",
operation: "SEND",
msg: msg
})
this.value = ""
}
})
})
.gap(0.5, em)
.boxSizing("border-box")
.width(100, pct)
.height(100, pct)
.horizontalAlign("center")
.verticalAlign("end")
.minHeight(0)
})
.backgroundColor("var(--main)")
.boxSizing("border-box")
.paddingVertical(1, em)
.width(100, pct)
.minHeight(0)
.flex("1 1 auto")
}
// input("Message", "70%")
// .paddingVertical(0.75, em)
// .boxSizing("border-box")
// .paddingHorizontal(2, em)
// .color("var(--accent)")
// .background("black")
// .marginBottom(1, em)
// .border("0.5px solid #6f5e4e")
// .borderRadius(100, px)
// .fontFamily("Arial")
// .fontSize(1, em)
// .onKeyDown(async function(e) {
// if (e.key === "Enter") {
// let msg = {
// forum: global.currentNetwork.abbreviation,
// text: this.value
// }
// await global.Socket.send({
// app: "FORUM",
// operation: "SEND",
// msg: msg
// })
// this.value = ""
// }
// })
// })
// .gap(0.5, em)
// .boxSizing("border-box")
// .width(100, pct)
// .height(100, pct)
// .horizontalAlign("center")
// .verticalAlign("end")
// .minHeight(0)
// })
// .backgroundColor("var(--main)")
// .boxSizing("border-box")
// .paddingVertical(1, em)
// .width(100, pct)
// .minHeight(0)
// .flex("1 1 auto")
// }
}
// }
register(Forum)
// register(Forum)

View File

@@ -1,193 +1,193 @@
import "../../components/LoadingCircle.js"
// import "../../components/LoadingCircle.js"
css(`
forumpanel- {
scrollbar-width: none;
-ms-overflow-style: none;
}
// css(`
// forumpanel- {
// scrollbar-width: none;
// -ms-overflow-style: none;
// }
forumpanel-::-webkit-scrollbar {
display: none;
width: 0px;
height: 0px;
}
// forumpanel-::-webkit-scrollbar {
// display: none;
// width: 0px;
// height: 0px;
// }
forumpanel-::-webkit-scrollbar-thumb {
background: transparent;
}
// forumpanel-::-webkit-scrollbar-thumb {
// background: transparent;
// }
forumpanel-::-webkit-scrollbar-track {
background: transparent;
}
`)
// forumpanel-::-webkit-scrollbar-track {
// background: transparent;
// }
// `)
class ForumPanel extends Shadow {
messages = []
isSending = false
// class ForumPanel extends Shadow {
// messages = []
// isSending = false
render() {
VStack(() => {
if(this.messages.length > 0) {
let previousDate = null
// 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 isMe = message.authorId === global.profile.id
const dateParts = this.parseDate(message.time);
const { date, time } = dateParts;
// for(let i=0; i<this.messages.length; i++) {
// let message = this.messages[i]
// const isMe = message.authorId === global.profile.id
// const dateParts = this.parseDate(message.time);
// const { date, time } = dateParts;
if (previousDate !== date) {
previousDate = date;
// if (previousDate !== date) {
// previousDate = date;
p(date)
.textAlign("center")
.opacity(0.6)
.fontWeight("bold")
.paddingTop(1, em)
.paddingBottom(0.5, em)
.color("var(--quillred)")
.borderTop(`1px solid var(--${i == 0 ? "transparent" : "divider"})`)
}
// p(date)
// .textAlign("center")
// .opacity(0.6)
// .fontWeight("bold")
// .paddingTop(1, em)
// .paddingBottom(0.5, em)
// .color("var(--quillred)")
// .borderTop(`1px solid var(--${i == 0 ? "transparent" : "divider"})`)
// }
VStack(() => {
HStack(() => {
h3(isMe ? "Me" : message.sentBy)
.color(isMe ? "var(--quillred)" : "var(--brown")
.margin(0)
// VStack(() => {
// HStack(() => {
// h3(isMe ? "Me" : message.sentBy)
// .color(isMe ? "var(--quillred)" : "var(--brown")
// .margin(0)
h3(`${date} ${time}`)
.opacity(0.5)
.color("var(--brown)")
.margin(0)
.marginLeft(0.5, em)
.fontSize(1, em)
// h3(`${date} ${time}`)
// .opacity(0.5)
// .color("var(--brown)")
// .margin(0)
// .marginLeft(0.5, em)
// .fontSize(1, em)
if (message.edited) {
p("(edited)")
.color("var(--brown)")
.letterSpacing(0.8, "px")
.opacity(0.8)
.fontWeight("bold")
.paddingLeft(0.25, em)
.fontSize(0.9, em)
}
})
.verticalAlign("center")
.marginBottom(0.1, em)
// if (message.edited) {
// p("(edited)")
// .color("var(--brown)")
// .letterSpacing(0.8, "px")
// .opacity(0.8)
// .fontWeight("bold")
// .paddingLeft(0.25, em)
// .fontSize(0.9, em)
// }
// })
// .verticalAlign("center")
// .marginBottom(0.1, em)
p(message.text)
.color("var(--accent)")
.borderLeft("1.5px solid var(--divider)")
.borderBottomLeftRadius("7.5px")
.paddingLeft(0.5, em)
.marginHorizontal(0.2, em)
.paddingVertical(0.2, em)
.boxSizing("border-box")
})
.marginBottom(0.05, em)
.onClick(async (finished, e) => {
if (finished) {
console.log(message.id)
let msg = {
forum: global.currentNetwork.abbreviation,
id: message.id,
text: "EDITED TEXT TEST!"
}
await global.Socket.send({
app: "FORUM",
operation: "PUT",
msg: msg
})
}
})
}
} else {
LoadingCircle()
}
})
.gap(1, em)
.fontSize(1.1, em)
.boxSizing("border-box")
.flex("1 1 auto")
.minHeight(0)
.overflowY("auto")
.width(100, pct)
.paddingBottom(2, em)
.paddingHorizontal(4, pct)
.backgroundColor("var(--main)")
.onAppear(async () => {
requestAnimationFrame(() => {
this.scrollTo({ top: 0, behavior: "smooth" });
});
if (!this.isSending) {
this.isSending = true
let res = await global.Socket.send({
app: "FORUM",
operation: "GET",
msg: {
forum: global.currentNetwork.abbreviation,
by: "network",
authorId: -999 // default
}
})
if(!res) console.error("failed to get messages")
if(res.msg.length > 0 && this.messages.length === 0) {
this.messages = res.msg.reverse()
this.rerender()
}
this.isSending = false
}
})
.onEvent("new-post", this.onNewPost)
.onEvent("deleted-post", this.onDeletedPost)
.onEvent("edited-post", this.onEditedPost)
}
// p(message.text)
// .color("var(--accent)")
// .borderLeft("1.5px solid var(--divider)")
// .borderBottomLeftRadius("7.5px")
// .paddingLeft(0.5, em)
// .marginHorizontal(0.2, em)
// .paddingVertical(0.2, em)
// .boxSizing("border-box")
// })
// .marginBottom(0.05, em)
// .onClick(async (finished, e) => {
// if (finished) {
// console.log(message.id)
// let msg = {
// forum: global.currentNetwork.abbreviation,
// id: message.id,
// text: "EDITED TEXT TEST!"
// }
// await global.Socket.send({
// app: "FORUM",
// operation: "PUT",
// msg: msg
// })
// }
// })
// }
// } else {
// LoadingCircle()
// }
// })
// .gap(1, em)
// .fontSize(1.1, em)
// .boxSizing("border-box")
// .flex("1 1 auto")
// .minHeight(0)
// .overflowY("auto")
// .width(100, pct)
// .paddingBottom(2, em)
// .paddingHorizontal(4, pct)
// .backgroundColor("var(--main)")
// .onAppear(async () => {
// requestAnimationFrame(() => {
// this.scrollTo({ top: 0, behavior: "smooth" });
// });
// if (!this.isSending) {
// this.isSending = true
// let res = await global.Socket.send({
// app: "FORUM",
// operation: "GET",
// msg: {
// forum: global.currentNetwork.abbreviation,
// by: "network",
// authorId: -999 // default
// }
// })
// if(!res) console.error("failed to get messages")
// if(res.msg.length > 0 && this.messages.length === 0) {
// this.messages = res.msg.reverse()
// this.rerender()
// }
// this.isSending = false
// }
// })
// .onEvent("new-post", this.onNewPost)
// .onEvent("deleted-post", this.onDeletedPost)
// .onEvent("edited-post", this.onEditedPost)
// }
onNewPost = (e) => {
let newPost = e.detail
if (this.messages && !this.messages.some(post => post.id === newPost.id)) {
this.messages.unshift(newPost)
this.rerender()
}
}
// onNewPost = (e) => {
// let newPost = e.detail
// if (this.messages && !this.messages.some(post => post.id === newPost.id)) {
// this.messages.unshift(newPost)
// this.rerender()
// }
// }
onDeletedPost = (e) => {
let deletedId = e.detail
const i = this.messages.findIndex(post => post.id === deletedId)
if (i !== -1) this.messages.splice(i, 1);
this.rerender()
}
// onDeletedPost = (e) => {
// let deletedId = e.detail
// const i = this.messages.findIndex(post => post.id === deletedId)
// if (i !== -1) this.messages.splice(i, 1);
// this.rerender()
// }
onEditedPost = (e) => {
let editedPost = e.detail
const i = this.messages.findIndex(post => post.id === editedPost.id)
if (i !== -1) {
this.messages.splice(i, 1)
this.messages.unshift(editedPost)
}
// onEditedPost = (e) => {
// let editedPost = e.detail
// const i = this.messages.findIndex(post => post.id === editedPost.id)
// if (i !== -1) {
// this.messages.splice(i, 1)
// this.messages.unshift(editedPost)
// }
this.rerender()
}
// 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;
// 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()}`;
// const [, mm, dd, yyyy, hh, min, ampm] = match;
// const date = `${mm}/${dd}/${yyyy}`;
// const time = `${hh}:${min}${ampm.toLowerCase()}`;
return { date, time };
}
// return { date, time };
// }
formatTime(str) {
const match = str.match(/-(\d+:\d+):\d+.*(am|pm)/i);
if (!match) return null;
// formatTime(str) {
// const match = str.match(/-(\d+:\d+):\d+.*(am|pm)/i);
// if (!match) return null;
const [_, hourMin, ampm] = match;
return hourMin + ampm.toLowerCase();
}
}
// const [_, hourMin, ampm] = match;
// return hourMin + ampm.toLowerCase();
// }
// }
register(ForumPanel)
// register(ForumPanel)

View File

@@ -1,75 +0,0 @@
import util from "../../util.js"
import server from "../../_/code/bridge/server.js"
css(`
jobcard- p {
font-size: 0.85em;
color: var(--darktext);
}
`)
class JobCard extends Shadow {
constructor(job) {
super()
this.job = job
}
render() {
VStack(() => {
HStack(() => {
h3(this.job.title)
.color("var(--text)")
.fontSize(1.3, em)
.fontWeight("normal")
.margin(0, em)
// Delete button
// if (this.job.creator_id === global.profile.id) {
// img(util.cssVariable("trash-src"), "1.5em")
// .marginRight(0.5, em)
// .onTap(() => {
// this.deleteJob(this.job)
// })
// }
})
.justifyContent("space-between")
.verticalAlign("center")
p(this.job.company ?? "No company added")
.marginTop(0.75, em)
p(this.job.location ?? "No location added")
.marginTop(0.25, em)
p(this.salary_number ? this.salaryLabel(this.job.salary_number, this.job.salary_period) : "No salary added")
.marginTop(0.75, em)
})
.paddingVertical(1.5, em)
.paddingHorizontal(3.5, em)
.marginHorizontal(1, em)
.borderRadius(10, px)
.background("var(--darkaccent)")
.border("1px solid var(--accent)")
.boxSizing("border-box")
}
salaryLabel(number, period) {
const formattedNumber = new Intl.NumberFormat('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(Number(number));
if (period === "one-time") {
return `One-time payment of $${formattedNumber}`
} else {
return `$${formattedNumber}/${period}`
}
}
async deleteJob(job) {
const result = await server.deleteJob(job.id, job.network_id, global.profile.id)
if (result.data === null) {
console.log("Failed to delete job")
}
}
}
register(JobCard)

View File

@@ -1,204 +0,0 @@
import server from "../../_/code/bridge/server"
class JobForm extends Shadow {
inputStyles(el) {
return el
.background("var(--main)")
.color("var(--text)")
.border("1px solid var(--accent)")
.fontSize(0.9, rem)
.backgroundColor("var(--darkaccent)")
.borderRadius(12, px)
.outline("none")
.onTouch((start) => {
if (start) {
this.style.backgroundColor = "var(--accent)"
} else {
this.style.backgroundColor = "var(--darkaccent)"
}
})
}
errorMessage = ""
render() {
ZStack(() => {
p("X")
.color("var(--darkred)")
.fontSize(2, em)
.position("absolute")
.fontFamily("Arial")
.marginTop(1, rem)
.marginLeft(1, rem)
.onTap(() => {
this.toggle()
})
form(() => {
VStack(() => {
h1("Create a Job")
.color("var(--text)")
.textAlign("center")
.fontFamily("Arial")
.marginTop(1.5, em)
input("Title", "70%")
.attr({ name: "title", type: "text" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Location", "70%")
.attr({ name: "location", type: "text" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Company", "70%")
.attr({ name: "company", type: "text" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
HStack(() => {
input("Salary", "30%")
.attr({ name: "salary_number", type: "number", min: "0", step: "0.01" })
.padding(1, em)
.marginHorizontal(1, em)
.styles(this.inputStyles)
select(() => {
option("One-time")
.attr({ value: "one-time"})
option("Hourly")
.attr({ value: "hour"})
option("Monthly")
.attr({ value: "month"})
option("Yearly")
.attr({ value: "year"})
})
.attr({ name: "salary_period" })
.width(40, pct)
.padding(1, em)
.marginHorizontal(1, em)
.styles(this.inputStyles)
})
.margin(1, em)
.boxSizing("border-box")
.verticalAlign("center")
.horizontalAlign("center")
input("Description", "70%")
.attr({ name: "description", type: "text" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
HStack(() => {
button("==>")
.padding(1, em)
.fontSize(0.9, rem)
.borderRadius(12, px)
.background("var(--searchbackground)")
.color("var(--text)")
.border("1px solid var(--accent)")
.boxSizing("border-box")
.onTouch(function (start) {
if (start) {
this.style.backgroundColor = "var(--accent)"
} else {
this.style.backgroundColor = "var(--searchbackground)"
}
})
})
.width(70, vw)
.margin("auto")
.fontSize(0.9, rem)
.paddingLeft(0, em)
.paddingRight(2, em)
.marginVertical(1, em)
.border("1px solid transparent")
p("")
.dynamicText("errormessage", "{{}}")
.margin("auto")
.marginTop(1, em)
.color("var(--text)")
.fontFamily("Arial")
.opacity(.7)
.padding(0.5, em)
.backgroundColor("var(--darkred)")
.width(100, pct)
.textAlign("center")
.boxSizing("border-box")
})
.horizontalAlign("center")
})
.onSubmit((e) => {
e.preventDefault()
const data = {
title: e.target.$('[name="title"]').value,
location: e.target.$('[name="location"]').value,
company: e.target.$('[name="company"]').value,
salary_number: e.target.$('[name="salary_number"]').value,
salary_period: e.target.$('[name="salary_period"]').value,
description: e.target.$('[name="description"]').value,
};
this.handleSend(data)
})
})
.position("fixed")
.height(window.visualViewport.height - 20, px)
.width(100, pct)
.top(100, vh)
.background("var(--main)")
.zIndex(4)
.borderTopLeftRadius("10px")
.borderTopRightRadius("10px")
.boxSizing("border-box")
.border("1px solid var(--accent)")
.transition("top .3s")
}
async handleSend(jobData) {
if (!jobData.title) {
this.$(".VStack > p")
.attr({ errorMessage: 'Jobs must include a title.' })
.display("")
return;
} else {
this.$(".VStack > p").style.display = "none"
}
const newJob = {
title: jobData.title,
location: jobData.location.trim() === '' ? null : jobData.location.trim(),
company: jobData.company.trim() === '' ? null : jobData.company.trim(),
salary_number: jobData.salary_number.trim() === '' ? null : jobData.salary_number,
salary_period: jobData.salary_number.trim() === '' ? null : jobData.salary_period,
description: jobData.description.trim() === '' ? null : jobData.description.trim()
}
const { data } = await server.addJob(newJob, global.currentNetwork.id, global.profile.id)
if (data.status === 200) {
console.log("Added new job: ", data)
this.toggle()
window.dispatchEvent(new CustomEvent('new-job', {
detail: { job: data.job }
}));
} else {
console.log("Failed to add new event: ", data)
this.$(".VStack > p")
.attr({ errorMessage: data.error })
.display("")
}
}
toggle() {
if(this.style.top === "15vh") {
this.style.top = "100vh"
this.pointerEvents = "none"
} else {
this.style.top = "15vh"
this.pointerEvents = "auto"
}
}
}
register(JobForm)

View File

@@ -1,153 +0,0 @@
import "./JobsSidebar.js"
import "./JobsGrid.js"
import "./JobCard.js"
import "./JobForm.js"
import "../../components/SearchBar.js"
import "../../components/AddButton.js"
import server from "../../_/code/bridge/server.js"
css(`
jobs- {
font-family: 'Arial';
scrollbar-width: none;
-ms-overflow-style: none;
}
jobs- h1 {
font-family: 'Bona';
}
jobs- .VStack::-webkit-scrollbar {
display: none;
width: 0px;
height: 0px;
}
jobs- .VStack::-webkit-scrollbar-thumb {
background: transparent;
}
jobs- .VStack::-webkit-scrollbar-track {
background: transparent;
}
`)
class Jobs extends Shadow {
static searchableKeys = ['title', 'description', 'location', 'company', 'salary_period', 'salary_number'];
constructor() {
super()
this.jobs = global.currentNetwork.data.jobs;
this.searchedJobs = [];
this.searchText = ""; // for mainting searchText after re-render
}
render() {
ZStack(() => {
JobForm()
VStack(() => {
HStack(() => {
SearchBar(this.searchText, "75vw")
AddButton()
})
VStack(() => {
if (!this.jobs || this.jobs == []) {
LoadingCircle()
} else if (this.searchText) {
if (this.searchedJobs.length > 0) {
for (let i = 0; i < this.searchedJobs.length; i++) {
JobCard(this.searchedJobs[i])
}
} else {
h2("Could not find any jobs with your search criteria.")
.color("var(--divider)")
.fontWeight("bold")
.marginTop(7.5, em)
.marginBottom(0.5, em)
.textAlign("center")
}
} else if (this.jobs.length > 0) {
for (let i = 0; i < this.jobs.length; i++) {
JobCard(this.jobs[i])
}
} else {
h2("No Jobs")
.color("var(--divider)")
.fontWeight("bold")
.marginTop(7.5, em)
.marginBottom(0.5, em)
.textAlign("center")
}
})
.overflowY("scroll")
.gap(0.75, em)
})
.boxSizing("border-box")
.height(100, pct)
.width(100, pct)
.onEvent("jobsearch", this.onJobSearch)
.onEvent("new-job", this.onNewJob)
})
}
onNewJob = (e) => {
let newJob = e.detail.job;
this.jobs.push(newJob)
this.rerender()
}
onJobSearch = (e) => {
let searchText = e.detail.searchText.toLowerCase().trim();
if (!searchText) {
this.searchedJobs = [];
} else {
this.searchedJobs = this.jobs.filter(job =>
Jobs.searchableKeys.some(key => {
if (key === "salary_number" || key === "salary_period") {
return String(job["salary_number"]).toLowerCase().includes(searchText.replace(/\/.*$/, '').replace(/[,$]/g, '')) && String(job["salary_period"]).toLowerCase().includes(searchText.replace(/^[^/]*\/?/, ''));
} else {
return String(job[key]).toLowerCase().includes(searchText);
}
})
);
}
this.searchText = searchText
this.rerender()
}
async getJobs(networkId) {
const fetchedJobs = await server.getJobs(networkId)
if (this.checkForUpdates(this.jobs, fetchedJobs.data)) {
this.jobs = fetchedJobs
this.rerender()
}
}
connectedCallback() {
this.getJobs(global.currentNetwork.id)
}
checkForUpdates(currentJobs, fetchedJobs) {
if (currentJobs.length !== fetchedJobs.length) return true;
const currentMap = new Map(currentJobs.map(job => [job.id, job]));
for (const fetchedJob of fetchedJobs) {
const currentJob = currentMap.get(fetchedJob.id);
// new job added
if (!currentJob) return true;
// existing job changed
if (currentJob.updated_at !== fetchedJob.updated_at) {
return true;
}
}
return false;
}
}
register(Jobs)

View File

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

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

@@ -1,81 +0,0 @@
import "../../components/TopBar.js"
import "./PeopleCard.js"
css(`
people- {
font-family: 'Arial';
}
people- h1 {
font-family: 'Bona';
}
people- p {
color: var(--darktext);
}
`)
class People extends Shadow {
people = "";
constructor() {
super()
this.people = global.currentNetwork.data.members;
}
render() {
VStack(() => {
VStack(() => {
if (this.people == "") {
LoadingCircle()
} else if (this.people.length > 0) {
for (let i = 0; i < this.people.length; i++) {
PeopleCard(this.people[i])
}
} else {
h2("No Members")
.color("var(--brown)")
.fontWeight("bold")
.marginTop(7.5, em)
.marginBottom(0.5, em)
.textAlign("center")
p("Invite people to this network!")
.textAlign("center")
.color("var(--darkbrown)")
}
})
.overflow("scroll")
.gap(0.8, em)
})
.position("relative")
.boxSizing("border-box")
.height(100, pct)
.width(100, pct)
}
convertDate(rawDate) {
const parsed = new Date(rawDate);
if (isNaN(parsed.getTime())) return rawDate;
const month = parsed.toLocaleString("en-US", { month: "long", timeZone: "UTC" });
const day = parsed.getUTCDate();
const year = parsed.getUTCFullYear();
const ordinal = (n) => {
const mod100 = n % 100;
if (mod100 >= 11 && mod100 <= 13) return `${n}th`;
switch (n % 10) {
case 1: return `${n}st`;
case 2: return `${n}nd`;
case 3: return `${n}rd`;
default: return `${n}th`;
}
};
return `${month} ${ordinal(day)}, ${year}`;
}
}
register(People)

View File

@@ -1,69 +0,0 @@
import util from "../../util"
class PeopleCard extends Shadow {
constructor(person) {
super()
this.person = person
this.imgSrc = null
}
render() {
HStack(() => {
HStack(() => {
if (this.imgSrc && !this.imgSrc.includes("null")) {
img(this.imgSrc, "3em", "3em")
.borderRadius(100, pct)
}
})
.boxSizing("border-box")
.height(3, em)
.width(3, em)
.border("1px solid var(--accent)")
.borderRadius(100, pct)
.background("var(--darkaccent)")
VStack(() => {
p(this.person.first_name + " " + this.person.last_name)
.color("var(--headertext)")
.marginVertical(0, em)
})
.verticalAlign("center")
.horizontalAlign("left")
})
.width(100, pct)
.boxSizing("border-box")
.paddingHorizontal(1.75, em)
.gap(1, em)
}
connectedCallback() {
if(this.person.image_path) {
this.imgSrc = `${util.HOST}${this.person.image_path}`
}
}
convertDate(rawDate) {
const parsed = new Date(rawDate);
if (isNaN(parsed.getTime())) return rawDate;
const month = parsed.toLocaleString("en-US", { month: "long", timeZone: "UTC" });
const day = parsed.getUTCDate();
const year = parsed.getUTCFullYear();
const ordinal = (n) => {
const mod100 = n % 100;
if (mod100 >= 11 && mod100 <= 13) return `${n}th`;
switch (n % 10) {
case 1: return `${n}st`;
case 2: return `${n}nd`;
case 3: return `${n}rd`;
default: return `${n}th`;
}
};
return `${month} ${ordinal(day)}, ${year}`;
}
}
register(PeopleCard)

View File

@@ -1,33 +0,0 @@
class AddButton extends Shadow {
render() {
p("+")
.fontWeight("bolder")
.paddingVertical(0.75, em)
.boxSizing("border-box")
.paddingHorizontal(1, em)
.background("var(--searchbackground)")
.color("var(--accent)")
.marginBottom(1, em)
.border("1px solid var(--accent)")
.borderRadius(15, px)
.onTap(() => {
this.handleAdd()
})
}
handleAdd() {
const app = global.currentApp()
switch (app) {
case "Jobs":
$("jobform-").toggle()
break;
case "Events":
$("eventform-").toggle()
break;
default:
break;
}
}
}
register(AddButton)

View File

@@ -1,40 +1,14 @@
import "../apps/Forum/Forum.js"
import "../apps/Messages/Messages.js"
import "../apps/Jobs/Jobs.js"
import "../apps/People/People.js"
import "../apps/Events/Events.js"
import "../apps/Announcements/Announcements.js"
import util from "../util.js"
class AppWindow extends Shadow {
render() {
ZStack(() => {
let app = global.currentApp()
switch(app) {
case "Dashboard":
Announcements()
break;
case "Jobs":
Jobs()
break;
case "Events":
Events()
break;
case "People":
People()
break;
default:
if(window[app]) {
window[app]()
} else {
this.getCustomApp(app)
}
break;
}
if(window[app]) {
window[app]()
} else {
this.getCustomApp(app)
}
})
.height(100, pct)
.overflowY("scroll")
@@ -44,6 +18,7 @@ class AppWindow extends Shadow {
}
async getCustomApp(app) {
if (app == "Dashboard") { app = "Announcements" }
await import(`${util.HOST}/apps/${app.toLowerCase()}/${app.toLowerCase()}.js`);
this.rerender()
}

View File

@@ -1,5 +1,6 @@
import "./AppWindow.js"
import "../Profile/Profile.js"
import "./TopBar.js"
class AppWindowContainer extends Shadow {
render() {

View File

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

@@ -1,72 +0,0 @@
css(`
searchbar- input::placeholder {
color: #5C504D;
}
`)
class SearchBar extends Shadow {
searchText
width
constructor(searchText, width) {
super()
this.searchText = searchText
this.width = width
}
render() {
form(() => {
input("Search", this.width)
.attr({ name: "searchText", type: "text" })
.attr({ value: this.searchText ? this.searchText : "" })
.paddingVertical(0.75, em)
.boxSizing("border-box")
.paddingHorizontal(1, em)
.background("var(--searchbackground)")
.color("gray")
.marginBottom(1, em)
.marginLeft(1, em)
.marginRight(0.5, em)
.border("1px solid color-mix(in srgb, var(--accent) 60%, transparent)")
.borderRadius(100, px)
.fontFamily("Arial")
.fontSize(1, em)
.cursor("not-allowed")
.onTouch(function (start) {
if (start) {
this.style.backgroundColor = "var(--accent)"
} else {
this.style.backgroundColor = "var(--searchbackground)"
}
})
})
.onSubmit(async (e) => {
e.preventDefault();
const data = new FormData(e.target);
this.dispatchSearchEvent(data.get("searchText"))
})
}
dispatchSearchEvent(searchText) {
const app = global.currentApp();
switch (app) {
case "Jobs":
window.dispatchEvent(new CustomEvent('jobsearch', {
detail: { searchText: searchText }
}));
break;
case "Events":
window.dispatchEvent(new CustomEvent('eventsearch', {
detail: { searchText: searchText }
}));
break;
case "Announcements":
window.dispatchEvent(new CustomEvent('announcementsearch', {
detail: { searchText: searchText }
}));
break;
}
}
}
register(SearchBar)

View File

@@ -43,6 +43,10 @@ export default defineConfig({
"/db": {
target: "http://localhost:10002",
changeOrigin: true
},
"/apps": {
target: "http://localhost:10002",
changeOrigin: true
}
},
host: true,