Add event + add job form

- Modified handlers to catch errors
- Added placeholder "No location added", etc. messages to Job/Event cards
- Added EventForm.js and JobForm.js for adding
- EventForm and JobForm are animated to slide up from bottom
- Modified openProfile/closeProfile logic
- Fixed SidebarItem().onClick() firing twice bug (switched to .onTap)
- Profile is now animated to slide up from the bottom
This commit is contained in:
2026-03-19 15:32:51 -04:00
parent 8dd2312aa0
commit 58589c56dd
11 changed files with 532 additions and 151 deletions

View File

@@ -111,14 +111,16 @@ class Profile extends Shadow {
} }
}) })
}) })
.backgroundColor("var(--main)") .backgroundColor("var(--main)")
.overflowX("hidden") .overflowX("hidden")
.height(window.visualViewport.height - 20, px) .height(window.visualViewport.height - 20, px)
.boxSizing("border-box") .boxSizing("border-box")
.width(100, pct) .width(100, pct)
.position("fixed") .position("fixed")
.top(20, px) .top(100, vh)
.zIndex(1000) .zIndex(5)
.transition("top .3s")
.pointerEvents("none")
} }
convertDate(rawDate) { convertDate(rawDate) {

View File

@@ -4,47 +4,91 @@ const handlers = {
}, },
async addEvent(newEvent, networkId, creatorId) { async addEvent(newEvent, networkId, creatorId) {
return await global.db.events.add(newEvent, networkId, creatorId) try {
return await global.db.events.add(newEvent, networkId, creatorId)
} catch (e) {
return { status: e.status, error: e.message }
}
}, },
async editEvent(id, updatedEvent, networkId, userId) { async editEvent(id, updatedEvent, networkId, userId) {
return await global.db.events.edit(id, updatedEvent, networkId, userId); try {
return await global.db.events.edit(id, updatedEvent, networkId, userId);
} catch (e) {
return { status: e.status, error: e.message }
}
}, },
async deleteEvent(id, networkId, userId) { async deleteEvent(id, networkId, userId) {
return await global.db.events.delete(id, networkId, userId); try {
return await global.db.events.delete(id, networkId, userId);
} catch (e) {
return { status: e.status, error: e.message }
}
}, },
async getEvent(id) { async getEvent(id) {
return global.db.events.getById(id) try {
return global.db.events.getById(id)
} catch (e) {
return { status: e.status, error: e.message }
}
}, },
async getEvents(networkId) { async getEvents(networkId) {
return global.db.events.getByNetwork(networkId) try {
return global.db.events.getByNetwork(networkId)
} catch (e) {
return { status: e.status, error: e.message }
}
}, },
async addJob(newJob, networkId, creatorId) { async addJob(newJob, networkId, creatorId) {
return await global.db.jobs.add(newJob, networkId, creatorId); try {
return await global.db.jobs.add(newJob, networkId, creatorId);
} catch (e) {
return { status: e.status, error: e.message }
}
}, },
async editJob(id, updatedJob, networkId, userId) { async editJob(id, updatedJob, networkId, userId) {
return await global.db.jobs.edit(id, updatedJob, networkId, userId); try {
return await global.db.jobs.edit(id, updatedJob, networkId, userId);
} catch (e) {
return { status: e.status, error: e.message }
}
}, },
async deleteJob(id, networkId, userId) { async deleteJob(id, networkId, userId) {
return await global.db.jobs.delete(id, networkId, userId); try {
return await global.db.jobs.delete(id, networkId, userId);
} catch (e) {
return { status: e.status, error: e.message }
}
}, },
async getJob(id) { async getJob(id) {
return await global.db.jobs.getById(id) try {
return await global.db.jobs.getById(id)
} catch (e) {
return { status: e.status, error: e.message }
}
}, },
async getJobs(networkId) { async getJobs(networkId) {
return global.db.jobs.getByNetwork(networkId) try {
return global.db.jobs.getByNetwork(networkId)
} catch (e) {
return { status: e.status, error: e.message }
}
}, },
async editBio(newBio, userId) { async editBio(newBio, userId) {
return global.db.members.editBio(newBio, userId) try {
return global.db.members.editBio(newBio, userId)
} catch (e) {
return { status: e.status, error: e.message }
}
} }
} }

View File

@@ -35,11 +35,11 @@ class EventCard extends Shadow {
.justifyContent("space-between") .justifyContent("space-between")
.verticalAlign("center") .verticalAlign("center")
p(this.event.location) p(this.event.location ?? "No location added")
.marginTop(0.75, em) .marginTop(0.75, em)
p(this.convertDate(this.event.time_start)) p(this.convertDate(this.event.time_start) ?? "No time included")
.marginTop(0.25, em) .marginTop(0.25, em)
p(this.event.description) p(this.event.description ?? "No description included")
.marginTop(0.75, em) .marginTop(0.75, em)
}) })
.paddingVertical(1.5, em) .paddingVertical(1.5, em)

View File

@@ -0,0 +1,175 @@
import server from "../../_/code/bridge/serverFunctions"
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,6 +1,7 @@
import "../../components/TopBar.js" import "../../components/TopBar.js"
import "../../components/LoadingCircle.js" import "../../components/LoadingCircle.js"
import "./EventCard.js" import "./EventCard.js"
import "./EventForm.js"
import server from "../../_/code/bridge/serverFunctions.js" import server from "../../_/code/bridge/serverFunctions.js"
import "../../components/SearchBar.js" import "../../components/SearchBar.js"
@@ -41,46 +42,57 @@ class Events extends Shadow {
} }
render() { render() {
VStack(() => { ZStack(() => {
SearchBar(this.searchText) EventForm()
VStack(() => { VStack(() => {
if (!this.events || this.events == []) { SearchBar(this.searchText)
LoadingCircle()
} else if (this.searchText) { VStack(() => {
if (this.searchedEvents.length > 0) { if (!this.events || this.events == []) {
for (let i = 0; i < this.searchedEvents.length; i++) { LoadingCircle()
EventCard(this.searchedEvents[i]) } 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 { } else {
h2("Could not find any events with your search criteria.") h2("No Events")
.color("var(--divider)") .color("var(--divider)")
.fontWeight("bold") .fontWeight("bold")
.marginTop(7.5, em) .marginTop(7.5, em)
.marginBottom(0.5, em) .marginBottom(0.5, em)
.textAlign("center") .textAlign("center")
} }
} else if (this.events.length > 0) { })
for (let i = 0; i < this.events.length; i++) { .overflowY("scroll")
EventCard(this.events[i]) .gap(0.75, em)
}
} else {
h2("No Events")
.color("var(--divider)")
.fontWeight("bold")
.marginTop(7.5, em)
.marginBottom(0.5, em)
.textAlign("center")
}
}) })
.overflowY("scroll") .boxSizing("border-box")
.gap(0.75, em) .height(100, pct)
.width(100, pct)
.onEvent("eventsearch", this.onEventSearch)
.onEvent("new-event", this.onNewEvent)
}) })
.boxSizing("border-box") }
.height(100, pct)
.width(100, pct) onNewEvent = (e) => {
.onEvent("eventsearch", this.onEventSearch) let newEvent = e.detail.event;
this.events.push(newEvent)
this.rerender()
} }
onEventSearch = (e) => { onEventSearch = (e) => {

View File

@@ -35,11 +35,11 @@ class JobCard extends Shadow {
.justifyContent("space-between") .justifyContent("space-between")
.verticalAlign("center") .verticalAlign("center")
p(this.job.company) p(this.job.company ?? "No company added")
.marginTop(0.75, em) .marginTop(0.75, em)
p(this.job.location) p(this.job.location ?? "No location added")
.marginTop(0.25, em) .marginTop(0.25, em)
p(this.salaryLabel(this.job.salary_number, this.job.salary_period)) p(this.salary_number ? this.salaryLabel(this.job.salary_number, this.job.salary_period) : "No salary added")
.marginTop(0.75, em) .marginTop(0.75, em)
}) })
.paddingVertical(1.5, em) .paddingVertical(1.5, em)

View File

@@ -1,4 +1,4 @@
import util from "../../util.js" import server from "../../_/code/bridge/serverFunctions"
class JobForm extends Shadow { class JobForm extends Shadow {
inputStyles(el) { inputStyles(el) {
@@ -19,59 +19,185 @@ class JobForm extends Shadow {
}) })
} }
errorMessage = ""
render() { render() {
form(() => { ZStack(() => {
VStack(() => { p("X")
input("Title", "70vw") .color("var(--darkred)")
.attr({ name: "title", type: "text" }) .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) .margin(1, em)
.padding(1, em) .boxSizing("border-box")
.styles(this.inputStyles) .verticalAlign("center")
input("Location", "70vw") .horizontalAlign("center")
.attr({ name: "location", type: "text" })
.margin(1, em) input("Description", "70%")
.padding(1, em) .attr({ name: "description", type: "text" })
.styles(this.inputStyles) .margin(1, em)
input("Company", "70vw") .padding(1, em)
.attr({ name: "company", type: "text" }) .styles(this.inputStyles)
.margin(1, em) HStack(() => {
.padding(1, em) button("==>")
.styles(this.inputStyles) .padding(1, em)
input("salary", "70vw") .fontSize(0.9, rem)
.attr({ name: "salary", type: "number" }) .borderRadius(12, px)
.margin(1, em) .background("var(--searchbackground)")
.padding(1, em) .color("var(--text)")
.styles(this.inputStyles) .border("1px solid var(--accent)")
input("Description", "70vw") .boxSizing("border-box")
.attr({ name: "description", type: "text" }) .onTouch(function (start) {
.margin(1, em) if (start) {
.padding(1, em) this.style.backgroundColor = "var(--accent)"
.styles(this.inputStyles) } else {
button("==>") this.style.backgroundColor = "var(--searchbackground)"
.margin(1, em) }
.padding(1, em) })
})
.width(70, vw)
.margin("auto")
.fontSize(0.9, rem) .fontSize(0.9, rem)
.borderRadius(12, px) .paddingLeft(0, em)
.background("var(--accent)") .paddingRight(2, em)
.color("var(--text)") .marginVertical(1, em)
.border("1px solid var(--accent)") .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("absolute") .position("fixed")
.height(90, pct) .height(window.visualViewport.height - 20, px)
.width(95, pct) .width(100, pct)
.top(50, pct).left(50, pct) .top(100, vh)
.center()
.background("var(--main)") .background("var(--main)")
.zIndex(100) .zIndex(4)
.borderTopLeftRadius("10px") .borderTopLeftRadius("10px")
.borderTopRightRadius("10px") .borderTopRightRadius("10px")
.boxSizing("border-box") .boxSizing("border-box")
.transform(`translate(-50%, -45%)`) .border("1px solid var(--accent)")
.transition("top .3s")
} }
tryThis() { async handleSend(jobData) {
console.log("hello2") 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"
}
} }
} }

View File

@@ -42,48 +42,57 @@ class Jobs extends Shadow {
} }
render() { render() {
VStack(() => { ZStack(() => {
// JobForm() JobForm()
SearchBar(this.searchText)
VStack(() => { VStack(() => {
if (!this.jobs || this.jobs == []) { SearchBar(this.searchText)
LoadingCircle()
} else if (this.searchText) { VStack(() => {
if (this.searchedJobs.length > 0) { if (!this.jobs || this.jobs == []) {
for (let i = 0; i < this.searchedJobs.length; i++) { LoadingCircle()
JobCard(this.searchedJobs[i]) } 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 { } else {
h2("Could not find any jobs with your search criteria.") h2("No Jobs")
.color("var(--divider)") .color("var(--divider)")
.fontWeight("bold") .fontWeight("bold")
.marginTop(7.5, em) .marginTop(7.5, em)
.marginBottom(0.5, em) .marginBottom(0.5, em)
.textAlign("center") .textAlign("center")
} }
} else if (this.jobs.length > 0) { })
for (let i = 0; i < this.jobs.length; i++) { .overflowY("scroll")
JobCard(this.jobs[i]) .gap(0.75, em)
}
} else {
h2("No Jobs")
.color("var(--divider)")
.fontWeight("bold")
.marginTop(7.5, em)
.marginBottom(0.5, em)
.textAlign("center")
}
}) })
.overflowY("scroll") .boxSizing("border-box")
.gap(0.75, em) .height(100, pct)
.width(100, pct)
.onEvent("jobsearch", this.onJobSearch)
.onEvent("new-job", this.onNewJob)
}) })
.boxSizing("border-box") }
.height(100, pct)
.width(100, pct) onNewJob = (e) => {
.onEvent("jobsearch", this.onJobSearch) let newJob = e.detail.job;
this.jobs.push(newJob)
this.rerender()
} }
onJobSearch = (e) => { onJobSearch = (e) => {

View File

@@ -16,7 +16,6 @@ class AppWindowContainer extends Shadow {
.gap(0) .gap(0)
Profile() Profile()
.display("none")
.zIndex(3) .zIndex(3)
}) })
.height(100, pct) .height(100, pct)
@@ -26,11 +25,13 @@ class AppWindowContainer extends Shadow {
} }
openProfile() { openProfile() {
this.$("profile-").display("") this.$("profile-").top(20, px)
this.$("profile-").pointerEvents("auto")
} }
closeProfile() { closeProfile() {
this.$("profile-").display("none") this.$("profile-").top(100, vh)
this.$("profile-").pointerEvents("none")
} }
} }

View File

@@ -51,6 +51,9 @@ class SearchBar extends Shadow {
.marginBottom(1, em) .marginBottom(1, em)
.border("1px solid var(--accent)") .border("1px solid var(--accent)")
.borderRadius(15, px) .borderRadius(15, px)
.onTap(() => {
this.handleAdd()
})
}) })
.width(100, pct) .width(100, pct)
.horizontalAlign("center") .horizontalAlign("center")
@@ -64,6 +67,20 @@ class SearchBar extends Shadow {
}) })
} }
handleAdd() {
const app = global.currentApp()
switch (app) {
case "Jobs":
$("jobform-").toggle()
break;
case "Events":
$("eventform-").toggle()
break;
default:
break;
}
}
dispatchSearchEvent(searchText) { dispatchSearchEvent(searchText) {
const app = global.currentApp(); const app = global.currentApp();
switch (app) { switch (app) {

View File

@@ -8,14 +8,15 @@ class Sidebar extends Shadow {
.fontWeight("bold") .fontWeight("bold")
.fontFamily("Arial") .fontFamily("Arial")
.marginLeft(2, em) .marginLeft(2, em)
.onClick(function (done) { .onTap(function (done) {
console.log("hello")
if(done) { if(done) {
if (this.innerText === "Logout") { if (this.innerText === "Logout") {
global.onLogout() global.onLogout()
return return
} else if (this.innerText === "Profile") { } else if (this.innerText === "Profile") {
$("appwindowcontainer-").openProfile() $("appwindowcontainer-").openProfile()
$("sidebar-").close() $("sidebar-").toggle()
return return
} }
} }
@@ -41,12 +42,6 @@ class Sidebar extends Shadow {
.zIndex(3) .zIndex(3)
} }
close() {
if(this.style.right !== "-71vw") {
this.style.right = "-71vw"
}
}
toggle() { toggle() {
if(this.style.right === "-71vw") { if(this.style.right === "-71vw") {
this.style.right = "0vw" this.style.right = "0vw"