Compare commits
23 Commits
d6d53aa4a5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
093007bc76 | ||
|
|
5001b8203d | ||
|
|
042648512f | ||
|
|
7cfcc01c99 | ||
|
|
4d2c515b7d | ||
|
|
fc81da9743 | ||
|
|
f51045c5e5 | ||
|
|
b22b12a7a5 | ||
|
|
4061e86ce7 | ||
|
|
661bf86a1a | ||
|
|
bdd260c2b5 | ||
|
|
21f654bbed | ||
|
|
b908354c04 | ||
|
|
402a99d55c | ||
|
|
dd9552aad1 | ||
| aaf9d56b1b | |||
|
|
d6520bf007 | ||
|
|
4134685b0a | ||
|
|
083b110c2d | ||
|
|
d3df5bb6cb | ||
|
|
d2982543d0 | ||
|
|
e3e24e5c17 | ||
|
|
b2bf4da13a |
6
.gitignore
vendored
@@ -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
@@ -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
@@ -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
@@ -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');
|
||||||
0
db/networks/comalyr/db.json
Normal file
4
db/personal/1/db.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"nodes": {},
|
||||||
|
"edges": {}
|
||||||
|
}
|
||||||
36
docker-compose.yml
Normal 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:
|
||||||
29
notes.js
@@ -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"))
|
||||||
|
.borderLeft("1px solid black")
|
||||||
.paddingLeft(9, px)
|
.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()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
134
server/db/db.js
@@ -5,10 +5,6 @@ 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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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++) {
|
||||||
|
|||||||
@@ -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) {
|
`;
|
||||||
this.indices = indices
|
|
||||||
}
|
}
|
||||||
|
|
||||||
schema = z.object({
|
async getByNetwork(networkId) {
|
||||||
id: z.number(),
|
return await sql`
|
||||||
type: z.string(),
|
SELECT * FROM member_network WHERE network_id = ${networkId}
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
37
server/db/model/edges/POST_BY_MEMBER.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
30
server/db/model/edges/POST_FROM_NETWORK.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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"
|
|
||||||
indices = null
|
|
||||||
|
|
||||||
constructor(indices) {
|
|
||||||
this.indices = indices
|
|
||||||
}
|
|
||||||
|
|
||||||
schema = z.object({
|
schema = z.object({
|
||||||
id: z.number(),
|
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
firstName: z.string(),
|
firstName: z.string(),
|
||||||
lastName: z.string(),
|
lastName: z.string(),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
created: z.string()
|
apps: z.array(z.string())
|
||||||
})
|
});
|
||||||
|
|
||||||
isHashed = (s) => {return s.startsWith("$argon2")}
|
isHashed = (s) => s.startsWith("$argon2");
|
||||||
|
|
||||||
async add(newMember, network = null) {
|
async add(newMember, networkId = null) {
|
||||||
const hash = await argon2.hash(newMember.password);
|
if (!this.isHashed(newMember.password)) {
|
||||||
newMember.password = hash
|
newMember.password = await argon2.hash(newMember.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = this.schema.safeParse(newMember);
|
||||||
|
if (!result.success) {
|
||||||
|
console.error(result.error);
|
||||||
|
throw new global.ServerError(400, "Invalid Member Data!");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
global.db.addNode(this.prefix, newMember)
|
const [member] = await sql`
|
||||||
if(network) {
|
INSERT INTO members ${sql({
|
||||||
global.db.edge.add({
|
email: newMember.email,
|
||||||
type: "IN",
|
first_name: newMember.firstName,
|
||||||
from: `${this.prefix}-${global.db.getCurrentIndex(this)}`,
|
last_name: newMember.lastName,
|
||||||
to: "NETWORK-1"
|
password: newMember.password,
|
||||||
})
|
apps: newMember.apps ?? ['Dashboard']
|
||||||
|
})}
|
||||||
|
RETURNING *
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (networkId) {
|
||||||
|
await sql`
|
||||||
|
INSERT INTO member_network ${sql({
|
||||||
|
member_id: member.id,
|
||||||
|
network_id: networkId,
|
||||||
|
type: 'IN'
|
||||||
|
})}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return member;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e);
|
||||||
throw new global.ServerError(400, "Failed to add member!");
|
throw new global.ServerError(400, "Failed to add member!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getByNetwork(id) {
|
async getByID(id) {
|
||||||
let connections = db.MEMBER_IN_NETWORK.getByNetwork(id)
|
const [member] = await sql`
|
||||||
let members = []
|
SELECT * FROM members WHERE id = ${id}
|
||||||
connections.forEach((conn) => {
|
`;
|
||||||
members.push(this.getByID(conn.from))
|
return member ?? null;
|
||||||
})
|
|
||||||
return members
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getByID(id) {
|
async getByEmail(email) {
|
||||||
if(typeof id === 'string') {
|
const [member] = await sql`
|
||||||
id = id.split("-")[1]
|
SELECT * FROM members WHERE email = ${email}
|
||||||
}
|
`;
|
||||||
return global.db.nodes[this.indices[0] + id - 1]
|
return member ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getByEmail(email) {
|
async getByNetwork(networkId) {
|
||||||
for(let i=this.indices[0]; i<this.indices[1]; i++) {
|
return await sql`
|
||||||
if(global.db.nodes[i].email === email) {
|
SELECT m.* FROM members m
|
||||||
return global.db.nodes[i]
|
JOIN member_network mn ON mn.member_id = m.id
|
||||||
}
|
WHERE mn.network_id = ${networkId}
|
||||||
}
|
`;
|
||||||
return null
|
}
|
||||||
|
|
||||||
|
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) : [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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}`]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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;
|
||||||
130
server/index.js
@@ -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()
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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)
|
||||||
|
if (newPost) {
|
||||||
|
global.Socket.broadcast({
|
||||||
|
event: "new-post",
|
||||||
|
app: "FORUM",
|
||||||
|
forum: msg.forum,
|
||||||
|
msg: newPost
|
||||||
|
})
|
||||||
return {success: true}
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,16 +83,30 @@ 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!")
|
|
||||||
|
|
||||||
|
if(this.appOperationSchema.safeParse(req).success) {
|
||||||
|
this.handleAppOperation(req, ws)
|
||||||
|
} else if(this.functionCallSchema.safeParse(req).success) {
|
||||||
|
this.handleFunction(req, ws)
|
||||||
|
} else {
|
||||||
|
throw new Error("Socket.handleMessage: Incoming ws message has incorrect format!")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Invalid WS message:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleAppOperation(req, ws) {
|
||||||
let responseData;
|
let responseData;
|
||||||
switch (req.app) {
|
switch (req.app) {
|
||||||
case "FORUM":
|
case "FORUM":
|
||||||
responseData = ForumHandler.handle(req.operation, req.msg, ws)
|
responseData = await ForumHandler.handle(req.operation, req.msg, ws)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "MESSAGES":
|
case "MESSAGES":
|
||||||
@@ -79,7 +114,7 @@ export default class Socket {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.error("unknown ws message")
|
console.log("unknown ws message")
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = {
|
let response = {
|
||||||
@@ -87,12 +122,20 @@ export default class Socket {
|
|||||||
}
|
}
|
||||||
response.msg = responseData
|
response.msg = responseData
|
||||||
|
|
||||||
if(!this.messageSchema.safeParse(response).success) throw new Error("Socket.handleMessage: Outgoing ws message has incorrect format!")
|
if(!this.appOperationSchema.safeParse(response).success) throw new Error("Socket.handleMessage: Outgoing ws message has incorrect format!")
|
||||||
ws.send(JSON.stringify(response))
|
ws.send(JSON.stringify(response))
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Invalid WS message:", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|||||||
56
serveradmin.md
Normal 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
|
||||||
34
ui/_/code/bridge/bridge.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
14
ui/_/code/bridge/handlers.js
Normal 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
|
||||||
10
ui/_/code/bridge/serverFunctions.js
Normal 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)
|
||||||
@@ -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)) {
|
||||||
|
if(value === null) {
|
||||||
|
this.removeAttribute(key)
|
||||||
|
} else {
|
||||||
this.setAttribute(key, value);
|
this.setAttribute(key, value);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
: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;
|
||||||
--parchment: #FEBA7D;
|
--parchment: #FEBA7D;
|
||||||
--gold: #FEBA7D;
|
--gold: #FEBA7D;
|
||||||
--divider: #bb7c36;
|
--divider: #bb7c36;
|
||||||
@@ -12,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;
|
||||||
@@ -79,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;
|
||||||
|
|||||||
@@ -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 = new WebSocket(url);
|
||||||
|
|
||||||
this.ws.addEventListener('open', () => {
|
this.ws.addEventListener('open', () => {
|
||||||
this.connectionTries = 0
|
this.connectionTries = 0;
|
||||||
console.log("Websocket connection established.");
|
console.log("WebSocket connection established.");
|
||||||
this.ws.addEventListener('message', this.receiveCB)
|
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;
|
||||||
|
|||||||
@@ -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
@@ -0,0 +1 @@
|
|||||||
|
export default {}
|
||||||
@@ -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
@@ -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
|
After Width: | Height: | Size: 17 KiB |
BIN
ui/_/icons/columnnewsmall.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
9
ui/_/icons/columnred.svg
Normal 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
|
After Width: | Height: | Size: 8.1 KiB |
3
ui/_/icons/settingsdark.svg
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
@@ -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"
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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('-', ' ')
|
||||||
|
.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('-', ' ')
|
||||||
|
.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);
|
||||||
|
|||||||
203
ui/desktop/apps/Dashboard/alt.js
Normal 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, '"')}">
|
||||||
|
${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)
|
||||||
@@ -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 = ""
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 = ""
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
67
ui/desktop/apps/Settings/Settings.js
Normal 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)
|
||||||
@@ -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)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
ui/desktop/components/LoadingCircleSmall.js
Normal 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)
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,46 +1,15 @@
|
|||||||
import Socket from "./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 = ""
|
|
||||||
|
|
||||||
async function openNetworkAndApp() {
|
|
||||||
|
|
||||||
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', {
|
|
||||||
detail: { name: window.currentApp }
|
|
||||||
});
|
|
||||||
window.dispatchEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(window.currentNetwork) { // 2 navigates fire on load: 1 initial, and one after the org redirect
|
|
||||||
document.title = `${window.currentNetwork.abbreviation} | Parchment`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("navigate", openNetworkAndApp)
|
|
||||||
|
|
||||||
window.openApp = 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
|
||||||
@@ -51,10 +20,109 @@ window.openApp = function(appName) {
|
|||||||
window.dispatchEvent(event)
|
window.dispatchEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.networkFromPath = function () {
|
async fetchOrgData() {
|
||||||
const pathname = window.location.pathname;
|
let personalSpace = this.currentNetwork === this.profile
|
||||||
const firstSegment = pathname.split('/').filter(Boolean)[0] || '';
|
let appData = await fetch(`/api/${personalSpace ? "my" : "org"}data/` + this.currentNetwork.id, {method: "GET"})
|
||||||
let networks = window.profile?.networks
|
let json = await appData.json()
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleStripeOnboarding() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const code = params.get("code");
|
||||||
|
const { returnTo, networkId } = JSON.parse(atob(params.get("state")));
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
console.log("success!: ", code)
|
||||||
|
await fetch("/api/stripe/onboarded", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ code, networkId })
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.href = returnTo
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error("Stripe code is not present!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNavigate = async () => {
|
||||||
|
console.log("onnavigate", this.getFirstPathSegment())
|
||||||
|
if(this.getFirstPathSegment() === "stripe") {
|
||||||
|
this.handleStripeOnboarding()
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedNetwork = this.networkFromPath()
|
||||||
|
let selectedApp = this.appFromPath()
|
||||||
|
|
||||||
|
if(!selectedNetwork) {
|
||||||
|
if(this.profile.networks.length > 0) {
|
||||||
|
let path = `/${this.getDefaultNetworkName()}/${this.getDefaultAppName()}`
|
||||||
|
history.replaceState({}, '', path)
|
||||||
|
}
|
||||||
|
} else if(!selectedApp) {
|
||||||
|
if(this.currentNetwork === window.profile) {
|
||||||
|
history.replaceState({}, '', `${window.location.pathname}/${window.profile.apps[0]}`)
|
||||||
|
} else {
|
||||||
|
history.replaceState({}, '', `${window.location.pathname}/${this.getDefaultAppName()}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedNetwork = this.networkFromPath()
|
||||||
|
selectedApp = this.appFromPath()
|
||||||
|
|
||||||
|
let networkChanged = this.currentNetwork !== selectedNetwork
|
||||||
|
let appChanged = this.currentApp !== selectedApp
|
||||||
|
if(networkChanged) {
|
||||||
|
this.currentNetwork = selectedNetwork
|
||||||
|
this.currentApp = selectedApp
|
||||||
|
const event = new CustomEvent('networkchange', {
|
||||||
|
detail: { name: this.currentNetwork }
|
||||||
|
});
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.currentNetwork.data) {
|
||||||
|
this.currentNetwork.data = await this.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++) {
|
for(let i = 0; i < networks.length; i++) {
|
||||||
let network = networks[i]
|
let network = networks[i]
|
||||||
if(network.abbreviation === firstSegment) {
|
if(network.abbreviation === firstSegment) {
|
||||||
@@ -62,16 +130,17 @@ window.networkFromPath = function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.appFromPath = function() {
|
appFromPath = function() {
|
||||||
const pathname = window.location.pathname;
|
const pathname = window.location.pathname;
|
||||||
const segments = pathname.split('/').filter(Boolean);
|
const segments = pathname.split('/').filter(Boolean);
|
||||||
const secondSegment = segments[1] || "";
|
const secondSegment = segments[1] || ""
|
||||||
const capitalized = secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1);
|
const capitalized = secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1);
|
||||||
return capitalized
|
return capitalized
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getProfile() {
|
async getProfile() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/profile", {
|
const res = await fetch("/profile", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -85,34 +154,23 @@ async function getProfile() {
|
|||||||
|
|
||||||
const profile = await res.json();
|
const profile = await res.json();
|
||||||
console.log("getProfile: ", profile);
|
console.log("getProfile: ", profile);
|
||||||
window.profile = profile
|
this.profile = profile
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialNetworkPath() {
|
constructor() {
|
||||||
let path = ""
|
window.addEventListener("navigate", this.onNavigate)
|
||||||
let defaultNetwork = window.profile.networks[0]
|
|
||||||
|
|
||||||
if(!networkFromPath()) {
|
this.getProfile().then(async () => {
|
||||||
path += (defaultNetwork.abbreviation + "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!appFromPath()) {
|
await this.onNavigate()
|
||||||
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()
|
Home()
|
||||||
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.global = new Global()
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
33
ui/mobile/Home.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import "./components/AppMenu.js"
|
||||||
|
import "./apps/Dashboard/Dashboard.js"
|
||||||
|
import "./apps/People/People.js"
|
||||||
|
|
||||||
|
class Home extends Shadow {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
ZStack(() => {
|
||||||
|
|
||||||
|
ZStack(() => {
|
||||||
|
console.log(global.currentApp)
|
||||||
|
switch(global.currentApp) {
|
||||||
|
case "Dashboard":
|
||||||
|
Dashboard()
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "People":
|
||||||
|
People()
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onNavigate(function () {
|
||||||
|
console.log("navigate")
|
||||||
|
this.rerender()
|
||||||
|
})
|
||||||
|
|
||||||
|
AppMenu()
|
||||||
|
})
|
||||||
|
.overflowX("hidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
register(Home)
|
||||||
11
ui/mobile/apps/Dashboard/Dashboard.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
class Dashboard extends Shadow {
|
||||||
|
render() {
|
||||||
|
VStack(() => {
|
||||||
|
console.log(global.currentNetwork)
|
||||||
|
})
|
||||||
|
.width(100, pct)
|
||||||
|
.height(100, pct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
register(Dashboard)
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import './ForumPanel.js'
|
|
||||||
|
|
||||||
css(`
|
|
||||||
forum- {
|
|
||||||
font-family: 'Bona';
|
|
||||||
}
|
|
||||||
|
|
||||||
forum- input::placeholder {
|
|
||||||
font-family: 'Bona Nova';
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
appearance: none; /* remove default style */
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
border: 1px solid var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"]:checked {
|
|
||||||
background-color: var(--red);
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
|
|
||||||
class Forum extends Shadow {
|
|
||||||
|
|
||||||
selectedForum = "HY"
|
|
||||||
|
|
||||||
render() {
|
|
||||||
ZStack(() => {
|
|
||||||
VStack(() => {
|
|
||||||
|
|
||||||
ForumPanel()
|
|
||||||
|
|
||||||
input("Message Parchment", "98%")
|
|
||||||
.paddingVertical(1, em)
|
|
||||||
.paddingLeft(2, pct)
|
|
||||||
.color("var(--accent)")
|
|
||||||
.background("var(--darkbrown)")
|
|
||||||
.marginBottom(6, em)
|
|
||||||
.border("none")
|
|
||||||
.fontSize(1, em)
|
|
||||||
.onKeyDown(function (e) {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
window.Socket.send({app: "FORUM", operation: "SEND", msg: {forum: "HY", text: this.value }})
|
|
||||||
this.value = ""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.gap(0.5, em)
|
|
||||||
.width(100, pct)
|
|
||||||
.height(100, vh)
|
|
||||||
.horizontalAlign("center")
|
|
||||||
.verticalAlign("end")
|
|
||||||
})
|
|
||||||
.onAppear(() => document.body.style.backgroundColor = "var(--darkbrown)")
|
|
||||||
.width(100, pct)
|
|
||||||
.height(100, pct)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
register(Forum)
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import "../../components/LoadingCircle.js"
|
|
||||||
|
|
||||||
class ForumPanel extends Shadow {
|
|
||||||
forums = [
|
|
||||||
"HY"
|
|
||||||
]
|
|
||||||
messages = []
|
|
||||||
|
|
||||||
render() {
|
|
||||||
VStack(() => {
|
|
||||||
if(this.messages.length > 0) {
|
|
||||||
|
|
||||||
let previousDate = null
|
|
||||||
|
|
||||||
for(let i=0; i<this.messages.length; i++) {
|
|
||||||
let message = this.messages[i]
|
|
||||||
const dateParts = this.parseDate(message.time);
|
|
||||||
const { date, time } = dateParts;
|
|
||||||
|
|
||||||
if (previousDate !== date) {
|
|
||||||
previousDate = date;
|
|
||||||
|
|
||||||
p(date)
|
|
||||||
.textAlign("center")
|
|
||||||
.opacity(0.5)
|
|
||||||
.marginVertical(1, em)
|
|
||||||
.color("var(--divider)")
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(() => {
|
|
||||||
HStack(() => {
|
|
||||||
p(message.sentBy)
|
|
||||||
.fontWeight("bold")
|
|
||||||
.marginBottom(0.3, em)
|
|
||||||
|
|
||||||
p(util.formatTime(message.time))
|
|
||||||
.opacity(0.2)
|
|
||||||
.marginLeft(1, em)
|
|
||||||
})
|
|
||||||
p(message.text)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LoadingCircle()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.gap(1, em)
|
|
||||||
.position("relative")
|
|
||||||
.overflow("scroll")
|
|
||||||
.height(100, pct)
|
|
||||||
.width(96, pct)
|
|
||||||
.paddingTop(5, em)
|
|
||||||
.paddingBottom(2, em)
|
|
||||||
.paddingLeft(4, pct)
|
|
||||||
.backgroundColor("var(--darkbrown)")
|
|
||||||
.onAppear(async () => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.scrollTop = this.scrollHeight
|
|
||||||
});
|
|
||||||
let res = await Socket.send({app: "FORUM", operation: "GET", msg: {forum: "HY", number: 100}})
|
|
||||||
if(!res) console.error("failed to get messages")
|
|
||||||
if(res.msg.length > 0 && this.messages.length === 0) {
|
|
||||||
this.messages = res.msg
|
|
||||||
this.rerender()
|
|
||||||
}
|
|
||||||
window.addEventListener("new-post", (e) => {
|
|
||||||
this.messages = e.detail
|
|
||||||
if(e.detail.length !== this.messages || e.detail.last.time !== this.messages.last.time || e.detail.first.time !== this.messages.first.time) {
|
|
||||||
this.rerender()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
parseDate(str) {
|
|
||||||
// Format: MM.DD.YYYY-HH:MM:SSxxxxxx(am|pm)
|
|
||||||
const match = str.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})-(\d{1,2}):(\d{2}).*(am|pm)$/i);
|
|
||||||
if (!match) return null;
|
|
||||||
|
|
||||||
const [, mm, dd, yyyy, hh, min, ampm] = match;
|
|
||||||
const date = `${mm}/${dd}/${yyyy}`;
|
|
||||||
const time = `${hh}:${min}${ampm.toLowerCase()}`;
|
|
||||||
|
|
||||||
return { date, time };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
register(ForumPanel)
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import "./JobsSidebar.js"
|
|
||||||
import "./JobsGrid.js"
|
|
||||||
|
|
||||||
css(`
|
|
||||||
jobs- {
|
|
||||||
font-family: 'Bona';
|
|
||||||
}
|
|
||||||
|
|
||||||
jobs- input::placeholder {
|
|
||||||
font-family: 'Bona Nova';
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
appearance: none; /* remove default style */
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
border: 1px solid var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"]:checked {
|
|
||||||
background-color: var(--red);
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
|
|
||||||
class Jobs extends Shadow {
|
|
||||||
jobs = [
|
|
||||||
{
|
|
||||||
title: "Austin Chapter Lead",
|
|
||||||
salary: "1% of Local Revenue",
|
|
||||||
company: "Parchment",
|
|
||||||
city: "Austin",
|
|
||||||
state: "TX"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
render() {
|
|
||||||
ZStack(() => {
|
|
||||||
HStack(() => {
|
|
||||||
JobsSidebar()
|
|
||||||
|
|
||||||
JobsGrid(this.jobs)
|
|
||||||
})
|
|
||||||
.width(100, "%")
|
|
||||||
.x(0).y(13, vh)
|
|
||||||
|
|
||||||
HStack(() => {
|
|
||||||
input("Search jobs... (Coming Soon!)", "45vw")
|
|
||||||
.attr({
|
|
||||||
"type": "text",
|
|
||||||
"disabled": "true"
|
|
||||||
})
|
|
||||||
.fontSize(1.1, em)
|
|
||||||
.paddingLeft(1.3, em)
|
|
||||||
.background("transparent")
|
|
||||||
.border("0.5px solid var(--divider)")
|
|
||||||
.outline("none")
|
|
||||||
.color("var(--accent)")
|
|
||||||
.opacity(0.5)
|
|
||||||
.borderRadius(10, px)
|
|
||||||
.background("grey")
|
|
||||||
.cursor("not-allowed")
|
|
||||||
|
|
||||||
button("+ Add Job")
|
|
||||||
.width(7, em)
|
|
||||||
.marginLeft(1, em)
|
|
||||||
.borderRadius(10, px)
|
|
||||||
.background("transparent")
|
|
||||||
.border("0.3px solid var(--accent2)")
|
|
||||||
.color("var(--accent)")
|
|
||||||
.fontFamily("Bona Nova")
|
|
||||||
.onHover(function (hovering) {
|
|
||||||
if(hovering) {
|
|
||||||
this.style.background = "var(--green)"
|
|
||||||
|
|
||||||
} else {
|
|
||||||
this.style.background = "transparent"
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onClick((clicking) => {
|
|
||||||
console.log(this, "clicked")
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
.x(55, vw).y(4, vh)
|
|
||||||
.position("absolute")
|
|
||||||
.transform("translateX(-50%)")
|
|
||||||
})
|
|
||||||
.width(100, "%")
|
|
||||||
.height(100, "%")
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
// Optional additional logic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
register(Jobs)
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
class JobsGrid extends Shadow {
|
|
||||||
jobs;
|
|
||||||
|
|
||||||
constructor(jobs) {
|
|
||||||
super()
|
|
||||||
this.jobs = jobs
|
|
||||||
}
|
|
||||||
|
|
||||||
boldUntilFirstSpace(text) {
|
|
||||||
const index = text.indexOf(' ');
|
|
||||||
if (index === -1) {
|
|
||||||
// No spaces — bold the whole thing
|
|
||||||
return `<b>${text}</b>`;
|
|
||||||
}
|
|
||||||
return `<b>${text.slice(0, index)}</b>${text.slice(index)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
VStack(() => {
|
|
||||||
h3("Results")
|
|
||||||
.marginTop(0.1, em)
|
|
||||||
.marginBottom(1, em)
|
|
||||||
.marginLeft(0.4, em)
|
|
||||||
.color("var(--accent2)")
|
|
||||||
|
|
||||||
if (this.jobs.length > 0) {
|
|
||||||
ZStack(() => {
|
|
||||||
for (let i = 0; i < this.jobs.length; i++) {
|
|
||||||
VStack(() => {
|
|
||||||
p(this.jobs[i].title)
|
|
||||||
.fontSize(1.2, em)
|
|
||||||
.fontWeight("bold")
|
|
||||||
.marginBottom(0.5, em)
|
|
||||||
p(this.jobs[i].company)
|
|
||||||
p(this.jobs[i].city + ", " + this.jobs[i].state)
|
|
||||||
.marginBottom(0.5, em)
|
|
||||||
p(this.boldUntilFirstSpace(this.jobs[i].salary))
|
|
||||||
})
|
|
||||||
.padding(1, em)
|
|
||||||
.borderRadius(5, "px")
|
|
||||||
.background("var(--darkbrown)")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.display("grid")
|
|
||||||
.gridTemplateColumns("repeat(auto-fill, minmax(250px, 1fr))")
|
|
||||||
.gap(1, em)
|
|
||||||
} else {
|
|
||||||
p("No Jobs!")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.height(100, vh)
|
|
||||||
.paddingLeft(2, em)
|
|
||||||
.paddingRight(2, em)
|
|
||||||
.paddingTop(2, em)
|
|
||||||
.gap(0, em)
|
|
||||||
.width(100, "%")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
register(JobsGrid)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
class JobsSidebar extends Shadow {
|
|
||||||
render() {
|
|
||||||
VStack(() => {
|
|
||||||
h3("Location")
|
|
||||||
.color("var(--accent2)")
|
|
||||||
.marginBottom(0, em)
|
|
||||||
|
|
||||||
HStack(() => {
|
|
||||||
input("Location", "100%")
|
|
||||||
.paddingLeft(3, em)
|
|
||||||
.paddingVertical(0.75, em)
|
|
||||||
.backgroundImage("/_/icons/locationPin.svg")
|
|
||||||
.backgroundRepeat("no-repeat")
|
|
||||||
.backgroundSize("18px 18px")
|
|
||||||
.backgroundPosition("10px center")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.paddingTop(1, em)
|
|
||||||
.paddingLeft(3, em)
|
|
||||||
.paddingRight(3, em)
|
|
||||||
.gap(1, em)
|
|
||||||
.minWidth(10, vw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
register(JobsSidebar)
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import "./MarketSidebar.js"
|
|
||||||
import "./MarketGrid.js"
|
|
||||||
|
|
||||||
css(`
|
|
||||||
market- {
|
|
||||||
font-family: 'Bona';
|
|
||||||
}
|
|
||||||
|
|
||||||
market- input::placeholder {
|
|
||||||
font-family: 'Bona Nova';
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
appearance: none; /* remove default style */
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
border: 1px solid var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"]:checked {
|
|
||||||
background-color: var(--red);
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
|
|
||||||
class Market extends Shadow {
|
|
||||||
|
|
||||||
listings = [
|
|
||||||
{
|
|
||||||
title: "Shield Lapel Pin",
|
|
||||||
stars: "5",
|
|
||||||
reviews: 1,
|
|
||||||
price: "$12",
|
|
||||||
company: "Parchment",
|
|
||||||
type: "new",
|
|
||||||
image: "/db/images/1",
|
|
||||||
madeIn: "America"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
render() {
|
|
||||||
ZStack(() => {
|
|
||||||
HStack(() => {
|
|
||||||
MarketSidebar()
|
|
||||||
|
|
||||||
MarketGrid(this.listings)
|
|
||||||
})
|
|
||||||
.width(100, "%")
|
|
||||||
.x(0).y(13, vh)
|
|
||||||
|
|
||||||
HStack(() => {
|
|
||||||
input("Search for products... (Coming Soon!)", "45vw")
|
|
||||||
.attr({
|
|
||||||
"type": "text",
|
|
||||||
"disabled": "true"
|
|
||||||
})
|
|
||||||
.fontSize(1.1, em)
|
|
||||||
.paddingLeft(1.3, em)
|
|
||||||
.background("transparent")
|
|
||||||
.border("0.5px solid var(--divider)")
|
|
||||||
.outline("none")
|
|
||||||
.color("var(--accent)")
|
|
||||||
.opacity(0.5)
|
|
||||||
.borderRadius(10, px)
|
|
||||||
.background("grey")
|
|
||||||
.cursor("not-allowed")
|
|
||||||
|
|
||||||
button("+ Add Item")
|
|
||||||
.width(7, em)
|
|
||||||
.marginLeft(1, em)
|
|
||||||
.borderRadius(10, px)
|
|
||||||
.background("transparent")
|
|
||||||
.border("0.5px solid var(--accent2)")
|
|
||||||
.color("var(--accent)")
|
|
||||||
.fontFamily("Bona Nova")
|
|
||||||
.onHover(function (hovering) {
|
|
||||||
if(hovering) {
|
|
||||||
this.style.background = "var(--green)"
|
|
||||||
|
|
||||||
} else {
|
|
||||||
this.style.background = "transparent"
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onClick((clicking) => {
|
|
||||||
console.log(this, "clicked")
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
.x(55, vw).y(4, vh)
|
|
||||||
.position("absolute")
|
|
||||||
.transform("translateX(-50%)")
|
|
||||||
})
|
|
||||||
.width(100, "%")
|
|
||||||
.height(100, "%")
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
// Optional additional logic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
register(Market)
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
class MarketGrid extends Shadow {
|
|
||||||
listings;
|
|
||||||
|
|
||||||
constructor(listings) {
|
|
||||||
super()
|
|
||||||
this.listings = listings
|
|
||||||
}
|
|
||||||
|
|
||||||
boldUntilFirstSpace(text) {
|
|
||||||
if(!text) return
|
|
||||||
const index = text.indexOf(' ');
|
|
||||||
if (index === -1) {
|
|
||||||
// No spaces — bold the whole thing
|
|
||||||
return `<b>${text}</b>`;
|
|
||||||
}
|
|
||||||
return `<b>${text.slice(0, index)}</b>${text.slice(index)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
VStack(() => {
|
|
||||||
h3("Results")
|
|
||||||
.marginTop(0.1, em)
|
|
||||||
.marginBottom(1, em)
|
|
||||||
.marginLeft(0.4, em)
|
|
||||||
.color("var(--accent)")
|
|
||||||
.opacity(0.7)
|
|
||||||
|
|
||||||
if (this.listings.length > 0) {
|
|
||||||
ZStack(() => {
|
|
||||||
// BuyModal()
|
|
||||||
|
|
||||||
let params = new URLSearchParams(window.location.search);
|
|
||||||
|
|
||||||
const ParchmentMade = params.get("Parchment-made") === "true";
|
|
||||||
const americaMade = params.get("america-made") === "true";
|
|
||||||
const newItem = params.get("new") === "true";
|
|
||||||
const usedItem = params.get("used") === "true";
|
|
||||||
|
|
||||||
|
|
||||||
let filtered = this.listings;
|
|
||||||
if (ParchmentMade) {
|
|
||||||
filtered = filtered.filter(item => item.madeIn === "Parchment");
|
|
||||||
}
|
|
||||||
if (americaMade) {
|
|
||||||
filtered = filtered.filter(item => item.madeIn === "America");
|
|
||||||
}
|
|
||||||
if (newItem) {
|
|
||||||
filtered = filtered.filter(item => item.type === "new");
|
|
||||||
}
|
|
||||||
if (usedItem) {
|
|
||||||
filtered = filtered.filter(item => item.type === "used");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < filtered.length; i++) {
|
|
||||||
const rating = filtered[i].stars
|
|
||||||
const percent = (rating / 5)
|
|
||||||
|
|
||||||
VStack(() => {
|
|
||||||
img(filtered[i].image)
|
|
||||||
.marginBottom(0.5, em)
|
|
||||||
|
|
||||||
p(filtered[i].company)
|
|
||||||
.marginBottom(0.5, em)
|
|
||||||
|
|
||||||
p(filtered[i].title)
|
|
||||||
.fontSize(1.2, em)
|
|
||||||
.fontWeight("bold")
|
|
||||||
.marginBottom(0.5, em)
|
|
||||||
|
|
||||||
HStack(() => {
|
|
||||||
p(filtered[i].stars)
|
|
||||||
.marginRight(0.2, em)
|
|
||||||
|
|
||||||
ZStack(() => {
|
|
||||||
div("★★★★★") // Empty stars (background)
|
|
||||||
.color("#ccc")
|
|
||||||
|
|
||||||
div("★★★★★") // Filled stars (foreground, clipped by width)
|
|
||||||
.color("#ffa500")
|
|
||||||
.position("absolute")
|
|
||||||
.top(0)
|
|
||||||
.left(0)
|
|
||||||
.whiteSpace("nowrap")
|
|
||||||
.overflow("hidden")
|
|
||||||
.width(percent * 5, em)
|
|
||||||
})
|
|
||||||
.display("inline-block")
|
|
||||||
.position("relative")
|
|
||||||
.fontSize(1.2, em)
|
|
||||||
.lineHeight(1)
|
|
||||||
|
|
||||||
p(filtered[i].reviews)
|
|
||||||
.marginLeft(0.2, em)
|
|
||||||
})
|
|
||||||
.marginBottom(0.5, em)
|
|
||||||
|
|
||||||
p(filtered[i].price)
|
|
||||||
.fontSize(1.75, em)
|
|
||||||
.marginBottom(0.5, em)
|
|
||||||
|
|
||||||
button("Coming Soon!")
|
|
||||||
.onClick((finished) => {
|
|
||||||
if(finished) {
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onHover(function (hovering) {
|
|
||||||
if(hovering) {
|
|
||||||
this.style.backgroundColor = "var(--green)"
|
|
||||||
} else {
|
|
||||||
this.style.backgroundColor = ""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
.padding(1, em)
|
|
||||||
.border("1px solid var(--accent2)")
|
|
||||||
.borderRadius(5, "px")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.display("grid")
|
|
||||||
.gridTemplateColumns("repeat(auto-fill, minmax(250px, 1fr))")
|
|
||||||
.gap(1, em)
|
|
||||||
} else {
|
|
||||||
p("No Listings!")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onQueryChanged(() => {
|
|
||||||
console.log("query did change yup")
|
|
||||||
this.rerender()
|
|
||||||
})
|
|
||||||
.height(100, vh)
|
|
||||||
.paddingLeft(2, em)
|
|
||||||
.paddingRight(2, em)
|
|
||||||
.gap(0, em)
|
|
||||||
.width(100, "%")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
register(MarketGrid)
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
class MarketSidebar extends Shadow {
|
|
||||||
|
|
||||||
handleChecked(e) {
|
|
||||||
let checked = e.target.checked
|
|
||||||
let label = $(`label[for="${e.target.id}"]`).innerText
|
|
||||||
if(checked) {
|
|
||||||
window.setQuery(label.toLowerCase(), true)
|
|
||||||
} else {
|
|
||||||
window.setQuery(label.toLowerCase(), null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
VStack(() => {
|
|
||||||
|
|
||||||
p("Make")
|
|
||||||
|
|
||||||
HStack(() => {
|
|
||||||
input()
|
|
||||||
.attr({
|
|
||||||
"type": "checkbox",
|
|
||||||
"id": "Parchment-check"
|
|
||||||
})
|
|
||||||
.onChange(this.handleChecked)
|
|
||||||
label("Parchment-Made")
|
|
||||||
.attr({
|
|
||||||
"for": "Parchment-check"
|
|
||||||
})
|
|
||||||
.marginLeft(0.5, em)
|
|
||||||
})
|
|
||||||
|
|
||||||
HStack(() => {
|
|
||||||
input()
|
|
||||||
.attr({
|
|
||||||
"type": "checkbox",
|
|
||||||
"id": "america-check"
|
|
||||||
})
|
|
||||||
.onChange(this.handleChecked)
|
|
||||||
label("America-Made")
|
|
||||||
.attr({
|
|
||||||
"for": "america-check"
|
|
||||||
})
|
|
||||||
.marginLeft(0.5, em)
|
|
||||||
})
|
|
||||||
|
|
||||||
p("Condition")
|
|
||||||
|
|
||||||
HStack(() => {
|
|
||||||
input()
|
|
||||||
.attr({
|
|
||||||
"type": "checkbox",
|
|
||||||
"id": "new-check"
|
|
||||||
})
|
|
||||||
.onChange(this.handleChecked)
|
|
||||||
label("New")
|
|
||||||
.attr({
|
|
||||||
"for": "new-check"
|
|
||||||
})
|
|
||||||
.marginLeft(0.5, em)
|
|
||||||
})
|
|
||||||
|
|
||||||
HStack(() => {
|
|
||||||
input()
|
|
||||||
.attr({
|
|
||||||
"type": "checkbox",
|
|
||||||
"id": "used-check"
|
|
||||||
})
|
|
||||||
.onChange(this.handleChecked)
|
|
||||||
label("Used")
|
|
||||||
.attr({
|
|
||||||
"for": "used-check"
|
|
||||||
})
|
|
||||||
.marginLeft(0.5, em)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.paddingTop(12, vh)
|
|
||||||
.paddingLeft(3, em)
|
|
||||||
.paddingRight(3, em)
|
|
||||||
.gap(1, em)
|
|
||||||
.minWidth(10, vw)
|
|
||||||
.userSelect('none')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
register(MarketSidebar)
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
import "./MessagesSidebar.js"
|
|
||||||
import "./MessagesPanel.js"
|
|
||||||
|
|
||||||
css(`
|
|
||||||
messages- {
|
|
||||||
font-family: 'Bona';
|
|
||||||
}
|
|
||||||
|
|
||||||
messages- input::placeholder {
|
|
||||||
font-family: 'Bona Nova';
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
appearance: none; /* remove default style */
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
border: 1px solid var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"]:checked {
|
|
||||||
background-color: var(--red);
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
|
|
||||||
class Messages extends Shadow {
|
|
||||||
conversations = []
|
|
||||||
selectedConvoID = null
|
|
||||||
onConversationSelect(i) {
|
|
||||||
console.log("convo selected: ", i)
|
|
||||||
this.selectedConvoID = i
|
|
||||||
this.$("messagessidebar-").rerender()
|
|
||||||
this.$("messagespanel-").rerender()
|
|
||||||
}
|
|
||||||
|
|
||||||
getConvoFromID(id) {
|
|
||||||
for(let i=0; i<this.conversations.length; i++) {
|
|
||||||
if(this.conversations[i].id === id) {
|
|
||||||
return this.conversations[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
ZStack(() => {
|
|
||||||
HStack(() => {
|
|
||||||
MessagesSidebar(this.conversations, this.selectedConvoID, this.onConversationSelect)
|
|
||||||
|
|
||||||
VStack(() => {
|
|
||||||
if(this.getConvoFromID(this.selectedConvoID)) {
|
|
||||||
MessagesPanel(this.getConvoFromID(this.selectedConvoID).messages)
|
|
||||||
} else {
|
|
||||||
MessagesPanel()
|
|
||||||
}
|
|
||||||
|
|
||||||
input("Send Message", "93%")
|
|
||||||
.paddingVertical(1, em)
|
|
||||||
.paddingHorizontal(2, em)
|
|
||||||
.color("var(--accent)")
|
|
||||||
.background("var(--darkbrown)")
|
|
||||||
.marginBottom(6, em)
|
|
||||||
.border("none")
|
|
||||||
.fontSize(1, em)
|
|
||||||
.onKeyDown((e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
window.Socket.send({app: "MESSAGES", operation: "SEND", msg: { conversation: `CONVERSATION-${this.selectedConvoID}`, text: e.target.value }})
|
|
||||||
e.target.value = ""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.gap(1, em)
|
|
||||||
.width(100, pct)
|
|
||||||
.horizontalAlign("center")
|
|
||||||
.verticalAlign("end")
|
|
||||||
})
|
|
||||||
.onAppear(async () => {
|
|
||||||
let res = await Socket.send({app: "MESSAGES", operation: "GET"})
|
|
||||||
if(!res) console.error("failed to get messages")
|
|
||||||
|
|
||||||
if(res.msg.length > 0 && this.conversations.length === 0) {
|
|
||||||
this.conversations = res.msg
|
|
||||||
this.selectedConvoID = this.conversations[0].id
|
|
||||||
this.rerender()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("new-message", (e) => {
|
|
||||||
let convoID = e.detail.conversationID
|
|
||||||
let messages = e.detail.messages
|
|
||||||
let convo = this.getConvoFromID(convoID)
|
|
||||||
convo.messages = messages
|
|
||||||
this.rerender()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.width(100, "%")
|
|
||||||
.height(87, vh)
|
|
||||||
.x(0).y(13, vh)
|
|
||||||
|
|
||||||
VStack(() => {
|
|
||||||
p("Add Message")
|
|
||||||
|
|
||||||
input("enter email...")
|
|
||||||
.color("var(--accent)")
|
|
||||||
.onKeyDown(function (e) {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
window.Socket.send({app: "MESSAGES", operation: "ADDCONVERSATION", msg: {email: this.value }})
|
|
||||||
this.value = ""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
p("x")
|
|
||||||
.onClick(function (done) {
|
|
||||||
if(done) {
|
|
||||||
this.parentElement.style.display = "none"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.xRight(2, em).y(2, em)
|
|
||||||
.fontSize(1.4, em)
|
|
||||||
.cursor("pointer")
|
|
||||||
|
|
||||||
})
|
|
||||||
.gap(1, em)
|
|
||||||
.verticalAlign("center")
|
|
||||||
.horizontalAlign("center")
|
|
||||||
.backgroundColor("black")
|
|
||||||
.border("1px solid var(--accent)")
|
|
||||||
.position("fixed")
|
|
||||||
.x(50, vw).y(50, vh)
|
|
||||||
.center()
|
|
||||||
.width(60, vw)
|
|
||||||
.height(60, vh)
|
|
||||||
.display("none")
|
|
||||||
.attr({id: "addPanel"})
|
|
||||||
|
|
||||||
HStack(() => {
|
|
||||||
input("Search messages... (Coming Soon!)", "45vw")
|
|
||||||
.attr({
|
|
||||||
"type": "text",
|
|
||||||
"disabled": "true"
|
|
||||||
})
|
|
||||||
.fontSize(1.1, em)
|
|
||||||
.paddingLeft(1.3, em)
|
|
||||||
.background("transparent")
|
|
||||||
.border("0.5px solid var(--divider)")
|
|
||||||
.outline("none")
|
|
||||||
.color("var(--accent)")
|
|
||||||
.opacity(0.5)
|
|
||||||
.borderRadius(10, px)
|
|
||||||
.background("grey")
|
|
||||||
.cursor("not-allowed")
|
|
||||||
|
|
||||||
button("+ New Message")
|
|
||||||
.width(13, em)
|
|
||||||
.marginLeft(1, em)
|
|
||||||
.borderRadius(10, px)
|
|
||||||
.background("transparent")
|
|
||||||
.border("0.5px solid var(--divider)")
|
|
||||||
.color("var(--accent)")
|
|
||||||
.fontFamily("Bona Nova")
|
|
||||||
.onHover(function (hovering) {
|
|
||||||
if(hovering) {
|
|
||||||
this.style.background = "var(--green)"
|
|
||||||
|
|
||||||
} else {
|
|
||||||
this.style.background = "transparent"
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onClick((done) => {
|
|
||||||
console.log("click")
|
|
||||||
if(done) {
|
|
||||||
this.$("#addPanel").style.display = "flex"
|
|
||||||
}
|
|
||||||
console.log(this, "clicked")
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
.x(55, vw).y(4, vh)
|
|
||||||
.position("absolute")
|
|
||||||
.transform("translateX(-50%)")
|
|
||||||
})
|
|
||||||
.width(100, "%")
|
|
||||||
.height(100, "%")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
register(Messages)
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import "../../components/LoadingCircle.js"
|
|
||||||
|
|
||||||
class MessagesPanel extends Shadow {
|
|
||||||
messages
|
|
||||||
|
|
||||||
constructor(messages) {
|
|
||||||
super()
|
|
||||||
this.messages = messages
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
VStack(() => {
|
|
||||||
if(this.messages) {
|
|
||||||
for(let i=0; i<this.messages.length; i++) {
|
|
||||||
let message = this.messages[i]
|
|
||||||
let fromMe = window.profile.email === message.from.email
|
|
||||||
VStack(() => {
|
|
||||||
HStack(() => {
|
|
||||||
p(message.from.firstName + " " + message.from.lastName)
|
|
||||||
.fontWeight("bold")
|
|
||||||
.marginBottom(0.3, em)
|
|
||||||
|
|
||||||
p(util.formatTime(message.time))
|
|
||||||
.opacity(0.2)
|
|
||||||
.marginLeft(1, em)
|
|
||||||
})
|
|
||||||
p(message.text)
|
|
||||||
})
|
|
||||||
.paddingVertical(0.5, em)
|
|
||||||
.marginLeft(fromMe ? 70 : 0, pct)
|
|
||||||
.paddingRight(fromMe ? 10 : 0, pct)
|
|
||||||
.marginRight(fromMe ? 0 : 70, pct)
|
|
||||||
.paddingLeft(fromMe ? 5 : 10, pct)
|
|
||||||
.background(fromMe ? "var(--brown)" : "var(--green)")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LoadingCircle()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onAppear(async () => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.scrollTop = this.scrollHeight
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.gap(1, em)
|
|
||||||
.position("relative")
|
|
||||||
.overflow("scroll")
|
|
||||||
.height(95, pct)
|
|
||||||
.width(100, pct)
|
|
||||||
.paddingTop(2, em)
|
|
||||||
.paddingBottom(2, em)
|
|
||||||
.backgroundColor("var(--darkbrown)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
register(MessagesPanel)
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
class MessagesSidebar extends Shadow {
|
|
||||||
conversations = []
|
|
||||||
selectedConvoID
|
|
||||||
onSelect
|
|
||||||
|
|
||||||
constructor(conversations, selectedConvoID, onSelect) {
|
|
||||||
super()
|
|
||||||
this.conversations = conversations
|
|
||||||
this.selectedConvoID = selectedConvoID
|
|
||||||
this.onSelect = onSelect
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
VStack(() => {
|
|
||||||
this.conversations.forEach((convo, i) => {
|
|
||||||
|
|
||||||
VStack(() => {
|
|
||||||
HStack(() => {
|
|
||||||
|
|
||||||
p(this.makeConvoTitle(convo.between))
|
|
||||||
.textAlign("left")
|
|
||||||
.marginLeft(0.5, inches)
|
|
||||||
.paddingTop(0.2, inches)
|
|
||||||
.width(100, pct)
|
|
||||||
.marginTop(0)
|
|
||||||
.fontSize(1, em)
|
|
||||||
.fontWeight("bold")
|
|
||||||
|
|
||||||
p(util.formatTime(convo.messages.last.time))
|
|
||||||
.paddingTop(0.2, inches)
|
|
||||||
.fontSize(0.8, em)
|
|
||||||
.marginRight(0.1, inches)
|
|
||||||
.color("var(--divider")
|
|
||||||
})
|
|
||||||
.justifyContent("space-between")
|
|
||||||
.marginBottom(0)
|
|
||||||
|
|
||||||
p(convo.messages.last.text)
|
|
||||||
.fontSize(0.8, em)
|
|
||||||
.textAlign("left")
|
|
||||||
.marginLeft(0.5, inches)
|
|
||||||
.marginBottom(2, em)
|
|
||||||
.color("var(--divider)")
|
|
||||||
})
|
|
||||||
.background(convo.id === this.selectedConvoID ? "var(--darkbrown)" : "")
|
|
||||||
.onClick(() => {
|
|
||||||
this.onSelect(i)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.minWidth(15, vw)
|
|
||||||
.height(100, vh)
|
|
||||||
.gap(0, em)
|
|
||||||
}
|
|
||||||
|
|
||||||
makeConvoTitle(members) {
|
|
||||||
let membersString = ""
|
|
||||||
for(let i=0; i<members.length; i++) {
|
|
||||||
let member = members[i]
|
|
||||||
if(member.email === window.profile.email) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if(members.length > 2) {
|
|
||||||
membersString += member.firstName
|
|
||||||
} else {
|
|
||||||
membersString += member.firstName + " " + member.lastName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return membersString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
register(MessagesSidebar)
|
|
||||||
7
ui/mobile/apps/People/People.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
class People extends Shadow {
|
||||||
|
render() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
register(People)
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
css(`
|
|
||||||
tasks- {
|
|
||||||
font-family: 'Bona';
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks- input::placeholder {
|
|
||||||
font-family: 'Bona Nova';
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
appearance: none; /* remove default style */
|
|
||||||
-webkit-appearance: none;
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
border: 1px solid var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"]:checked {
|
|
||||||
background-color: var(--red);
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
|
|
||||||
class Tasks extends Shadow {
|
|
||||||
projects = [
|
|
||||||
{
|
|
||||||
"title": "Blockcatcher",
|
|
||||||
"tasks": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
columns = [
|
|
||||||
{
|
|
||||||
"title": "backlog",
|
|
||||||
"tasks": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
render() {
|
|
||||||
ZStack(() => {
|
|
||||||
HStack(() => {
|
|
||||||
VStack(() => {
|
|
||||||
h3("Projects")
|
|
||||||
.marginTop(0)
|
|
||||||
.marginBottom(1, em)
|
|
||||||
.marginLeft(0.4, em)
|
|
||||||
|
|
||||||
if (this.projects.length >= 1) {
|
|
||||||
for(let i = 0; i < this.projects.length; i++) {
|
|
||||||
p(this.projects[i].title)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p("No Projects!")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.height(100, vh)
|
|
||||||
.paddingLeft(2, em)
|
|
||||||
.paddingRight(2, em)
|
|
||||||
.paddingTop(2, em)
|
|
||||||
.gap(0, em)
|
|
||||||
.borderRight("0.5px solid var(--accent2)")
|
|
||||||
|
|
||||||
HStack(() => {
|
|
||||||
if (this.columns.length >= 1) {
|
|
||||||
for(let i = 0; i < this.columns.length; i++) {
|
|
||||||
p(this.columns[i].name)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p("No Conversations!")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.height(100, vh)
|
|
||||||
.paddingLeft(2, em)
|
|
||||||
.paddingRight(2, em)
|
|
||||||
.paddingTop(2, em)
|
|
||||||
.gap(0, em)
|
|
||||||
.borderRight("0.5px solid var(--accent2)")
|
|
||||||
})
|
|
||||||
.width(100, "%")
|
|
||||||
.x(0).y(13, vh)
|
|
||||||
.borderTop("0.5px solid var(--accent2)")
|
|
||||||
|
|
||||||
p("0 Items")
|
|
||||||
.position("absolute")
|
|
||||||
.x(50, vw).y(50, vh)
|
|
||||||
.transform("translate(-50%, -50%)")
|
|
||||||
|
|
||||||
HStack(() => {
|
|
||||||
input("Search tasks...", "45vw")
|
|
||||||
.attr({
|
|
||||||
"type": "text"
|
|
||||||
})
|
|
||||||
.fontSize(1.1, em)
|
|
||||||
.paddingLeft(1.3, em)
|
|
||||||
.background("transparent")
|
|
||||||
.border("0.5px solid var(--accent2)")
|
|
||||||
.outline("none")
|
|
||||||
.color("var(--accent)")
|
|
||||||
.borderRadius(10, px)
|
|
||||||
|
|
||||||
button("Search")
|
|
||||||
.marginLeft(2, em)
|
|
||||||
.borderRadius(10, px)
|
|
||||||
.background("transparent")
|
|
||||||
.border("0.5px solid var(--accent2)")
|
|
||||||
.color("var(--accent)")
|
|
||||||
.fontFamily("Bona Nova")
|
|
||||||
.onHover(function (hovering) {
|
|
||||||
if(hovering) {
|
|
||||||
this.style.background = "var(--green)"
|
|
||||||
|
|
||||||
} else {
|
|
||||||
this.style.background = "transparent"
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
button("+ New Task")
|
|
||||||
.width(9, em)
|
|
||||||
.marginLeft(1, em)
|
|
||||||
.borderRadius(10, px)
|
|
||||||
.background("transparent")
|
|
||||||
.border("0.5px solid var(--accent2)")
|
|
||||||
.color("var(--accent)")
|
|
||||||
.fontFamily("Bona Nova")
|
|
||||||
.onHover(function (hovering) {
|
|
||||||
if(hovering) {
|
|
||||||
this.style.background = "var(--green)"
|
|
||||||
|
|
||||||
} else {
|
|
||||||
this.style.background = "transparent"
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onClick((clicking) => {
|
|
||||||
console.log(this, "clicked")
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
.x(55, vw).y(4, vh)
|
|
||||||
.position("absolute")
|
|
||||||
.transform("translateX(-50%)")
|
|
||||||
})
|
|
||||||
.width(100, "%")
|
|
||||||
.height(100, "%")
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
// Optional additional logic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
register(Tasks)
|
|
||||||
@@ -8,9 +8,8 @@ class AppMenu extends Shadow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
console.log("rendering")
|
|
||||||
HStack(() => {
|
HStack(() => {
|
||||||
img("/_/icons/Column.svg", "1.5em", "1.5em")
|
img("/_/icons/House.svg", "1.5em", "1.5em")
|
||||||
.attr({app: "forum"})
|
.attr({app: "forum"})
|
||||||
.padding(0.5, em)
|
.padding(0.5, em)
|
||||||
.borderRadius(10, px)
|
.borderRadius(10, px)
|
||||||
@@ -21,13 +20,12 @@ class AppMenu extends Shadow {
|
|||||||
if(finished) {
|
if(finished) {
|
||||||
this.onNewSelection()
|
this.onNewSelection()
|
||||||
}
|
}
|
||||||
e.target.style.background = "var(--accent)"
|
e.target.style.background = "var(--bone)"
|
||||||
console.log(e.target, e.target.style.background)
|
|
||||||
if(finished) {
|
if(finished) {
|
||||||
window.navigateTo("/")
|
window.navigateTo("/")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
img("/_/icons/letter.svg", "1.5em", "1.5em")
|
img("/_/icons/people.svg", "1.5em", "1.5em")
|
||||||
.attr({app: "messages"})
|
.attr({app: "messages"})
|
||||||
.padding(0.5, em)
|
.padding(0.5, em)
|
||||||
.borderRadius(10, px)
|
.borderRadius(10, px)
|
||||||
@@ -43,22 +41,6 @@ class AppMenu extends Shadow {
|
|||||||
window.navigateTo("/messages")
|
window.navigateTo("/messages")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
img("/_/icons/jobs.svg", "1.5em", "1.5em")
|
|
||||||
.attr({app: "jobs"})
|
|
||||||
.padding(0.5, em)
|
|
||||||
.borderRadius(10, px)
|
|
||||||
.onAppear(function () {
|
|
||||||
this.style.border = "1px solid black"
|
|
||||||
})
|
|
||||||
.onClick((finished, e) => {
|
|
||||||
if(finished) {
|
|
||||||
this.onNewSelection()
|
|
||||||
}
|
|
||||||
e.target.style.background = "#9392bb"
|
|
||||||
if(finished) {
|
|
||||||
window.navigateTo("/jobs")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.borderTop("1px solid black")
|
.borderTop("1px solid black")
|
||||||
.height("auto")
|
.height("auto")
|
||||||
@@ -67,7 +49,7 @@ class AppMenu extends Shadow {
|
|||||||
.zIndex(1)
|
.zIndex(1)
|
||||||
.x(0).yBottom(0)
|
.x(0).yBottom(0)
|
||||||
.justifyContent("space-between")
|
.justifyContent("space-between")
|
||||||
.paddingHorizontal(4, em)
|
.paddingHorizontal(7, em)
|
||||||
.paddingVertical(1, em)
|
.paddingVertical(1, em)
|
||||||
.width(100, vw)
|
.width(100, vw)
|
||||||
.boxSizing("border-box")
|
.boxSizing("border-box")
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import "./AppMenu.js"
|
|
||||||
import "../apps/Forum/Forum.js"
|
|
||||||
import "../apps/Messages/Messages.js"
|
|
||||||
import "../apps/Jobs/Jobs.js"
|
|
||||||
|
|
||||||
class Home extends Shadow {
|
|
||||||
|
|
||||||
render() {
|
|
||||||
ZStack(() => {
|
|
||||||
|
|
||||||
ZStack(() => {
|
|
||||||
switch(window.location.pathname) {
|
|
||||||
case "/":
|
|
||||||
Forum()
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "/messages":
|
|
||||||
Messages()
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "/jobs":
|
|
||||||
Jobs()
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onNavigate(function () {
|
|
||||||
console.log("navigate")
|
|
||||||
this.rerender()
|
|
||||||
})
|
|
||||||
|
|
||||||
AppMenu()
|
|
||||||
})
|
|
||||||
.overflowX("hidden")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
register(Home)
|
|
||||||
@@ -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/logo.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>
|
||||||
|
|||||||
@@ -1,8 +1,120 @@
|
|||||||
import Socket from "/_/code/ws/Socket.js"
|
import Socket from "/_/code/ws/Socket.js"
|
||||||
import "./components/Home.js"
|
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()
|
||||||
|
|
||||||
|
global.currentNetwork = ""
|
||||||
|
global.currentApp = ""
|
||||||
|
|
||||||
|
async function openNetworkAndApp() {
|
||||||
|
// console.log("currentApp: ", currentApp, "currentnet: ", currentNetwork, "nfrompath: ", networkFromPath(), "afrompath", appFromPath())
|
||||||
|
|
||||||
|
if(global.currentNetwork !== networkFromPath()) {
|
||||||
|
global.currentNetwork = networkFromPath()
|
||||||
|
const event = new CustomEvent('networkchange', {
|
||||||
|
detail: { name: currentNetwork }
|
||||||
|
});
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!global.currentNetwork.data) {
|
||||||
|
let appData = await fetch("/api/orgdata/" + global.profile.networks[0].id, {method: "GET"})
|
||||||
|
let json = await appData.json()
|
||||||
|
global.currentNetwork.data = json
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("current: ", global.currentApp, "afrompath: ", appFromPath())
|
||||||
|
if(global.currentApp !== appFromPath()) {
|
||||||
|
global.currentApp = appFromPath()
|
||||||
|
const event = new CustomEvent('appchange', {
|
||||||
|
detail: { name: global.currentApp }
|
||||||
|
});
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(global.currentNetwork) { // 2 navigates fire on load: 1 initial, and one after the org redirect
|
||||||
|
document.title = `${global.currentNetwork.abbreviation} | Forum`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("navigate", openNetworkAndApp)
|
||||||
|
|
||||||
|
global.currentApp = function(appName) {
|
||||||
|
const appUrl = appName.charAt(0).toLowerCase() + appName.slice(1);
|
||||||
|
let parts = window.location.pathname.split('/').filter(Boolean);
|
||||||
|
let newPath = "/" + parts[0] + "/" + appUrl
|
||||||
|
window.navigateTo(newPath)
|
||||||
|
const event = new CustomEvent('appchange', {
|
||||||
|
detail: { name: appName }
|
||||||
|
});
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.networkFromPath = function () {
|
||||||
|
const pathname = window.location.pathname;
|
||||||
|
const firstSegment = pathname.split('/').filter(Boolean)[0] || '';
|
||||||
|
let networks = global.profile?.networks
|
||||||
|
for(let i = 0; i < networks.length; i++) {
|
||||||
|
let network = networks[i]
|
||||||
|
if(network.abbreviation === firstSegment) {
|
||||||
|
return network
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.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 function 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);
|
||||||
|
global.profile = profile
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialNetworkPath() {
|
||||||
|
let path = ""
|
||||||
|
let defaultNetwork = global.profile.networks[0]
|
||||||
|
|
||||||
|
if(!networkFromPath()) {
|
||||||
|
path += (defaultNetwork.abbreviation + "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!appFromPath()) {
|
||||||
|
let defaultApp = defaultNetwork.apps[0]
|
||||||
|
path += defaultApp.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
getProfile().then(async () => {
|
||||||
|
|
||||||
|
if(global.profile.networks.length > 0) {
|
||||||
|
let path = getInitialNetworkPath()
|
||||||
|
window.navigateTo(path)
|
||||||
|
}
|
||||||
|
|
||||||
Home()
|
Home()
|
||||||
|
})
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# Installs
|
|
||||||
|
|
||||||
Stripe CLI
|
|
||||||
|
|
||||||
stripe listen --forward-to localhost:3003/webhook
|
|
||||||