Compare commits

..

20 Commits

Author SHA1 Message Date
Sam
093007bc76 fixing mounted app routes, fixing sql for prod 2026-03-11 21:47:06 +00:00
metacryst
5001b8203d small 2026-03-10 19:45:49 -05:00
metacryst
042648512f readme 2026-03-10 19:09:53 -05:00
metacryst
7cfcc01c99 working with db, docker working, small error with personal data 2026-03-10 19:09:47 -05:00
Sam
4d2c515b7d working on droplet, adding app components 2026-03-10 21:15:47 +00:00
metacryst
fc81da9743 more styling 2026-03-06 08:08:27 -06:00
metacryst
f51045c5e5 table styling 2026-03-06 08:04:10 -06:00
metacryst
b22b12a7a5 showing more info for stripe members, showing data in proper table 2026-03-06 07:58:03 -06:00
metacryst
4061e86ce7 showing stripe subscribers. loading really slow. 2026-03-05 05:41:45 -06:00
metacryst
661bf86a1a Stripe integration flow 2026-03-05 00:29:34 -06:00
metacryst
bdd260c2b5 nicer buttons 2026-02-28 04:56:19 -06:00
metacryst
21f654bbed Styling improvements 2026-02-27 02:05:39 -06:00
metacryst
b908354c04 better formatted time and ability to view messages in large mode 2026-02-26 17:30:21 -06:00
metacryst
402a99d55c Adding Stripe onboarding (mostly), changing name and branding to Forum 2026-02-26 02:12:24 -06:00
metacryst
dd9552aad1 rebranding to forum, adding settings and stripe page 2026-02-20 03:51:31 -06:00
aaf9d56b1b CRUD operations for Posts
- added all CRUD operations for Post model
- add() also creates POST_BY_MEMBER and POST_FROM_NETWORK edges
- delete() also deletes associated edges
- delete() currently does not reallocate node/edge indices after deleting
- will write the data to the db.json file and global.db.nodes
2026-02-08 22:34:18 -05:00
metacryst
d6520bf007 Merge branch 'main' of https://git.sun.museum/sam/parchment.page 2026-02-04 23:39:41 -06:00
metacryst
4134685b0a fixing new member signup 2026-02-04 18:11:43 -06:00
metacryst
083b110c2d cors settings, endpoint adjustments so mobile can hit 2026-01-29 08:21:15 -06:00
metacryst
d3df5bb6cb switching networks works, established server functions 2026-01-16 05:22:52 -06:00
77 changed files with 1940 additions and 826 deletions

6
.gitignore vendored
View File

@@ -4,3 +4,9 @@ node_modules
.env .env
db/db.json db/db.json
ui/_/code/env.js
apps/*
csv.csv
csv.js
csv.json

6
Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
FROM node:18-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "server/index.js"]

58
db/01_init.sql Normal file
View File

@@ -0,0 +1,58 @@
-- members
CREATE TABLE IF NOT EXISTS members (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
password TEXT NOT NULL,
apps TEXT[] DEFAULT '{}',
created TIMESTAMPTZ DEFAULT NOW()
);
-- networks
CREATE TABLE IF NOT EXISTS networks (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
logo TEXT,
abbreviation TEXT,
apps TEXT[] DEFAULT '{}',
stripe_account_id TEXT,
stripe_access_token TEXT,
created TIMESTAMPTZ DEFAULT NOW()
);
-- edges (member in network)
CREATE TABLE IF NOT EXISTS member_network (
id SERIAL PRIMARY KEY,
member_id INTEGER REFERENCES members(id),
network_id INTEGER REFERENCES networks(id),
type TEXT NOT NULL DEFAULT 'IN',
created TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(member_id, network_id)
);
-- create schema
CREATE SCHEMA IF NOT EXISTS org_1;
-- join form table
CREATE TABLE IF NOT EXISTS org_1.join_form (
id SERIAL PRIMARY KEY,
fname TEXT NOT NULL,
lname TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
county TEXT,
time TIMESTAMPTZ DEFAULT NOW()
);
-- contact form table
CREATE TABLE IF NOT EXISTS org_1.contact_form (
id SERIAL PRIMARY KEY,
fname TEXT NOT NULL,
lname TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
message TEXT,
county TEXT,
time TIMESTAMPTZ DEFAULT NOW()
);

31
db/02_seed.sql Normal file
View File

@@ -0,0 +1,31 @@
-- members
INSERT INTO members (email, first_name, last_name, password, apps, created) VALUES
('samrussell99@pm.me', 'Sam', 'Russell', '$argon2id$v=19$m=65536,t=3,p=4$n/8BaBisEnBaQNbkxzs1VA$dvvnupWNtB5w5qTBgEciDsNA6rOgXaEypcEK1A0ndLM', '{"Dashboard"}', '2026-01-15 09:58:01.0072'),
('freddyjkrueger@gmail.com', 'Freddy','Krueger', '$argon2id$v=19$m=65536,t=3,p=4$ioAYDPtyUulykMrH9W7q9A$lG43cq6Dj3/n1+bJrkupWpB5Xro3UIQaVd9rjuJJ6nM', '{"Dashboard"}', '2026-01-13 13:38:46.0810'),
('harmysmarmy@gmail.com', 'Harmy', 'Smarmy', '$argon2id$v=19$m=65536,t=3,p=4$FAhGtCtqNAQ19tBYD73wXQ$0AM/khyBFFuX2mv0ieqtGfsXRgtEldWKFwyeV3BA3Xk', '{"Dashboard"}', '2026-01-13 13:41:41.0722');
-- networks
INSERT INTO networks (name, logo, abbreviation, apps, stripe_account_id, stripe_access_token, created) VALUES
('Comal County Young Republicans', 'comalyr.svg', 'comalyr', '{"Dashboard","People","Settings"}', 'acct_1Sn6DwLpyskwAml9', 'sk_test_51Sn6DwLpyskwAml9JzWYlSee2we70EZ2nCwgrEVFGkaPdzXLUBXEDeiMSaVUZtvUowEl5nh07N3CEFYymjDvMXok00vaBYPNgn', '2026-01-10 09:58:01.0074'),
('Hyperia', 'hyperia.svg', 'hyperia', '{"Dashboard","People","Settings"}', 'acct_1S4w0GHZemeF9CKR', 'sk_test_51S4w0GHZemeF9CKR2P1B4N7eO51aUcgTuOY5KRiKYXAQXuGaKblXitmBp58fYA200E0N7nHzgZxin1eNkhp2K1xB00q9hFNuQM', '2026-01-10 09:58:01.0074');
-- member_network edges
INSERT INTO member_network (member_id, network_id, type, created) VALUES
(1, 1, 'IN', '2025-11-24 00:54:36.0784'),
(2, 1, 'IN', '2026-01-13 13:14:28.0178'),
(3, 1, 'IN', '2026-01-13 13:28:35.0701'),
(1, 2, 'IN', '2026-01-13 13:28:35.0701');
-- join form seed data
INSERT INTO org_1.join_form (fname, lname, email, phone, county, time) VALUES
('James', 'Mitchell', 'james.mitchell@gmail.com', '512-555-0101', 'Comal', '2025-12-16 23:11:31.0011'),
('Rachel', 'Torres', 'rachel.torres@yahoo.com', '512-555-0102', 'Bexar', '2025-12-19 19:23:12.0717'),
('David', 'Nguyen', 'david.nguyen@gmail.com', '830-555-0103', 'Comal', '2026-01-06 16:55:29.0288'),
('Emily', 'Sanders', 'emily.sanders@outlook.com', '210-555-0104', 'Hays', '2026-01-07 17:14:01.0711');
-- contact form seed data
INSERT INTO org_1.contact_form (fname, lname, email, phone, county, message, time) VALUES
('Marcus', 'Webb', 'marcus.webb@gmail.com', '512-555-0201', 'Comal', 'Interested in volunteering at upcoming events.', '2025-12-29 13:20:28.0157'),
('Sandra', 'Holloway', 'sandra.holloway@gmail.com', '830-555-0202', 'Comal', 'Would love to connect with your organization.', '2025-12-30 22:10:24.0971'),
('Robert', 'Finley', 'robert.finley@gmail.com', '210-555-0203', 'Comal', 'Looking forward to getting more involved locally.', '2026-01-10 21:23:51.0073'),
('Barbara', 'Crane', 'barbara.crane@outlook.com', '512-555-0204', 'Comal', 'Please reach out regarding the next meeting schedule.', '2026-01-10 21:23:54.0841');

View File

4
db/personal/1/db.json Normal file
View File

@@ -0,0 +1,4 @@
{
"nodes": {},
"edges": {}
}

36
docker-compose.yml Normal file
View File

@@ -0,0 +1,36 @@
version: "3.9"
services:
server:
container_name: server
build:
context: .
dockerfile: Dockerfile
ports:
- "10002:10002"
volumes:
- .:/usr/src/app
- node_modules:/usr/src/app/node_modules
- ./db:/usr/src/app/db
depends_on:
- db
env_file:
- .env
db:
image: postgres:16-alpine
restart: always
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
ports:
- "5432:5432"
volumes:
- db-data:/var/lib/postgresql/data
- ./db/01_init.sql:/docker-entrypoint-initdb.d/01_init.sql
- ./db/02_seed.sql:/docker-entrypoint-initdb.d/02_seed.sql
volumes:
db-data:
node_modules:

View File

@@ -7,15 +7,36 @@ img(`db/images/${networks[i].logo}`, "2.25em", "2.25em")
} }
}) })
.cursor("default") .cursor("default")
.DEFAULT() .Default()
.opacity(0) .opacity(0)
.borderLeft(0) .borderLeft(0)
.paddingLeft(10, px) .paddingLeft(10, px)
.HOVERED() .Hovered()
.opacity(0.8) .opacity(0.8)
.classStyle("selected") .Watch(global.currentNetwork)
.borderLeft("1px solid var(--accent)") .If((val) => val === this.getAttribute("network"))
.paddingLeft(9, px) .borderLeft("1px solid black")
.paddingLeft(9, px)
.ElseIf((val) => val !== this.getAttribute("network"))
.borderLeft("0")
.paddingLeft(10, px)
render() {
ZStack(() => {
Watch(global.currentNetwork)
.Case("Dashboard", () => {
Dashboard()
})
.Case("People", () => {
People()
})
})
.overflow("scroll")
.position("absolute")
.onEvent("resize", () => {
this.rerender()
})

View File

@@ -1,10 +1,13 @@
{ {
"name": "Parchment", "name": "Forum",
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "node server/index.js" "start": "node server/index.js"
}, },
"workspaces": [
"apps/*"
],
"dependencies": { "dependencies": {
"argon2": "^0.44.0", "argon2": "^0.44.0",
"chalk": "^4.1.2", "chalk": "^4.1.2",
@@ -15,6 +18,7 @@
"express-useragent": "^2.0.2", "express-useragent": "^2.0.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"moment": "^2.30.1", "moment": "^2.30.1",
"postgres": "^3.4.8",
"stripe": "^20.0.0", "stripe": "^20.0.0",
"ws": "^8.18.3", "ws": "^8.18.3",
"zod": "^4.1.12" "zod": "^4.1.12"

10
readme.md Normal file
View File

@@ -0,0 +1,10 @@
## Start site and db (does it work?)
docker compose up --build
## Start docker db
docker compose up db
## Re-seed db
docker compose down -v

View File

@@ -28,21 +28,42 @@ export default class AuthHandler {
} }
} }
getProfile(req, res) { getRequestEmail(req, res) {
const token = req.cookies.auth_token; const token = req.cookies?.auth_token;
if (!token) return res.status(401).send({ error: "No auth token" }); if (!token) return res.status(401).send({ error: "No auth token" });
try { try {
const payload = jwt.verify(token, process.env.JWT_SECRET); const payload = jwt.verify(token, process.env.JWT_SECRET);
const email = payload.email; const email = payload.email;
return email
} catch (e) {
console.error("Error getting profile: ", e)
throw new Error("Error getting email: invalid auth token")
}
}
const user = db.members.getByEmail(email); async getProfile(req, res) {
let connections = db.MEMBER_IN_NETWORK.getByMember(db.members.prefix + "-" + user.id) try {
let userOrgs = connections.map((c) => { let email = global.auth.getRequestEmail(req, res)
return db.networks.get(c.to)
})
res.send({ email: user.email, name: user.firstName + " " + user.lastName, networks: userOrgs}); const user = await db.members.getByEmail(email);
const connections = await db.MEMBER_IN_NETWORK.getByMember(user.id);
const userOrgs = await Promise.all(connections.map(async (c) => {
const network = await db.networks.get(c.network_id);
delete network.stripe_account_id;
delete network.stripe_access_token;
return network;
}));
res.send({
id: user.id,
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
networks: userOrgs,
apps: user.apps
});
} catch (e) { } catch (e) {
console.error("Error getting profile: ", e) console.error("Error getting profile: ", e)
res.status(401).send({ error: "Invalid token" }); res.status(401).send({ error: "Invalid token" });
@@ -51,7 +72,7 @@ export default class AuthHandler {
async login(req, res) { async login(req, res) {
const { email, password } = req.body; const { email, password } = req.body;
let foundUser = global.db.members.getByEmail(email) let foundUser = await global.db.members.getByEmail(email)
if(!foundUser) { if(!foundUser) {
res.status(400).json({ error: 'Incorrect email.' }); res.status(400).json({ error: 'Incorrect email.' });
return; return;

View File

@@ -1,14 +1,10 @@
import fs from 'fs/promises'; import fs from 'fs/promises';
import chalk from 'chalk'; import chalk from 'chalk';
import path from 'path'; import path from 'path';
import {nodeModels, edgeModels} from './model/import.js' import { nodeModels, edgeModels } from './model/import.js'
import Edge from "./model/edge.js" import Edge from "./model/edge.js"
export default class Database { export default class Database {
nodes = new Array(10000).fill(0);
edges = new Array(10000).fill(0);
constructor() { constructor() {
let values = Object.values(nodeModels) let values = Object.values(nodeModels)
for(let i = 0; i < values.length; i++) { for(let i = 0; i < values.length; i++) {
@@ -22,135 +18,5 @@ export default class Database {
this[key] = eValues[i] this[key] = eValues[i]
} }
this.edge = new Edge() this.edge = new Edge()
this.loadData()
}
getCurrentIndex(model) {
return model.indices[1] - model.indices[0]
}
getNextIndex(model) {
return model.indices[1] - model.indices[0] + 1
}
addNode(prefix, node) {
try {
let model = nodeModels[prefix]
if(model) {
let toAdd = {
id: this.getNextIndex(model),
...node,
}
if(!toAdd.created) toAdd.created = global.currentTime()
let schema = model.schema
let result = schema.safeParse(toAdd)
if(result.success) {
this.nodes[model.indices[1]] = toAdd
model.indices[1]++;
} else {
console.error(result.error)
throw new global.ServerError(400, "Invalid Data!");
}
} else {
throw new Error("Type does not exist for node: " + id)
}
} catch(e) {
throw e
}
}
addEdge(prefix, edge) {
try {
let type = prefix
let model = edgeModels[type]
if(model) {
let toAdd = {
id: model.indices[1] - model.indices[0] + 1,
...edge
}
if(!toAdd.created) toAdd.created = global.currentTime()
let schema = model.schema
let result = schema.safeParse(toAdd)
if(result.success) {
this.edges[model.indices[1]] = toAdd
model.indices[1]++;
} else {
console.error(result.error)
throw new global.ServerError(400, "Invalid Data!");
}
} else {
throw new Error("Type does not exist for edge: " + id)
}
} catch(e) {
throw e
}
}
async loadData() {
const dbData = await fs.readFile(path.join(process.cwd(), 'db/db.json'), 'utf8');
let dbJson;
try {
dbJson = JSON.parse(dbData);
} catch {
dbJson = []
}
let nodes = dbJson["nodes"];
let entries = Object.entries(nodes)
for(let i=0; i<entries.length; i++) {
let entry = entries[i]
let id = entry[0]; let node = entry[1];
this.addNode(id.split("-")[0], node)
}
let edges = dbJson["edges"];
let edgeEntries = Object.entries(edges)
for(let i=0; i<edgeEntries.length; i++) {
let entry = edgeEntries[i]
let id = entry[0]; let node = entry[1];
this.addEdge(id.split("-")[0], node)
}
setInterval(() => {
global.db.saveData()
}, 5000)
}
async saveData() {
let data = {
"nodes": {},
"edges": {}
}
let nModels = Object.values(nodeModels)
this.nodes.forEach((entry, i) => {
if(entry) {
for(let j = 0; j < nModels.length; j++) {
let model = nModels[j]
let indices = model.indices
if(i >= indices[0] && i < indices[1]) {
let prefix = model.prefix
data.nodes[prefix + "-" + entry.id] = entry
}
}
}
})
let eModels = Object.values(edgeModels)
this.edges.forEach((entry, i) => {
if(entry) {
for(let j = 0; j < eModels.length; j++) {
let model = eModels[j]
let indices = model.indices
if(i >= indices[0] && i < indices[1]) {
let prefix = model.prefix
data.edges[prefix + "-" + entry.id] = entry
}
}
}
})
let string = JSON.stringify(data, null, 4)
await fs.writeFile(path.join(process.cwd(), 'db/db.json'), string, "utf8");
} }
} }

View File

@@ -40,6 +40,7 @@ export default class Conversation {
function populateMemberProfilesFromIDs(ids) { function populateMemberProfilesFromIDs(ids) {
let result = [] let result = []
for(let i=0; i<ids.length; i++) { for(let i=0; i<ids.length; i++) {
console.warn("conversation.js 43: This call to global.db needs to be awaited")
result[i] = global.db.members.get(ids[i]) result[i] = global.db.members.get(ids[i])
} }
return result return result

View File

@@ -48,6 +48,7 @@ export default class Message {
let entry = this.entries[i] let entry = this.entries[i]
if(entry.conversation = convoID) { if(entry.conversation = convoID) {
let userID = entry.from let userID = entry.from
console.warn("dm.js 51: This call to global.db needs to be awaited")
let fromUser = global.db.members.get(userID) let fromUser = global.db.members.get(userID)
let newObj = { let newObj = {
...entry ...entry

View File

@@ -1,13 +1,21 @@
export default class Edge { export default class Edge {
add(n) { splitEdge(n) {
let toPrefix = n.to.split("-")[0] let toPrefix = n.to.split("-")[0]
let fromPrefix = n.from.split("-")[0] let fromPrefix = n.from.split("-")[0]
let type = n.type let type = n.type
let prefix = `${fromPrefix}_${type}_${toPrefix}` return `${fromPrefix}_${type}_${toPrefix}`
}
add(n) {
let prefix = this.splitEdge(n)
global.db.addEdge(prefix, n) global.db.addEdge(prefix, n)
} }
delete(n) {
let prefix = this.splitEdge(n)
global.db.deleteEdge(prefix, n.from.split("-")[1])
}
getByFrom(fromID) { getByFrom(fromID) {
let result = [] let result = []
for(let i = 0; i < this.entries.length; i++) { for(let i = 0; i < this.entries.length; i++) {

View File

@@ -1,39 +1,16 @@
import { z } from 'zod' // server/db/model/edges/MEMBER_IN_NETWORK.js
import sql from '../../sql.js';
export default class MEMBER_IN_NETWORK { export default class MEMBER_IN_NETWORK {
prefix = "MEMBER_IN_NETWORK" async getByMember(memberId) {
indices = null return await sql`
SELECT * FROM member_network WHERE member_id = ${memberId}
`;
}
constructor(indices) { async getByNetwork(networkId) {
this.indices = indices return await sql`
} SELECT * FROM member_network WHERE network_id = ${networkId}
`;
schema = z.object({ }
id: z.number(),
type: z.string(),
from: z.string(),
to: z.string(),
created: z.string()
})
.strict()
getByMember(stringID) {
let result = []
for(let i = this.indices[0]; i < this.indices[1]; i++) {
if(global.db.edges[i].from === stringID) {
result.push(global.db.edges[i])
}
}
return result
}
getByNetwork(id) {
let result = []
for(let i = this.indices[0]; i < this.indices[1]; i++) {
if(global.db.edges[i].to === `${global.db.networks.prefix}-${id}`) {
result.push(global.db.edges[i])
}
}
return result
}
} }

View File

@@ -0,0 +1,37 @@
import { z } from 'zod'
export default class POST_BY_MEMBER {
prefix = "POST_BY_MEMBER"
indices = null
constructor(indices) {
this.indices = indices
}
schema = z.object({
id: z.number(),
type: z.string(),
from: z.string(),
to: z.string(),
created: z.string()
})
.strict()
getAuthorId(postId) {
for (let i = this.indices[0]; i < this.indices[1]; i++) {
if (global.db.edges[i].from === postId) {
return Number(global.db.edges[i].to.split("-")[1]);
}
}
}
getByMember(stringID) {
let result = []
for(let i = this.indices[0]; i < this.indices[1]; i++) {
if(global.db.edges[i].from === stringID) {
result.push(global.db.edges[i])
}
}
return result
}
}

View File

@@ -0,0 +1,30 @@
import { z } from 'zod'
export default class POST_FROM_NETWORK {
prefix = "POST_FROM_NETWORK"
indices = null
constructor(indices) {
this.indices = indices
}
schema = z.object({
id: z.number(),
type: z.string(),
from: z.string(),
to: z.string(),
created: z.string()
})
.strict()
getByNetwork(id) {
let result = []
for(let i = this.indices[0]; i < this.indices[1]; i++) {
console.warn("POST_FROM_NETWORK.js 23: This call to global.db needs to be awaited")
if(global.db.edges[i].to === `${global.db.networks.prefix}-${id}`) {
result.push(global.db.edges[i])
}
}
return result
}
}

View File

@@ -1,6 +1,7 @@
import { z } from 'zod'; import { z } from 'zod';
export default class Post { export default class Post {
prefix = "POST"
indices = null indices = null
constructor(indices) { constructor(indices) {
@@ -8,13 +9,16 @@ export default class Post {
} }
schema = z.object({ schema = z.object({
id: z.number(),
text: z.string(), text: z.string(),
time: z.string(), time: z.string(),
sentBy: z.string() sentBy: z.string(),
edited: z.boolean()
}) })
makeID(forum, number) { makeID(forum, number) {
return `POST-${forum}-${number}` // return `POST-${forum}-${number}`
return `POST-${number}`
} }
save(post, id) { save(post, id) {
@@ -32,30 +36,136 @@ export default class Post {
} }
} }
get(forum, number) { get(forum, by, authorId = null) {
let result = [] let result = []
let limit = Math.min(number, this.entries.length) if (by === "member" && authorId) {
for(let i=1; i<=limit; i++) { result = this.getByMember(authorId)
let id = this.makeID(forum, i) } else { // network
let post = this.entries[this.ids[id]] console.warn("post.js 44: This call to global.db needs to be awaited")
let {firstName, lastName} = global.db.members.get(post.sentBy) let { id: networkId } = global.db.networks.getByAbbreviation(forum)
let seededObj = { result = this.getByNetwork(networkId)
...post
}
seededObj.sentByID = post.sentBy
seededObj.sentBy = firstName + " " + lastName
result.push(seededObj)
} }
return result return result
} }
getByID(id) {
if(typeof id === 'string') {
id = id.split("-")[1]
}
return {
id,
authorId: this.getAuthor(id),
...global.db.nodes[this.indices[0] + (id - 1)]
}
}
getAuthor(postId) {
return db.POST_BY_MEMBER.getAuthorId(`${this.prefix}-${postId}`);
}
getByNetwork(id) {
let connections = db.POST_FROM_NETWORK.getByNetwork(id)
let posts = []
connections.forEach((conn) => {
posts.push(this.getByID(conn.from))
})
return posts
}
getByMember(stringID) {
let connections = db.POST_BY_MEMBER.getByMember(stringID)
let posts = []
connections.forEach((conn) => {
posts.push(this.getByID(conn.from))
})
return posts
}
async edit(id, text, userEmail) {
try {
console.warn("post.js 86: This call to global.db needs to be awaited")
let userId = global.db.members.getByEmail(userEmail).id
let postToEdit = this.getByID(id)
if (postToEdit.authorId !== userId) {
throw new global.ServerError(401, "Not authorized to delete post!")
}
postToEdit.text = text
postToEdit.time = global.currentTime()
postToEdit.edited = true;
let { authorId, ...rest } = postToEdit
global.db.updateNode(this.prefix, id, rest)
return postToEdit
} catch(e) {
console.error(e)
throw new global.ServerError(400, "Failed to delete post!")
}
}
async delete(id, forum, userEmail) {
try {
console.warn("post.js 110: This call to global.db needs to be awaited")
let userId = global.db.members.getByEmail(userEmail).id
console.warn("post.js 110: This call to global.db needs to be awaited")
let network = global.db.networks.getByAbbreviation(forum)
if (this.getAuthor(id) !== userId) {
throw new global.ServerError(401, "Not authorized to delete post!")
}
global.db.deleteNode(this.prefix, id)
if(network) {
global.db.edge.delete({
type: "FROM",
from: `${this.prefix}-${id}`,
to: `NETWORK-${network.id}`
})
}
global.db.edge.delete({
type: "BY",
from: `${this.prefix}-${id}`,
to: `MEMBER-${userId}`
})
} catch(e) {
console.error(e)
throw new global.ServerError(400, "Failed to delete post!")
}
}
async add(text, forum, userEmail) { async add(text, forum, userEmail) {
let newPost = {} let newPost = {}
console.warn("post.js 141: This call to global.db needs to be awaited")
let sender = global.db.members.getByEmail(userEmail)
console.warn("post.js 143: This call to global.db needs to be awaited")
let network = global.db.networks.getByAbbreviation(forum)
newPost.text = text newPost.text = text
newPost.sentBy = db.members.getIDFromEmail(userEmail)
newPost.time = global.currentTime() newPost.time = global.currentTime()
let idNumber = this.entries.length+1 try {
super.add(this.makeID(forum, idNumber), newPost) newPost.sentBy = `${sender.firstName} ${sender.lastName}`
global.db.addNode(this.prefix, newPost)
if(network) {
global.db.edge.add({
type: "FROM",
from: `${this.prefix}-${global.db.getCurrentIndex(this)}`,
to: `NETWORK-${network.id}`
})
}
global.db.edge.add({
type: "BY",
from: `${this.prefix}-${global.db.getCurrentIndex(this)}`,
to: `MEMBER-${sender.id}`
})
return `${this.prefix}-${global.db.getCurrentIndex(this)}`
} catch(e) {
console.error(e)
throw new global.ServerError(400, "Failed to add member!");
}
} }
} }

View File

@@ -1,36 +1,36 @@
import Title from "./title.js"
import Network from "./network.js" import Network from "./network.js"
import Member from './member.js' import Member from './member.js'
import Payment from "./payment.js"
import Post from "./forum/post.js" import Post from "./forum/post.js"
import Conversation from "./dms/conversation.js" import Conversation from "./dms/conversation.js"
import DM from "./dms/dm.js" import DM from "./dms/dm.js"
import MEMBER_IN_NETWORK from "./edges/MEMBER_IN_NETWORK.js" import MEMBER_IN_NETWORK from "./edges/MEMBER_IN_NETWORK.js"
import POST_FROM_NETWORK from "./edges/POST_FROM_NETWORK.js"
import POST_BY_MEMBER from "./edges/POST_BY_MEMBER.js"
let nIndices = { let nIndices = {
"MEMBER" : [0, 0], // [id, startIndex, nextEmptyIndex "MEMBER" : [0, 0], // [id, startIndex, nextEmptyIndex
"NETWORK" : [100, 100], "NETWORK" : [100, 100],
"TITLE" : [200, 200],
"PAYMENT" : [300, 300],
"POST" : [400, 400], "POST" : [400, 400],
"CONVERSATION" : [3000, 3000], "CONVERSATION" : [3000, 3000],
"DM" : [6000, 6000], "DM" : [6000, 6000],
} }
let eIndices = { let eIndices = {
"MEMBER_IN_NETWORK": [0, 0] "MEMBER_IN_NETWORK": [0, 0],
"POST_FROM_NETWORK": [4000, 4000],
"POST_BY_MEMBER": [7000, 7000]
} }
export let nodeModels = { export let nodeModels = {
MEMBER: new Member(nIndices.MEMBER), MEMBER: new Member(nIndices.MEMBER),
NETWORK: new Network(nIndices.NETWORK), NETWORK: new Network(nIndices.NETWORK),
TITLE: new Title(nIndices.TITLE),
PAYMENT: new Payment(nIndices.PAYMENT),
POST: new Post(nIndices.POST), POST: new Post(nIndices.POST),
CONVERSATION: new Conversation(nIndices.CONVERSATION), CONVERSATION: new Conversation(nIndices.CONVERSATION),
DM: new DM(nIndices.DM), DM: new DM(nIndices.DM),
} }
export let edgeModels = { export let edgeModels = {
MEMBER_IN_NETWORK: new MEMBER_IN_NETWORK(eIndices.MEMBER_IN_NETWORK) MEMBER_IN_NETWORK: new MEMBER_IN_NETWORK(eIndices.MEMBER_IN_NETWORK),
POST_FROM_NETWORK: new POST_FROM_NETWORK(eIndices.POST_FROM_NETWORK),
POST_BY_MEMBER: new POST_BY_MEMBER(eIndices.POST_BY_MEMBER)
} }

View File

@@ -1,66 +1,85 @@
import path from 'path';
import fs from 'fs';
import argon2 from 'argon2'; import argon2 from 'argon2';
import { z } from 'zod'; import { z } from 'zod';
import sql from '../sql.js';
export default class Member { export default class Member {
prefix = "MEMBER" schema = z.object({
indices = null email: z.string().email(),
firstName: z.string(),
lastName: z.string(),
password: z.string(),
apps: z.array(z.string())
});
constructor(indices) { isHashed = (s) => s.startsWith("$argon2");
this.indices = indices
async add(newMember, networkId = null) {
if (!this.isHashed(newMember.password)) {
newMember.password = await argon2.hash(newMember.password);
} }
schema = z.object({ const result = this.schema.safeParse(newMember);
id: z.number(), if (!result.success) {
email: z.string().email(), console.error(result.error);
firstName: z.string(), throw new global.ServerError(400, "Invalid Member Data!");
lastName: z.string(),
password: z.string(),
created: z.string()
})
isHashed = (s) => {return s.startsWith("$argon2")}
async add(newMember, network = null) {
const hash = await argon2.hash(newMember.password);
newMember.password = hash
try {
global.db.addNode(this.prefix, newMember)
if(network) {
global.db.edge.add({
type: "IN",
from: `${this.prefix}-${global.db.getCurrentIndex(this)}`,
to: "NETWORK-1"
})
}
} catch(e) {
console.error(e)
throw new global.ServerError(400, "Failed to add member!");
}
} }
getByNetwork(id) { try {
let connections = db.MEMBER_IN_NETWORK.getByNetwork(id) const [member] = await sql`
let members = [] INSERT INTO members ${sql({
connections.forEach((conn) => { email: newMember.email,
members.push(this.getByID(conn.from)) first_name: newMember.firstName,
}) last_name: newMember.lastName,
return members password: newMember.password,
} apps: newMember.apps ?? ['Dashboard']
})}
RETURNING *
`;
getByID(id) { if (networkId) {
if(typeof id === 'string') { await sql`
id = id.split("-")[1] INSERT INTO member_network ${sql({
} member_id: member.id,
return global.db.nodes[this.indices[0] + id - 1] network_id: networkId,
} type: 'IN'
})}
`;
}
getByEmail(email) { return member;
for(let i=this.indices[0]; i<this.indices[1]; i++) { } catch (e) {
if(global.db.nodes[i].email === email) { console.error(e);
return global.db.nodes[i] throw new global.ServerError(400, "Failed to add member!");
}
}
return null
} }
}
async getByID(id) {
const [member] = await sql`
SELECT * FROM members WHERE id = ${id}
`;
return member ?? null;
}
async getByEmail(email) {
const [member] = await sql`
SELECT * FROM members WHERE email = ${email}
`;
return member ?? null;
}
async getByNetwork(networkId) {
return await sql`
SELECT m.* FROM members m
JOIN member_network mn ON mn.member_id = m.id
WHERE mn.network_id = ${networkId}
`;
}
async getPersonalData(id) {
const filePath = path.join(global.db.PERSONAL_DATA_PATH, id, "db.json");
const raw = await fs.promises.readFile(filePath, "utf8");
return raw.trim() ? JSON.parse(raw) : [];
}
} }

View File

@@ -1,50 +1,61 @@
import { z } from 'zod'; import { z } from 'zod';
import sql from '../sql.js';
export default class Network { export default class Network {
prefix = `NETWORK`
indices = null
constructor(indices) {
this.indices = indices
}
schema = z.object({ schema = z.object({
id: z.number(),
name: z.string(), name: z.string(),
apps: z.array(z.string()), apps: z.array(z.string()),
logo: z.string(), logo: z.string(),
abbreviation: z.string(), abbreviation: z.string(),
created: z.string() stripeAccountId: z.string().nullable(),
}) stripeAccessToken: z.string().nullable()
.strict() }).strict();
get(stringID) { async get(id) {
let id = stringID.split("-")[1] const [network] = await sql`
let index = this.indices[0] + (id - 1) SELECT * FROM networks WHERE id = ${id}
return global.db.nodes[index] `;
return network ?? null;
} }
save(n) { async getByAbbreviation(abbreviation) {
let id = `${this.prefix}-${n.id}` const [network] = await sql`
let result = this.schema.safeParse(n) SELECT * FROM networks WHERE abbreviation = ${abbreviation}
if(result.success) { `;
try { return network ?? null;
super.add(id, n)
} catch(e) {
console.error(e)
throw e
}
} else {
console.error(result.error)
throw new global.ServerError(400, "Invalid Member Data!");
}
} }
add(n) { async update(id, data) {
let toSave = { if (data.id || data.created) {
id: this.entries.length+1, throw new Error("Can't update id or created time!");
...n
} }
this.save(toSave)
const [updated] = await sql`
UPDATE networks SET ${sql(data, ...Object.keys(data))}
WHERE id = ${id}
RETURNING *
`;
return updated;
}
async add(n) {
let result = this.schema.safeParse(n);
if (!result.success) {
console.error(result.error);
throw new global.ServerError(400, "Invalid Network Data!");
}
const [network] = await sql`
INSERT INTO networks ${sql({
name: n.name,
apps: n.apps,
logo: n.logo,
abbreviation: n.abbreviation,
stripe_account_id: n.stripeAccountId,
stripe_access_token: n.stripeAccessToken
})}
RETURNING *
`;
return network;
} }
} }

View File

@@ -1,47 +0,0 @@
import { z } from 'zod';
export default class Payment {
prefix = "PAYMENT"
indices = null
constructor(indices) {
this.indices = indices
}
schema = z.object({
id: z.number(),
name: z.string(),
email: z.string(),
time: z.string(),
amount: z.number(),
product: z.string(),
})
save(payment) {
let id = `${this.prefix}-${payment.id}`
let result = this.schema.safeParse(payment)
if(result.success) {
try {
super.add(id, payment)
} catch(e) {
console.error(e)
throw e
}
} else {
console.error(result.error)
throw new global.ServerError(400, "Invalid Member Data!");
}
}
add(paymentObj) {
let toSave = {
id: this.entries.length+1,
...paymentObj
}
this.save(toSave)
}
get(id) {
return this.entries[this.ids[`${this.prefix}-${id}`]]
}
}

View File

@@ -1,29 +0,0 @@
import { z } from 'zod';
export default class Title {
prefix = `TITLE`
indices = null
constructor(indices) {
this.indices = indices
}
schema = z.object({
id: z.number(),
name: z.string()
})
save(newTitle) {
let id = `HY-${this.entries.length+1}`
if(this.validate(id, newTitle)) {
try {
global.db.add(id, newTitle)
} catch(e) {
console.error(e)
throw e
}
} else {
throw new global.ServerError(400, "Invalid Member Data!");
}
}
}

View File

@@ -47,9 +47,11 @@ export let PAYMENT = z.object({
}) })
export let POST = z.object({ export let POST = z.object({
id: z.number(),
text: z.string(), text: z.string(),
time: z.string(), time: z.string(),
sentBy: z.string() sentBy: z.string(),
edited: z.boolean()
}) })
export let CONVSRSATION = z.object({ export let CONVSRSATION = z.object({

7
server/db/sql.js Normal file
View File

@@ -0,0 +1,7 @@
// server/db/sql.js
import postgres from 'postgres';
import 'dotenv/config'
const sql = postgres(process.env.DATABASE_URL);
export default sql;

View File

@@ -16,18 +16,20 @@ import Socket from './ws/ws.js'
import Database from "./db/db.js" import Database from "./db/db.js"
import AuthHandler from './auth.js'; import AuthHandler from './auth.js';
import PaymentsHandler from "./payments.js" import PaymentsHandler from "./payments.js"
import sql from "./db/sql.js"
class Server { class Server {
db; db;
auth; auth;
UIPath = path.join(__dirname, '../ui') UIPath = path.join(__dirname, '../ui')
DBPath = path.join(__dirname, '../db') DBPath = path.join(__dirname, '../db')
ComalPath = path.join(os.homedir(), 'Sites/comalyr.com') ComalPath = path.join(os.homedir(), 'comalyr.com')
registerRoutes(router) { registerRoutes(router) {
/* Stripe */ /* Stripe */
router.post("/create-checkout-session", PaymentsHandler.newSubscription) router.post("/create-checkout-session", PaymentsHandler.newSubscription)
router.post("/webhook", express.raw({ type: "application/json" }), PaymentsHandler.webhook) router.post("/webhook", express.raw({ type: "application/json" }), PaymentsHandler.webhook)
router.post('/api/stripe/onboarded', PaymentsHandler.finishConnectSetup)
/* Auth */ /* Auth */
router.post('/login', this.auth.login) router.post('/login', this.auth.login)
@@ -36,36 +38,46 @@ class Server {
/* Site */ /* Site */
router.post('/free', this.newUserSubmission) router.post('/free', this.newUserSubmission)
router.get('/db/images/*', this.getUserImage) router.get('/api/orgdata/*', this.getOrgData)
router.get('/app/orgdata/*', this.getOrgData) router.get('/api/mydata/*', this.getPersonalData)
router.get('/*', this.get) router.get('/*', this.get)
return router return router
} }
getPersonalData = async (req, res, next) => {
try {
const memberId = req.params[0]
let data = await global.db.members.getPersonalData(memberId)
res.json({
data
});
} catch (err) {
next(err);
}
}
getOrgData = async (req, res, next) => { getOrgData = async (req, res, next) => {
try { try {
const networkId = req.params[0] const networkId = req.params[0]
if(networkId === "1") { if (networkId === "1") {
const pathOne = path.join(this.ComalPath, "db", "join.json"); const [join, contact, members, stripeMembers] = await Promise.all([
const pathTwo = path.join(this.ComalPath, "db", "contact.json"); sql`SELECT * FROM org_1.join_form ORDER BY time DESC`,
sql`SELECT * FROM org_1.contact_form ORDER BY time DESC`,
const [joinRaw, contactRaw] = await Promise.all([ db.members.getByNetwork(networkId),
fs.promises.readFile(pathOne, "utf8"), payments.getCustomers(networkId)
fs.promises.readFile(pathTwo, "utf8")
]); ]);
const join = joinRaw.trim() ? JSON.parse(joinRaw) : [];
const contact = contactRaw.trim() ? JSON.parse(contactRaw) : [];
const members = db.members.getByNetwork(networkId)
res.json({ res.json({
join, join,
contact, contact,
members members,
stripeMembers
}); });
} else { } else {
const members = db.members.getByNetwork(networkId) const members = await db.members.getByNetwork(networkId)
res.json({ res.json({
members members
@@ -122,25 +134,6 @@ class Server {
} }
} }
getUserImage = async (req, res) => {
function getFileByNumber(dir, number) {
const files = fs.readdirSync(dir);
const match = files.find(file => {
const base = path.parse(file).name; // filename without extension
return base === String(number);
});
return match ? path.join(dir, match) : null;
}
let filePath = path.join(this.DBPath, "images", path.basename(req.url))
if(filePath) {
res.sendFile(filePath)
} else {
return res.status(404).json({error: "Can't find image"})
}
}
get = async (req, res) => { get = async (req, res) => {
let url = req.url let url = req.url
@@ -165,6 +158,8 @@ class Server {
filePath = path.join(this.UIPath, url); filePath = path.join(this.UIPath, url);
} else if(url.includes("75820185")) { } else if(url.includes("75820185")) {
filePath = path.join(this.UIPath, platformFolder, url.split("75820185")[1]); filePath = path.join(this.UIPath, platformFolder, url.split("75820185")[1]);
} else if(url.startsWith("/db")) {
filePath = path.join(this.DBPath, url.replace('/db', ''));
} else { } else {
filePath = path.join(this.UIPath, platformFolder, "index.html"); filePath = path.join(this.UIPath, platformFolder, "index.html");
} }
@@ -204,13 +199,66 @@ class Server {
next(); next();
} }
constructor() { async mountApps(app) {
const appsDir = path.join(__dirname, '../apps');
if (!fs.existsSync(appsDir)) {
fs.mkdirSync(appsDir, { recursive: true });
console.log('created apps directory');
return;
}
const appFolders = fs.readdirSync(appsDir);
for (const folder of appFolders) {
const indexPath = path.join(appsDir, folder, 'index.js');
if (!fs.existsSync(indexPath)) continue;
const module = await import(indexPath);
const appModule = module.default;
if (appModule?.type === 'app') {
app.use(`/apps/${folder}`, appModule.app);
console.log(`mounted app: /apps/${folder}`);
} else if (appModule?.type === 'router') {
app.use(`/apps/${folder}`, appModule.router);
console.log(`mounted router: /apps/${folder}`);
}
}
}
async init() {
this.db = new Database() this.db = new Database()
global.db = this.db
this.auth = new AuthHandler() this.auth = new AuthHandler()
global.db = this.db
global.auth = this.auth
global.payments = PaymentsHandler
const app = express(); const app = express();
app.post("/webhook", express.raw({ type: "application/json" }), PaymentsHandler.webhook) app.post("/webhook", express.raw({ type: "application/json" }), PaymentsHandler.webhook)
app.use(cors({ origin: '*' })); const allowedOrigins = new Set([
"https://www.frm.so",
"https://frm.so",
"http://localhost:5174",
"http://sam.local:5174",
"http://localhost:5173",
"http://sam.local:5173",
"http://localhost:10002",
"http://sam.local:10002",
"capacitor://localhost",
"http://localhost"
]);
app.use(cors({
origin(origin, cb) {
if (!origin) return cb(null, true); // native / curl
if (allowedOrigins.has(origin)) {
return cb(null, true);
}
return cb(new Error("Blocked by CORS"));
},
credentials: true
}));
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
app.use(cookieParser()); app.use(cookieParser());
@@ -219,6 +267,8 @@ class Server {
app.use(this.logRequest); app.use(this.logRequest);
app.use(this.logResponse); app.use(this.logResponse);
await this.mountApps(app) // must be completed before router is registered
let router = express.Router(); let router = express.Router();
this.registerRoutes(router) this.registerRoutes(router)
app.use('/', router); app.use('/', router);
@@ -228,7 +278,7 @@ class Server {
const PORT = 10002; const PORT = 10002;
server.listen(PORT, () => { server.listen(PORT, () => {
console.log("\n") console.log("\n")
console.log(chalk.yellow("*************** parchment.page ********")) console.log(chalk.yellow("*************** frm.so ********"))
console.log(chalk.yellowBright(`Server is running on port ${PORT}: http://localhost`)); console.log(chalk.yellowBright(`Server is running on port ${PORT}: http://localhost`));
console.log(chalk.yellow("***************************************")) console.log(chalk.yellow("***************************************"))
console.log("\n") console.log("\n")
@@ -242,6 +292,10 @@ class Server {
Object.preventExtensions(this); Object.preventExtensions(this);
} }
constructor() {
this.init()
}
} }
const server = new Server() const server = new Server()

View File

@@ -6,6 +6,74 @@ const stripe = new Stripe(process.env.STRIPE_SECRET);
export default class PaymentsHandler { export default class PaymentsHandler {
static async finishConnectSetup(req, res) {
const { code, networkId } = req.body;
const response = await stripe.oauth.token({
grant_type: "authorization_code",
code,
});
const { stripe_user_id, access_token } = response;
await db.networks.update(
networkId,
{
stripe_account_id: stripe_user_id,
stripe_access_token: access_token, // rarely used, long-term access token for the platform
}
);
res.json({ success: true });
}
static async getProfile(networkId) {
let network = await global.db.networks.get(networkId)
if (network) {
if (network.stripe_account_id) {
const account = await stripe.accounts.retrieve(network.stripe_account_id);
return {
chargesEnabled: account.charges_enabled,
payoutsEnabled: account.payouts_enabled,
detailsSubmitted: account.details_submitted,
email: account.email,
country: account.country,
}
} else {
return { connected: false }
}
} else {
throw new Error(`Network ${networkId} not found`)
}
}
static async getCustomers(networkId) {
let network = await global.db.networks.get(networkId)
if(!network.stripe_account_id) {
throw new Error("Can't get customers for account that doesn't exist!")
}
const customers = await stripe.customers.list(
{ limit: 100, expand: ['data.subscriptions'] },
{ stripeAccount: network.stripe_account_id }
);
return customers.data.map(customer => ({
id: customer.id,
email: customer.email,
created: customer.created,
name: customer.name,
phone: customer.phone,
city: customer.address?.city,
subscriptions: customer.subscriptions?.data.map(sub => ({
id: sub.id,
status: sub.status,
amount: sub.items.data[0]?.price.unit_amount / 100, // converts cents to dollars
currency: sub.items.data[0]?.price.currency,
interval: sub.items.data[0]?.price.recurring?.interval
}))
}))
}
static async newSubscription(req, res) { static async newSubscription(req, res) {
try { try {
const session = await stripe.checkout.sessions.create({ const session = await stripe.checkout.sessions.create({

View File

@@ -8,35 +8,99 @@ const sendSchema = z.object({
const getSchema = z.object({ const getSchema = z.object({
forum: z.string(), forum: z.string(),
number: z.number() by: z.string(),
authorId: z.number()
}) })
.strict() .strict()
const deleteSchema = z.object({
forum: z.string(),
id: z.number()
})
.strict()
const putSchema = z.object({
forum: z.string(),
id: z.number(),
text: z.string()
})
.strict()
export default class ForumHandler { export default class ForumHandler {
static handleSend(msg, ws) { static async handleSend(msg, ws) {
try { try {
global.db.posts.add(msg.text, msg.forum, ws.userEmail) let postId = await global.db.posts.add(msg.text, msg.forum, ws.userEmail)
global.Socket.broadcast({event: "new-post", app: "FORUM", forum: msg.forum, msg: this.handleGet({forum: msg.forum, number: 100})}) let newPost = global.db.posts.getByID(postId)
return {success: true} if (newPost) {
global.Socket.broadcast({
event: "new-post",
app: "FORUM",
forum: msg.forum,
msg: newPost
})
return {success: true}
} else {
return {success: false}
}
} catch(e) { } catch(e) {
console.error(e) console.error(e)
} }
} }
static handleGet(msg) { static handleGet(msg) {
let data = global.db.posts.get(msg.forum, msg.number) let data = global.db.posts.get(msg.forum, msg.by, msg.authorId)
return data return data
} }
static handle(operation, msg, ws) { static async handleDelete(msg, ws) {
try {
await global.db.posts.delete(msg.id, msg.forum, ws.userEmail)
global.Socket.broadcast({
event: "deleted-post",
app: "FORUM",
forum: msg.forum,
msg: msg.id
})
return {success: true}
} catch(e) {
console.error(e)
return {success: false}
}
}
static async handlePut(msg, ws) {
try {
let editedPost = await global.db.posts.edit(msg.id, msg.text, ws.userEmail)
console.log(editedPost)
if (editedPost) {
global.Socket.broadcast({
event: "edited-post",
app: "FORUM",
forum: msg.forum,
msg: editedPost
})
}
return {success: true}
} catch(e) {
console.error(e)
return {success: false}
}
}
static async handle(operation, msg, ws) {
switch(operation) { switch(operation) {
case "SEND": case "SEND":
if(!sendSchema.safeParse(msg).success) throw new Error("Incorrectly formatted Forum ws message!") if(!sendSchema.safeParse(msg).success) throw new Error("Incorrectly formatted Forum ws message!")
return this.handleSend(msg, ws) return await this.handleSend(msg, ws)
case "GET": case "GET":
if(!getSchema.safeParse(msg).success) throw new Error("Incorrectly formatted Forum ws message!") if(!getSchema.safeParse(msg).success) throw new Error("Incorrectly formatted Forum ws message!")
return this.handleGet(msg) return this.handleGet(msg)
case "DELETE":
if(!deleteSchema.safeParse(msg).success) throw new Error("Incorrectly formatted Forum ws message!")
return this.handleDelete(msg, ws)
case "PUT":
if (!putSchema.safeParse(msg).success) throw new Error("Incorrectly formatted ws message!")
return this.handlePut(msg, ws)
} }
} }

View File

@@ -9,6 +9,7 @@ const sendSchema = z.object({
export default class MessagesHandler { export default class MessagesHandler {
static handleSend(msg, ws) { static handleSend(msg, ws) {
console.warn("MessagesHandler.js 12: These calls to global.db need to be awaited")
let user = global.db.members.getByEmail(ws.userEmail) let user = global.db.members.getByEmail(ws.userEmail)
let convo = global.db.conversations.get(msg.conversation) let convo = global.db.conversations.get(msg.conversation)
if(convo.between.includes(`MEMBER-${user.id}`)) { if(convo.between.includes(`MEMBER-${user.id}`)) {
@@ -22,6 +23,7 @@ export default class MessagesHandler {
} }
static handleGet(ws) { static handleGet(ws) {
console.warn("MessagesHandler.js 26: These calls to global.db need to be awaited")
let user = global.db.members.getByEmail(ws.userEmail) let user = global.db.members.getByEmail(ws.userEmail)
let data = global.db.conversations.getByMember(`MEMBER-${user.id}`) let data = global.db.conversations.getByMember(`MEMBER-${user.id}`)
return data return data

View File

@@ -2,15 +2,30 @@ import { WebSocket, WebSocketServer } from 'ws';
import { z } from 'zod'; import { z } from 'zod';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import server from "../../ui/_/code/bridge/serverFunctions.js"
import ForumHandler from "./handlers/ForumHandler.js" import ForumHandler from "./handlers/ForumHandler.js"
import MessagesHandler from "./handlers/MessagesHandler.js" import MessagesHandler from "./handlers/MessagesHandler.js"
export default class Socket { export default class Socket {
wss; wss;
messageSchema = z.object({
functionCallSchema = z.object({
id: z.string(), id: z.string(),
app: z.string(), name: z.string(),
args: z.array(z.any()),
data: z.union([
z.object({}).passthrough(), // allows any object
z.array(z.any()) // allows any array
]).optional()
})
.strict()
appOperationSchema = z.object({
id: z.string(),
app: z.string().optional(),
operation: z.string().optional(), operation: z.string().optional(),
msg: z.union([ msg: z.union([
z.object({}).passthrough(), // allows any object z.object({}).passthrough(), // allows any object
z.array(z.any()) // allows any array z.array(z.any()) // allows any array
@@ -45,7 +60,13 @@ export default class Socket {
const cookies = parseCookies(req.headers.cookie); const cookies = parseCookies(req.headers.cookie);
const token = cookies.auth_token; const token = cookies.auth_token;
if (!token) throw new Error("No auth token"); if (!token) throw new Error("No auth token");
const payload = jwt.verify(token, process.env.JWT_SECRET); let payload;
try {
payload = jwt.verify(token, process.env.JWT_SECRET);
} catch(e) {
console.error("error: jwt is expired ", e)
return
}
ws.userEmail = payload.email; ws.userEmail = payload.email;
ws.on('message', (msg) => { ws.on('message', (msg) => {
@@ -62,39 +83,61 @@ export default class Socket {
// Build a system where the ws obj is updated every time on navigate, so it already has context // Build a system where the ws obj is updated every time on navigate, so it already has context
// this way, we can only send broadcast messages to clients that actually have that app / subapp open // this way, we can only send broadcast messages to clients that actually have that app / subapp open
handleMessage = (msg, ws) => { handleMessage = async (msg, ws) => {
try { try {
const text = msg.toString(); const text = msg.toString();
const req = JSON.parse(text); const req = JSON.parse(text);
if(!this.messageSchema.safeParse(req).success) throw new Error("Socket.handleMessage: Incoming ws message has incorrect format!")
let responseData; if(this.appOperationSchema.safeParse(req).success) {
switch (req.app) { this.handleAppOperation(req, ws)
case "FORUM": } else if(this.functionCallSchema.safeParse(req).success) {
responseData = ForumHandler.handle(req.operation, req.msg, ws) this.handleFunction(req, ws)
break; } else {
throw new Error("Socket.handleMessage: Incoming ws message has incorrect format!")
case "MESSAGES":
responseData = MessagesHandler.handle(req.operation, req.msg, ws)
break;
default:
console.error("unknown ws message")
} }
let response = {
...req
}
response.msg = responseData
if(!this.messageSchema.safeParse(response).success) throw new Error("Socket.handleMessage: Outgoing ws message has incorrect format!")
ws.send(JSON.stringify(response))
} catch (e) { } catch (e) {
console.error("Invalid WS message:", e); console.error("Invalid WS message:", e);
} }
} }
async handleAppOperation(req, ws) {
let responseData;
switch (req.app) {
case "FORUM":
responseData = await ForumHandler.handle(req.operation, req.msg, ws)
break;
case "MESSAGES":
responseData = MessagesHandler.handle(req.operation, req.msg, ws)
break;
default:
console.log("unknown ws message")
}
let response = {
...req
}
response.msg = responseData
if(!this.appOperationSchema.safeParse(response).success) throw new Error("Socket.handleMessage: Outgoing ws message has incorrect format!")
ws.send(JSON.stringify(response))
}
async handleFunction(req, ws) {
let responseData = await server[req.name](...req.args)
let response = {
...req
}
response.data = responseData
if(!this.functionCallSchema.safeParse(response).success) throw new Error("Socket.handleMessage: Outgoing ws message has incorrect format!")
ws.send(JSON.stringify(response))
}
broadcast(event) { broadcast(event) {
if (!this.wss) return; if (!this.wss) return;

56
serveradmin.md Normal file
View File

@@ -0,0 +1,56 @@
# ssh
ssh root@167.172.247.123
# certs
certbot --nginx -d comalyr.com -d ...
# Code Server
curl -fsSL https://code-server.dev/install.sh | sh
pm2 start code-server --name code
cat ~/.config/code-server/config.yaml
# pm2
## For Running the Servers
## start your app
pm2 start "npm run start" --name comalyr
## see running processes
pm2 list
## view logs
pm2 logs
pm2 logs comalyr
## restart
pm2 restart comalyr
## stop
pm2 stop comalyr
## save all current processes to be run on startup
pm2 save
# nginx
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log
cat /var/log/nginx/access.log
## list config
cat /etc/nginx/sites-available/norn
nano /etc/nginx/sites-available/norn
## restart
systemctl restart nginx
# Postgres
systemctl status postgresql
# debugging
curl http://localhost:8080
curl -v https://comalyr.com 2>&1 | head -30
this will list out the process of connecting

View File

@@ -0,0 +1,34 @@
export 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
})
return res
}
/**
* 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]
return function (...args) {
if (IS_NODE) {
return orig(...args)
} else {
return bridgeSend(prop, args)
}
}
}
})
}

View File

@@ -0,0 +1,14 @@
import fs from "fs"
const handlers = {
getProfile(one, two) {
fs.writeFileSync("output.txt", `${one} ${two}`)
return "written to disk"
},
async getStripeProfile(networkId) {
return global.payments.getProfile(networkId)
}
}
export default handlers

View File

@@ -0,0 +1,10 @@
import { createBridge, IS_NODE } from "./bridge.js"
let handlers = {}
if (IS_NODE) {
const mod = await import("./handlers.js")
handlers = mod.default
}
export default createBridge(handlers)

View File

@@ -1,6 +1,10 @@
/* /*
Sam Russell Sam Russell
Captured Sun Captured Sun
3.4.26 - Making horizontalAlign() and verticalAlign() methods of checking for stacks more robust
2.27.26 - Adding parentShadow() function
2.16.26 - Adding event objects to the onTouch callbacks
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 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.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. 12.17.25 - [Hyperia] - adding width, height functions. adding "e" to onClick. adding the non-window $$ funcs.
@@ -27,7 +31,6 @@ let oldPushState = history.pushState;
history.pushState = function pushState() { history.pushState = function pushState() {
let ret = oldPushState.apply(this, arguments); let ret = oldPushState.apply(this, arguments);
window.dispatchEvent(new Event('pushstate')); window.dispatchEvent(new Event('pushstate'));
window.dispatchEvent(new Event('navigate'));
return ret; return ret;
}; };
@@ -53,8 +56,8 @@ window.setQuery = function(key, value) {
}; };
window.navigateTo = function(url) { window.navigateTo = function(url) {
window.dispatchEvent(new Event('navigate'));
window.history.pushState({}, '', url); window.history.pushState({}, '', url);
window.dispatchEvent(new Event('navigate'));
} }
window.setLocation = function(url) { window.setLocation = function(url) {
@@ -310,6 +313,17 @@ HTMLElement.prototype.rerender = function() {
quill.rerender(this) quill.rerender(this)
} }
HTMLElement.prototype.parentShadow = function(selector) {
let el = this
while(el !== document.body) {
el = el.parentElement
if(el instanceof Shadow) {
return el
}
}
return null
}
/* Styling */ /* Styling */
window.pct = "%" window.pct = "%"
@@ -660,11 +674,11 @@ HTMLElement.prototype.centerY = function () {
}; };
HTMLElement.prototype.verticalAlign = function (value) { HTMLElement.prototype.verticalAlign = function (value) {
const direction = getComputedStyle(this).flexDirection; // if(!this.classList.contains("HStack") && !this.classList.contains("VStack")) {
if(!direction) { // throw new Error("verticalAlign can be only be used on HStacks or VStacks!")
throw new Error("verticalAlign can be only be used on HStacks or VStacks!") // }
}
const direction = getComputedStyle(this).flexDirection;
if (direction === "column" || direction === "column-reverse") { if (direction === "column" || direction === "column-reverse") {
this.style.justifyContent = value; this.style.justifyContent = value;
} else { } else {
@@ -674,11 +688,11 @@ HTMLElement.prototype.verticalAlign = function (value) {
} }
HTMLElement.prototype.horizontalAlign = function (value) { HTMLElement.prototype.horizontalAlign = function (value) {
const direction = getComputedStyle(this).flexDirection; if(!this.classList.contains("HStack") && !this.classList.contains("VStack")) {
if(!direction) {
throw new Error("horizontalAlign can be only be used on HStacks or VStacks!") throw new Error("horizontalAlign can be only be used on HStacks or VStacks!")
} }
const direction = getComputedStyle(this).flexDirection;
if (direction === "column" || direction === "column-reverse") { if (direction === "column" || direction === "column-reverse") {
this.style.alignItems = value; this.style.alignItems = value;
} else { } else {
@@ -1066,9 +1080,9 @@ HTMLElement.prototype.onSubmit = function(cb) {
}; };
HTMLElement.prototype.onTouch = function(cb) { HTMLElement.prototype.onTouch = function(cb) {
const onStart = () => cb.call(this, true); const onStart = (e) => cb.call(this, true, e);
const onEnd = () => cb.call(this, false); const onEnd = (e) => cb.call(this, false, e);
const onCancel = () => cb.call(this, null); const onCancel = (e) => cb.call(this, null, e);
this._storeListener("touchstart", onStart); this._storeListener("touchstart", onStart);
this._storeListener("touchend", onEnd); this._storeListener("touchend", onEnd);
this._storeListener("touchcancel", onCancel); this._storeListener("touchcancel", onCancel);
@@ -1197,7 +1211,11 @@ HTMLElement.prototype.attr = function(attributes) {
} }
for (const [key, value] of Object.entries(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; return this;
}; };

View File

@@ -1,8 +1,9 @@
:root { :root {
--main: var(--parchment); --main: #ffe6d0;
--app: var(--parchment); --app: var(--parchment);
--accent: black; --accent: rgb(57, 26, 0);
--accent2: black; --accent2: rgb(57, 26, 0);
--window: #fff1e4;
--bone: #fff2e7; --bone: #fff2e7;
--parchment: #FEBA7D; --parchment: #FEBA7D;
@@ -13,30 +14,51 @@
--quillred: #DE3F3F; --quillred: #DE3F3F;
--brown: #812A18; --brown: #812A18;
--darkbrown: #3f0808; --darkbrown: #3f0808;
--divider: rgb(223 201 169);
--house-src: /_/icons/house.svg; --house-src: /_/icons/house.svg;
--nodes-src: /_/icons/nodes.svg; --nodes-src: /_/icons/nodes.svg;
--forum-src: /_/icons/forum.svg; --forum-src: /_/icons/forum.svg;
--people-src: /_/icons/people.svg; --people-src: /_/icons/people.svg;
--settings-src: /_/icons/settings.svg;
}
:root.parchment {
--main: var(--parchment);
--app: var(--parchment);
--accent: rgb(57, 26, 0);
--accent2: rgb(57, 26, 0);
--window: #ffc28b;
--divider: rgb(205 152 76);
--house-src: /_/icons/house.svg;
--nodes-src: /_/icons/nodes.svg;
--forum-src: /_/icons/forum.svg;
--people-src: /_/icons/people.svg;
--settings-src: /_/icons/settings.svg;
} }
:root.dark { :root.dark {
--main: #000000; --main: #000000;
--app: #180404; --app: #180404;
--accent: #695b4d; --accent: #935757;
--accent2: var(--gold); --accent2: var(--gold);
--window: #340f0f;
--divider: rgb(69 34 34);
--house-src: /_/icons/housedark.svg; --house-src: /_/icons/housedark.svg;
--nodes-src: /_/icons/nodesdark.svg; --nodes-src: /_/icons/nodesdark.svg;
--forum-src: /_/icons/forumdark.svg; --forum-src: /_/icons/forumdark.svg;
--people-src: /_/icons/peopledark.svg; --people-src: /_/icons/peopledark.svg;
--settings-src: /_/icons/settingsdark.svg;
} }
:root.red { :root.red {
--main: var(--red); --main: var(--red);
--app: var(--red); --app: var(--red);
--accent: black; --accent: rgb(57, 0, 0);
--accent2: var(--green); --accent2: var(--green);
--window: #ff0000;
--house-src: /_/icons/house.svg; --house-src: /_/icons/house.svg;
--nodes-src: /_/icons/nodes.svg; --nodes-src: /_/icons/nodes.svg;
@@ -80,6 +102,22 @@ body {
color: var(--accent); color: var(--accent);
} }
:root.red body {
background-color: #e70101;
}
:root.dark body {
background-color: #000000;
}
:root.dark app-window {
border: 1px solid #7a6b55;
}
:root app-window {
border-bottom: 2px solid var(--window);
}
@media (max-width: 480px) { @media (max-width: 480px) {
body, html{ body, html{
overflow-x: hidden; overflow-x: hidden;

View File

@@ -1,62 +1,62 @@
class Connection { class Connection {
connectionTries = 0 connectionTries = 0;
ws; ws;
linkCreated; receiveCB;
wsStatus;
constructor(receiveCB) { constructor(receiveCB) {
this.receiveCB = receiveCB;
this.init() this.init()
this.receiveCB = receiveCB
} }
init() { init = async () => {
if(window.location.hostname.includes("local")) { return new Promise((resolve, reject) => {
this.ws = new WebSocket("ws://" + window.location.host) const url = window.location.hostname.includes("local")
} else { ? "ws://" + window.location.host
this.ws = new WebSocket("wss://" + window.location.hostname + window.location.pathname) : "wss://" + window.location.hostname + window.location.pathname;
}
this.ws.addEventListener('open', () => { this.ws = new WebSocket(url);
this.connectionTries = 0
console.log("Websocket connection established."); this.ws.addEventListener('open', () => {
this.ws.addEventListener('message', this.receiveCB) 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
});
}); });
this.ws.addEventListener("close", () => {
this.checkOpen();
console.log('Websocket Closed')
})
} }
async checkOpen() { checkOpen = async () => {
if (this.ws.readyState === WebSocket.OPEN) { if (this.ws.readyState === WebSocket.OPEN) {
return true return true;
} else { } else {
await this.sleep(this.connectionTries < 20 ? 5000 : 60000) await this.sleep(this.connectionTries < 20 ? 5000 : 60000);
this.connectionTries++ this.connectionTries++;
console.log('Reestablishing connection') console.log('Reestablishing connection');
this.init() await this.init();
} }
} }
sleep = (time) => { sleep = (time) => new Promise(resolve => setTimeout(resolve, time));
return new Promise(resolve => {
setTimeout(resolve, time);
});
}
send = (msg) => { send = (msg) => {
console.log("sending")
if (this.ws.readyState === WebSocket.OPEN) { if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(msg); this.ws.send(msg);
} } else if (this.connectionTries === 0) {
else if(this.connectionTries === 0) { setTimeout(() => this.send(msg), 100);
setTimeout(() => { } else {
this.send(msg) console.error('No WebSocket connection: Cannot send message');
}, 100)
}
else {
console.error('No websocket connection: Cannot send message');
} }
} }
} }
export default Connection export default Connection;

View File

@@ -10,6 +10,10 @@ export default class Socket {
this.connection = new Connection(this.receive); this.connection = new Connection(this.receive);
} }
async init() {
await this.connection.init()
}
isOpen() { isOpen() {
if(this.connection.checkOpen()) { if(this.connection.checkOpen()) {
return true; return true;

1
ui/_/code/ws/shim/fs.js Normal file
View File

@@ -0,0 +1 @@
export default {}

View File

@@ -1,4 +1,9 @@
<svg width="28" height="32" viewBox="0 0 28 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="266" height="266" viewBox="0 0 266 266" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.52366 0.0493342C4.88415 0.328928 1.12711 1.09781 0.253382 1.93659L0 2.18124V3.45688V4.73253L0.244645 4.9597C0.541713 5.23929 0.917417 5.43152 1.69504 5.69363C2.42023 5.94702 2.60372 5.96449 2.44645 5.77227C1.99211 5.22182 3.27649 4.584 5.7142 4.16461C8.0558 3.75395 9.35765 3.67532 13.5428 3.67532C17.728 3.67532 19.0299 3.75395 21.3715 4.16461C23.8354 4.584 25.0935 5.22182 24.6305 5.78974C24.5169 5.9208 24.5344 5.92954 24.7877 5.87712C25.3382 5.77227 26.4915 5.26551 26.7886 5.01212L27.0856 4.75001V3.45688V2.16376L26.7886 1.90164C25.9498 1.16771 22.8743 0.4862 18.7852 0.136707C17.3523 0.00564766 11.1401 -0.0467762 9.52366 0.0493342Z" fill="black"/> <path d="M199.559 33H66.4395V50.9268H199.559V33Z" fill="black"/>
<path d="M10.6246 5.30045C8.06453 5.44899 5.65304 5.82469 4.49971 6.26156C3.80073 6.52367 3.49492 6.83822 3.49492 7.27508V7.62458L4.0978 7.61584C4.63077 7.6071 4.73562 7.63331 4.93658 7.82553C5.06764 7.94786 5.20743 8.11386 5.25986 8.20997C5.31228 8.31482 5.33849 11.3292 5.32976 16.79L5.32102 25.2128H5.76662H6.20349V16.423C6.20349 6.60231 6.16854 7.15276 6.79762 6.89064C7.18207 6.73337 7.75873 6.80327 8.06453 7.03918C8.58877 7.45857 8.56256 6.82948 8.56256 18.1268V28.4456H9.17417H9.78578V17.8734C9.78578 11.4428 9.81199 7.24013 9.86442 7.14402C10.0741 6.75958 10.3974 6.56736 10.9216 6.53241C11.5158 6.48873 11.9526 6.68968 12.1361 7.0916C12.2148 7.26635 12.241 10.1671 12.2322 19.4549V31.591H13.5865H14.9408V19.4636C14.9408 7.59836 14.9408 7.33624 15.1155 7.06539C15.6136 6.24408 16.9853 6.34893 17.3436 7.24013C17.4571 7.52846 17.4746 8.89148 17.4746 18.0132V28.4543L18.0687 28.4281L18.6541 28.4019L18.6279 18.2229C18.6017 11.2069 18.6279 7.94786 18.6891 7.7469C18.9774 6.82948 20.2443 6.48873 20.7861 7.18771C20.9695 7.41488 20.9695 7.4673 20.9695 16.3095V25.2128H21.4064H21.8433V16.8424C21.8433 8.708 21.852 8.47209 22.018 8.20124C22.2714 7.77311 22.5597 7.63331 23.1189 7.64205H23.6169L23.5645 7.2314C23.5296 6.94307 23.4597 6.76832 23.2937 6.63726C22.1403 5.63247 16.0155 4.99465 10.6246 5.30045Z" fill="black"/> <path d="M167.678 143.667C166.524 142.424 164.993 141.736 163.351 141.736C159.957 141.736 157.184 144.776 157.184 148.503V232.679H169.519V148.503C169.519 146.662 168.876 144.953 167.678 143.667Z" fill="black"/>
<path d="M184.494 232.679V110.342C181.788 111.407 178.859 112.006 175.797 112.006H90.2011C87.1393 112.006 84.2107 111.407 81.5039 110.342V232.679H93.1519V148.503C93.1519 142.934 97.4117 138.408 102.648 138.408C105.221 138.408 107.618 139.473 109.415 141.403C111.167 143.289 112.144 145.818 112.144 148.503V232.679H123.503V132.395C123.503 126.826 127.763 122.3 132.999 122.3C135.551 122.3 137.947 123.365 139.766 125.273C141.541 127.181 142.495 129.711 142.495 132.395V232.679H153.855V148.503C153.855 142.934 158.114 138.408 163.35 138.408C165.924 138.408 168.32 139.473 170.117 141.403C171.87 143.289 172.846 145.818 172.846 148.503V232.679H184.494Z" fill="black"/>
<path d="M137.324 127.56C136.148 126.317 134.618 125.629 132.998 125.629C129.603 125.629 126.83 128.669 126.83 132.396V232.68H139.166V132.396C139.166 130.555 138.522 128.846 137.324 127.56Z" fill="black"/>
<path d="M106.975 143.667C105.821 142.424 104.29 141.736 102.648 141.736C99.2538 141.736 96.4805 144.776 96.4805 148.503V232.679H108.816V148.503C108.816 146.662 108.173 144.953 106.975 143.667Z" fill="black"/>
<path d="M192.106 100.58C184.54 95.8101 179.482 87.4014 179.482 77.8389C179.482 77.728 179.482 77.6393 179.482 77.5283H86.5197C86.5197 77.5283 86.5197 77.728 86.5197 77.8389C86.5197 87.4236 81.4612 95.8323 73.8955 100.58C77.6451 105.483 83.5467 108.678 90.2027 108.678H175.799C182.455 108.678 188.356 105.483 192.106 100.58Z" fill="black"/>
<path d="M59.5844 101.402C72.5858 101.402 83.1688 90.8185 83.1688 77.8393C83.1688 77.7283 83.1688 77.6396 83.1688 77.5287H58.1201C57.2104 77.5287 56.4561 76.7743 56.4561 75.8647C56.4561 74.955 57.2104 74.2007 58.1201 74.2007H207.88C208.79 74.2007 209.544 74.955 209.544 75.8647C209.544 76.7743 208.79 77.5287 207.88 77.5287H182.831C182.831 77.5287 182.831 77.7283 182.831 77.8393C182.831 90.8407 193.414 101.402 206.416 101.402C219.417 101.402 230 90.8185 230 77.8393C230 64.8601 219.417 54.2549 206.416 54.2549H59.5844C46.583 54.2549 36 64.8379 36 77.8393C36 90.8407 46.583 101.402 59.5844 101.402Z" fill="black"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

9
ui/_/icons/columnnew.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg width="262" height="270" viewBox="0 0 262 270" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M220.889 0H41.1084V24.2104H220.889V0Z" fill="black"/>
<path d="M177.833 149.457C176.275 147.779 174.207 146.85 171.99 146.85C167.406 146.85 163.66 150.955 163.66 155.989V269.67H180.32V155.989C180.32 153.502 179.451 151.195 177.833 149.457Z" fill="black"/>
<path d="M200.544 269.67V104.452C196.889 105.89 192.934 106.699 188.799 106.699H73.1998C69.0648 106.699 65.1096 105.89 61.4541 104.452V269.67H77.1849V155.989C77.1849 148.468 82.9379 142.355 90.0092 142.355C93.485 142.355 96.721 143.794 99.1481 146.4C101.515 148.947 102.834 152.363 102.834 155.989V269.67H118.175V134.235C118.175 126.714 123.928 120.602 130.999 120.602C134.445 120.602 137.681 122.04 140.138 124.617C142.535 127.194 143.823 130.61 143.823 134.235V269.67H159.165V155.989C159.165 148.468 164.918 142.355 171.989 142.355C175.465 142.355 178.701 143.794 181.128 146.4C183.495 148.947 184.813 152.363 184.813 155.989V269.67H200.544Z" fill="black"/>
<path d="M136.841 127.704C135.253 126.027 133.185 125.098 130.998 125.098C126.413 125.098 122.668 129.203 122.668 134.236V269.671H139.328V134.236C139.328 131.75 138.459 129.442 136.841 127.704Z" fill="black"/>
<path d="M95.8524 149.457C94.2943 147.779 92.2268 146.85 90.0095 146.85C85.4251 146.85 81.6797 150.955 81.6797 155.989V269.67H98.3393V155.989C98.3393 153.502 97.4704 151.195 95.8524 149.457Z" fill="black"/>
<path d="M210.824 91.2682C200.606 84.8261 193.775 73.4699 193.775 60.5557C193.775 60.4059 193.775 60.286 193.775 60.1362H68.2279C68.2279 60.1362 68.2279 60.4059 68.2279 60.5557C68.2279 73.4999 61.3962 84.856 51.1787 91.2682C56.2425 97.8901 64.2128 102.205 73.2018 102.205H188.801C197.79 102.205 205.76 97.8901 210.824 91.2682Z" fill="black"/>
<path d="M31.8511 92.3773C49.4096 92.3773 63.7022 78.0848 63.7022 60.5562C63.7022 60.4064 63.7022 60.2865 63.7022 60.1367H29.8735C28.645 60.1367 27.6263 59.1179 27.6263 57.8894C27.6263 56.6609 28.645 55.6422 29.8735 55.6422H232.126C233.355 55.6422 234.374 56.6609 234.374 57.8894C234.374 59.1179 233.355 60.1367 232.126 60.1367H198.298C198.298 60.1367 198.298 60.4064 198.298 60.5562C198.298 78.1147 212.59 92.3773 230.149 92.3773C247.707 92.3773 262 78.0848 262 60.5562C262 43.0276 247.707 28.7051 230.149 28.7051H31.8511C14.2925 28.7051 0 42.9976 0 60.5562C0 78.1147 14.2925 92.3773 31.8511 92.3773Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
ui/_/icons/columnnewbig.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

9
ui/_/icons/columnred.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg width="266" height="266" viewBox="0 0 266 266" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M199.821 33H66.4883V50.9555H199.821V33Z" fill="#DE2626"/>
<path d="M167.888 143.844C166.732 142.6 165.199 141.911 163.555 141.911C160.155 141.911 157.377 144.955 157.377 148.688V232.999H169.732V148.688C169.732 146.844 169.088 145.133 167.888 143.844Z" fill="#DE2626"/>
<path d="M184.732 232.999V110.466C182.021 111.533 179.088 112.133 176.021 112.133H90.2882C87.2216 112.133 84.2883 111.533 81.5771 110.466V232.999H93.2438V148.688C93.2438 143.111 97.5104 138.577 102.755 138.577C105.333 138.577 107.733 139.644 109.533 141.577C111.288 143.466 112.266 146 112.266 148.688V232.999H123.644V132.555C123.644 126.977 127.91 122.444 133.155 122.444C135.71 122.444 138.11 123.511 139.933 125.422C141.71 127.333 142.666 129.866 142.666 132.555V232.999H154.044V148.688C154.044 143.111 158.31 138.577 163.555 138.577C166.132 138.577 168.532 139.644 170.332 141.577C172.088 143.466 173.066 146 173.066 148.688V232.999H184.732Z" fill="#DE2626"/>
<path d="M137.487 127.712C136.309 126.467 134.776 125.778 133.153 125.778C129.753 125.778 126.976 128.823 126.976 132.556V233H139.331V132.556C139.331 130.712 138.687 129.001 137.487 127.712Z" fill="#DE2626"/>
<path d="M107.088 143.844C105.933 142.6 104.399 141.911 102.755 141.911C99.3549 141.911 96.5771 144.955 96.5771 148.688V232.999H108.933V148.688C108.933 146.844 108.288 145.133 107.088 143.844Z" fill="#DE2626"/>
<path d="M192.356 100.688C184.778 95.9107 179.711 87.4885 179.711 77.9107C179.711 77.7996 179.711 77.7107 179.711 77.5996H86.6005C86.6005 77.5996 86.6005 77.7996 86.6005 77.9107C86.6005 87.5107 81.5338 95.9329 73.9561 100.688C77.7116 105.6 83.6227 108.8 90.2893 108.8H176.022C182.689 108.8 188.6 105.6 192.356 100.688Z" fill="#DE2626"/>
<path d="M59.6221 101.511C72.6443 101.511 83.2443 90.9107 83.2443 77.9107C83.2443 77.7996 83.2443 77.7107 83.2443 77.5996H58.1555C57.2444 77.5996 56.4888 76.8441 56.4888 75.9329C56.4888 75.0218 57.2444 74.2663 58.1555 74.2663H208.155C209.066 74.2663 209.822 75.0218 209.822 75.9329C209.822 76.8441 209.066 77.5996 208.155 77.5996H183.066C183.066 77.5996 183.066 77.7996 183.066 77.9107C183.066 90.9329 193.666 101.511 206.688 101.511C219.71 101.511 230.31 90.9107 230.31 77.9107C230.31 64.9108 219.71 54.2886 206.688 54.2886H59.6221C46.6 54.2886 36 64.8885 36 77.9107C36 90.9329 46.6 101.511 59.6221 101.511Z" fill="#DE2626"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

3
ui/_/icons/settings.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -21,6 +21,7 @@ class Home extends Shadow {
option("Light").attr({value: "light"}) option("Light").attr({value: "light"})
option("Dark").attr({value: "dark"}) option("Dark").attr({value: "dark"})
option("Red").attr({value: "red"}) option("Red").attr({value: "red"})
option("Parchment").attr({value: "parchment"})
$(`option[value=${selected}]`).selected = "true" $(`option[value=${selected}]`).selected = "true"
}) })

View File

@@ -1,7 +1,7 @@
class Dashboard extends Shadow { class Dashboard extends Shadow {
COL = { COL = {
time: 17, time: 14,
fname: 6, fname: 6,
lname: 6, lname: 6,
email: 20, email: 20,
@@ -15,11 +15,49 @@ class Dashboard extends Shadow {
.width(this.COL[type], pct) .width(this.COL[type], pct)
.whiteSpace("nowrap") .whiteSpace("nowrap")
.overflowX("auto") .overflowX("auto")
.overflowY("hidden"); .overflowY("hidden")
}
spawnModal(msg) {
let div = document.createElement("div")
div.classList.add("modal")
div.innerText = msg
div.style.left = "50vw"
div.style.top = "50vh"
div.style.padding = "5em"
div.style.borderRadius = "12px"
div.style.position = "fixed"
div.style.backgroundColor = "var(--accent)"
div.style.color = "var(--main)"
div.style.transform = "translate(-50%, -50%)"
document.body.appendChild(div)
let p = document.createElement("p")
p.innerText = "X"
p.color = "red"
p.style.position = "absolute"
p.style.top = "1em"
p.style.right = "2em"
p.style.cursor = "pointer"
p.onClick((finished) => {
if(finished) {
p.parentElement.remove()
}
})
div.appendChild(p)
} }
render() { render() {
VStack(() => { VStack(() => {
if(window.location.pathname.startsWith("/my")) {
h1(global.profile.first_name + " " + global.profile.last_name);
return
}
else if(!window.location.pathname.includes("comalyr")) {
return
}
h1("Website Inquiries"); h1("Website Inquiries");
p("Contact Us") p("Contact Us")
@@ -34,21 +72,40 @@ class Dashboard extends Shadow {
p("Last").width(this.COL.lname, pct).fontWeight("bold").whiteSpace("nowrap"); p("Last").width(this.COL.lname, pct).fontWeight("bold").whiteSpace("nowrap");
p("Email").width(this.COL.email, pct).fontWeight("bold").whiteSpace("nowrap"); p("Email").width(this.COL.email, pct).fontWeight("bold").whiteSpace("nowrap");
p("Phone").width(this.COL.phone, pct).fontWeight("bold").whiteSpace("nowrap"); p("Phone").width(this.COL.phone, pct).fontWeight("bold").whiteSpace("nowrap");
p("Message").width(this.COL.message, pct).fontWeight("bold").whiteSpace("nowrap"); p("Message").width(this.COL.message, pct).fontWeight("bold").whiteSpace("nowrap")
p("County").width(this.COL.county, pct).fontWeight("bold").whiteSpace("nowrap"); p("County").width(this.COL.county, pct).fontWeight("bold").whiteSpace("nowrap");
}) })
.width(95, pct) .width(95, pct)
.maxWidth(95, pct) .maxWidth(95, pct)
.gap(8); .gap(8);
window.currentNetwork.data.contact.forEach((entry) => { global.currentNetwork.data.contact.forEach((entry) => {
HStack(() => { HStack(() => {
this.cell("time", entry.time); this.cell("time", entry.time.replace(/:\d{6}(?=am|pm)/i, '')
.replace('-', '&nbsp;&nbsp;&nbsp;')
.replace(/(\d{1,2}:\d{2})(am|pm)/i, (_, time, meridiem) => {
const [h, m] = time.split(':');
return `${h.padStart(2, '0')}:${m}${meridiem.toLowerCase()}`;
}));
this.cell("fname", entry.fname); this.cell("fname", entry.fname);
this.cell("lname", entry.lname); this.cell("lname", entry.lname);
this.cell("email", entry.email); this.cell("email", entry.email);
this.cell("phone", entry.phone); this.cell("phone", entry.phone);
this.cell("message", entry.message); this.cell("message", entry.message)
.cursor("pointer")
.onHover(function (hovering) {
if(hovering) {
this.style.outline = "1px solid black"
} else {
this.style.outline = ""
}
})
.onClick((done) => {
if(done) {
this.spawnModal(entry.message)
}
})
this.cell("county", entry.county ?? "Not Specified"); this.cell("county", entry.county ?? "Not Specified");
}) })
.width(95, pct) .width(95, pct)
@@ -74,9 +131,14 @@ class Dashboard extends Shadow {
.maxWidth(95, pct) .maxWidth(95, pct)
.gap(8); .gap(8);
window.currentNetwork.data.join.forEach((entry) => { global.currentNetwork.data.join.forEach((entry) => {
HStack(() => { HStack(() => {
this.cell("time", entry.time); this.cell("time", entry.time.replace(/:\d{6}(?=am|pm)/i, '')
.replace('-', '&nbsp;&nbsp;&nbsp;')
.replace(/(\d{1,2}:\d{2})(am|pm)/i, (_, time, meridiem) => {
const [h, m] = time.split(':');
return `${h.padStart(2, '0')}:${m}${meridiem.toLowerCase()}`;
}));
this.cell("fname", entry.fname); this.cell("fname", entry.fname);
this.cell("lname", entry.lname); this.cell("lname", entry.lname);
this.cell("email", entry.email); this.cell("email", entry.email);

View File

@@ -0,0 +1,203 @@
class Dashboard extends Shadow {
gridStyle() {
return {
table: {
'background-color': 'var(--window)',
'color': 'var(--accent)',
'box-shadow': 'none',
'font-size': '0.9em',
'max-width': '90%'
},
th: {
'background-color': 'var(--window)',
'color': 'var(--accent)',
'border': 'none',
'border-bottom': '1px solid var(--divider)'
},
td: {
'background-color': 'var(--window)',
'color': 'var(--accent)',
'border': 'none',
'border-bottom': '1px solid var(--divider)'
}
}
}
gridCSS() {
css(`
input.gridjs-input {
background-color: var(--window);
color: var(--accent);
border: none;
border-bottom: 1px solid var(--divider);
border-radius: 0;
margin-left: 5em
}
.gridjs-wrapper {
box-shadow: none;
padding-left: 4em;
max-width: 100%;
overflow-x: auto;
}
`)
}
render() {
VStack(() => {
if(window.location.pathname.startsWith("/my")) {
h1(global.profile.first_name + " " + global.profile.last_name);
return
}
else if(!window.location.pathname.includes("comalyr")) {
return
}
h1("Website Inquiries")
.paddingLeft(5, pct)
VStack(() => {
VStack(() => {
p("Contact Us")
.marginBottom(2, vh)
.marginLeft(5, em)
.fontStyle("italic")
ZStack(() => {
new gridjs.Grid({
columns: [
{
name: "Time",
sort: {
compare: (a, b) => {
const parse = (str) => {
const [datePart, timePart] = str.trim().split(/\s+/);
const [month, day, year] = datePart.split('.');
const normalized = timePart.replace(/(\d+):(\d+)(am|pm)/i, (_, h, m, meridiem) => {
let hours = parseInt(h);
if (meridiem.toLowerCase() === 'pm' && hours !== 12) hours += 12;
if (meridiem.toLowerCase() === 'am' && hours === 12) hours = 0;
return `${String(hours).padStart(2, '0')}:${m}:00`;
});
return new Date(`${year}-${month}-${day}T${normalized}`);
}
return parse(a) - parse(b);
}
}
},
"First", "Last", "Email", "Phone",
{
name: "Message",
formatter: (cell) => gridjs.html(`
<div class="messageCell" style="
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
cursor: pointer;
max-width: 300px;
" data-message="${cell.replace(/"/g, '&quot;')}">
${cell}
</div>
`)
},
"County"
],
data: global.currentNetwork.data.contact.map(e => [
e.time.replace(/:\d{6}(?=am|pm)/i, '')
.replace('-', ' ')
.replace(/(\d{1,2}:\d{2})(am|pm)/i, (_, time, meridiem) => {
const [h, m] = time.split(':');
return `${h.padStart(2, '0')}:${m}${meridiem.toLowerCase()}`;
}),
e.fname,
e.lname,
e.email,
e.phone,
e.message,
e.county ?? "Not Specified"
]),
sort: {
multiColumn: false,
initialState: {
columnIndex: 0,
direction: "desc"
}
},
search: true,
style: this.gridStyle()
}).render(quill.rendering.last)
this.gridCSS()
})
.overflow("hidden")
})
.gap(0.5, em)
VStack(() => {
p("Join")
.marginBottom(2, vh)
.marginLeft(5, em)
.fontStyle("italic")
ZStack(() => {
new gridjs.Grid({
columns: [
{
name: "Time",
sort: {
compare: (a, b) => {
const parse = (str) => {
const [datePart, timePart] = str.trim().split(/\s+/);
const [month, day, year] = datePart.split('.');
const normalized = timePart.replace(/(\d+):(\d+)(am|pm)/i, (_, h, m, meridiem) => {
let hours = parseInt(h);
if (meridiem.toLowerCase() === 'pm' && hours !== 12) hours += 12;
if (meridiem.toLowerCase() === 'am' && hours === 12) hours = 0;
return `${String(hours).padStart(2, '0')}:${m}:00`;
});
return new Date(`${year}-${month}-${day}T${normalized}`);
}
return parse(a) - parse(b);
}
}
},
"First", "Last", "Email", "Phone", "County"
],
data: global.currentNetwork.data.join.map(e => [
e.time.replace(/:\d{6}(?=am|pm)/i, '')
.replace('-', ' ')
.replace(/(\d{1,2}:\d{2})(am|pm)/i, (_, time, meridiem) => {
const [h, m] = time.split(':');
return `${h.padStart(2, '0')}:${m}${meridiem.toLowerCase()}`;
}),
e.fname,
e.lname,
e.email,
e.phone,
e.county ?? "Not Specified"
]),
sort: true,
search: true,
style: this.gridStyle()
}).render(quill.rendering.last)
this.gridCSS()
})
.marginBottom(3, em)
.overflow("hidden")
})
.gap(0.5, em)
})
.gap(4, em)
})
.gap(0.5, em)
.paddingTop(4, pct)
.width(100, pct)
.height(100, pct);
}
}
register(Dashboard)

View File

@@ -69,7 +69,7 @@ class Forum extends Shadow {
ForumPanel() ForumPanel()
input("Message Parchment", "98%") input("Message Forum", "98%")
.paddingVertical(1, em) .paddingVertical(1, em)
.paddingLeft(2, pct) .paddingLeft(2, pct)
.color("var(--accent)") .color("var(--accent)")
@@ -79,7 +79,7 @@ class Forum extends Shadow {
.fontSize(1, em) .fontSize(1, em)
.onKeyDown(function (e) { .onKeyDown(function (e) {
if (e.key === "Enter") { if (e.key === "Enter") {
window.Socket.send({app: "FORUM", operation: "SEND", msg: {forum: "HY", text: this.value }}) global.Socket.send({app: "FORUM", operation: "SEND", msg: {forum: "HY", text: this.value }})
this.value = "" this.value = ""
} }
}) })

View File

@@ -30,7 +30,7 @@ class Jobs extends Shadow {
{ {
title: "Austin Chapter Lead", title: "Austin Chapter Lead",
salary: "1% of Local Revenue", salary: "1% of Local Revenue",
company: "Parchment", company: "Forum",
city: "Austin", city: "Austin",
state: "TX" state: "TX"
} }

View File

@@ -33,7 +33,7 @@ class Market extends Shadow {
stars: "5", stars: "5",
reviews: 1, reviews: 1,
price: "$12", price: "$12",
company: "Parchment", company: "Forum",
type: "new", type: "new",
image: "/db/images/1", image: "/db/images/1",
madeIn: "America" madeIn: "America"

View File

@@ -65,7 +65,7 @@ class Messages extends Shadow {
.fontSize(1, em) .fontSize(1, em)
.onKeyDown((e) => { .onKeyDown((e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
window.Socket.send({app: "MESSAGES", operation: "SEND", msg: { conversation: `CONVERSATION-${this.selectedConvoID}`, text: e.target.value }}) global.Socket.send({app: "MESSAGES", operation: "SEND", msg: { conversation: `CONVERSATION-${this.selectedConvoID}`, text: e.target.value }})
e.target.value = "" e.target.value = ""
} }
}) })
@@ -104,7 +104,7 @@ class Messages extends Shadow {
.color("var(--accent)") .color("var(--accent)")
.onKeyDown(function (e) { .onKeyDown(function (e) {
if (e.key === "Enter") { if (e.key === "Enter") {
window.Socket.send({app: "MESSAGES", operation: "ADDCONVERSATION", msg: {email: this.value }}) global.Socket.send({app: "MESSAGES", operation: "ADDCONVERSATION", msg: {email: this.value }})
this.value = "" this.value = ""
} }
}) })

View File

@@ -13,7 +13,7 @@ class MessagesPanel extends Shadow {
if(this.messages) { if(this.messages) {
for(let i=0; i<this.messages.length; i++) { for(let i=0; i<this.messages.length; i++) {
let message = this.messages[i] let message = this.messages[i]
let fromMe = window.profile.email === message.from.email let fromMe = global.profile.email === message.from.email
VStack(() => { VStack(() => {
HStack(() => { HStack(() => {
p(message.from.firstName + " " + message.from.lastName) p(message.from.firstName + " " + message.from.lastName)

View File

@@ -57,7 +57,7 @@ class MessagesSidebar extends Shadow {
let membersString = "" let membersString = ""
for(let i=0; i<members.length; i++) { for(let i=0; i<members.length; i++) {
let member = members[i] let member = members[i]
if(member.email === window.profile.email) { if(member.email === global.profile.email) {
continue; continue;
} }
if(members.length > 2) { if(members.length > 2) {

View File

@@ -4,20 +4,125 @@ class People extends Shadow {
h1("Members") h1("Members")
.fontWeight("bold") .fontWeight("bold")
.marginBottom(2, em) .marginBottom(2, em)
.marginLeft(2, em)
for(let i = 0; i < window.currentNetwork.data.members.length; i++) { VStack(() => {
let member = window.currentNetwork.data.members[i] VStack(() => {
HStack(() => { p("Officers")
p(member.firstName + " " + member.lastName) .marginBottom(2, vh)
.width(10, pct) .marginLeft(4, em)
p(member.email) .fontStyle("italic")
.width(10, pct)
ZStack(() => {
new gridjs.Grid({
columns: ["Name", "Email"],
data: global.currentNetwork.data.members.map(m => [
m.first_name + " " + m.last_name,
m.email
]),
sort: true,
search: true,
style: {
table: {
'background-color': 'var(--window)',
'color': 'var(--accent)',
'box-shadow': 'none',
'font-size': '0.9em',
'max-width': '90%'
},
th: {
'background-color': 'var(--window)',
'color': 'var(--accent)',
'border': 'none',
},
td: {
'background-color': 'var(--window)',
'color': 'var(--accent)',
'border': 'none',
'border-bottom': '1px solid var(--divider)'
}
}
}).render(quill.rendering.last)
})
.overflow("hidden")
}) })
} .gap(0.5, em)
VStack(() => {
p("Stripe Subscribers")
.marginBottom(2, vh)
.marginLeft(4, em)
.fontStyle("italic")
ZStack(() => {
new gridjs.Grid({
columns: ["Amount", "Name", "Email", "Phone", "County", "Created"],
data: global.currentNetwork.data.stripeMembers.map(m => [
"$" + m.subscriptions[0].amount,
m.name,
m.email,
m.phone,
m.city,
new Date(m.created * 1000).toLocaleString('en-US', {
timeZone: 'America/Chicago',
hour: 'numeric',
minute: '2-digit',
month: 'numeric',
day: 'numeric',
year: 'numeric'
})
]),
sort: true,
search: true,
style: {
input: {
'background-color': 'var(--window)',
},
table: {
'background-color': 'var(--window)',
'color': 'var(--accent)',
'box-shadow': 'none',
'font-size': '0.9em',
'max-width': '90%'
},
th: {
'background-color': 'var(--window)',
'color': 'var(--accent)',
'border': 'none',
},
td: {
'background-color': 'var(--window)',
'color': 'var(--accent)',
'border': 'none',
'border-bottom': '1px solid var(--divider)'
}
}
}).render(quill.rendering.last)
css(`
input.gridjs-input {
background-color: var(--window);
color: var(--accent);
border: none;
border-bottom: 1px solid var(--divider);
border-radius: 0;
margin-left: 4em
}
.gridjs-wrapper {
box-shadow: none;
padding-left: 3em;
}
`)
})
.marginBottom(3, em)
})
.gap(0.5, em)
})
.gap(4, em)
}) })
.gap(0.5, em) .gap(0.5, em)
.paddingTop(4, pct) .paddingTop(4, pct)
.paddingLeft(5, pct)
.width(100, pct) .width(100, pct)
.height(100, pct); .height(100, pct);
} }

View File

@@ -0,0 +1,67 @@
import env from "/_/code/env.js"
import server from "/_/code/bridge/serverFunctions.js"
import "../../components/LoadingCircleSmall.js"
class Settings extends Shadow {
stripeDetails = null
handleConnectStripe = () => {
const state = btoa(JSON.stringify({
returnTo: window.location.href,
networkId: global.currentNetwork.id
}));
const params = new URLSearchParams({
response_type: 'code',
client_id: env.client_id,
scope: 'read_write',
redirect_uri: `${env.baseURL}/stripe/onboardingcomplete`,
state,
});
window.location.href = `https://connect.stripe.com/oauth/authorize?${params}`;
};
render() {
ZStack(() => {
h1("Settings")
.fontWeight("bold")
.marginTop(4, pct)
.marginLeft(5, pct)
VStack(() => {
HStack(() => {
p("Stripe Integration")
if(this.stripeDetails && this.stripeDetails.data.email) {
p("Connected")
} else if(this.stripeDetails && this.stripeDetails.data.connected === false) {
button("Set up Stripe")
.maxWidth(10, em)
.onClick((done) => {
this.handleConnectStripe()
})
} else {
LoadingCircleSmall()
}
})
.gap(10, pct)
.verticalAlign("center")
})
.gap(0.5, em)
.paddingLeft(5, pct)
.marginTop(4, em)
})
}
async getStripeProfile() {
this.stripeDetails = await server.getStripeProfile(global.currentNetwork.id)
this.rerender()
}
connectedCallback() {
this.getStripeProfile()
}
}
register(Settings)

View File

@@ -2,12 +2,33 @@ class AppMenu extends Shadow {
images = { images = {
"Dashboard": {src: "house-src", size: "1.5em"}, "Dashboard": {src: "house-src", size: "1.5em"},
"People": {src: "people-src", size: "1.7em"} "People": {src: "people-src", size: "1.7em"},
"Settings": {src: "settings-src", size: "1.7em"}
}
unselectedIconStyle(el) {
return el
.paddingBottom(5, px)
.borderBottom("")
}
selectedIconStyle(el) {
return el
.paddingBottom(4, px)
.borderBottom("1px solid var(--accent)")
}
onAppChange() {
let icons = this.$$("img")
icons.forEach((icon) => {
icon.styles(this.unselectedIconStyle)
})
let selected = this.$(`img[app="${global.currentApp}"]`)
selected.styles(this.selectedIconStyle)
} }
render() { render() {
VStack(() => { VStack(() => {
function cssVariable(value) { function cssVariable(value) {
return getComputedStyle(document.documentElement) return getComputedStyle(document.documentElement)
.getPropertyValue("--" + value) .getPropertyValue("--" + value)
@@ -15,27 +36,32 @@ class AppMenu extends Shadow {
} }
HStack(() => { HStack(() => {
let currentNetwork = window.currentNetwork let currentNetwork = global.currentNetwork
if(!currentNetwork) return if(!currentNetwork) return
let currentApp = window.currentApp let currentApp = global.currentApp
for(let i = 0; i < currentNetwork.apps.length; i++) { for(let i = 0; i < currentNetwork.apps.length; i++) {
let app = currentNetwork.apps[i] let app = currentNetwork.apps[i]
img(cssVariable(this.images[app].src), this.images[app].size) img(cssVariable(this.images[app].src), this.images[app].size)
.attr({app: app}) .attr({app: app})
.padding(0.3, em) .padding(0.3, em)
.paddingBottom(currentApp === app ? 4 : 5, px) .styles(currentApp === app ? this.selectedIconStyle : this.unselectedIconStyle)
.borderBottom(currentApp === app ? "1px solid var(--accent)" : "") .onClick(function (done) {
.onClick((done) => { if(!done) {
if(done) window.openApp(app) this.style.transform = "translateY(10%)"
} else {
this.style.transform = ""
global.openApp(app)
}
}) })
.onHover(function (hovering) { .onHover(function (hovering) {
if(hovering) { if(hovering) {
this.style.opacity = 0.8 this.style.translateY = -0.1
} else { } else {
this.style.opacity = "" this.style.translateY = ""
} }
}) })
.cursor("pointer")
} }
}) })
.justifyContent("center") .justifyContent("center")
@@ -47,6 +73,10 @@ class AppMenu extends Shadow {
}) })
.onEvent("appchange", () => { .onEvent("appchange", () => {
// console.log("app hello?") // BUG: Quill is not acknowledging this event unless there is something else in the function body // console.log("app hello?") // BUG: Quill is not acknowledging this event unless there is something else in the function body
this.onAppChange()
})
.onEvent("networkchange", () => {
// console.log(global.currentApp)
this.rerender() this.rerender()
}) })
.position("fixed") .position("fixed")
@@ -54,7 +84,6 @@ class AppMenu extends Shadow {
.width(100, vw) .width(100, vw)
.height(2.5, em) .height(2.5, em)
.paddingVertical(0.7, em) .paddingVertical(0.7, em)
.borderTop("1px solid var(--accent)")
} }
} }

View File

@@ -5,6 +5,7 @@ import "../apps/Messages/Messages.js"
import "../apps/Market/Market.js" import "../apps/Market/Market.js"
import "../apps/Jobs/Jobs.js" import "../apps/Jobs/Jobs.js"
import "../apps/People/People.js" import "../apps/People/People.js"
import "../apps/Settings/Settings.js"
class AppWindow extends Shadow { class AppWindow extends Shadow {
@@ -22,28 +23,46 @@ class AppWindow extends Shadow {
render() { render() {
ZStack(() => { ZStack(() => {
switch(window.currentApp) { switch(global.currentApp) {
case "Dashboard": case "Dashboard":
Dashboard() Dashboard()
break; break;
case "People": case "People":
People() People()
break; break;
case "Settings":
Settings()
break;
default:
this.innerHTML = `
<iframe
src="/apps/parchment"
sandbox="allow-scripts allow-same-origin allow-forms"
width="100%"
height="100%"
>
</iframe>
`
} }
}) })
.overflow("scroll") .overflow("scroll")
.position("absolute") .position("absolute")
.width(window.innerWidth - this.calculateWidth() + 10, px)
.height(window.innerHeight - this.calculateHeight() + 10, px)
.borderRadius(15, px)
.boxShadow("rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.08) 0px 0px 0px 1px")
.background("var(--window)")
.x(this.calculateWidth(), px)
.yBottom(this.calculateHeight(), px)
.onEvent("resize", () => { .onEvent("resize", () => {
this.rerender() this.rerender()
}) })
.width(window.innerWidth - this.calculateWidth(), px)
.height(window.innerHeight - this.calculateHeight(), px)
.background("var(--app)")
.x(this.calculateWidth(), px)
.yBottom(this.calculateHeight(), px)
.onEvent("appchange", () => { .onEvent("appchange", () => {
this.rerender() this.rerender()
}) })
.onEvent("networkchange", () => {
this.rerender()
})
} }
} }

View File

@@ -0,0 +1,23 @@
class LoadingCircleSmall extends Shadow {
render() {
div()
.borderRadius(100, pct)
.width(1, em).height(1, em)
.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(LoadingCircleSmall)

View File

@@ -8,7 +8,7 @@ class ProfileMenu extends Shadow {
p("Email: ") p("Email: ")
.fontWeight("bold") .fontWeight("bold")
p(window.profile?.email) p(global.profile?.email)
}) })
.gap(1, em) .gap(1, em)
@@ -16,7 +16,7 @@ class ProfileMenu extends Shadow {
p("Name: ") p("Name: ")
.fontWeight("bold") .fontWeight("bold")
p(window.profile?.name) p(global.profile?.name)
}) })
.gap(1, em) .gap(1, em)
@@ -37,8 +37,8 @@ class ProfileMenu extends Shadow {
.center() .center()
.display("none") .display("none")
.onAppear(async () => { .onAppear(async () => {
if(!window.profile) { if(!global.profile) {
window.profile = await this.fetchProfile() global.profile = await this.fetchProfile()
this.rerender() this.rerender()
} }
}) })

View File

@@ -1,36 +1,60 @@
class Sidebar extends Shadow { class Sidebar extends Shadow {
currentNetwork = null
toggleSelectedStyles(el) {
let currentlySelected = $("img[selected]")
if(currentlySelected) {
currentlySelected.removeAttribute("selected")
currentlySelected.style.borderLeft = ""
currentlySelected.style.paddingLeft = "10px"
}
el.setAttribute("selected", "")
el.style.borderLeft = "1px solid var(--accent)"
el.style.paddingLeft = "9px"
}
render() { render() {
VStack(() => { VStack(() => {
img(document.documentElement.classList.contains("red") ? "/_/icons/quillblack.svg" : "/_/icons/quill.svg", "2.5em", "2.5em") let selected = window.location.pathname.startsWith("/my")
img(document.documentElement.classList.contains("red") ? "/_/icons/column.svg" : "/_/icons/columnred.svg", "2.5em", "2.5em")
.marginTop(6, vh) .marginTop(6, vh)
.marginBottom(2, vh) .marginBottom(2, vh)
.attr({selected: selected ? "" : null})
.paddingLeft(selected ? 9 : 10, px)
.borderLeft(selected ? "1px solid var(--accent)" : "0")
.cursor("pointer")
.transition("transform .1s")
.onClick(function (done, e) {
if(!done) {
this.style.transform = "translateX(-10%)"
} else {
this.style.transform = ""
this.parentShadow().toggleSelectedStyles(e.target)
window.navigateTo("/my")
}
})
let networks = window.profile.networks let networks = global.profile.networks
for(let i=0; i<networks.length; i++) { for(let i=0; i<networks.length; i++) {
let selected = window.location.pathname.startsWith("/" + networks[i].abbreviation) let selected = window.location.pathname.startsWith("/" + networks[i].abbreviation)
img(`/db/images/${networks[i].logo}`, "2.25em", "2.25em") img(`/db/images/${networks[i].logo}`, "2.25em", "2.25em")
.marginTop(3, vh) .marginTop(3, vh)
.paddingRight(0.5, em) .attr({selected: selected ? "" : null})
.paddingLeft(selected ? 9 : 10, px) .paddingLeft(selected ? 9 : 10, px)
.borderLeft(selected ? "1px solid var(--accent)" : "0") .borderLeft(selected ? "1px solid var(--accent)" : "0")
.onHover(function (hovering) { .cursor("pointer")
if(hovering) { .transition("transform .1s")
this.style.opacity = 0.8 .onClick(function (done, e) {
if(!done) {
this.style.transform = "translateX(-10%)"
} else { } else {
this.style.opacity = "" this.style.transform = ""
this.parentShadow().toggleSelectedStyles(e.target)
window.navigateTo(`/${networks[i].abbreviation}`)
} }
}) })
.onClick(function (finished) {
if(finished) {
this.setAttribute("selected", "")
this.style.borderLeft = "1px solid var(--accent)"
this.style.paddingLeft = "9px"
}
})
.cursor("default")
} }
}) })
.paddingLeft(1.5, em) .paddingLeft(1.5, em)
@@ -38,10 +62,9 @@ class Sidebar extends Shadow {
.position("fixed") .position("fixed")
.x(0).y(0) .x(0).y(0)
.height(100, vh) .height(100, vh)
.borderRight("1px solid var(--accent)")
.zIndex(3) .zIndex(3)
.onEvent("themechange", () => { .onEvent("themechange", () => {
console.log("change") console.log("why is this needed smg")
this.rerender() this.rerender()
}) })
} }

View File

@@ -1,9 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Parchment</title> <title>Forum</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/_/icons/quill.svg"> <link href="https://unpkg.com/gridjs/dist/theme/mermaid.min.css" rel="stylesheet"/>
<script src="https://unpkg.com/gridjs/dist/gridjs.umd.js"></script>
<link rel="icon" href="/_/icons/columnred.svg">
<link rel="stylesheet" href="/_/code/shared.css"> <link rel="stylesheet" href="/_/code/shared.css">
<script> <script>
if(localStorage.getItem("theme")) { if(localStorage.getItem("theme")) {
@@ -14,6 +16,13 @@
} }
} }
</script> </script>
<script type="importmap">
{
"imports": {
"fs": "/_/code/ws/shim/fs.js"
}
}
</script>
<script src="/_/code/quill.js"></script> <script src="/_/code/quill.js"></script>
<script src="/_/code/zod.js"></script> <script src="/_/code/zod.js"></script>
<script type="module" src="75820185/index.js"></script> <script type="module" src="75820185/index.js"></script>

View File

@@ -1,118 +1,176 @@
import Socket from "/_/code/ws/Socket.js" import Socket from "/_/code/ws/Socket.js"
import "./Home.js" import "./Home.js"
import util from "./util.js" import util from "./util.js"
window.util = util
window.Socket = new Socket() let Global = class {
Socket = new Socket()
profile = null
currentNetwork = ""
currentApp = ""
util = util
window.currentNetwork = "" openApp = function(appName) {
window.currentApp = "" const appUrl = appName.charAt(0).toLowerCase() + appName.slice(1);
let parts = window.location.pathname.split('/').filter(Boolean);
async function openNetworkAndApp() { let newPath = "/" + parts[0] + "/" + appUrl
window.navigateTo(newPath)
if(window.currentNetwork !== networkFromPath()) {
window.currentNetwork = networkFromPath()
const event = new CustomEvent('networkchanged', {
detail: { name: currentNetwork }
});
window.dispatchEvent(event)
}
if(!window.currentNetwork.data) {
let appData = await fetch("/app/orgdata/" + window.profile.networks[0].id, {method: "GET"})
let json = await appData.json()
window.currentNetwork.data = json
}
if(window.currentApp !== appFromPath()) {
window.currentApp = appFromPath()
const event = new CustomEvent('appchange', { const event = new CustomEvent('appchange', {
detail: { name: window.currentApp } detail: { name: appName }
}); });
window.dispatchEvent(event) window.dispatchEvent(event)
} }
if(window.currentNetwork) { // 2 navigates fire on load: 1 initial, and one after the org redirect async fetchOrgData() {
document.title = `${window.currentNetwork.abbreviation} | Parchment` 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
} }
}
window.addEventListener("navigate", openNetworkAndApp) async handleStripeOnboarding() {
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
const { returnTo, networkId } = JSON.parse(atob(params.get("state")));
window.openApp = function(appName) { if (code) {
const appUrl = appName.charAt(0).toLowerCase() + appName.slice(1); console.log("success!: ", code)
let parts = window.location.pathname.split('/').filter(Boolean); await fetch("/api/stripe/onboarded", {
let newPath = "/" + parts[0] + "/" + appUrl method: "POST",
window.navigateTo(newPath) headers: { "Content-Type": "application/json" },
const event = new CustomEvent('appchange', { body: JSON.stringify({ code, networkId })
detail: { name: appName } });
});
window.dispatchEvent(event)
}
window.networkFromPath = function () { window.location.href = returnTo
const pathname = window.location.pathname;
const firstSegment = pathname.split('/').filter(Boolean)[0] || ''; } else {
let networks = window.profile?.networks throw new Error("Stripe code is not present!")
for(let i = 0; i < networks.length; i++) {
let network = networks[i]
if(network.abbreviation === firstSegment) {
return network
} }
} }
}
window.appFromPath = function() { onNavigate = async () => {
const pathname = window.location.pathname; console.log("onnavigate", this.getFirstPathSegment())
const segments = pathname.split('/').filter(Boolean); if(this.getFirstPathSegment() === "stripe") {
const secondSegment = segments[1] || ""; this.handleStripeOnboarding()
const capitalized = secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1); }
return capitalized
}
async function getProfile() { let selectedNetwork = this.networkFromPath()
try { let selectedApp = this.appFromPath()
const res = await fetch("/profile", {
method: "GET", if(!selectedNetwork) {
credentials: "include", if(this.profile.networks.length > 0) {
headers: { let path = `/${this.getDefaultNetworkName()}/${this.getDefaultAppName()}`
"Content-Type": "application/json" 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()}`)
}
}
if (!res.ok) throw new Error("Failed to fetch profile"); selectedNetwork = this.networkFromPath()
selectedApp = this.appFromPath()
const profile = await res.json(); let networkChanged = this.currentNetwork !== selectedNetwork
console.log("getProfile: ", profile); let appChanged = this.currentApp !== selectedApp
window.profile = profile if(networkChanged) {
} catch (err) { this.currentNetwork = selectedNetwork
console.error(err); this.currentApp = selectedApp
const event = new CustomEvent('networkchange', {
detail: { name: this.currentNetwork }
});
window.dispatchEvent(event)
}
if(!this.currentNetwork.data) {
this.currentNetwork.data = await this.fetchOrgData()
}
if(appChanged && !networkChanged) {
this.currentApp = selectedApp
const event = new CustomEvent('appchange', {
detail: { name: this.currentApp }
});
window.dispatchEvent(event)
}
document.title = (this.currentNetwork === this.profile) ? "Forum" : `${this.currentNetwork.abbreviation} | Forum`
}
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()
}
getFirstPathSegment() {
return window.location.pathname.split('/').filter(Boolean)[0] || '';
}
networkFromPath = function () {
const firstSegment = this.getFirstPathSegment()
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.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 () => {
await this.onNavigate()
Home()
})
} }
} }
function getInitialNetworkPath() { window.global = new Global()
let path = ""
let defaultNetwork = window.profile.networks[0]
if(!networkFromPath()) {
path += (defaultNetwork.abbreviation + "/")
}
if(!appFromPath()) {
let defaultApp = defaultNetwork.apps[0]
path += defaultApp.toLowerCase()
}
return path
}
getProfile().then(async () => {
if(window.profile.networks.length > 0) {
let path = getInitialNetworkPath()
window.navigateTo(path)
}
Home()
})

View File

@@ -1,62 +0,0 @@
class Connection {
connectionTries = 0
ws;
linkCreated;
wsStatus;
constructor(receiveCB) {
this.init()
this.receiveCB = receiveCB
}
init() {
if(window.location.hostname.includes("local")) {
this.ws = new WebSocket("ws://" + window.location.host)
} else {
this.ws = new WebSocket("wss://" + window.location.hostname + window.location.pathname)
}
this.ws.addEventListener('open', () => {
this.connectionTries = 0
console.log("Websocket connection established.");
this.ws.addEventListener('message', this.receiveCB)
});
this.ws.addEventListener("close", () => {
this.checkOpen();
console.log('Websocket Closed')
})
}
async checkOpen() {
if (this.ws.readyState === WebSocket.OPEN) {
return true
} else {
await this.sleep(this.connectionTries < 20 ? 5000 : 60000)
this.connectionTries++
console.log('Reestablishing connection')
this.init()
}
}
sleep = (time) => {
return new Promise(resolve => {
setTimeout(resolve, time);
});
}
send = (msg) => {
console.log("sending")
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

View File

@@ -1,45 +0,0 @@
import Connection from "./Connection.js";
export default class Socket {
connection;
disabled = true;
requestID = 1;
pending = new Map();
constructor() {
this.connection = new Connection(this.receive);
}
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
}));
}
}

View File

@@ -8,8 +8,8 @@ class Home extends Shadow {
ZStack(() => { ZStack(() => {
ZStack(() => { ZStack(() => {
console.log(window.currentApp) console.log(global.currentApp)
switch(window.currentApp) { switch(global.currentApp) {
case "Dashboard": case "Dashboard":
Dashboard() Dashboard()
break; break;

View File

@@ -1,7 +1,7 @@
class Dashboard extends Shadow { class Dashboard extends Shadow {
render() { render() {
VStack(() => { VStack(() => {
console.log(window.currentNetwork) console.log(global.currentNetwork)
}) })
.width(100, pct) .width(100, pct)
.height(100, pct) .height(100, pct)

View File

@@ -1,9 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Parchment</title> <title>Forum</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/_/icons/quill.svg"> <link rel="icon" href="/_/icons/columnred.svg">
<link rel="stylesheet" href="/_/code/shared.css"> <link rel="stylesheet" href="/_/code/shared.css">
<script src="/_/code/quill.js"></script> <script src="/_/code/quill.js"></script>
<script src="/_/code/zod.js"></script> <script src="/_/code/zod.js"></script>

View File

@@ -4,45 +4,45 @@ import "./Home.js"
import util from "./util.js" import util from "./util.js"
window.util = util window.util = util
window.Socket = new Socket() global.Socket = new Socket()
window.currentNetwork = "" global.currentNetwork = ""
window.currentApp = "" global.currentApp = ""
async function openNetworkAndApp() { async function openNetworkAndApp() {
// console.log("currentApp: ", currentApp, "currentnet: ", currentNetwork, "nfrompath: ", networkFromPath(), "afrompath", appFromPath()) // console.log("currentApp: ", currentApp, "currentnet: ", currentNetwork, "nfrompath: ", networkFromPath(), "afrompath", appFromPath())
if(window.currentNetwork !== networkFromPath()) { if(global.currentNetwork !== networkFromPath()) {
window.currentNetwork = networkFromPath() global.currentNetwork = networkFromPath()
const event = new CustomEvent('networkchanged', { const event = new CustomEvent('networkchange', {
detail: { name: currentNetwork } detail: { name: currentNetwork }
}); });
window.dispatchEvent(event) window.dispatchEvent(event)
} }
if(!window.currentNetwork.data) { if(!global.currentNetwork.data) {
let appData = await fetch("/app/orgdata/" + window.profile.networks[0].id, {method: "GET"}) let appData = await fetch("/api/orgdata/" + global.profile.networks[0].id, {method: "GET"})
let json = await appData.json() let json = await appData.json()
window.currentNetwork.data = json global.currentNetwork.data = json
} }
console.log("current: ", window.currentApp, "afrompath: ", appFromPath()) console.log("current: ", global.currentApp, "afrompath: ", appFromPath())
if(window.currentApp !== appFromPath()) { if(global.currentApp !== appFromPath()) {
window.currentApp = appFromPath() global.currentApp = appFromPath()
const event = new CustomEvent('appchange', { const event = new CustomEvent('appchange', {
detail: { name: window.currentApp } detail: { name: global.currentApp }
}); });
window.dispatchEvent(event) window.dispatchEvent(event)
} }
if(window.currentNetwork) { // 2 navigates fire on load: 1 initial, and one after the org redirect if(global.currentNetwork) { // 2 navigates fire on load: 1 initial, and one after the org redirect
document.title = `${window.currentNetwork.abbreviation} | Parchment` document.title = `${global.currentNetwork.abbreviation} | Forum`
} }
} }
window.addEventListener("navigate", openNetworkAndApp) window.addEventListener("navigate", openNetworkAndApp)
window.openApp = function(appName) { global.currentApp = function(appName) {
const appUrl = appName.charAt(0).toLowerCase() + appName.slice(1); const appUrl = appName.charAt(0).toLowerCase() + appName.slice(1);
let parts = window.location.pathname.split('/').filter(Boolean); let parts = window.location.pathname.split('/').filter(Boolean);
let newPath = "/" + parts[0] + "/" + appUrl let newPath = "/" + parts[0] + "/" + appUrl
@@ -56,7 +56,7 @@ window.openApp = function(appName) {
window.networkFromPath = function () { window.networkFromPath = function () {
const pathname = window.location.pathname; const pathname = window.location.pathname;
const firstSegment = pathname.split('/').filter(Boolean)[0] || ''; const firstSegment = pathname.split('/').filter(Boolean)[0] || '';
let networks = window.profile?.networks let networks = global.profile?.networks
for(let i = 0; i < networks.length; i++) { for(let i = 0; i < networks.length; i++) {
let network = networks[i] let network = networks[i]
if(network.abbreviation === firstSegment) { if(network.abbreviation === firstSegment) {
@@ -87,7 +87,7 @@ async function getProfile() {
const profile = await res.json(); const profile = await res.json();
console.log("getProfile: ", profile); console.log("getProfile: ", profile);
window.profile = profile global.profile = profile
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@@ -95,7 +95,7 @@ async function getProfile() {
function getInitialNetworkPath() { function getInitialNetworkPath() {
let path = "" let path = ""
let defaultNetwork = window.profile.networks[0] let defaultNetwork = global.profile.networks[0]
if(!networkFromPath()) { if(!networkFromPath()) {
path += (defaultNetwork.abbreviation + "/") path += (defaultNetwork.abbreviation + "/")
@@ -111,7 +111,7 @@ function getInitialNetworkPath() {
getProfile().then(async () => { getProfile().then(async () => {
if(window.profile.networks.length > 0) { if(global.profile.networks.length > 0) {
let path = getInitialNetworkPath() let path = getInitialNetworkPath()
window.navigateTo(path) window.navigateTo(path)
} }

View File

@@ -1,9 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="public"> <html lang="en" class="public">
<head> <head>
<title>Parchment</title> <title>Forum</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/_/icons/quill.svg"> <link rel="icon" href="/_/icons/columnred.svg">
<link rel="stylesheet" href="/_/code/shared.css"> <link rel="stylesheet" href="/_/code/shared.css">
<link <link
rel="preload" rel="preload"

View File

@@ -8,9 +8,9 @@ class Home extends Shadow {
MainContent() { MainContent() {
return VStack(() => { return VStack(() => {
HStack(() => { HStack(() => {
img("/_/icons/quill.svg", "6vmax") img("/_/icons/columnred.svg", "6vmax")
p("PARCHMENT") p("FORUM")
.fontFamily("Nabla") .fontFamily("Nabla")
.fontSize(6, vmax) .fontSize(6, vmax)
.marginLeft(1, rem) .marginLeft(1, rem)
@@ -32,13 +32,15 @@ class Home extends Shadow {
.verticalAlign("center") .verticalAlign("center")
.horizontalAlign("center") .horizontalAlign("center")
p("Parchment is a platform for small to medium-sized communities.") p("Forum is a platform for communities.")
.color("var(--quillred)") .color("var(--quillred)")
.borderTop("1px solid var(--quillred)") .borderTop("1px solid var(--quillred)")
.verticalAlign("center")
.borderHorizontal("1px solid var(--quillred)") .borderHorizontal("1px solid var(--quillred)")
.height(5, em) .height(5, em)
.fontSize(window.isMobile() ? 1.5 : 1.2, vmax) .fontSize(window.isMobile() ? 2 : 1.2, vmax)
.paddingTop(2, vmax) .paddingTop(2, vmax)
.paddingBottom(window.isMobile() ? 1 : 0, em)
.paddingHorizontal(2, vmax) .paddingHorizontal(2, vmax)
}) })

View File

@@ -8,7 +8,7 @@ class SignIn extends Shadow {
render() { render() {
ZStack(() => { ZStack(() => {
img("/_/icons/quill.svg", window.isMobile() ? "5vmax" : "3vmax") img("/_/icons/columnred.svg", window.isMobile() ? "5vmax" : "3vmax")
.position("absolute") .position("absolute")
.top(2, em) .top(2, em)
.left(2, em) .left(2, em)

View File

@@ -2,7 +2,7 @@ class SignUp extends Shadow {
render() { render() {
ZStack(() => { ZStack(() => {
img("/_/icons/quill.svg", window.isMobile() ? "5vmax" : "3vmax") img("/_/icons/columnred.svg", window.isMobile() ? "5vmax" : "3vmax")
.position("absolute") .position("absolute")
.top(2, em) .top(2, em)
.left(2, em) .left(2, em)
@@ -15,8 +15,8 @@ class SignUp extends Shadow {
h2("$50 / Month Subscription") h2("$50 / Month Subscription")
.color("var(--quillred)") .color("var(--quillred)")
p(" - Access to Parchment Online and Parchment Desktop") p(" - Access to Forum Online and Forum Desktop")
p(" - Ability to Create Networks") p(" - Ability to Create Groups")
p(" - Up to 5GB Storage Space") p(" - Up to 5GB Storage Space")
button("Buy") button("Buy")

View File

@@ -11,7 +11,7 @@ class SignupForm extends Shadow {
render() { render() {
ZStack(() => { ZStack(() => {
img("/_/icons/quill.svg", window.isMobile() ? "5vmax" : "3vmax") img("/_/icons/columnred.svg", window.isMobile() ? "5vmax" : "3vmax")
.position("absolute") .position("absolute")
.top(2, em) .top(2, em)
.left(2, em) .left(2, em)

View File

@@ -1,5 +0,0 @@
# Installs
Stripe CLI
stripe listen --forward-to localhost:3003/webhook