diff --git a/README.md b/readme.md similarity index 81% rename from README.md rename to readme.md index 2f6215b..e1c6661 100644 --- a/README.md +++ b/readme.md @@ -16,7 +16,6 @@ npm start In src/manifest.json, "#31d53d" refers to the green color which is visible in the background in the web version. This is not visible in the built version. ### Running iOS - https://capacitorjs.com/docs/ios#adding-the-ios-platform npm install @capacitor/ios @@ -24,4 +23,7 @@ npx cap add ios npx cap open ios To Rerun: -npm run build && npx cap copy ios \ No newline at end of file +npm run build && npx cap copy ios + +### Note +You need to be in mobile mode in order for the app to work, in the top right corner of dev tools. diff --git a/src/Home.js b/src/Home.js new file mode 100644 index 0000000..d1cade4 --- /dev/null +++ b/src/Home.js @@ -0,0 +1,39 @@ +import "./components/Sidebar.js" +import "./components/AppMenu.js" +import "./apps/Forum/Forum.js" +import "./apps/Messages/Messages.js" +import "./apps/Jobs/Jobs.js" + +class Home extends Shadow { + + render() { + ZStack(() => { + Sidebar() + + ZStack(() => { + switch(window.location.pathname) { + case "/": + Forum() + break; + + case "/messages": + Messages() + break; + + case "/jobs": + Jobs() + break; + } + }) + .onNavigate(function () { + console.log("navigate") + this.rerender() + }) + + AppMenu() + }) + .overflowX("hidden") + } +} + +register(Home) \ No newline at end of file diff --git a/src/_/code/quill.js b/src/_/code/quill.js index 19242eb..e144543 100644 --- a/src/_/code/quill.js +++ b/src/_/code/quill.js @@ -1,6 +1,7 @@ /* Sam Russell Captured Sun + 1.16.26 - Moving nav event dispatch out of pushState, adding null feature to attr() 1.5.26 - Switching verticalAlign and horizontalAlign names, adding borderVertical and Horizontal 12.26.25 - State for arrays, nested objects. State for stacks (Shadow-only) 12.17.25 - [Hyperia] - adding width, height functions. adding "e" to onClick. adding the non-window $$ funcs. @@ -27,7 +28,6 @@ let oldPushState = history.pushState; history.pushState = function pushState() { let ret = oldPushState.apply(this, arguments); window.dispatchEvent(new Event('pushstate')); - window.dispatchEvent(new Event('navigate')); return ret; }; @@ -51,10 +51,15 @@ window.setQuery = function(key, value) { return newUrl; }; - + window.navigateTo = function(url) { - window.dispatchEvent(new Event('navigate')); window.history.pushState({}, '', url); + window.dispatchEvent(new Event('navigate')); +} + +window.setLocation = function(url) { + window.dispatchEvent(new Event('navigate')); + window.history.replaceState({}, '', url); } /* $ SELECTOR */ @@ -91,8 +96,6 @@ console.green = function(message) { } /* GET CSS VARIABLES FOR DARK OR LIGHT MODE */ -window.darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; -document.documentElement.classList.add(darkMode ? 'dark' : 'light'); window.getColor = function(name) { const rootStyles = getComputedStyle(document.documentElement); @@ -815,6 +818,20 @@ window.input = function(placeholder = "", width, height) { return el } +window.select = function(cb) { + let el = document.createElement("select") + el.render = cb + quill.render(el) + return el +} + +window.option = function(placeholder = "") { + let el = document.createElement("option") + el.innerText = placeholder + quill.render(el) + return el +} + window.label = function(inside) { let el = document.createElement("label") if(typeof inside === "string") { @@ -1034,7 +1051,7 @@ HTMLElement.prototype.onInput = function(cb) { }; HTMLElement.prototype.onChange = function(cb) { - if(!this.matches('input, textarea, [contenteditable=""], [contenteditable="true"]')) + if(!this.matches('input, textarea, select, [contenteditable=""], [contenteditable="true"]')) throw new Error("Can't put input event on non-input element!") this._storeListener("change", cb); return this; @@ -1067,7 +1084,7 @@ HTMLElement.prototype.onTap = function(cb) { - We can't just put a listener on the element, because a window "navigate" event won't trigger it - We can't just put a listener on the window, because the "this" variable will only refer to the window - And, if we try to re-add that scope using bind(), it makes the return value of .toString() unreadable, which means we cannot detect duplicate listeners. -- Therefore, we attach a global navigate event to the window, and each navigate event in this array, and manually trigger each event when the global one fires. +- Therefore, we attach a global navigate event to the window, and store each navigate event in this navigateListeners array, and manually trigger each event on the elements when the global one fires. */ navigateListeners = [] window.addEventListener("navigate", () => { @@ -1180,7 +1197,11 @@ HTMLElement.prototype.attr = function(attributes) { } for (const [key, value] of Object.entries(attributes)) { - this.setAttribute(key, value); + if(value === null) { + this.removeAttribute(key) + } else { + this.setAttribute(key, value); + } } return this; }; diff --git a/src/_/code/styles.css b/src/_/code/styles.css index 2f0b2fe..7ca742d 100644 --- a/src/_/code/styles.css +++ b/src/_/code/styles.css @@ -1,5 +1,5 @@ :root { - --main: #AEBDFF; + --main: #FFE9C8; --accent: #60320c; --text: #340000; --yellow: #f1f3c3; @@ -9,4 +9,17 @@ :root { } +} + +html, +body { + padding: 0; + margin: 0; +} + +body { + 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/_/icons/column.svg b/src/_/icons/column.svg new file mode 100644 index 0000000..841bf5f --- /dev/null +++ b/src/_/icons/column.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/_/icons/column2.svg b/src/_/icons/column2.svg new file mode 100644 index 0000000..ab79e11 --- /dev/null +++ b/src/_/icons/column2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/_/icons/columnwhite.svg b/src/_/icons/columnwhite.svg new file mode 100644 index 0000000..5be3f63 --- /dev/null +++ b/src/_/icons/columnwhite.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/_/icons/forum.png b/src/_/icons/forum.png new file mode 100644 index 0000000..4b0fa1b Binary files /dev/null and b/src/_/icons/forum.png differ diff --git a/src/_/icons/jobs.svg b/src/_/icons/jobs.svg new file mode 100644 index 0000000..f4a811f --- /dev/null +++ b/src/_/icons/jobs.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/_/icons/letter.svg b/src/_/icons/letter.svg new file mode 100644 index 0000000..9a1d4d8 --- /dev/null +++ b/src/_/icons/letter.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/apps/Forum/Forum.js b/src/apps/Forum/Forum.js new file mode 100644 index 0000000..a507efc --- /dev/null +++ b/src/apps/Forum/Forum.js @@ -0,0 +1,66 @@ +import './ForumPanel.js' + +css(` + forum- { + font-family: 'Bona'; + } + + forum- input::placeholder { + font-family: 'Bona Nova'; + font-size: 0.9em; + color: var(--accent); + } + + input[type="checkbox"] { + appearance: none; /* remove default style */ + -webkit-appearance: none; + width: 1em; + height: 1em; + border: 1px solid var(--accent); + } + + input[type="checkbox"]:checked { + background-color: var(--red); + } +`) + +class Forum extends Shadow { + + selectedForum = "HY" + + render() { + ZStack(() => { + VStack(() => { + + ForumPanel() + + input("Message Hyperia", "98%") + .paddingVertical(1, em) + .paddingLeft(2, pct) + .color("var(--accent)") + .background("var(--darkbrown)") + .marginBottom(6, em) + .border("none") + .fontSize(1, em) + .onKeyDown(function (e) { + if (e.key === "Enter") { + window.Socket.send({app: "FORUM", operation: "SEND", msg: {forum: "HY", text: this.value }}) + this.value = "" + } + }) + }) + .gap(0.5, em) + .width(100, pct) + .height(100, vh) + .horizontalAlign("center") + .verticalAlign("end") + }) + .onAppear(() => document.body.style.backgroundColor = "var(--darkbrown)") + .width(100, pct) + .height(100, pct) + } + + +} + +register(Forum) \ No newline at end of file diff --git a/src/apps/Forum/ForumPanel.js b/src/apps/Forum/ForumPanel.js new file mode 100644 index 0000000..9dfdde6 --- /dev/null +++ b/src/apps/Forum/ForumPanel.js @@ -0,0 +1,88 @@ +import "../../components/LoadingCircle.js" + +class ForumPanel extends Shadow { + forums = [ + "HY" + ] + messages = [] + + render() { + VStack(() => { + if(this.messages.length > 0) { + + let previousDate = null + + for(let i=0; i { + HStack(() => { + p(message.sentBy) + .fontWeight("bold") + .marginBottom(0.3, em) + + p(util.formatTime(message.time)) + .opacity(0.2) + .marginLeft(1, em) + }) + p(message.text) + }) + } + } else { + LoadingCircle() + } + }) + .gap(1, em) + .position("relative") + .overflow("scroll") + .height(100, pct) + .width(96, pct) + .paddingTop(5, em) + .paddingBottom(2, em) + .paddingLeft(4, pct) + .backgroundColor("var(--darkbrown)") + .onAppear(async () => { + requestAnimationFrame(() => { + this.scrollTop = this.scrollHeight + }); + let res = await Socket.send({app: "FORUM", operation: "GET", msg: {forum: "HY", number: 100}}) + if(!res) console.error("failed to get messages") + if(res.msg.length > 0 && this.messages.length === 0) { + this.messages = res.msg + this.rerender() + } + window.addEventListener("new-post", (e) => { + this.messages = e.detail + if(e.detail.length !== this.messages || e.detail.last.time !== this.messages.last.time || e.detail.first.time !== this.messages.first.time) { + this.rerender() + } + }) + }) + } + + parseDate(str) { + // Format: MM.DD.YYYY-HH:MM:SSxxxxxx(am|pm) + const match = str.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})-(\d{1,2}):(\d{2}).*(am|pm)$/i); + if (!match) return null; + + const [, mm, dd, yyyy, hh, min, ampm] = match; + const date = `${mm}/${dd}/${yyyy}`; + const time = `${hh}:${min}${ampm.toLowerCase()}`; + + return { date, time }; + } +} + +register(ForumPanel) \ No newline at end of file diff --git a/src/apps/Jobs/Jobs.js b/src/apps/Jobs/Jobs.js new file mode 100644 index 0000000..bb72598 --- /dev/null +++ b/src/apps/Jobs/Jobs.js @@ -0,0 +1,101 @@ +import "./JobsSidebar.js" +import "./JobsGrid.js" + +css(` + jobs- { + font-family: 'Bona'; + } + + jobs- input::placeholder { + font-family: 'Bona Nova'; + font-size: 0.9em; + color: var(--accent); + } + + input[type="checkbox"] { + appearance: none; /* remove default style */ + -webkit-appearance: none; + width: 1em; + height: 1em; + border: 1px solid var(--accent); + } + + input[type="checkbox"]:checked { + background-color: var(--red); + } +`) + +class Jobs extends Shadow { + jobs = [ + { + title: "Austin Chapter Lead", + salary: "1% of Local Revenue", + company: "Hyperia", + city: "Austin", + state: "TX" + } + ] + + render() { + ZStack(() => { + HStack(() => { + JobsSidebar() + + JobsGrid(this.jobs) + }) + .width(100, "%") + .x(0).y(13, vh) + + HStack(() => { + input("Search jobs... (Coming Soon!)", "45vw") + .attr({ + "type": "text", + "disabled": "true" + }) + .fontSize(1.1, em) + .paddingLeft(1.3, em) + .background("transparent") + .border("0.5px solid var(--divider)") + .outline("none") + .color("var(--accent)") + .opacity(0.5) + .borderRadius(10, px) + .background("grey") + .cursor("not-allowed") + + button("+ Add Job") + .width(7, em) + .marginLeft(1, em) + .borderRadius(10, px) + .background("transparent") + .border("0.3px solid var(--accent2)") + .color("var(--accent)") + .fontFamily("Bona Nova") + .onHover(function (hovering) { + if(hovering) { + this.style.background = "var(--green)" + + } else { + this.style.background = "transparent" + + } + }) + .onClick((clicking) => { + console.log(this, "clicked") + }) + + }) + .x(55, vw).y(4, vh) + .position("absolute") + .transform("translateX(-50%)") + }) + .width(100, "%") + .height(100, "%") + } + + connectedCallback() { + // Optional additional logic + } +} + +register(Jobs) \ No newline at end of file diff --git a/src/apps/Jobs/JobsGrid.js b/src/apps/Jobs/JobsGrid.js new file mode 100644 index 0000000..2af5d4f --- /dev/null +++ b/src/apps/Jobs/JobsGrid.js @@ -0,0 +1,60 @@ +class JobsGrid extends Shadow { + jobs; + + constructor(jobs) { + super() + this.jobs = jobs + } + + boldUntilFirstSpace(text) { + const index = text.indexOf(' '); + if (index === -1) { + // No spaces — bold the whole thing + return `${text}`; + } + return `${text.slice(0, index)}${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) diff --git a/src/apps/Jobs/JobsSidebar.js b/src/apps/Jobs/JobsSidebar.js new file mode 100644 index 0000000..1546cec --- /dev/null +++ b/src/apps/Jobs/JobsSidebar.js @@ -0,0 +1,26 @@ +class JobsSidebar extends Shadow { + render() { + VStack(() => { + h3("Location") + .color("var(--accent2)") + .marginBottom(0, em) + + HStack(() => { + input("Location", "100%") + .paddingLeft(3, em) + .paddingVertical(0.75, em) + .backgroundImage("/_/icons/locationPin.svg") + .backgroundRepeat("no-repeat") + .backgroundSize("18px 18px") + .backgroundPosition("10px center") + }) + }) + .paddingTop(1, em) + .paddingLeft(3, em) + .paddingRight(3, em) + .gap(1, em) + .minWidth(10, vw) + } +} + +register(JobsSidebar) \ No newline at end of file diff --git a/src/apps/Market/Market.js b/src/apps/Market/Market.js new file mode 100644 index 0000000..e3bceeb --- /dev/null +++ b/src/apps/Market/Market.js @@ -0,0 +1,105 @@ +import "./MarketSidebar.js" +import "./MarketGrid.js" + +css(` + market- { + font-family: 'Bona'; + } + + market- input::placeholder { + font-family: 'Bona Nova'; + font-size: 0.9em; + color: var(--accent); + } + + input[type="checkbox"] { + appearance: none; /* remove default style */ + -webkit-appearance: none; + width: 1em; + height: 1em; + border: 1px solid var(--accent); + } + + input[type="checkbox"]:checked { + background-color: var(--red); + } +`) + +class Market extends Shadow { + + listings = [ + { + title: "Shield Lapel Pin", + stars: "5", + reviews: 1, + price: "$12", + company: "Hyperia", + type: "new", + image: "/db/images/1", + madeIn: "America" + } + ] + + render() { + ZStack(() => { + HStack(() => { + MarketSidebar() + + MarketGrid(this.listings) + }) + .width(100, "%") + .x(0).y(13, vh) + + HStack(() => { + input("Search for products... (Coming Soon!)", "45vw") + .attr({ + "type": "text", + "disabled": "true" + }) + .fontSize(1.1, em) + .paddingLeft(1.3, em) + .background("transparent") + .border("0.5px solid var(--divider)") + .outline("none") + .color("var(--accent)") + .opacity(0.5) + .borderRadius(10, px) + .background("grey") + .cursor("not-allowed") + + button("+ Add Item") + .width(7, em) + .marginLeft(1, em) + .borderRadius(10, px) + .background("transparent") + .border("0.5px solid var(--accent2)") + .color("var(--accent)") + .fontFamily("Bona Nova") + .onHover(function (hovering) { + if(hovering) { + this.style.background = "var(--green)" + + } else { + this.style.background = "transparent" + + } + }) + .onClick((clicking) => { + console.log(this, "clicked") + }) + + }) + .x(55, vw).y(4, vh) + .position("absolute") + .transform("translateX(-50%)") + }) + .width(100, "%") + .height(100, "%") + } + + connectedCallback() { + // Optional additional logic + } +} + +register(Market) diff --git a/src/apps/Market/MarketGrid.js b/src/apps/Market/MarketGrid.js new file mode 100644 index 0000000..8740f9e --- /dev/null +++ b/src/apps/Market/MarketGrid.js @@ -0,0 +1,140 @@ +class MarketGrid extends Shadow { + listings; + + constructor(listings) { + super() + this.listings = listings + } + + boldUntilFirstSpace(text) { + if(!text) return + const index = text.indexOf(' '); + if (index === -1) { + // No spaces — bold the whole thing + return `${text}`; + } + return `${text.slice(0, index)}${text.slice(index)}`; + } + + render() { + VStack(() => { + h3("Results") + .marginTop(0.1, em) + .marginBottom(1, em) + .marginLeft(0.4, em) + .color("var(--accent)") + .opacity(0.7) + + if (this.listings.length > 0) { + ZStack(() => { + // BuyModal() + + let params = new URLSearchParams(window.location.search); + + const hyperiaMade = params.get("hyperia-made") === "true"; + const americaMade = params.get("america-made") === "true"; + const newItem = params.get("new") === "true"; + const usedItem = params.get("used") === "true"; + + + let filtered = this.listings; + if (hyperiaMade) { + filtered = filtered.filter(item => item.madeIn === "Hyperia"); + } + if (americaMade) { + filtered = filtered.filter(item => item.madeIn === "America"); + } + if (newItem) { + filtered = filtered.filter(item => item.type === "new"); + } + if (usedItem) { + filtered = filtered.filter(item => item.type === "used"); + } + + for (let i = 0; i < filtered.length; i++) { + const rating = filtered[i].stars + const percent = (rating / 5) + + VStack(() => { + img(filtered[i].image) + .marginBottom(0.5, em) + + p(filtered[i].company) + .marginBottom(0.5, em) + + p(filtered[i].title) + .fontSize(1.2, em) + .fontWeight("bold") + .marginBottom(0.5, em) + + HStack(() => { + p(filtered[i].stars) + .marginRight(0.2, em) + + ZStack(() => { + div("★★★★★") // Empty stars (background) + .color("#ccc") + + div("★★★★★") // Filled stars (foreground, clipped by width) + .color("#ffa500") + .position("absolute") + .top(0) + .left(0) + .whiteSpace("nowrap") + .overflow("hidden") + .width(percent * 5, em) + }) + .display("inline-block") + .position("relative") + .fontSize(1.2, em) + .lineHeight(1) + + p(filtered[i].reviews) + .marginLeft(0.2, em) + }) + .marginBottom(0.5, em) + + p(filtered[i].price) + .fontSize(1.75, em) + .marginBottom(0.5, em) + + button("Coming Soon!") + .onClick((finished) => { + if(finished) { + + } + }) + .onHover(function (hovering) { + if(hovering) { + this.style.backgroundColor = "var(--green)" + } else { + this.style.backgroundColor = "" + } + }) + + }) + .padding(1, em) + .border("1px solid var(--accent2)") + .borderRadius(5, "px") + } + }) + .display("grid") + .gridTemplateColumns("repeat(auto-fill, minmax(250px, 1fr))") + .gap(1, em) + } else { + p("No Listings!") + } + }) + .onQueryChanged(() => { + console.log("query did change yup") + this.rerender() + }) + .height(100, vh) + .paddingLeft(2, em) + .paddingRight(2, em) + .gap(0, em) + .width(100, "%") + } +} + +register(MarketGrid) \ No newline at end of file diff --git a/src/apps/Market/MarketSidebar.js b/src/apps/Market/MarketSidebar.js new file mode 100644 index 0000000..9324618 --- /dev/null +++ b/src/apps/Market/MarketSidebar.js @@ -0,0 +1,85 @@ +class MarketSidebar extends Shadow { + + handleChecked(e) { + let checked = e.target.checked + let label = $(`label[for="${e.target.id}"]`).innerText + if(checked) { + window.setQuery(label.toLowerCase(), true) + } else { + window.setQuery(label.toLowerCase(), null) + } + } + + render() { + VStack(() => { + + p("Make") + + HStack(() => { + input() + .attr({ + "type": "checkbox", + "id": "hyperia-check" + }) + .onChange(this.handleChecked) + label("Hyperia-Made") + .attr({ + "for": "hyperia-check" + }) + .marginLeft(0.5, em) + }) + + HStack(() => { + input() + .attr({ + "type": "checkbox", + "id": "america-check" + }) + .onChange(this.handleChecked) + label("America-Made") + .attr({ + "for": "america-check" + }) + .marginLeft(0.5, em) + }) + + p("Condition") + + HStack(() => { + input() + .attr({ + "type": "checkbox", + "id": "new-check" + }) + .onChange(this.handleChecked) + label("New") + .attr({ + "for": "new-check" + }) + .marginLeft(0.5, em) + }) + + HStack(() => { + input() + .attr({ + "type": "checkbox", + "id": "used-check" + }) + .onChange(this.handleChecked) + label("Used") + .attr({ + "for": "used-check" + }) + .marginLeft(0.5, em) + }) + }) + .paddingTop(12, vh) + .paddingLeft(3, em) + .paddingRight(3, em) + .gap(1, em) + .minWidth(10, vw) + .userSelect('none') + } +} + +register(MarketSidebar) \ No newline at end of file diff --git a/src/apps/Messages/Messages.js b/src/apps/Messages/Messages.js new file mode 100644 index 0000000..40b9722 --- /dev/null +++ b/src/apps/Messages/Messages.js @@ -0,0 +1,188 @@ +import "./MessagesSidebar.js" +import "./MessagesPanel.js" + +css(` + messages- { + font-family: 'Bona'; + } + + messages- input::placeholder { + font-family: 'Bona Nova'; + font-size: 0.9em; + color: var(--accent); + } + + input[type="checkbox"] { + appearance: none; /* remove default style */ + -webkit-appearance: none; + width: 1em; + height: 1em; + border: 1px solid var(--accent); + } + + input[type="checkbox"]:checked { + background-color: var(--red); + } +`) + +class Messages extends Shadow { + conversations = [] + selectedConvoID = null + onConversationSelect(i) { + console.log("convo selected: ", i) + this.selectedConvoID = i + this.$("messagessidebar-").rerender() + this.$("messagespanel-").rerender() + } + + getConvoFromID(id) { + for(let i=0; i { + HStack(() => { + MessagesSidebar(this.conversations, this.selectedConvoID, this.onConversationSelect) + + VStack(() => { + if(this.getConvoFromID(this.selectedConvoID)) { + MessagesPanel(this.getConvoFromID(this.selectedConvoID).messages) + } else { + MessagesPanel() + } + + input("Send Message", "93%") + .paddingVertical(1, em) + .paddingHorizontal(2, em) + .color("var(--accent)") + .background("var(--darkbrown)") + .marginBottom(6, em) + .border("none") + .fontSize(1, em) + .onKeyDown((e) => { + if (e.key === "Enter") { + window.Socket.send({app: "MESSAGES", operation: "SEND", msg: { conversation: `CONVERSATION-${this.selectedConvoID}`, text: e.target.value }}) + e.target.value = "" + } + }) + }) + .gap(1, em) + .width(100, pct) + .horizontalAlign("center") + .verticalAlign("end") + }) + .onAppear(async () => { + let res = await Socket.send({app: "MESSAGES", operation: "GET"}) + if(!res) console.error("failed to get messages") + + if(res.msg.length > 0 && this.conversations.length === 0) { + this.conversations = res.msg + this.selectedConvoID = this.conversations[0].id + this.rerender() + } + + window.addEventListener("new-message", (e) => { + let convoID = e.detail.conversationID + let messages = e.detail.messages + let convo = this.getConvoFromID(convoID) + convo.messages = messages + this.rerender() + }) + }) + .width(100, "%") + .height(87, vh) + .x(0).y(13, vh) + + VStack(() => { + p("Add Message") + + input("enter email...") + .color("var(--accent)") + .onKeyDown(function (e) { + if (e.key === "Enter") { + window.Socket.send({app: "MESSAGES", operation: "ADDCONVERSATION", msg: {email: this.value }}) + this.value = "" + } + }) + + p("x") + .onClick(function (done) { + if(done) { + this.parentElement.style.display = "none" + } + }) + .xRight(2, em).y(2, em) + .fontSize(1.4, em) + .cursor("pointer") + + }) + .gap(1, em) + .alignVertical("center") + .alignHorizontal("center") + .backgroundColor("black") + .border("1px solid var(--accent)") + .position("fixed") + .x(50, vw).y(50, vh) + .center() + .width(60, vw) + .height(60, vh) + .display("none") + .attr({id: "addPanel"}) + + HStack(() => { + input("Search messages... (Coming Soon!)", "45vw") + .attr({ + "type": "text", + "disabled": "true" + }) + .fontSize(1.1, em) + .paddingLeft(1.3, em) + .background("transparent") + .border("0.5px solid var(--divider)") + .outline("none") + .color("var(--accent)") + .opacity(0.5) + .borderRadius(10, px) + .background("grey") + .cursor("not-allowed") + + button("+ New Message") + .width(13, em) + .marginLeft(1, em) + .borderRadius(10, px) + .background("transparent") + .border("0.5px solid var(--divider)") + .color("var(--accent)") + .fontFamily("Bona Nova") + .onHover(function (hovering) { + if(hovering) { + this.style.background = "var(--green)" + + } else { + this.style.background = "transparent" + + } + }) + .onClick((done) => { + console.log("click") + if(done) { + this.$("#addPanel").style.display = "flex" + } + console.log(this, "clicked") + }) + + }) + .x(55, vw).y(4, vh) + .position("absolute") + .transform("translateX(-50%)") + }) + .width(100, "%") + .height(100, "%") + } +} + +register(Messages) \ No newline at end of file diff --git a/src/apps/Messages/MessagesPanel.js b/src/apps/Messages/MessagesPanel.js new file mode 100644 index 0000000..b608212 --- /dev/null +++ b/src/apps/Messages/MessagesPanel.js @@ -0,0 +1,56 @@ +import "../../components/LoadingCircle.js" + +class MessagesPanel extends Shadow { + messages + + constructor(messages) { + super() + this.messages = messages + } + + render() { + VStack(() => { + if(this.messages) { + for(let i=0; i { + HStack(() => { + p(message.from.firstName + " " + message.from.lastName) + .fontWeight("bold") + .marginBottom(0.3, em) + + p(util.formatTime(message.time)) + .opacity(0.2) + .marginLeft(1, em) + }) + p(message.text) + }) + .paddingVertical(0.5, em) + .marginLeft(fromMe ? 70 : 0, pct) + .paddingRight(fromMe ? 10 : 0, pct) + .marginRight(fromMe ? 0 : 70, pct) + .paddingLeft(fromMe ? 5 : 10, pct) + .background(fromMe ? "var(--brown)" : "var(--green)") + } + } else { + LoadingCircle() + } + }) + .onAppear(async () => { + requestAnimationFrame(() => { + this.scrollTop = this.scrollHeight + }); + }) + .gap(1, em) + .position("relative") + .overflow("scroll") + .height(95, pct) + .width(100, pct) + .paddingTop(2, em) + .paddingBottom(2, em) + .backgroundColor("var(--darkbrown)") + } +} + +register(MessagesPanel) \ No newline at end of file diff --git a/src/apps/Messages/MessagesSidebar.js b/src/apps/Messages/MessagesSidebar.js new file mode 100644 index 0000000..453a91d --- /dev/null +++ b/src/apps/Messages/MessagesSidebar.js @@ -0,0 +1,73 @@ +class MessagesSidebar extends Shadow { + conversations = [] + selectedConvoID + onSelect + + constructor(conversations, selectedConvoID, onSelect) { + super() + this.conversations = conversations + this.selectedConvoID = selectedConvoID + this.onSelect = onSelect + } + + render() { + VStack(() => { + this.conversations.forEach((convo, i) => { + + VStack(() => { + HStack(() => { + + p(this.makeConvoTitle(convo.between)) + .textAlign("left") + .marginLeft(0.5, inches) + .paddingTop(0.2, inches) + .width(100, pct) + .marginTop(0) + .fontSize(1, em) + .fontWeight("bold") + + p(util.formatTime(convo.messages.last.time)) + .paddingTop(0.2, inches) + .fontSize(0.8, em) + .marginRight(0.1, inches) + .color("var(--divider") + }) + .justifyContent("space-between") + .marginBottom(0) + + p(convo.messages.last.text) + .fontSize(0.8, em) + .textAlign("left") + .marginLeft(0.5, inches) + .marginBottom(2, em) + .color("var(--divider)") + }) + .background(convo.id === this.selectedConvoID ? "var(--darkbrown)" : "") + .onClick(() => { + this.onSelect(i) + }) + }) + }) + .minWidth(15, vw) + .height(100, vh) + .gap(0, em) + } + + makeConvoTitle(members) { + let membersString = "" + for(let i=0; i 2) { + membersString += member.firstName + } else { + membersString += member.firstName + " " + member.lastName + } + } + return membersString + } +} + +register(MessagesSidebar) \ No newline at end of file diff --git a/src/apps/Tasks/Tasks.js b/src/apps/Tasks/Tasks.js new file mode 100644 index 0000000..4b0e733 --- /dev/null +++ b/src/apps/Tasks/Tasks.js @@ -0,0 +1,153 @@ +css(` + tasks- { + font-family: 'Bona'; + } + + tasks- input::placeholder { + font-family: 'Bona Nova'; + font-size: 0.9em; + color: var(--accent); + } + + input[type="checkbox"] { + appearance: none; /* remove default style */ + -webkit-appearance: none; + width: 1em; + height: 1em; + border: 1px solid var(--accent); + } + + input[type="checkbox"]:checked { + background-color: var(--red); + } +`) + +class Tasks extends Shadow { + projects = [ + { + "title": "Blockcatcher", + "tasks": {} + } + ] + columns = [ + { + "title": "backlog", + "tasks": {} + } + ] + + render() { + ZStack(() => { + HStack(() => { + VStack(() => { + h3("Projects") + .marginTop(0) + .marginBottom(1, em) + .marginLeft(0.4, em) + + if (this.projects.length >= 1) { + for(let i = 0; i < this.projects.length; i++) { + p(this.projects[i].title) + } + } else { + p("No Projects!") + } + }) + .height(100, vh) + .paddingLeft(2, em) + .paddingRight(2, em) + .paddingTop(2, em) + .gap(0, em) + .borderRight("0.5px solid var(--accent2)") + + HStack(() => { + if (this.columns.length >= 1) { + for(let i = 0; i < this.columns.length; i++) { + p(this.columns[i].name) + } + } else { + p("No Conversations!") + } + }) + .height(100, vh) + .paddingLeft(2, em) + .paddingRight(2, em) + .paddingTop(2, em) + .gap(0, em) + .borderRight("0.5px solid var(--accent2)") + }) + .width(100, "%") + .x(0).y(13, vh) + .borderTop("0.5px solid var(--accent2)") + + p("0 Items") + .position("absolute") + .x(50, vw).y(50, vh) + .transform("translate(-50%, -50%)") + + HStack(() => { + input("Search tasks...", "45vw") + .attr({ + "type": "text" + }) + .fontSize(1.1, em) + .paddingLeft(1.3, em) + .background("transparent") + .border("0.5px solid var(--accent2)") + .outline("none") + .color("var(--accent)") + .borderRadius(10, px) + + button("Search") + .marginLeft(2, em) + .borderRadius(10, px) + .background("transparent") + .border("0.5px solid var(--accent2)") + .color("var(--accent)") + .fontFamily("Bona Nova") + .onHover(function (hovering) { + if(hovering) { + this.style.background = "var(--green)" + + } else { + this.style.background = "transparent" + + } + }) + + button("+ New Task") + .width(9, em) + .marginLeft(1, em) + .borderRadius(10, px) + .background("transparent") + .border("0.5px solid var(--accent2)") + .color("var(--accent)") + .fontFamily("Bona Nova") + .onHover(function (hovering) { + if(hovering) { + this.style.background = "var(--green)" + + } else { + this.style.background = "transparent" + + } + }) + .onClick((clicking) => { + console.log(this, "clicked") + }) + + }) + .x(55, vw).y(4, vh) + .position("absolute") + .transform("translateX(-50%)") + }) + .width(100, "%") + .height(100, "%") + } + + connectedCallback() { + // Optional additional logic + } +} + +register(Tasks) \ No newline at end of file diff --git a/src/components/AppMenu.js b/src/components/AppMenu.js new file mode 100644 index 0000000..73aa805 --- /dev/null +++ b/src/components/AppMenu.js @@ -0,0 +1,68 @@ +class AppMenu extends Shadow { + selected = "" + + onNewSelection() { + this.$$("img").forEach((image) => { + image.style.background = "" + }) + } + + render() { + console.log("rendering") + HStack(() => { + img("/_/icons/Column.svg", "1.5em", "1.5em") + .attr({app: "forum"}) + .padding(0.5, em) + .borderRadius(10, px) + .onClick((finished, e) => { + if(finished) { + this.onNewSelection() + } + e.target.style.background = "var(--accent)" + console.log(e.target, e.target.style.background) + if(finished) { + window.navigateTo("/") + } + }) + img("/_/icons/letter.svg", "1.5em", "1.5em") + .attr({app: "messages"}) + .padding(0.5, em) + .borderRadius(10, px) + .onClick((finished, e) => { + if(finished) { + this.onNewSelection() + } + e.target.style.background = "rgb(112 150 114)" + if(finished) { + window.navigateTo("/messages") + } + }) + img("/_/icons/jobs.svg", "1.5em", "1.5em") + .attr({app: "jobs"}) + .padding(0.5, em) + .borderRadius(10, px) + .onClick((finished, e) => { + if(finished) { + this.onNewSelection() + } + e.target.style.background = "#9392bb" + if(finished) { + window.navigateTo("/jobs") + } + }) + }) + .borderTop("1px solid black") + .height("auto") + .position('fixed') + .background("var(--main)") + .zIndex(1) + .x(0).yBottom(0) + .justifyContent("space-between") + .paddingHorizontal(4, em) + .paddingVertical(1, em) + .width(100, vw) + .boxSizing("border-box") + } +} + +register(AppMenu) \ No newline at end of file diff --git a/src/components/LoadingCircle.js b/src/components/LoadingCircle.js new file mode 100644 index 0000000..f71a86c --- /dev/null +++ b/src/components/LoadingCircle.js @@ -0,0 +1,25 @@ +class LoadingCircle extends Shadow { + render() { + div() + .borderRadius(100, pct) + .width(2, em).height(2, em) + .x(45, pct).y(50, pct) + .center() + .backgroundColor("var(--accent") + .transition("transform 1.75s ease-in-out") + .onAppear(function () { + let growing = true; + + setInterval(() => { + if (growing) { + this.style.transform = "scale(1.5)"; + } else { + this.style.transform = "scale(0.7)"; + } + growing = !growing; + }, 750); + }); + } +} + +register(LoadingCircle) \ No newline at end of file diff --git a/src/components/Sidebar.js b/src/components/Sidebar.js new file mode 100644 index 0000000..75f5180 --- /dev/null +++ b/src/components/Sidebar.js @@ -0,0 +1,46 @@ +class Sidebar extends Shadow { + + SidebarItem(text) { + return p(text) + .fontSize(1.5, em) + .fontWeight("bold") + .fontFamily("Sedan SC") + .marginLeft(2, em) + .fontStyle("italic") + .onClick(function () { + if(this.innerText === "Home") { + window.navigateTo("/") + return + } + window.navigateTo(this.innerText.toLowerCase().replace(/\s+/g, "")) + }) + } + + render() { + VStack(() => { + this.SidebarItem("Home") + this.SidebarItem("Map") + this.SidebarItem("Logout") + }) + .gap(2, em) + .paddingTop(30, vh) + .height(100, vh) + .width(70, vw) + .borderLeft("1px solid black") + .position("fixed") + .background("var(--main)") + .xRight(-70, vw) + .transition("right .3s") + .zIndex(1) + } + + toggle() { + if(this.style.right === "-70vw") { + this.style.right = "0vw" + } else { + this.style.right = "-70vw" + } + } +} + +register(Sidebar) \ No newline at end of file diff --git a/src/css/style.css b/src/css/style.css deleted file mode 100644 index 2b35d05..0000000 --- a/src/css/style.css +++ /dev/null @@ -1,12 +0,0 @@ -html, -body { - padding: 0; - margin: 0; -} - -body { - 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/index.html b/src/index.html index bbb5f72..39e98bc 100644 --- a/src/index.html +++ b/src/index.html @@ -2,7 +2,7 @@ - Blockcatcher + Forum - + diff --git a/src/index.js b/src/index.js index c1feeba..5892631 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,3 @@ -import "./js/Home.js" +import "./Home.js" Home() document.body.style.backgroundColor = "var(--main)" \ No newline at end of file diff --git a/src/js/Home.js b/src/js/Home.js deleted file mode 100644 index 2a6c5df..0000000 --- a/src/js/Home.js +++ /dev/null @@ -1,197 +0,0 @@ -class Home extends Shadow { - - tracking = false - logs = [] - timer = null - - async startTracking() { - if (!navigator.geolocation) { - alert("Geolocation is not supported by your browser."); - return; - } - - this.tracking = true - - navigator.geolocation.requestPermission?.() || Promise.resolve('granted'); - - const permission = await new Promise((resolve) => { - navigator.geolocation.getCurrentPosition( - () => resolve('granted'), - () => resolve('denied') - ); - }); - - if (permission === 'denied') { - alert("Location permission required"); - this.tracking = false - return; - } - - const timer = setInterval(async () => { - navigator.geolocation.getCurrentPosition( - async (pos) => { - const { latitude, longitude } = pos.coords; - const now = new Date(); - const log = `${now.toISOString()} — (${latitude}, ${longitude})`; - const timestamp = this.formatCentralTime(now); - - this.logs = [log, ...this.logs] - - this.updateLogs() - await this.sendLocation(latitude, longitude, timestamp); - }, - (err) => console.error("Location error:", err), - { enableHighAccuracy: true } - ); - }, 1000); - - this.timer = timer - } - - stopTracking() { - if (this.timer) { - clearInterval(this.timer); - this.timer = null; - this.tracking = false; - } - } - - render() { - ZStack(() => { - // Title bar - HStack(() => { - img("./_/icons/runner.svg", "2em", "2em") - p("San Marcos, TX") - .fontSize(1.2, em) - img("./_/icons/hamburger.svg", "2em", "2em") - }) - .gap(15, vw) - .position("fixed") - .top(0) - .left(0) - .width(100, vw) - .paddingTop(2, em) - .paddingBottom(2, em) - .borderBottom("0.1rem solid var(--text)") - .backgroundColor("var(--yellow)") - .fontWeight("bold") - .display("flex") - .alignItems("center") - .justifyContent("center") - .zIndex(10) - - VStack(() => { - button(() => { - if(this.tracking) { - Rectangle("48%", "48%") - .fill("#264B61") - .stroke("0.2em", "black") - } else { - Triangle("58%", "58%") - .fill("#9F292B") - .stroke("0.2em", "black") - } - }) - .attr({"id": "playButton"}) - .width(120, px) - .height(120, px) - .x(50, vw).y(50, vh) - .center() - .borderRadius(50, "%") - .backgroundColor(this.tracking ? "#CD593E" : "#A6EABD") - .border("0.2em solid black") - .cursor("pointer") - .display("flex") - .alignItems("center") - .justifyContent("center") - .onTap(() => { - console.log("tapped") - if (this.tracking) { - this.stopTracking(); - this.rerender() - } else { - this.startTracking(); - this.rerender() - } - }) - - VStack(() => { - this.logs.map(log => - div(log) - .paddingHorizontal("8px") - .paddingVertical("12px") - .backgroundColor("rgba(255,255,255,0.9)") - .borderRadius(6, px) - .marginBottom(6, px) - .fontSize(0.9, rem) - .color("#222") - .fontFamily("monospace") - ) - }) - .attr({"id": "logList"}) - .flex(1) - .overflowY("auto") - .paddingHorizontal(16, px) - }) - .marginTop(7, em) - }) - .overflowX("hidden") - .width(100, vw) - .height(100, vh) - .display("block") - .margin(0) - .color("var(--text)") - .fontFamily("Arial") - } - - showTracking() { - this.$("#playButton").rerender() - this.$("#playButton").style.backgroundColor = this.tracking ? "#CD593E" : "#A6EABD" - } - - updateLogs() { - let list = this.$("#logList") - list.rerender() - list.attr({"id": "logList"}) - } - - formatCentralTime(date) { - return new Intl.DateTimeFormat('en-US', { - timeZone: 'America/Chicago', - month: '2-digit', - day: '2-digit', - year: '2-digit', - hour: 'numeric', - minute: '2-digit', - second: '2-digit', - hour12: true - }) - .format(date) - .replace(/,/, '') // "04/05/25, 2:30:00 PM" → "04/05/25 2:30:00 PM" - .replace(/\//g, '.') // → "04.05.25 2:30:00 PM" - .toLowerCase() - .replace(' pm', 'pm') - .replace(' am', 'am'); - } - - async sendLocation(lat, lon, timestamp) { - try { - const resp = await fetch('http://localhost:3008/api/location', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - name: "Freddy Krueger", - latitude: lat, - longitude: lon, - timestamp - }) - }); - if (!resp.ok) throw new Error(resp.status); - console.log('Location sent'); - } catch (e) { - console.error('Failed to send location:', e); - } - } -} - -register(Home) \ No newline at end of file