From 881c9408b6749c4be62a08b839425788a5b93afb Mon Sep 17 00:00:00 2001 From: metacryst Date: Mon, 23 Mar 2026 05:26:55 -0500 Subject: [PATCH] new announcements page, adding searchbar and message input. --- ios/App/Podfile | 1 + ios/App/Podfile.lock | 8 +- package.json | 1 + src/.env.development | 2 +- src/Profile/Profile.js | 146 ++++++------- src/_/code/bridge/handlers.js | 143 ------------ src/_/code/bridge/server.js | 10 + src/_/code/bridge/serverFunctions.js | 145 ++++++++++++- src/_/code/shared.css | 24 ++- src/apps/Announcements/Announcement.js | 90 ++++++++ src/apps/Announcements/Announcements.js | 276 ++++++++++++++++-------- src/apps/Announcements/Old.js | 150 +++++++++++++ src/apps/Announcements/Panel.js | 2 +- src/apps/Events/EventCard.js | 2 +- src/apps/Events/EventForm.js | 2 +- src/apps/Events/Events.js | 7 +- src/apps/Jobs/JobCard.js | 2 +- src/apps/Jobs/JobForm.js | 2 +- src/apps/Jobs/Jobs.js | 8 +- src/components/AddButton.js | 33 +++ src/components/SearchBar.js | 147 +++++-------- src/index.js | 31 ++- 22 files changed, 809 insertions(+), 423 deletions(-) delete mode 100644 src/_/code/bridge/handlers.js create mode 100644 src/_/code/bridge/server.js create mode 100644 src/apps/Announcements/Announcement.js create mode 100644 src/apps/Announcements/Old.js create mode 100644 src/components/AddButton.js diff --git a/ios/App/Podfile b/ios/App/Podfile index de304d2..015c5f4 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -16,6 +16,7 @@ def capacitor_pods pod 'CapacitorGoogleMaps', :path => '../../node_modules/@capacitor/google-maps' pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics' pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences' + pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications' pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen' end diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index 9b3f1b4..d13a4a2 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -15,6 +15,8 @@ PODS: - Capacitor - CapacitorPreferences (7.0.4): - Capacitor + - CapacitorPushNotifications (7.0.6): + - Capacitor - CapacitorSplashScreen (7.0.3): - Capacitor - Google-Maps-iOS-Utils (5.0.0): @@ -34,6 +36,7 @@ DEPENDENCIES: - "CapacitorGoogleMaps (from `../../node_modules/@capacitor/google-maps`)" - "CapacitorHaptics (from `../../node_modules/@capacitor/haptics`)" - "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)" + - "CapacitorPushNotifications (from `../../node_modules/@capacitor/push-notifications`)" - "CapacitorSplashScreen (from `../../node_modules/@capacitor/splash-screen`)" SPEC REPOS: @@ -57,6 +60,8 @@ EXTERNAL SOURCES: :path: "../../node_modules/@capacitor/haptics" CapacitorPreferences: :path: "../../node_modules/@capacitor/preferences" + CapacitorPushNotifications: + :path: "../../node_modules/@capacitor/push-notifications" CapacitorSplashScreen: :path: "../../node_modules/@capacitor/splash-screen" @@ -68,11 +73,12 @@ SPEC CHECKSUMS: CapacitorGoogleMaps: 20b5445a532f80dbb120fa99941fd094bcc88af6 CapacitorHaptics: d17da7dd984cae34111b3f097ccd3e21f9feec62 CapacitorPreferences: d82a7e3b95fcab43a553268b803356522910d153 + CapacitorPushNotifications: c6158ba6f3777f281a675aa43e4011e9723e822b CapacitorSplashScreen: d06ae8804808e9f649a08e7bb7f283c77b688084 Google-Maps-iOS-Utils: 66d6de12be1ce6d3742a54661e7a79cb317a9321 GoogleMaps: 8939898920281c649150e0af74aa291c60f2e77d IONGeolocationLib: 20f9d0248a0b5264511fb57a37e25dd2badf797a -PODFILE CHECKSUM: 4f01ce5c2aa76ddcb757b8ad50c993f40e6aeb33 +PODFILE CHECKSUM: 7fad0e16088b635c7bc1980fea245d4a60cc4bbe COCOAPODS: 1.15.2 diff --git a/package.json b/package.json index ace5952..0a52829 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@capacitor/haptics": "^7.0.3", "@capacitor/ios": "^7.4.4", "@capacitor/preferences": "^7.0.4", + "@capacitor/push-notifications": "^7.0.6", "@capacitor/splash-screen": "^7.0.3" }, "devDependencies": { diff --git a/src/.env.development b/src/.env.development index 3452b5c..9206f33 100644 --- a/src/.env.development +++ b/src/.env.development @@ -1 +1 @@ -VITE_API_URL= \ No newline at end of file +VITE_API_URL=https://frm.so \ No newline at end of file diff --git a/src/Profile/Profile.js b/src/Profile/Profile.js index d034c0c..9603eb6 100644 --- a/src/Profile/Profile.js +++ b/src/Profile/Profile.js @@ -1,4 +1,4 @@ -import server from "../_/code/bridge/serverFunctions"; +import server from "../_/code/bridge/server"; import util from "../util"; css(` @@ -66,83 +66,83 @@ class Profile extends Shadow { form(() => { input("Image Upload", "0px", "0px") - .attr({ name: "image-upload", type: "file" }) - .display("none") - .visibility("hidden") - .onChange((e) => { - this.handleUpload(e.target.files[0]); - }) + .attr({ name: "image-upload", type: "file" }) + .display("none") + .visibility("hidden") + .onChange((e) => { + this.handleUpload(e.target.files[0]); + }) VStack(() => { - HStack(() => { - if (global.profile.image_path) { - img(`${util.HOST}${global.profile.image_path}`, "10em", "10em") - .borderRadius(100, pct) - } - }) - .boxSizing("border-box") - .height(10, em) - .width(10, em) - .border("1px solid var(--accent)") - .borderRadius(100, pct) - .background("var(--darkaccent)") - .onTap(() => { - const inputSelector = this.$('[name="image-upload"]'); - inputSelector.click() - }) - - p("Tap to edit") - .color("var(--headertext)") - .opacity(0.5) - .marginTop(0.5, em) - - h1(this.profile.first_name + " " + this.profile.last_name) - .color("var(--headertext") - .width(70, pct) - .marginVertical(0.25, em) - .textAlign("center") - - p("Joined " + this.convertDate(this.profile.created)) - .color("var(--headertext)") - .marginBottom(0.5, em) - - h2("Bio") - .color("var(--headertext") - .margin(0) - .paddingVertical(0.9, em) - .borderTop("2px solid var(--divider)") - .width(70, pct) - .textAlign("center") - - textarea(this.bioText ? this.bioText : "Tap to start typing...") - .attr({ name: "bioinput" }) - .padding(1, em) - .width(90, pct) - .height(15, em) - .boxSizing("border-box") - .background("var(--searchbackground)") - .color("var(--darktext)") - .border("1px solid color-mix(in srgb, var(--accent) 60%, transparent)") - .borderRadius(12, px) - .fontFamily("Arial") - .fontSize(1.1, em) - .outline("none") - .onAppear((e) => { - if (this.bioText) { - $("profile- textarea").innerText = this.bioText - } + HStack(() => { + if (global.profile.image_path) { + img(`${util.HOST}${global.profile.image_path}`, "10em", "10em") + .borderRadius(100, pct) + } }) - .lineHeight(1.2, em) + .boxSizing("border-box") + .height(10, em) + .width(10, em) + .border("1px solid var(--accent)") + .borderRadius(100, pct) + .background("var(--darkaccent)") + .onTap(() => { + const inputSelector = this.$('[name="image-upload"]'); + inputSelector.click() + }) + + p("Tap to edit") + .color("var(--headertext)") + .opacity(0.5) + .marginTop(0.5, em) + + h1(this.profile.first_name + " " + this.profile.last_name) + .color("var(--headertext") + .width(70, pct) + .marginVertical(0.25, em) + .textAlign("center") + + p("Joined " + this.convertDate(this.profile.created)) + .color("var(--headertext)") + .marginBottom(0.5, em) + + h2("Bio") + .color("var(--headertext") + .margin(0) + .paddingVertical(0.9, em) + .borderTop("2px solid var(--divider)") + .width(70, pct) + .textAlign("center") + + textarea(this.bioText ? this.bioText : "Tap to start typing...") + .attr({ name: "bioinput" }) + .padding(1, em) + .width(90, pct) + .height(15, em) + .boxSizing("border-box") + .background("var(--searchbackground)") + .color("var(--darktext)") + .border("1px solid color-mix(in srgb, var(--accent) 60%, transparent)") + .borderRadius(12, px) + .fontFamily("Arial") + .fontSize(1.1, em) + .outline("none") + .onAppear((e) => { + if (this.bioText) { + $("profile- textarea").innerText = this.bioText + } + }) + .lineHeight(1.2, em) button("Save Bio") - .padding(1, em) - .fontSize(1.1, em) - .borderRadius(12, px) - .background("var(--searchbackground)") - .color("var(--text)") - .border("1px solid var(--accent)") - .boxSizing("border-box") - .marginVertical(0.75, em) + .padding(1, em) + .fontSize(1.1, em) + .borderRadius(12, px) + .background("var(--searchbackground)") + .color("var(--text)") + .border("1px solid var(--accent)") + .boxSizing("border-box") + .marginVertical(0.75, em) }) .horizontalAlign("center") .marginTop(5, em) diff --git a/src/_/code/bridge/handlers.js b/src/_/code/bridge/handlers.js deleted file mode 100644 index 41da64e..0000000 --- a/src/_/code/bridge/handlers.js +++ /dev/null @@ -1,143 +0,0 @@ -const handlers = { - async getStripeProfile(networkId) { - return global.payments.getProfile(networkId) - }, - - async addEvent(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) { - 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) { - try { - return await global.db.events.delete(id, networkId, userId); - } catch (e) { - return { status: e.status, error: e.message } - } - }, - - async getEvent(id) { - try { - return global.db.events.getById(id) - } catch (e) { - return { status: e.status, error: e.message } - } - }, - - async getEvents(networkId) { - try { - return global.db.events.getByNetwork(networkId) - } catch (e) { - return { status: e.status, error: e.message } - } - }, - - async addJob(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) { - 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) { - try { - return await global.db.jobs.delete(id, networkId, userId); - } catch (e) { - return { status: e.status, error: e.message } - } - }, - - async getJob(id) { - try { - return await global.db.jobs.getById(id) - } catch (e) { - return { status: e.status, error: e.message } - } - }, - - async getJobs(networkId) { - try { - return global.db.jobs.getByNetwork(networkId) - } catch (e) { - return { status: e.status, error: e.message } - } - }, - - async addAnnouncement(text, networkId, userId) { - try { - return await global.db.announcements.add(text, networkId, userId) - } catch (e) { - return { status: e.status, error: e.message } - } - }, - - async editAnnouncement(newAnnouncement, userId) { - try { - return await global.db.announcements.edit(newAnnouncement, userId); - } catch (e) { - return { status: e.status, error: e.message } - } - }, - - async deleteAnnouncement(id, userId) { - try { - return await global.db.announcements.delete(id, userId); - } catch (e) { - return { status: e.status, error: e.message } - } - }, - - async getAnnouncement(id) { - try { - return global.db.announcements.getById(id) - } catch (e) { - return { status: e.status, error: e.message } - } - }, - - async getAnnouncements(networkId) { - try { - return global.db.announcements.getByNetwork(networkId) - } catch (e) { - return { status: e.status, error: e.message } - } - }, - - async getUserAnnouncements(userId) { - try { - return global.db.announcements.getByCreator(userId) - } catch (e) { - return { status: e.status, error: e.message } - } - }, - - async editBio(newBio, userId) { - try { - return global.db.members.editBio(newBio, userId) - } catch (e) { - return { status: e.status, error: e.message } - } - } -} - -export default handlers \ No newline at end of file diff --git a/src/_/code/bridge/server.js b/src/_/code/bridge/server.js new file mode 100644 index 0000000..cc309ce --- /dev/null +++ b/src/_/code/bridge/server.js @@ -0,0 +1,10 @@ +import { createBridge, IS_NODE } from "./bridge.js" + +let handlers = {} + +if (IS_NODE) { + const mod = await import("./serverFunctions.js") + handlers = mod.default +} + +export default createBridge(handlers) \ No newline at end of file diff --git a/src/_/code/bridge/serverFunctions.js b/src/_/code/bridge/serverFunctions.js index b86c19e..41da64e 100644 --- a/src/_/code/bridge/serverFunctions.js +++ b/src/_/code/bridge/serverFunctions.js @@ -1,10 +1,143 @@ -import { createBridge, IS_NODE } from "./bridge.js" +const handlers = { + async getStripeProfile(networkId) { + return global.payments.getProfile(networkId) + }, -let handlers = {} + async addEvent(newEvent, networkId, creatorId) { + try { + return await global.db.events.add(newEvent, networkId, creatorId) + } catch (e) { + return { status: e.status, error: e.message } + } + }, -if (IS_NODE) { - const mod = await import("./handlers.js") - handlers = mod.default + async editEvent(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) { + try { + return await global.db.events.delete(id, networkId, userId); + } catch (e) { + return { status: e.status, error: e.message } + } + }, + + async getEvent(id) { + try { + return global.db.events.getById(id) + } catch (e) { + return { status: e.status, error: e.message } + } + }, + + async getEvents(networkId) { + try { + return global.db.events.getByNetwork(networkId) + } catch (e) { + return { status: e.status, error: e.message } + } + }, + + async addJob(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) { + 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) { + try { + return await global.db.jobs.delete(id, networkId, userId); + } catch (e) { + return { status: e.status, error: e.message } + } + }, + + async getJob(id) { + try { + return await global.db.jobs.getById(id) + } catch (e) { + return { status: e.status, error: e.message } + } + }, + + async getJobs(networkId) { + try { + return global.db.jobs.getByNetwork(networkId) + } catch (e) { + return { status: e.status, error: e.message } + } + }, + + async addAnnouncement(text, networkId, userId) { + try { + return await global.db.announcements.add(text, networkId, userId) + } catch (e) { + return { status: e.status, error: e.message } + } + }, + + async editAnnouncement(newAnnouncement, userId) { + try { + return await global.db.announcements.edit(newAnnouncement, userId); + } catch (e) { + return { status: e.status, error: e.message } + } + }, + + async deleteAnnouncement(id, userId) { + try { + return await global.db.announcements.delete(id, userId); + } catch (e) { + return { status: e.status, error: e.message } + } + }, + + async getAnnouncement(id) { + try { + return global.db.announcements.getById(id) + } catch (e) { + return { status: e.status, error: e.message } + } + }, + + async getAnnouncements(networkId) { + try { + return global.db.announcements.getByNetwork(networkId) + } catch (e) { + return { status: e.status, error: e.message } + } + }, + + async getUserAnnouncements(userId) { + try { + return global.db.announcements.getByCreator(userId) + } catch (e) { + return { status: e.status, error: e.message } + } + }, + + async editBio(newBio, userId) { + try { + return global.db.members.editBio(newBio, userId) + } catch (e) { + return { status: e.status, error: e.message } + } + } } -export default createBridge(handlers) \ No newline at end of file +export default handlers \ No newline at end of file diff --git a/src/_/code/shared.css b/src/_/code/shared.css index ba588ae..79848a4 100644 --- a/src/_/code/shared.css +++ b/src/_/code/shared.css @@ -18,7 +18,7 @@ --darkgrey: #5c4646; --headertext: #433c36e2; - --searchbackground: #dfc9ac; + --searchbackground: #ffeed8; --loginButton: var(--main); --loginBackground: #d96b6b; @@ -83,21 +83,27 @@ } } +input { + outline: none; + caret-color: var(--text); /* hide real caret */ +} + input::placeholder { font-family: Arial; + color: #5C504D; } html, body { - padding: 0; - margin: 0; - font-family: Arial; + padding: 0; + margin: 0; + font-family: Arial; } body { - background-color: var(--main); - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); + background-color: var(--main); + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); } \ No newline at end of file diff --git a/src/apps/Announcements/Announcement.js b/src/apps/Announcements/Announcement.js new file mode 100644 index 0000000..8b8fbb5 --- /dev/null +++ b/src/apps/Announcements/Announcement.js @@ -0,0 +1,90 @@ +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) \ No newline at end of file diff --git a/src/apps/Announcements/Announcements.js b/src/apps/Announcements/Announcements.js index d609aa4..1b13d69 100644 --- a/src/apps/Announcements/Announcements.js +++ b/src/apps/Announcements/Announcements.js @@ -1,125 +1,176 @@ -import './Panel.js' -import server from '../../_/code/bridge/serverFunctions.js' +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- input::placeholder { - font-family: 'Bona Nova'; - font-size: 0.9em; - color: var(--accent); + announcements- .VStack::-webkit-scrollbar { + display: none; + width: 0px; + height: 0px; } - input::placeholder { - font-family: Arial; + announcements- .VStack::-webkit-scrollbar-thumb { + background: transparent; } - 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); + announcements- .VStack::-webkit-scrollbar-track { + background: transparent; } `) class Announcements extends Shadow { - announcements; + static searchableKeys = ['message', 'author']; constructor() { super() this.announcements = global.currentNetwork.data.announcements.sort((a, b) => new Date(b.created) - new Date(a.created)); - console.log(this.announcements) - } + this.searchedAnnouncements = []; + this.searchText = ""; + } render() { ZStack(() => { + VStack(() => { + SearchBar(this.searchText, "90vw") - 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 + 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]) } - this.value = "" + } 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) => { + this.handleUpload(e.target.files[0]); + }) + + 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") + }, 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) + } }) - .gap(1, em) .boxSizing("border-box") - .width(100, pct) .height(100, pct) - .horizontalAlign("center") - .verticalAlign("end") - .minHeight(0) + .width(100, pct) + .onEvent("announcementsearch", this.onAnnouncementSearch) + .onEvent("new-announcement", this.onNewAnnouncement) + .onEvent("deleted-announcement", this.onDeletedAnnouncement) + .onEvent("edited-announcement", this.onEditedAnnouncement) }) - .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() - } + addPhoto() { + console.log("hey") } onNewAnnouncement = (e) => { @@ -138,13 +189,54 @@ class Announcements extends Shadow { onEditedAnnouncement = (e) => { let editedAnnouncement = e.detail - const i = this.announcements.findIndex(ann => ann.id === editedPost.id) - if (i !== -1) { + 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) \ No newline at end of file diff --git a/src/apps/Announcements/Old.js b/src/apps/Announcements/Old.js new file mode 100644 index 0000000..d609aa4 --- /dev/null +++ b/src/apps/Announcements/Old.js @@ -0,0 +1,150 @@ +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) \ No newline at end of file diff --git a/src/apps/Announcements/Panel.js b/src/apps/Announcements/Panel.js index 0bdae03..5c576a7 100644 --- a/src/apps/Announcements/Panel.js +++ b/src/apps/Announcements/Panel.js @@ -1,5 +1,5 @@ import "../../components/LoadingCircle.js" -import server from "../../_/code/bridge/serverFunctions.js" +import server from "../../_/code/bridge/server.js" css(` panel- { diff --git a/src/apps/Events/EventCard.js b/src/apps/Events/EventCard.js index 9d80e62..924dc21 100644 --- a/src/apps/Events/EventCard.js +++ b/src/apps/Events/EventCard.js @@ -1,5 +1,5 @@ import util from "../../util" -import server from "../../_/code/bridge/serverFunctions.js" +import server from "../../_/code/bridge/server.js" css(` eventcard- p { diff --git a/src/apps/Events/EventForm.js b/src/apps/Events/EventForm.js index f0c77aa..cf3403d 100644 --- a/src/apps/Events/EventForm.js +++ b/src/apps/Events/EventForm.js @@ -1,4 +1,4 @@ -import server from "../../_/code/bridge/serverFunctions" +import server from "../../_/code/bridge/server" class EventForm extends Shadow { inputStyles(el) { diff --git a/src/apps/Events/Events.js b/src/apps/Events/Events.js index 25f2c3f..74223b9 100644 --- a/src/apps/Events/Events.js +++ b/src/apps/Events/Events.js @@ -2,7 +2,7 @@ import "../../components/TopBar.js" import "../../components/LoadingCircle.js" import "./EventCard.js" import "./EventForm.js" -import server from "../../_/code/bridge/serverFunctions.js" +import server from "../../_/code/bridge/server.js" import "../../components/SearchBar.js" css(` @@ -47,7 +47,10 @@ class Events extends Shadow { EventForm() VStack(() => { - SearchBar(this.searchText) + HStack(() => { + SearchBar(this.searchText, "75vw") + AddButton() + }) VStack(() => { if (!this.events || this.events == []) { diff --git a/src/apps/Jobs/JobCard.js b/src/apps/Jobs/JobCard.js index 97bcb11..a7594ed 100644 --- a/src/apps/Jobs/JobCard.js +++ b/src/apps/Jobs/JobCard.js @@ -1,5 +1,5 @@ import util from "../../util.js" -import server from "../../_/code/bridge/serverFunctions.js" +import server from "../../_/code/bridge/server.js" css(` jobcard- p { diff --git a/src/apps/Jobs/JobForm.js b/src/apps/Jobs/JobForm.js index 72c83fd..faefb0b 100644 --- a/src/apps/Jobs/JobForm.js +++ b/src/apps/Jobs/JobForm.js @@ -1,4 +1,4 @@ -import server from "../../_/code/bridge/serverFunctions" +import server from "../../_/code/bridge/server" class JobForm extends Shadow { inputStyles(el) { diff --git a/src/apps/Jobs/Jobs.js b/src/apps/Jobs/Jobs.js index 27e20e0..5d327e8 100644 --- a/src/apps/Jobs/Jobs.js +++ b/src/apps/Jobs/Jobs.js @@ -3,7 +3,8 @@ import "./JobsGrid.js" import "./JobCard.js" import "./JobForm.js" import "../../components/SearchBar.js" -import server from "../../_/code/bridge/serverFunctions.js" +import "../../components/AddButton.js" +import server from "../../_/code/bridge/server.js" css(` jobs- { @@ -47,7 +48,10 @@ class Jobs extends Shadow { JobForm() VStack(() => { - SearchBar(this.searchText) + HStack(() => { + SearchBar(this.searchText, "75vw") + AddButton() + }) VStack(() => { if (!this.jobs || this.jobs == []) { diff --git a/src/components/AddButton.js b/src/components/AddButton.js new file mode 100644 index 0000000..3e505a4 --- /dev/null +++ b/src/components/AddButton.js @@ -0,0 +1,33 @@ +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) \ No newline at end of file diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js index e047f34..58f33b7 100644 --- a/src/components/SearchBar.js +++ b/src/components/SearchBar.js @@ -5,97 +5,68 @@ css(` `) class SearchBar extends Shadow { - constructor(searchText) { - super() - this.searchText = searchText - } + searchText + width + constructor(searchText, width) { + super() + this.searchText = searchText + this.width = width + } - render() { - form(() => { - HStack(() => { - input("Search", "80%") - .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) - .border("1px solid color-mix(in srgb, var(--accent) 60%, transparent)") - .borderRadius(100, px) - .fontFamily("Arial") - .fontSize(1, em) - .outline("none") - .cursor("not-allowed") - .onTouch(function (start) { - if (start) { - this.style.backgroundColor = "var(--accent)" - } else { - this.style.backgroundColor = "var(--searchbackground)" - } - }) - // .onInput(async (e) => { - // e.preventDefault(); - // console.log("helloooo submitted") - // }) // can be used for live updating + 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")) + }) + } - 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() - }) - }) - .width(100, pct) - .horizontalAlign("center") - .verticalAlign("center") - .gap(0.5, em) - }) - .onSubmit(async (e) => { - e.preventDefault(); - const data = new FormData(e.target); - this.dispatchSearchEvent(data.get("searchText")) - }) - } - - handleAdd() { - const app = global.currentApp() - switch (app) { - case "Jobs": - $("jobform-").toggle() - break; - case "Events": - $("eventform-").toggle() - break; - default: - break; - } - } - - 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; - } - } + 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) \ No newline at end of file diff --git a/src/index.js b/src/index.js index a16e970..fb99471 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,6 @@ +import { PushNotifications } from '@capacitor/push-notifications'; + +import server from './_/code/bridge/server.js' import Socket from "/_/code/ws/Socket.js" import "./Home/Home.js" import "./Home/AuthPage/AuthPage.js" @@ -12,7 +15,7 @@ let Global = class { currentApp() { const pathname = window.location.pathname; - const segments = pathname.split('/').filter(Boolean); + const segments = pathname.split('/').filter(Boolean) const secondSegment = segments[1] || "" const capitalized = secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1); return capitalized @@ -154,7 +157,33 @@ let Global = class { location.reload() } + async setupPushNotifications() { + const permission = await PushNotifications.requestPermissions(); + + if (permission.receive === 'granted') { + await PushNotifications.register(); + } + + PushNotifications.addListener('registration', async (token) => { + console.log('Device token:', token.value) + const stored = localStorage.getItem('deviceToken') + if (stored === token.value) return; + + await server.updatePushToken(token.value) + localStorage.setItem('deviceToken', token.value) + }); + + PushNotifications.addListener('registrationError', (error) => { + console.error('Registration error:', error) + }); + + PushNotifications.addListener('pushNotificationReceived', (notification) => { + console.log('Notification received:', notification) + }); + } + constructor() { + this.setupPushNotifications() window.addEventListener("navigate", this.onNavigate) this.getProfile().then(async (status) => {