diff --git a/src/Login.js b/src/Login.js new file mode 100644 index 0000000..72fe4b5 --- /dev/null +++ b/src/Login.js @@ -0,0 +1,47 @@ +class Login extends Shadow { + inputStyles(el) { + return el + .background("var(--main)") + .color("var(--accent)") + .border("1px solid var(--accent)") + } + + render() { + ZStack(() => { + img("/_/icons/column.svg", window.isMobile() ? "5vmax" : "3vmax") + .position("absolute") + .top(2, em) + .left(2, em) + .onClick((done) => { + window.navigateTo("/") + }) + + form(() => { + input("Email", "60vw") + .attr({name: "email", type: "email"}) + .margin(1, em) + .padding(1, em) + .styles(this.inputStyles) + input("Password", "60vw") + .attr({name: "password", type: "password"}) + .margin(1, em) + .padding(1, em) + .styles(this.inputStyles) + button("Submit") + .margin(1, em) + .padding(1, em) + .background("var(--main)") + .color("var(--accent)") + .border("1px solid var(--accent)") + }) + .attr({action: "/login", method: "POST"}) + .x(50, vw).y(50, vh) + .center() + }) + .background("var(--main)") + .width(100, vw) + .height(100, vh) + } +} + +register(Login) \ No newline at end of file diff --git a/src/_/code/bridge/bridge.js b/src/_/code/bridge/bridge.js new file mode 100644 index 0000000..be3ae76 --- /dev/null +++ b/src/_/code/bridge/bridge.js @@ -0,0 +1,38 @@ +const IS_NODE = + typeof process !== "undefined" && + process.versions?.node != null + +async function bridgeSend(name, args) { + // Example browser implementation: send function call to server + const res = await global.Socket.send({ + name: name, + args: args + }) + + const json = await res.json() + if (!res.ok) throw new Error(json.error) + return json.result +} + +/** + * Wraps an object of functions so that: + * - Node calls the real function + * - Browser calls bridgeSend + */ +export function createBridge(funcs) { + return new Proxy(funcs, { + get(target, prop) { + const orig = target[prop] + + if (typeof orig !== "function") return orig + + return function (...args) { + if (IS_NODE) { + return orig(...args) + } else { + return bridgeSend(prop, args) + } + } + } + }) +} diff --git a/src/_/code/bridge/serverFunctions.js b/src/_/code/bridge/serverFunctions.js new file mode 100644 index 0000000..52682f1 --- /dev/null +++ b/src/_/code/bridge/serverFunctions.js @@ -0,0 +1,11 @@ +import fs from "fs" +import { createBridge } from "./bridge.js" + +const handlers = { + getProfile(one, two) { + fs.writeFileSync("output.txt", `${one} ${two}`) + return "written to disk" + }, +} + +export const { getProfile } = createBridge(handlers) diff --git a/src/_/code/ws/Connection.js b/src/_/code/ws/Connection.js new file mode 100644 index 0000000..9119a35 --- /dev/null +++ b/src/_/code/ws/Connection.js @@ -0,0 +1,61 @@ +class Connection { + connectionTries = 0; + ws; + receiveCB; + + constructor(receiveCB) { + this.receiveCB = receiveCB; + } + + init = async () => { + return new Promise((resolve, reject) => { + const url = window.location.hostname.includes("local") + ? "ws://" + window.location.host + : "wss://" + window.location.hostname + window.location.pathname; + + this.ws = new WebSocket(url); + + this.ws.addEventListener('open', () => { + this.connectionTries = 0; + console.log("WebSocket connection established."); + this.ws.addEventListener('message', this.receiveCB); + resolve(this.ws); // resolve when open + }); + + this.ws.addEventListener('close', () => { + console.log('WebSocket closed'); + this.checkOpen(); // attempt reconnection + }); + + this.ws.addEventListener('error', (err) => { + console.error('WebSocket error', err); + reject(err); // reject if error occurs + }); + }); + } + + checkOpen = async () => { + if (this.ws.readyState === WebSocket.OPEN) { + return true; + } else { + await this.sleep(this.connectionTries < 20 ? 5000 : 60000); + this.connectionTries++; + console.log('Reestablishing connection'); + await this.init(); + } + } + + sleep = (time) => new Promise(resolve => setTimeout(resolve, time)); + + send = (msg) => { + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.send(msg); + } else if (this.connectionTries === 0) { + setTimeout(() => this.send(msg), 100); + } else { + console.error('No WebSocket connection: Cannot send message'); + } + } +} + +export default Connection; diff --git a/src/_/code/ws/Socket.js b/src/_/code/ws/Socket.js new file mode 100644 index 0000000..55c249c --- /dev/null +++ b/src/_/code/ws/Socket.js @@ -0,0 +1,49 @@ +import Connection from "./Connection.js"; + +export default class Socket { + connection; + disabled = true; + requestID = 1; + pending = new Map(); + + constructor() { + this.connection = new Connection(this.receive); + } + + async init() { + await this.connection.init() + } + + isOpen() { + if(this.connection.checkOpen()) { + return true; + } else { + return false; + } + } + + send(msg) { + return new Promise(resolve => { + const id = (++this.requestID).toString(); + this.pending.set(id, resolve); + this.connection.send(JSON.stringify({ id, ...msg })); + }); + } + + receive = (event) => { + const msg = JSON.parse(event.data); + if (msg.id && this.pending.has(msg.id)) { + this.pending.get(msg.id)(msg); + this.pending.delete(msg.id); + return; + } else { + this.onBroadcast(msg) + } + } + + onBroadcast(msg) { + window.dispatchEvent(new CustomEvent(msg.event, { + detail: msg.msg + })); + } +} \ No newline at end of file diff --git a/src/_/code/ws/shim/fs.js b/src/_/code/ws/shim/fs.js new file mode 100644 index 0000000..56004c9 --- /dev/null +++ b/src/_/code/ws/shim/fs.js @@ -0,0 +1 @@ +export default {} \ No newline at end of file diff --git a/src/env.js b/src/env.js new file mode 100644 index 0000000..f59515e --- /dev/null +++ b/src/env.js @@ -0,0 +1,3 @@ +class env { + BASE_URL = "https://parchment.page" +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 5892631..cfafaae 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,154 @@ +import Socket from "/_/code/ws/Socket.js" import "./Home.js" -Home() -document.body.style.backgroundColor = "var(--main)" \ No newline at end of file +import "./Login.js" + +let Global = class { + Socket = new Socket() + profile = null + currentNetwork = "" + currentApp = "" + + openApp = function(appName) { + const appUrl = appName.charAt(0).toLowerCase() + appName.slice(1); + let parts = window.location.pathname.split('/').filter(Boolean); + let newPath = "/" + parts[0] + "/" + appUrl + window.navigateTo(newPath) + const event = new CustomEvent('appchange', { + detail: { name: appName } + }); + window.dispatchEvent(event) + } + + async fetchAppData() { + let personalSpace = this.currentNetwork === this.profile + let appData = await fetch(`/api/${personalSpace ? "my" : "org"}data/` + this.currentNetwork.id, {method: "GET"}) + let json = await appData.json() + return json + } + + onNavigate = async () => { + + let selectedNetwork = this.networkFromPath() + let selectedApp = this.appFromPath() + + if(!selectedNetwork) { + if(this.profile.networks.length > 0) { + let path = `/${this.getDefaultNetworkName()}/${this.getDefaultAppName()}` + history.replaceState({}, '', path) + } + } else if(!selectedApp) { + if(this.currentNetwork === window.profile) { + history.replaceState({}, '', `${window.location.pathname}/${window.profile.apps[0]}`) + } else { + history.replaceState({}, '', `${window.location.pathname}/${this.getDefaultAppName()}`) + } + } + + selectedNetwork = this.networkFromPath() + selectedApp = this.appFromPath() + + let networkChanged = this.currentNetwork !== selectedNetwork + let appChanged = this.currentApp !== selectedApp + if(networkChanged) { + this.currentNetwork = selectedNetwork + this.currentApp = selectedApp + const event = new CustomEvent('networkchange', { + detail: { name: this.currentNetwork } + }); + window.dispatchEvent(event) + } + + if(!this.currentNetwork.data) { + this.currentNetwork.data = await this.fetchAppData() + } + + if(appChanged && !networkChanged) { + this.currentApp = selectedApp + const event = new CustomEvent('appchange', { + detail: { name: this.currentApp } + }); + window.dispatchEvent(event) + } + + document.title = (this.currentNetwork === this.profile) ? "Parchment" : `${this.currentNetwork.abbreviation} | Parchment` + } + + setCurrentNetworkAndApp() { + this.currentNetwork = this.networkFromPath() + + } + + getDefaultNetworkName() { + let defaultNetwork = this.profile.networks[0] + return defaultNetwork.abbreviation + } + + getDefaultAppName() { + let defaultNetwork = this.profile.networks[0] + return defaultNetwork.apps[0].toLowerCase() + } + + networkFromPath = function () { + const pathname = window.location.pathname; + const firstSegment = pathname.split('/').filter(Boolean)[0] || ''; + if(firstSegment === "my") { + return this.profile + } else { + let networks = this.profile.networks + for(let i = 0; i < networks.length; i++) { + let network = networks[i] + if(network.abbreviation === firstSegment) { + return network + } + } + } + } + + appFromPath = function() { + const pathname = window.location.pathname; + const segments = pathname.split('/').filter(Boolean); + const secondSegment = segments[1] || "" + const capitalized = secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1); + return capitalized + } + + async getProfile() { + try { + const res = await fetch("/profile", { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json" + } + }); + + if(res.status === 401) { + return res.status + } + if (!res.ok) throw new Error("Failed to fetch profile"); + + const profile = await res.json(); + console.log("getProfile: ", profile); + this.profile = profile + } catch (err) { + console.error(err); + } + } + + constructor() { + window.addEventListener("navigate", this.onNavigate) + + this.getProfile().then(async (status) => { + + if(status !== 401) { + console.log("it's legit") + await this.onNavigate() + Home() + } else { + Login() + } + }) + } +} + +window.global = new Global() \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 86aeb5d..7889a87 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,6 +8,20 @@ export default defineConfig({ emptyOutDir: true, }, server: { + proxy: { + "/login": { + target: "http://localhost:10002", + changeOrigin: true + }, + "/profile": { + target: "http://localhost:10002", + changeOrigin: true + }, + "/api": { + target: "http://localhost:10002", + changeOrigin: true + } + }, host: true, allowedHosts: ['sam.local'], }