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

@@ -35,11 +35,11 @@ class EventCard extends Shadow {
.justifyContent("space-between")
.verticalAlign("center")
p(this.event.location)
p(this.event.location ?? "No location added")
.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)
p(this.event.description)
p(this.event.description ?? "No description included")
.marginTop(0.75, 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/LoadingCircle.js"
import "./EventCard.js"
import "./EventForm.js"
import server from "../../_/code/bridge/serverFunctions.js"
import "../../components/SearchBar.js"
@@ -41,46 +42,57 @@ class Events extends Shadow {
}
render() {
VStack(() => {
ZStack(() => {
SearchBar(this.searchText)
EventForm()
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])
SearchBar(this.searchText)
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("Could not find any events with your search criteria.")
.color("var(--divider)")
.fontWeight("bold")
.marginTop(7.5, em)
.marginBottom(0.5, em)
.textAlign("center")
h2("No Events")
.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)
})
.overflowY("scroll")
.gap(0.75, em)
.boxSizing("border-box")
.height(100, pct)
.width(100, pct)
.onEvent("eventsearch", this.onEventSearch)
.onEvent("new-event", this.onNewEvent)
})
.boxSizing("border-box")
.height(100, pct)
.width(100, pct)
.onEvent("eventsearch", this.onEventSearch)
}
onNewEvent = (e) => {
let newEvent = e.detail.event;
this.events.push(newEvent)
this.rerender()
}
onEventSearch = (e) => {

View File

@@ -35,11 +35,11 @@ class JobCard extends Shadow {
.justifyContent("space-between")
.verticalAlign("center")
p(this.job.company)
p(this.job.company ?? "No company added")
.marginTop(0.75, em)
p(this.job.location)
p(this.job.location ?? "No location added")
.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)
})
.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 {
inputStyles(el) {
@@ -19,59 +19,185 @@ class JobForm extends Shadow {
})
}
errorMessage = ""
render() {
form(() => {
VStack(() => {
input("Title", "70vw")
.attr({ name: "title", type: "text" })
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)
.padding(1, em)
.styles(this.inputStyles)
input("Location", "70vw")
.attr({ name: "location", type: "text" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Company", "70vw")
.attr({ name: "company", type: "text" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("salary", "70vw")
.attr({ name: "salary", type: "number" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Description", "70vw")
.attr({ name: "description", type: "text" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
button("==>")
.margin(1, em)
.padding(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)
.borderRadius(12, px)
.background("var(--accent)")
.color("var(--text)")
.border("1px solid var(--accent)")
.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("absolute")
.height(90, pct)
.width(95, pct)
.top(50, pct).left(50, pct)
.center()
.position("fixed")
.height(window.visualViewport.height - 20, px)
.width(100, pct)
.top(100, vh)
.background("var(--main)")
.zIndex(100)
.zIndex(4)
.borderTopLeftRadius("10px")
.borderTopRightRadius("10px")
.boxSizing("border-box")
.transform(`translate(-50%, -45%)`)
.border("1px solid var(--accent)")
.transition("top .3s")
}
tryThis() {
console.log("hello2")
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"
}
}
}

View File

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