Compare commits
13 Commits
083b110c2d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc81da9743 | ||
|
|
f51045c5e5 | ||
|
|
b22b12a7a5 | ||
|
|
4061e86ce7 | ||
|
|
661bf86a1a | ||
|
|
bdd260c2b5 | ||
|
|
21f654bbed | ||
|
|
b908354c04 | ||
|
|
402a99d55c | ||
|
|
dd9552aad1 | ||
| aaf9d56b1b | |||
|
|
d6520bf007 | ||
|
|
4134685b0a |
3
.gitignore
vendored
@@ -3,4 +3,5 @@ package-lock.json
|
||||
node_modules
|
||||
.env
|
||||
|
||||
db/db.json
|
||||
db/db.json
|
||||
ui/_/code/env.js
|
||||
0
db/networks/comalyr/db.json
Normal file
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Parchment",
|
||||
"name": "Forum",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -28,18 +28,31 @@ export default class AuthHandler {
|
||||
}
|
||||
}
|
||||
|
||||
getProfile(req, res) {
|
||||
getRequestEmail(req, res) {
|
||||
const token = req.cookies?.auth_token;
|
||||
if (!token) return res.status(401).send({ error: "No auth token" });
|
||||
|
||||
try {
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const email = payload.email;
|
||||
return email
|
||||
} catch (e) {
|
||||
console.error("Error getting profile: ", e)
|
||||
throw new Error("Error getting email: invalid auth token")
|
||||
}
|
||||
}
|
||||
|
||||
getProfile(req, res) {
|
||||
try {
|
||||
let email = global.auth.getRequestEmail(req, res)
|
||||
|
||||
const user = db.members.getByEmail(email);
|
||||
let connections = db.MEMBER_IN_NETWORK.getByMember(db.members.prefix + "-" + user.id)
|
||||
let userOrgs = connections.map((c) => {
|
||||
return db.networks.get(c.to)
|
||||
let network = db.networks.get(c.to)
|
||||
delete network.stripeAccountId
|
||||
delete network.stripeAccessToken
|
||||
return network
|
||||
})
|
||||
|
||||
res.send({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'fs/promises';
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
import {nodeModels, edgeModels} from './model/import.js'
|
||||
import { nodeModels, edgeModels } from './model/import.js'
|
||||
import Edge from "./model/edge.js"
|
||||
import { fileURLToPath } from "url"
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
@@ -37,6 +37,8 @@ export default class Database {
|
||||
return model.indices[1] - model.indices[0] + 1
|
||||
}
|
||||
|
||||
// add a get in here that returns a safe copy, not the actual reference
|
||||
|
||||
addNode(prefix, node) {
|
||||
try {
|
||||
let model = nodeModels[prefix]
|
||||
@@ -63,6 +65,39 @@ export default class Database {
|
||||
}
|
||||
}
|
||||
|
||||
deleteNode(prefix, id) {
|
||||
try {
|
||||
let model = nodeModels[prefix]
|
||||
if(model) {
|
||||
this.nodes[model.indices[0] + (id - 1)] = 0
|
||||
} else {
|
||||
throw new Error("Type does not exist for node: " + id)
|
||||
}
|
||||
} catch(e) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
updateNode(prefix, id, toEdit) {
|
||||
try {
|
||||
let model = nodeModels[prefix]
|
||||
if(model) {
|
||||
let schema = model.schema
|
||||
let result = schema.safeParse(toEdit)
|
||||
if(result.success) {
|
||||
this.nodes[model.indices[0] + (id - 1)] = toEdit
|
||||
} else {
|
||||
console.error(result.error)
|
||||
throw new global.ServerError(400, "Invalid Data!");
|
||||
}
|
||||
} else {
|
||||
throw new Error("Type does not exist for node: " + prefix)
|
||||
}
|
||||
} catch(e) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
addEdge(prefix, edge) {
|
||||
try {
|
||||
let type = prefix
|
||||
@@ -83,7 +118,20 @@ export default class Database {
|
||||
throw new global.ServerError(400, "Invalid Data!");
|
||||
}
|
||||
} else {
|
||||
throw new Error("Type does not exist for edge: " + id)
|
||||
throw new Error("Type does not exist for edge: " + prefix)
|
||||
}
|
||||
} catch(e) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
deleteEdge(prefix, id) {
|
||||
try {
|
||||
let model = edgeModels[prefix]
|
||||
if(model) {
|
||||
this.edges[model.indices[0] + (id - 1)] = 0
|
||||
} else {
|
||||
throw new Error("Type does not exist for edge: " + prefix)
|
||||
}
|
||||
} catch(e) {
|
||||
throw e
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
export default class Edge {
|
||||
add(n) {
|
||||
splitEdge(n) {
|
||||
let toPrefix = n.to.split("-")[0]
|
||||
let fromPrefix = n.from.split("-")[0]
|
||||
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)
|
||||
}
|
||||
|
||||
delete(n) {
|
||||
let prefix = this.splitEdge(n)
|
||||
global.db.deleteEdge(prefix, n.from.split("-")[1])
|
||||
}
|
||||
|
||||
getByFrom(fromID) {
|
||||
let result = []
|
||||
for(let i = 0; i < this.entries.length; i++) {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
29
server/db/model/edges/POST_FROM_NETWORK.js
Normal file
@@ -0,0 +1,29 @@
|
||||
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++) {
|
||||
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';
|
||||
|
||||
export default class Post {
|
||||
prefix = "POST"
|
||||
indices = null
|
||||
|
||||
constructor(indices) {
|
||||
@@ -8,13 +9,16 @@ export default class Post {
|
||||
}
|
||||
|
||||
schema = z.object({
|
||||
id: z.number(),
|
||||
text: z.string(),
|
||||
time: z.string(),
|
||||
sentBy: z.string()
|
||||
sentBy: z.string(),
|
||||
edited: z.boolean()
|
||||
})
|
||||
|
||||
makeID(forum, number) {
|
||||
return `POST-${forum}-${number}`
|
||||
// return `POST-${forum}-${number}`
|
||||
return `POST-${number}`
|
||||
}
|
||||
|
||||
save(post, id) {
|
||||
@@ -32,30 +36,130 @@ export default class Post {
|
||||
}
|
||||
}
|
||||
|
||||
get(forum, number) {
|
||||
get(forum, by, authorId = null) {
|
||||
let result = []
|
||||
let limit = Math.min(number, this.entries.length)
|
||||
for(let i=1; i<=limit; i++) {
|
||||
let id = this.makeID(forum, i)
|
||||
let post = this.entries[this.ids[id]]
|
||||
let {firstName, lastName} = global.db.members.get(post.sentBy)
|
||||
let seededObj = {
|
||||
...post
|
||||
}
|
||||
seededObj.sentByID = post.sentBy
|
||||
seededObj.sentBy = firstName + " " + lastName
|
||||
result.push(seededObj)
|
||||
if (by === "member" && authorId) {
|
||||
result = this.getByMember(authorId)
|
||||
} else { // network
|
||||
let { id: networkId } = global.db.networks.getByAbbreviation(forum)
|
||||
result = this.getByNetwork(networkId)
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
let userId = global.db.members.getByEmail(userEmail).id
|
||||
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) {
|
||||
let newPost = {}
|
||||
newPost.text = text
|
||||
newPost.sentBy = db.members.getIDFromEmail(userEmail)
|
||||
newPost.time = global.currentTime()
|
||||
let sender = global.db.members.getByEmail(userEmail)
|
||||
let network = global.db.networks.getByAbbreviation(forum)
|
||||
|
||||
let idNumber = this.entries.length+1
|
||||
super.add(this.makeID(forum, idNumber), newPost)
|
||||
newPost.text = text
|
||||
newPost.time = global.currentTime()
|
||||
|
||||
try {
|
||||
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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import Post from "./forum/post.js"
|
||||
import Conversation from "./dms/conversation.js"
|
||||
import DM from "./dms/dm.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 = {
|
||||
"MEMBER" : [0, 0], // [id, startIndex, nextEmptyIndex
|
||||
@@ -18,7 +20,9 @@ let nIndices = {
|
||||
}
|
||||
|
||||
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 = {
|
||||
@@ -32,5 +36,7 @@ export let nodeModels = {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export default class Member {
|
||||
async add(newMember, network = null) {
|
||||
const hash = await argon2.hash(newMember.password);
|
||||
newMember.password = hash
|
||||
newMember.apps = ["Dashboard"]
|
||||
|
||||
try {
|
||||
global.db.addNode(this.prefix, newMember)
|
||||
@@ -42,15 +43,6 @@ export default class Member {
|
||||
}
|
||||
}
|
||||
|
||||
getByNetwork(id) {
|
||||
let connections = db.MEMBER_IN_NETWORK.getByNetwork(id)
|
||||
let members = []
|
||||
connections.forEach((conn) => {
|
||||
members.push(this.getByID(conn.from))
|
||||
})
|
||||
return members
|
||||
}
|
||||
|
||||
async getPersonalData(id) {
|
||||
const filePath = path.join(global.db.PERSONAL_DATA_PATH, id, "db.json");
|
||||
|
||||
@@ -62,6 +54,15 @@ export default class Member {
|
||||
return result
|
||||
}
|
||||
|
||||
getByNetwork(id) {
|
||||
let connections = db.MEMBER_IN_NETWORK.getByNetwork(id)
|
||||
let members = []
|
||||
connections.forEach((conn) => {
|
||||
members.push(this.getByID(conn.from))
|
||||
})
|
||||
return members
|
||||
}
|
||||
|
||||
getByID(id) {
|
||||
if(typeof id === 'string') {
|
||||
id = id.split("-")[1]
|
||||
|
||||
@@ -14,14 +14,42 @@ export default class Network {
|
||||
apps: z.array(z.string()),
|
||||
logo: z.string(),
|
||||
abbreviation: z.string(),
|
||||
created: z.string()
|
||||
created: z.string(),
|
||||
stripeAccountId: z.string().nullable(),
|
||||
stripeAccessToken: z.string().nullable()
|
||||
})
|
||||
.strict()
|
||||
|
||||
get(stringID) {
|
||||
let id = stringID.split("-")[1]
|
||||
get(id) {
|
||||
if(id.length > 1) {
|
||||
id = id.split("-")[1]
|
||||
}
|
||||
let index = this.indices[0] + (id - 1)
|
||||
return global.db.nodes[index]
|
||||
return structuredClone(global.db.nodes[index])
|
||||
}
|
||||
|
||||
getByAbbreviation(abbreviation) {
|
||||
for(let i=this.indices[0]; i<this.indices[1]; i++) {
|
||||
if(global.db.nodes[i].abbreviation === abbreviation) {
|
||||
return global.db.nodes[i]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
update(id, data) {
|
||||
if(data.id || data.created) {
|
||||
throw new Error("Can't update node id or created time!")
|
||||
}
|
||||
let currentObject = this.get(id)
|
||||
let newObject = {...currentObject, ...data}
|
||||
|
||||
try {
|
||||
global.db.updateNode(this.prefix, newObject.id, newObject)
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
throw new global.ServerError(400, "Failed to add member!");
|
||||
}
|
||||
}
|
||||
|
||||
save(n) {
|
||||
|
||||
@@ -47,9 +47,11 @@ export let PAYMENT = z.object({
|
||||
})
|
||||
|
||||
export let POST = z.object({
|
||||
id: z.number(),
|
||||
text: z.string(),
|
||||
time: z.string(),
|
||||
sentBy: z.string()
|
||||
sentBy: z.string(),
|
||||
edited: z.boolean()
|
||||
})
|
||||
|
||||
export let CONVSRSATION = z.object({
|
||||
|
||||
@@ -28,6 +28,7 @@ class Server {
|
||||
/* Stripe */
|
||||
router.post("/create-checkout-session", PaymentsHandler.newSubscription)
|
||||
router.post("/webhook", express.raw({ type: "application/json" }), PaymentsHandler.webhook)
|
||||
router.post('/api/stripe/onboarded', PaymentsHandler.finishConnectSetup)
|
||||
|
||||
/* Auth */
|
||||
router.post('/login', this.auth.login)
|
||||
@@ -73,11 +74,14 @@ class Server {
|
||||
const join = joinRaw.trim() ? JSON.parse(joinRaw) : [];
|
||||
const contact = contactRaw.trim() ? JSON.parse(contactRaw) : [];
|
||||
const members = db.members.getByNetwork(networkId)
|
||||
let stripeMembers = await payments.getCustomers(networkId)
|
||||
console.log("stripemenbers: ", stripeMembers)
|
||||
|
||||
res.json({
|
||||
join,
|
||||
contact,
|
||||
members
|
||||
members,
|
||||
stripeMembers
|
||||
});
|
||||
} else {
|
||||
const members = db.members.getByNetwork(networkId)
|
||||
@@ -221,13 +225,15 @@ class Server {
|
||||
|
||||
constructor() {
|
||||
this.db = new Database()
|
||||
global.db = this.db
|
||||
this.auth = new AuthHandler()
|
||||
global.db = this.db
|
||||
global.auth = this.auth
|
||||
global.payments = PaymentsHandler
|
||||
const app = express();
|
||||
app.post("/webhook", express.raw({ type: "application/json" }), PaymentsHandler.webhook)
|
||||
const allowedOrigins = new Set([
|
||||
"https://www.parchment.page",
|
||||
"https://parchment.page",
|
||||
"https://www.frm.so",
|
||||
"https://frm.so",
|
||||
"http://localhost:5174",
|
||||
"http://sam.local:5174",
|
||||
"http://localhost:5173",
|
||||
@@ -266,7 +272,7 @@ class Server {
|
||||
const PORT = 10002;
|
||||
server.listen(PORT, () => {
|
||||
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.yellow("***************************************"))
|
||||
console.log("\n")
|
||||
|
||||
@@ -6,6 +6,75 @@ const stripe = new Stripe(process.env.STRIPE_SECRET);
|
||||
|
||||
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,
|
||||
{
|
||||
stripeAccountId: stripe_user_id,
|
||||
stripeAccessToken: access_token, // rarely used, long-term access token for the platform
|
||||
}
|
||||
);
|
||||
|
||||
res.json({ success: true });
|
||||
}
|
||||
|
||||
static async getProfile(networkId) {
|
||||
let network = global.db.networks.get(networkId)
|
||||
if (network) {
|
||||
if (network.stripeAccountId) {
|
||||
const account = await stripe.accounts.retrieve(network.stripeAccountId);
|
||||
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 = global.db.networks.get(networkId)
|
||||
if(!network.stripeAccountId) {
|
||||
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.stripeAccountId }
|
||||
);
|
||||
console.log(customers)
|
||||
|
||||
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) {
|
||||
try {
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
|
||||
@@ -8,35 +8,99 @@ const sendSchema = z.object({
|
||||
|
||||
const getSchema = z.object({
|
||||
forum: z.string(),
|
||||
number: z.number()
|
||||
by: z.string(),
|
||||
authorId: z.number()
|
||||
})
|
||||
.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 {
|
||||
static handleSend(msg, ws) {
|
||||
static async handleSend(msg, ws) {
|
||||
try {
|
||||
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})})
|
||||
return {success: true}
|
||||
let postId = await global.db.posts.add(msg.text, msg.forum, ws.userEmail)
|
||||
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}
|
||||
} else {
|
||||
return {success: false}
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
case "SEND":
|
||||
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":
|
||||
if(!getSchema.safeParse(msg).success) throw new Error("Incorrectly formatted Forum ws message!")
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,16 +2,30 @@ import { WebSocket, WebSocketServer } from 'ws';
|
||||
import { z } from 'zod';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import * as serverFunctions from "../../ui/_/code/bridge/serverFunctions.js"
|
||||
import server from "../../ui/_/code/bridge/serverFunctions.js"
|
||||
import ForumHandler from "./handlers/ForumHandler.js"
|
||||
import MessagesHandler from "./handlers/MessagesHandler.js"
|
||||
|
||||
export default class Socket {
|
||||
wss;
|
||||
messageSchema = z.object({
|
||||
|
||||
functionCallSchema = z.object({
|
||||
id: 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(),
|
||||
|
||||
msg: z.union([
|
||||
z.object({}).passthrough(), // allows any object
|
||||
z.array(z.any()) // allows any array
|
||||
@@ -46,7 +60,13 @@ export default class Socket {
|
||||
const cookies = parseCookies(req.headers.cookie);
|
||||
const token = cookies.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.on('message', (msg) => {
|
||||
@@ -63,44 +83,61 @@ export default class Socket {
|
||||
|
||||
// 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
|
||||
handleMessage = (msg, ws) => {
|
||||
handleMessage = async (msg, ws) => {
|
||||
|
||||
try {
|
||||
const text = msg.toString();
|
||||
const req = JSON.parse(text);
|
||||
if(!this.messageSchema.safeParse(req).success) throw new Error("Socket.handleMessage: Incoming ws message has incorrect format!")
|
||||
|
||||
let responseData;
|
||||
switch (req.app) {
|
||||
case "FORUM":
|
||||
responseData = ForumHandler.handle(req.operation, req.msg, ws)
|
||||
break;
|
||||
|
||||
case "MESSAGES":
|
||||
responseData = MessagesHandler.handle(req.operation, req.msg, ws)
|
||||
break;
|
||||
|
||||
default:
|
||||
if(!req.app) {
|
||||
let func = req.msg
|
||||
responseData = serverFunctions[func.name](...args)
|
||||
} else {
|
||||
console.error("unknown ws message")
|
||||
}
|
||||
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!")
|
||||
}
|
||||
|
||||
let response = {
|
||||
...req
|
||||
}
|
||||
response.msg = responseData
|
||||
|
||||
if(!this.messageSchema.safeParse(response).success) throw new Error("Socket.handleMessage: Outgoing ws message has incorrect format!")
|
||||
ws.send(JSON.stringify(response))
|
||||
|
||||
} catch (e) {
|
||||
console.error("Invalid WS message:", e);
|
||||
}
|
||||
}
|
||||
|
||||
async handleAppOperation(req, ws) {
|
||||
let responseData;
|
||||
switch (req.app) {
|
||||
case "FORUM":
|
||||
responseData = await ForumHandler.handle(req.operation, req.msg, ws)
|
||||
break;
|
||||
|
||||
case "MESSAGES":
|
||||
responseData = MessagesHandler.handle(req.operation, req.msg, ws)
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log("unknown ws message")
|
||||
}
|
||||
|
||||
let response = {
|
||||
...req
|
||||
}
|
||||
response.msg = responseData
|
||||
|
||||
if(!this.appOperationSchema.safeParse(response).success) throw new Error("Socket.handleMessage: Outgoing ws message has incorrect format!")
|
||||
ws.send(JSON.stringify(response))
|
||||
}
|
||||
|
||||
async handleFunction(req, ws) {
|
||||
let responseData = await server[req.name](...req.args)
|
||||
|
||||
let response = {
|
||||
...req
|
||||
}
|
||||
response.data = responseData
|
||||
|
||||
if(!this.functionCallSchema.safeParse(response).success) throw new Error("Socket.handleMessage: Outgoing ws message has incorrect format!")
|
||||
ws.send(JSON.stringify(response))
|
||||
}
|
||||
|
||||
broadcast(event) {
|
||||
if (!this.wss) return;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const IS_NODE =
|
||||
export const IS_NODE =
|
||||
typeof process !== "undefined" &&
|
||||
process.versions?.node != null
|
||||
|
||||
@@ -9,9 +9,7 @@ async function bridgeSend(name, args) {
|
||||
args: args
|
||||
})
|
||||
|
||||
const json = await res.json()
|
||||
if (!res.ok) throw new Error(json.error)
|
||||
return json.result
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,8 +22,6 @@ export function createBridge(funcs) {
|
||||
get(target, prop) {
|
||||
const orig = target[prop]
|
||||
|
||||
if (typeof orig !== "function") return orig
|
||||
|
||||
return function (...args) {
|
||||
if (IS_NODE) {
|
||||
return orig(...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
|
||||
@@ -1,11 +1,10 @@
|
||||
import fs from "fs"
|
||||
import { createBridge } from "./bridge.js"
|
||||
import { createBridge, IS_NODE } from "./bridge.js"
|
||||
|
||||
const handlers = {
|
||||
getProfile(one, two) {
|
||||
fs.writeFileSync("output.txt", `${one} ${two}`)
|
||||
return "written to disk"
|
||||
},
|
||||
let handlers = {}
|
||||
|
||||
if (IS_NODE) {
|
||||
const mod = await import("./handlers.js")
|
||||
handlers = mod.default
|
||||
}
|
||||
|
||||
export const { getProfile } = createBridge(handlers)
|
||||
export default createBridge(handlers)
|
||||
@@ -1,6 +1,9 @@
|
||||
/*
|
||||
Sam Russell
|
||||
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
|
||||
12.26.25 - State for arrays, nested objects. State for stacks (Shadow-only)
|
||||
@@ -310,6 +313,17 @@ HTMLElement.prototype.rerender = function() {
|
||||
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 */
|
||||
|
||||
window.pct = "%"
|
||||
@@ -660,11 +674,11 @@ HTMLElement.prototype.centerY = function () {
|
||||
};
|
||||
|
||||
HTMLElement.prototype.verticalAlign = function (value) {
|
||||
// if(!this.classList.contains("HStack") && !this.classList.contains("VStack")) {
|
||||
// throw new Error("verticalAlign can be only be used on HStacks or VStacks!")
|
||||
// }
|
||||
|
||||
const direction = getComputedStyle(this).flexDirection;
|
||||
if(!direction) {
|
||||
throw new Error("verticalAlign can be only be used on HStacks or VStacks!")
|
||||
}
|
||||
|
||||
if (direction === "column" || direction === "column-reverse") {
|
||||
this.style.justifyContent = value;
|
||||
} else {
|
||||
@@ -674,11 +688,11 @@ HTMLElement.prototype.verticalAlign = function (value) {
|
||||
}
|
||||
|
||||
HTMLElement.prototype.horizontalAlign = function (value) {
|
||||
const direction = getComputedStyle(this).flexDirection;
|
||||
if(!direction) {
|
||||
if(!this.classList.contains("HStack") && !this.classList.contains("VStack")) {
|
||||
throw new Error("horizontalAlign can be only be used on HStacks or VStacks!")
|
||||
}
|
||||
|
||||
|
||||
const direction = getComputedStyle(this).flexDirection;
|
||||
if (direction === "column" || direction === "column-reverse") {
|
||||
this.style.alignItems = value;
|
||||
} else {
|
||||
@@ -1066,9 +1080,9 @@ HTMLElement.prototype.onSubmit = function(cb) {
|
||||
};
|
||||
|
||||
HTMLElement.prototype.onTouch = function(cb) {
|
||||
const onStart = () => cb.call(this, true);
|
||||
const onEnd = () => cb.call(this, false);
|
||||
const onCancel = () => cb.call(this, null);
|
||||
const onStart = (e) => cb.call(this, true, e);
|
||||
const onEnd = (e) => cb.call(this, false, e);
|
||||
const onCancel = (e) => cb.call(this, null, e);
|
||||
this._storeListener("touchstart", onStart);
|
||||
this._storeListener("touchend", onEnd);
|
||||
this._storeListener("touchcancel", onCancel);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
:root {
|
||||
--main: var(--parchment);
|
||||
--main: #ffe6d0;
|
||||
--app: var(--parchment);
|
||||
--accent: black;
|
||||
--accent2: black;
|
||||
--accent: rgb(57, 26, 0);
|
||||
--accent2: rgb(57, 26, 0);
|
||||
--window: #fff1e4;
|
||||
|
||||
--bone: #fff2e7;
|
||||
--parchment: #FEBA7D;
|
||||
@@ -18,25 +19,43 @@
|
||||
--nodes-src: /_/icons/nodes.svg;
|
||||
--forum-src: /_/icons/forum.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;
|
||||
|
||||
--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 {
|
||||
--main: #000000;
|
||||
--app: #180404;
|
||||
--accent: #695b4d;
|
||||
--accent: #8a7454;
|
||||
--accent2: var(--gold);
|
||||
--window: #340f0f;
|
||||
|
||||
--house-src: /_/icons/housedark.svg;
|
||||
--nodes-src: /_/icons/nodesdark.svg;
|
||||
--forum-src: /_/icons/forumdark.svg;
|
||||
--people-src: /_/icons/peopledark.svg;
|
||||
--settings-src: /_/icons/settingsdark.svg;
|
||||
}
|
||||
|
||||
:root.red {
|
||||
--main: var(--red);
|
||||
--app: var(--red);
|
||||
--accent: black;
|
||||
--accent: rgb(57, 0, 0);
|
||||
--accent2: var(--green);
|
||||
--window: #ff0000;
|
||||
|
||||
--house-src: /_/icons/house.svg;
|
||||
--nodes-src: /_/icons/nodes.svg;
|
||||
@@ -80,6 +99,22 @@ body {
|
||||
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) {
|
||||
body, html{
|
||||
overflow-x: hidden;
|
||||
|
||||
@@ -5,6 +5,7 @@ class Connection {
|
||||
|
||||
constructor(receiveCB) {
|
||||
this.receiveCB = receiveCB;
|
||||
this.init()
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<svg width="28" height="32" viewBox="0 0 28 32" 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="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"/>
|
||||
<svg width="266" height="266" viewBox="0 0 266 266" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M199.559 33H66.4395V50.9268H199.559V33Z" 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>
|
||||
|
||||
|
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("Dark").attr({value: "dark"})
|
||||
option("Red").attr({value: "red"})
|
||||
option("Parchment").attr({value: "parchment"})
|
||||
|
||||
$(`option[value=${selected}]`).selected = "true"
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class Dashboard extends Shadow {
|
||||
|
||||
COL = {
|
||||
time: 17,
|
||||
time: 14,
|
||||
fname: 6,
|
||||
lname: 6,
|
||||
email: 20,
|
||||
@@ -15,7 +15,36 @@ class Dashboard extends Shadow {
|
||||
.width(this.COL[type], pct)
|
||||
.whiteSpace("nowrap")
|
||||
.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() {
|
||||
@@ -43,7 +72,7 @@ class Dashboard extends Shadow {
|
||||
p("Last").width(this.COL.lname, 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("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");
|
||||
})
|
||||
.width(95, pct)
|
||||
@@ -51,13 +80,32 @@ class Dashboard extends Shadow {
|
||||
.gap(8);
|
||||
|
||||
global.currentNetwork.data.contact.forEach((entry) => {
|
||||
|
||||
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("lname", entry.lname);
|
||||
this.cell("email", entry.email);
|
||||
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");
|
||||
})
|
||||
.width(95, pct)
|
||||
@@ -85,7 +133,12 @@ class Dashboard extends Shadow {
|
||||
|
||||
global.currentNetwork.data.join.forEach((entry) => {
|
||||
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("lname", entry.lname);
|
||||
this.cell("email", entry.email);
|
||||
|
||||
@@ -69,7 +69,7 @@ class Forum extends Shadow {
|
||||
|
||||
ForumPanel()
|
||||
|
||||
input("Message Parchment", "98%")
|
||||
input("Message Forum", "98%")
|
||||
.paddingVertical(1, em)
|
||||
.paddingLeft(2, pct)
|
||||
.color("var(--accent)")
|
||||
|
||||
@@ -30,7 +30,7 @@ class Jobs extends Shadow {
|
||||
{
|
||||
title: "Austin Chapter Lead",
|
||||
salary: "1% of Local Revenue",
|
||||
company: "Parchment",
|
||||
company: "Forum",
|
||||
city: "Austin",
|
||||
state: "TX"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class Market extends Shadow {
|
||||
stars: "5",
|
||||
reviews: 1,
|
||||
price: "$12",
|
||||
company: "Parchment",
|
||||
company: "Forum",
|
||||
type: "new",
|
||||
image: "/db/images/1",
|
||||
madeIn: "America"
|
||||
|
||||
@@ -4,20 +4,123 @@ class People extends Shadow {
|
||||
h1("Members")
|
||||
.fontWeight("bold")
|
||||
.marginBottom(2, em)
|
||||
.marginLeft(4, em)
|
||||
|
||||
for(let i = 0; i < global.currentNetwork.data.members.length; i++) {
|
||||
let member = global.currentNetwork.data.members[i]
|
||||
HStack(() => {
|
||||
p(member.firstName + " " + member.lastName)
|
||||
.width(10, pct)
|
||||
p(member.email)
|
||||
.width(10, pct)
|
||||
VStack(() => {
|
||||
VStack(() => {
|
||||
p("Officers")
|
||||
.marginBottom(2, vh)
|
||||
.marginLeft(8, em)
|
||||
.fontStyle("italic")
|
||||
|
||||
ZStack(() => {
|
||||
new gridjs.Grid({
|
||||
columns: ["Name", "Email"],
|
||||
data: global.currentNetwork.data.members.map(m => [
|
||||
m.firstName + " " + m.lastName,
|
||||
m.email
|
||||
]),
|
||||
sort: true,
|
||||
search: true,
|
||||
style: {
|
||||
table: {
|
||||
'background-color': 'var(--window)',
|
||||
'color': 'var(--accent)',
|
||||
'box-shadow': 'none',
|
||||
'font-size': '0.9em'
|
||||
},
|
||||
th: {
|
||||
'background-color': 'var(--window)',
|
||||
'color': 'var(--accent)',
|
||||
'border': 'none',
|
||||
'border-bottom': '1px solid rgb(69 53 53)'
|
||||
},
|
||||
td: {
|
||||
'background-color': 'var(--window)',
|
||||
'color': 'var(--accent)',
|
||||
'border': 'none',
|
||||
'border-bottom': '1px solid rgb(69 53 53)'
|
||||
}
|
||||
}
|
||||
}).render(quill.rendering.last)
|
||||
})
|
||||
})
|
||||
}
|
||||
.gap(0.5, em)
|
||||
|
||||
VStack(() => {
|
||||
p("Stripe Subscribers")
|
||||
.marginBottom(2, vh)
|
||||
.marginLeft(8, em)
|
||||
.fontStyle("italic")
|
||||
|
||||
ZStack(() => {
|
||||
new gridjs.Grid({
|
||||
columns: ["Amount", "Name", "Email", "Phone", "Area", "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'
|
||||
},
|
||||
th: {
|
||||
'background-color': 'var(--window)',
|
||||
'color': 'var(--accent)',
|
||||
'border': 'none',
|
||||
'border-bottom': '1px solid rgb(69 53 53)'
|
||||
},
|
||||
td: {
|
||||
'background-color': 'var(--window)',
|
||||
'color': 'var(--accent)',
|
||||
'border': 'none',
|
||||
'border-bottom': '1px solid rgb(69 53 53)'
|
||||
}
|
||||
}
|
||||
}).render(quill.rendering.last)
|
||||
|
||||
css(`
|
||||
input.gridjs-input {
|
||||
background-color: var(--window);
|
||||
color: var(--accent);
|
||||
border: none;
|
||||
border-bottom: 1px solid rgb(69 53 53);
|
||||
border-radius: 0;
|
||||
margin-left: 8em
|
||||
}
|
||||
|
||||
.gridjs-wrapper {
|
||||
box-shadow: none;
|
||||
padding-left: 8em;
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
.gap(0.5, em)
|
||||
})
|
||||
.gap(4, em)
|
||||
})
|
||||
.gap(0.5, em)
|
||||
.paddingTop(4, pct)
|
||||
.paddingLeft(5, pct)
|
||||
.width(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,18 +2,39 @@ class AppMenu extends Shadow {
|
||||
|
||||
images = {
|
||||
"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() {
|
||||
VStack(() => {
|
||||
|
||||
function cssVariable(value) {
|
||||
return getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--" + value)
|
||||
.trim();
|
||||
}
|
||||
|
||||
|
||||
HStack(() => {
|
||||
let currentNetwork = global.currentNetwork
|
||||
if(!currentNetwork) return
|
||||
@@ -24,18 +45,23 @@ class AppMenu extends Shadow {
|
||||
img(cssVariable(this.images[app].src), this.images[app].size)
|
||||
.attr({app: app})
|
||||
.padding(0.3, em)
|
||||
.paddingBottom(currentApp === app ? 4 : 5, px)
|
||||
.borderBottom(currentApp === app ? "1px solid var(--accent)" : "")
|
||||
.onClick((done) => {
|
||||
if(done) global.openApp(app)
|
||||
.styles(currentApp === app ? this.selectedIconStyle : this.unselectedIconStyle)
|
||||
.onClick(function (done) {
|
||||
if(!done) {
|
||||
this.style.transform = "translateY(10%)"
|
||||
} else {
|
||||
this.style.transform = ""
|
||||
global.openApp(app)
|
||||
}
|
||||
})
|
||||
.onHover(function (hovering) {
|
||||
if(hovering) {
|
||||
this.style.opacity = 0.8
|
||||
this.style.translateY = -0.1
|
||||
} else {
|
||||
this.style.opacity = ""
|
||||
this.style.translateY = ""
|
||||
}
|
||||
})
|
||||
.cursor("pointer")
|
||||
}
|
||||
})
|
||||
.justifyContent("center")
|
||||
@@ -47,7 +73,7 @@ class AppMenu extends Shadow {
|
||||
})
|
||||
.onEvent("appchange", () => {
|
||||
// console.log("app hello?") // BUG: Quill is not acknowledging this event unless there is something else in the function body
|
||||
this.rerender()
|
||||
this.onAppChange()
|
||||
})
|
||||
.onEvent("networkchange", () => {
|
||||
// console.log(global.currentApp)
|
||||
@@ -58,7 +84,6 @@ class AppMenu extends Shadow {
|
||||
.width(100, vw)
|
||||
.height(2.5, 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/Jobs/Jobs.js"
|
||||
import "../apps/People/People.js"
|
||||
import "../apps/Settings/Settings.js"
|
||||
|
||||
class AppWindow extends Shadow {
|
||||
|
||||
@@ -29,13 +30,18 @@ class AppWindow extends Shadow {
|
||||
case "People":
|
||||
People()
|
||||
break;
|
||||
case "Settings":
|
||||
Settings()
|
||||
break;
|
||||
}
|
||||
})
|
||||
.overflow("scroll")
|
||||
.position("absolute")
|
||||
.width(window.innerWidth - this.calculateWidth(), px)
|
||||
.height(window.innerHeight - this.calculateHeight(), px)
|
||||
.background("var(--app)")
|
||||
.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", () => {
|
||||
|
||||
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)
|
||||
@@ -17,16 +17,20 @@ class Sidebar extends Shadow {
|
||||
VStack(() => {
|
||||
let selected = window.location.pathname.startsWith("/my")
|
||||
|
||||
img(document.documentElement.classList.contains("red") ? "/_/icons/quillblack.svg" : "/_/icons/quill.svg", "2.5em", "2.5em")
|
||||
img(document.documentElement.classList.contains("red") ? "/_/icons/column.svg" : "/_/icons/columnred.svg", "2.5em", "2.5em")
|
||||
.marginTop(6, vh)
|
||||
.marginBottom(2, vh)
|
||||
.attr({selected: selected ? "" : null})
|
||||
.paddingRight(0.5, em)
|
||||
.paddingLeft(selected ? 9 : 10, px)
|
||||
.borderLeft(selected ? "1px solid var(--accent)" : "0")
|
||||
.onClick((done, e) => {
|
||||
if(done) {
|
||||
this.toggleSelectedStyles(e.target)
|
||||
.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")
|
||||
}
|
||||
})
|
||||
@@ -38,23 +42,19 @@ class Sidebar extends Shadow {
|
||||
img(`/db/images/${networks[i].logo}`, "2.25em", "2.25em")
|
||||
.marginTop(3, vh)
|
||||
.attr({selected: selected ? "" : null})
|
||||
.paddingRight(0.5, em)
|
||||
.paddingLeft(selected ? 9 : 10, px)
|
||||
.borderLeft(selected ? "1px solid var(--accent)" : "0")
|
||||
.onHover(function (hovering) {
|
||||
if(hovering) {
|
||||
this.style.opacity = 0.8
|
||||
.cursor("pointer")
|
||||
.transition("transform .1s")
|
||||
.onClick(function (done, e) {
|
||||
if(!done) {
|
||||
this.style.transform = "translateX(-10%)"
|
||||
} else {
|
||||
this.style.opacity = ""
|
||||
}
|
||||
})
|
||||
.onClick((done, e) => {
|
||||
if(done) {
|
||||
this.toggleSelectedStyles(e.target)
|
||||
this.style.transform = ""
|
||||
this.parentShadow().toggleSelectedStyles(e.target)
|
||||
window.navigateTo(`/${networks[i].abbreviation}`)
|
||||
}
|
||||
})
|
||||
.cursor("default")
|
||||
}
|
||||
})
|
||||
.paddingLeft(1.5, em)
|
||||
@@ -62,7 +62,6 @@ class Sidebar extends Shadow {
|
||||
.position("fixed")
|
||||
.x(0).y(0)
|
||||
.height(100, vh)
|
||||
.borderRight("1px solid var(--accent)")
|
||||
.zIndex(3)
|
||||
.onEvent("themechange", () => {
|
||||
console.log("why is this needed smg")
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Parchment</title>
|
||||
<title>Forum</title>
|
||||
<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">
|
||||
<script>
|
||||
if(localStorage.getItem("theme")) {
|
||||
|
||||
@@ -27,7 +27,31 @@ let Global = class {
|
||||
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()
|
||||
@@ -71,7 +95,7 @@ let Global = class {
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
|
||||
document.title = (this.currentNetwork === this.profile) ? "Parchment" : `${this.currentNetwork.abbreviation} | Parchment`
|
||||
document.title = (this.currentNetwork === this.profile) ? "Forum" : `${this.currentNetwork.abbreviation} | Forum`
|
||||
}
|
||||
|
||||
setCurrentNetworkAndApp() {
|
||||
@@ -89,9 +113,12 @@ let Global = class {
|
||||
return defaultNetwork.apps[0].toLowerCase()
|
||||
}
|
||||
|
||||
getFirstPathSegment() {
|
||||
return window.location.pathname.split('/').filter(Boolean)[0] || '';
|
||||
}
|
||||
|
||||
networkFromPath = function () {
|
||||
const pathname = window.location.pathname;
|
||||
const firstSegment = pathname.split('/').filter(Boolean)[0] || '';
|
||||
const firstSegment = this.getFirstPathSegment()
|
||||
if(firstSegment === "my") {
|
||||
return this.profile
|
||||
} else {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Parchment</title>
|
||||
<title>Forum</title>
|
||||
<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">
|
||||
<script src="/_/code/quill.js"></script>
|
||||
<script src="/_/code/zod.js"></script>
|
||||
|
||||
@@ -36,7 +36,7 @@ async function openNetworkAndApp() {
|
||||
}
|
||||
|
||||
if(global.currentNetwork) { // 2 navigates fire on load: 1 initial, and one after the org redirect
|
||||
document.title = `${global.currentNetwork.abbreviation} | Parchment`
|
||||
document.title = `${global.currentNetwork.abbreviation} | Forum`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="public">
|
||||
<head>
|
||||
<title>Parchment</title>
|
||||
<title>Forum</title>
|
||||
<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="preload"
|
||||
|
||||
@@ -8,9 +8,9 @@ class Home extends Shadow {
|
||||
MainContent() {
|
||||
return VStack(() => {
|
||||
HStack(() => {
|
||||
img("/_/icons/quill.svg", "6vmax")
|
||||
img("/_/icons/columnred.svg", "6vmax")
|
||||
|
||||
p("PARCHMENT")
|
||||
p("FORUM")
|
||||
.fontFamily("Nabla")
|
||||
.fontSize(6, vmax)
|
||||
.marginLeft(1, rem)
|
||||
@@ -32,7 +32,7 @@ class Home extends Shadow {
|
||||
.verticalAlign("center")
|
||||
.horizontalAlign("center")
|
||||
|
||||
p("Parchment is a platform for small to medium-sized communities.")
|
||||
p("Forum is a platform for communities.")
|
||||
.color("var(--quillred)")
|
||||
.borderTop("1px solid var(--quillred)")
|
||||
.verticalAlign("center")
|
||||
|
||||
@@ -8,7 +8,7 @@ class SignIn extends Shadow {
|
||||
|
||||
render() {
|
||||
ZStack(() => {
|
||||
img("/_/icons/quill.svg", window.isMobile() ? "5vmax" : "3vmax")
|
||||
img("/_/icons/columnred.svg", window.isMobile() ? "5vmax" : "3vmax")
|
||||
.position("absolute")
|
||||
.top(2, em)
|
||||
.left(2, em)
|
||||
|
||||
@@ -2,7 +2,7 @@ class SignUp extends Shadow {
|
||||
render() {
|
||||
|
||||
ZStack(() => {
|
||||
img("/_/icons/quill.svg", window.isMobile() ? "5vmax" : "3vmax")
|
||||
img("/_/icons/columnred.svg", window.isMobile() ? "5vmax" : "3vmax")
|
||||
.position("absolute")
|
||||
.top(2, em)
|
||||
.left(2, em)
|
||||
@@ -15,8 +15,8 @@ class SignUp extends Shadow {
|
||||
h2("$50 / Month Subscription")
|
||||
.color("var(--quillred)")
|
||||
|
||||
p(" - Access to Parchment Online and Parchment Desktop")
|
||||
p(" - Ability to Create Networks")
|
||||
p(" - Access to Forum Online and Forum Desktop")
|
||||
p(" - Ability to Create Groups")
|
||||
p(" - Up to 5GB Storage Space")
|
||||
|
||||
button("Buy")
|
||||
|
||||
@@ -11,7 +11,7 @@ class SignupForm extends Shadow {
|
||||
|
||||
render() {
|
||||
ZStack(() => {
|
||||
img("/_/icons/quill.svg", window.isMobile() ? "5vmax" : "3vmax")
|
||||
img("/_/icons/columnred.svg", window.isMobile() ? "5vmax" : "3vmax")
|
||||
.position("absolute")
|
||||
.top(2, em)
|
||||
.left(2, em)
|
||||
|
||||