This commit is contained in:
metacryst
2026-01-04 07:58:23 -06:00
parent b50468eb5a
commit 6a435ac11a
122 changed files with 13995 additions and 19 deletions

6
.gitignore vendored
View File

@@ -1,2 +1,6 @@
.DS_Store
package-lock.json
node_modules
package-lock.json
.env
db/db.json

View File

@@ -1,9 +1,10 @@
{
"name": "Parchment",
"version": "1.0.0",
"scripts": {
"start": "node server/index.js"
},
"type": "module",
"scripts": {
"start": "node server/index.js"
},
"dependencies": {
"argon2": "^0.44.0",
"chalk": "^4.1.2",

89
server/auth.js Normal file
View File

@@ -0,0 +1,89 @@
import dotenv from 'dotenv';
import jwt from 'jsonwebtoken';
import argon2 from 'argon2';
dotenv.config();
export default class AuthHandler {
ips = new Map()
#secret
constructor() {
this.#secret = process.env.JWT_SECRET;
}
isLoggedInUser(req, res) {
const token = req.cookies.auth_token;
if (!token) {
return false;
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
return true;
} catch (err) {
return false;
}
}
getProfile(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;
const user = db.members.getByEmail(email);
res.send({ email: user.email, name: user.firstName + " " + user.lastName });
} catch (err) {
res.status(401).send({ error: "Invalid token" });
}
}
async login(req, res) {
const { email, password } = req.body;
let foundUser = global.db.members.getByEmail(email)
if(!foundUser) {
res.status(400).json({ error: 'Incorrect email.' });
return;
}
const storedHash = foundUser.password
const valid = await argon2.verify(storedHash, password);
if (!valid) {
res.status(400).json({ error: 'Incorrect password.' });
} else {
const payload = { email: foundUser.email };
console.log(payload)
const secret = process.env.JWT_SECRET;
const options = { expiresIn: "2h" };
const token = jwt.sign(payload, secret, options);
res.cookie("auth_token", token, {
httpOnly: true, // cannot be accessed by JS
secure: process.env.ENV === "production", // only over HTTPS
sameSite: "lax", // like SameSiteLaxMode
maxAge: 2 * 60 * 60 * 1000, // 2 hours in milliseconds
path: "/", // available on entire site
domain: process.env.ENV === "production" ? "." + process.env.BASE_URL : undefined
});
res.redirect("/")
}
}
logout(req, res) {
res.cookie('auth_token', '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 0, // expire immediately
path: '/',
domain: process.env.ENV === "production" ? "." + process.env.BASE_URL : undefined
});
res.redirect("/")
}
}

107
server/db/db.js Normal file
View File

@@ -0,0 +1,107 @@
import fs from 'fs/promises';
import chalk from 'chalk';
import path from 'path';
import Titles from "./model/Titles.js"
import Members from './model/Members.js'
import Tokens from './model/Tokens.js'
import Payments from "./model/Payments.js"
import Posts from "./model/Forum/Posts.js"
import Conversations from "./model/Messages/Conversations.js"
import Messages from "./model/Messages/Messages.js"
export default class Database {
titles = new Titles()
members = new Members()
tokens = new Tokens()
payments = new Payments()
posts = new Posts()
conversations = new Conversations()
messages = new Messages()
fromID = {
"HY": this.titles,
"MEMBER": this.members,
"TOKEN": this.tokens,
"PAYMENT": this.payments,
"POST": this.posts,
"CONVERSATION": this.conversations,
"DM": this.messages
}
constructor() {
this.loadData()
}
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];
let type = id.split("-")[0]
try {
let collection = this.fromID[type]
if(collection) {
collection.save(node, id)
} else {
throw new Error("Type does not exist for node: ", id)
}
} catch(e) {
throw e
}
}
setInterval(() => {
console.log("saving db")
global.db.saveData()
}, 5000)
}
async saveData() {
let data = {
"nodes": {
},
"edges": {
}
}
let arrs = [
this.titles.entries,
this.members.entries,
this.tokens.entries,
this.posts.entries,
this.conversations.entries,
this.messages.entries,
this.payments.entries,
]
let ids = [
Object.entries(this.titles.ids),
Object.entries(this.members.ids),
Object.entries(this.tokens.ids),
Object.entries(this.posts.ids),
Object.entries(this.conversations.ids),
Object.entries(this.messages.ids),
Object.entries(this.payments.ids),
]
for(let i=0; i<arrs.length; i++) {
let arr = arrs[i]
for(let j=0; j<arr.length; j++) {
data.nodes[ids[i][j][0]] = arr[j]
}
}
let string = JSON.stringify(data, null, 4)
await fs.writeFile(path.join(process.cwd(), 'db/db.json'), string, "utf8");
}
}

View File

@@ -0,0 +1,56 @@
import OrderedObject from "../OrderedObject.js"
import { z } from 'zod';
export default class Posts extends OrderedObject {
schema = z.object({
text: z.string(),
time: z.string(),
sentBy: z.string()
})
makeID(forum, number) {
return `POST-${forum}-${number}`
}
save(post, id) {
let result = this.schema.safeParse(post)
if(result.success) {
try {
super.add(id, post)
} catch(e) {
console.error(e)
throw e
}
} else {
console.error("Failed parsing member: ", result.error)
throw new global.ServerError(400, "Invalid Member Data!: ");
}
}
get(forum, number) {
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)
}
return result
}
async add(text, forum, userEmail) {
let newPost = {}
newPost.text = text
newPost.sentBy = db.members.getIDFromEmail(userEmail)
newPost.time = global.currentTime()
let idNumber = this.entries.length+1
super.add(this.makeID(forum, idNumber), newPost)
}
}

View File

@@ -0,0 +1,77 @@
import OrderedObject from "./OrderedObject.js"
import argon2 from 'argon2';
import { z } from 'zod';
export default class Members extends OrderedObject {
addressSchema = z.object({
address1: z.string(),
address2: z.string().optional(),
zip: z.string().regex(/^\d{5}(-\d{4})?$/),
state: z.string(),
city: z.string()
})
schema = z.object({
id: z.number(),
email: z.string().email(),
firstName: z.string(),
lastName: z.string(),
password: z.string(),
tokenUsed: z.string().regex(
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
"Invalid UUID"
),
joined: z.string(),
address: this.addressSchema,
})
isHashed = (s) => {return s.startsWith("$argon2")}
save(member) {
let id = `MEMBER-${member.id}`
let result = this.schema.safeParse(member)
if(result.success) {
try {
super.add(id, member)
} catch(e) {
console.error(e)
throw e
}
} else {
console.error("Failed parsing member: ", result.error)
throw new global.ServerError(400, "Invalid Member Data!: ");
}
}
async add(newMember, tokenID) {
newMember.tokenUsed = tokenID
const hash = await argon2.hash(newMember.password);
newMember.password = hash
newMember.joined = global.currentTime()
newMember.id = this.entries.length+1
this.save(newMember)
}
get(id) {
return this.entries[this.ids[id]]
}
getByEmail(email) {
for(let i=0; i<this.entries.length; i++) {
if(this.entries[i].email === email) {
return this.entries[i]
}
}
return null
}
getIDFromEmail(email) {
let index = 0
for(let i=0; i<this.entries.length; i++) {
if(this.entries[i].email === email) {
index = i
break
}
}
return Object.entries(this.ids)[index][0]
}
}

View File

@@ -0,0 +1,62 @@
import OrderedObject from "../OrderedObject.js"
import { z } from 'zod';
export default class Conversations extends OrderedObject {
schema = z.object({
id: z.number(),
between: z.array(z.string()),
lastUpdated: z.string()
}).strict()
save(convo) {
let id = `CONVERSATION-${convo.id}`
let result = this.schema.safeParse(convo)
if(result.success) {
try {
super.add(id, convo)
} catch(e) {
console.error(e)
throw e
}
} else {
console.error(result.error)
throw new global.ServerError(400, "Invalid Conversation Data!");
}
}
get(convoID) {
console.log("convo getting, ", convoID)
return this.entries[this.ids[convoID]]
}
getByMember(userID) {
let convos = []
function populateMemberProfilesFromIDs(ids) {
let result = []
for(let i=0; i<ids.length; i++) {
result[i] = global.db.members.get(ids[i])
}
return result
}
for(let i=0; i<this.entries.length; i++) {
let convo = this.entries[i]
console.log(convo, userID)
if(convo.between.includes(userID)) {
console.log("found user convo: ", convo.id)
let messages = global.db.messages.getByConversation(`CONVERSATION-${convo.id}`)
let result = {
...convo,
messages,
}
result.between = populateMemberProfilesFromIDs(convo.between)
convos.push(result)
}
}
return convos
}
}

View File

@@ -0,0 +1,57 @@
import OrderedObject from "../OrderedObject.js"
import { z } from 'zod';
export default class Messages extends OrderedObject {
schema = z.object({
id: z.number(),
conversation: z.string(),
from: z.string(),
text: z.string(),
time: z.string()
}).strict()
save(msg) {
let id = `DM-${msg.id}`
let result = this.schema.safeParse(msg)
if(result.success) {
try {
super.add(id, msg)
} catch(e) {
console.error(e)
throw e
}
} else {
console.error(result.error)
throw new global.ServerError(400, "Invalid Conversation Data!");
}
}
add(convo, text, userID) {
let newMessage = {}
newMessage.time = global.currentTime()
newMessage.from = userID
newMessage.conversation = convo
newMessage.text = text
newMessage.id = this.entries.length+1
console.log(newMessage)
this.save(newMessage)
}
getByConversation(convoID) {
let result = []
for(let i=0; i<this.entries.length; i++) {
let entry = this.entries[i]
if(entry.conversation = convoID) {
let userID = entry.from
let fromUser = global.db.members.get(userID)
let newObj = {
...entry
}
newObj.from = fromUser
result.push(newObj)
}
}
return result
}
}

View File

@@ -0,0 +1,27 @@
export default class OrderedObject {
entries = []
ids = {}
indexes = []
add(id, data) {
if(this.ids[id]) {
console.error(`Can't add item ${id}: id already exists`)
throw new global.ServerError(400, `Member with this email already exists`)
}
this.entries.push(data)
this.ids[id] = this.entries.length - 1
}
update(id, data) {
let index = this.ids[id]
this.entries[index] = data
}
delete(key) {
if (typeof key === "number") {
return this.entries[key]
} else {
return this.entries[this.ids[key]]
}
}
}

View File

@@ -0,0 +1,42 @@
import OrderedObject from "./OrderedObject.js"
import { z } from 'zod';
export default class Payments extends OrderedObject {
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 = `PAYMENT-${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[`PAYMENT-${id}`]]
}
}

52
server/db/model/Titles.js Normal file
View File

@@ -0,0 +1,52 @@
import OrderedObject from "./OrderedObject.js"
export default class Titles extends OrderedObject {
save(newTitle) {
let id = `HY-${this.entries.length+1}`
if(this.validate(id, newTitle)) {
try {
super.add(id, newTitle)
} catch(e) {
console.error(e)
throw e
}
} else {
throw new global.ServerError(400, "Invalid Member Data!");
}
}
validate(id, node) {
let checkID = () => {
let split = id.split("-")
return (
split.length === 2
&& split[0] === "HY"
&& !isNaN(Number(split[1]))
)
}
let idres = checkID()
if(!idres) {
return false
}
let checkFields = () => {
let fields = [
"fullName",
]
for(let i = 0; i < fields.length; i++) {
if(!node[fields[i]]) {
throw new Error(`Title ${id} is missing trait ${fields[i]}`)
return false
}
}
return true
}
let fieldres = checkFields()
if(!fieldres) {
return false
}
return true
}
}

41
server/db/model/Tokens.js Normal file
View File

@@ -0,0 +1,41 @@
import OrderedObject from "./OrderedObject.js"
import { z } from 'zod';
export default class Tokens extends OrderedObject {
schema = z.object({
index: z.number(),
url: z.string(),
uuid: z.string().regex(
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
"Invalid UUID"
),
used: z.boolean(),
})
markUsed(uuid) {
let token = this.get(uuid)
token.used = true
super.update(`TOKEN-${uuid}`, token)
}
save(token) {
let id = `TOKEN-${token.uuid}`
let result = this.schema.safeParse(token)
if(result.success) {
try {
super.add(id, token)
} catch(e) {
console.error(e)
throw e
}
} else {
console.error(result.error)
throw new global.ServerError(400, "Invalid Member Data!");
}
}
get(uuid) {
return this.entries[this.ids[`TOKEN-${uuid}`]]
}
}

View File

@@ -1,31 +1,41 @@
import express from 'express'
import cors from 'cors'
import cookieParser from 'cookie-parser'
import http from 'http'
import fs from 'fs'
import chalk from 'chalk'
import moment from 'moment'
import path from 'path'
import * as useragent from "express-useragent";
import express from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import http from 'http';
import fs from 'fs';
import chalk from 'chalk';
import moment from 'moment';
import path from 'path';
import * as useragent from 'express-useragent';
import { fileURLToPath } from "url"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
import "./util.js"
import Socket from './ws/ws.js'
import Database from "./db/db.js"
import AuthHandler from './auth.js';
import PaymentsHandler from "./payments.js"
export default class Server {
class Server {
db;
auth;
UIPath = path.join(global.__dirname, '../ui')
DBPath = path.join(global.__dirname, './db')
UIPath = path.join(__dirname, '../ui')
DBPath = path.join(__dirname, '../db')
registerRoutes(router) {
/* Stripe */
router.post("/create-checkout-session", PaymentsHandler.danceTicket)
router.post("/webhook", express.raw({ type: "application/json" }), PaymentsHandler.webhook)
/* Auth */
router.post('/login', this.auth.login)
router.get('/profile', this.auth.getProfile)
router.get('/signout', this.auth.logout)
/* Site */
router.get('/signup', this.verifyToken, this.get)
router.post('/signup', this.verifyToken, this.newUserSubmission)
router.get('/db/images/*', this.getUserImage)
router.get('/*', this.get)
return router
}
@@ -44,6 +54,21 @@ export default class Server {
next()
}
newUserSubmission = async (req, res) => {
const { token } = req.query;
try {
let tokenData = db.tokens.get(token)
if(tokenData.used) throw new global.ServerError(400, "Token alredy used!")
await db.members.add(req.body, tokenData.uuid)
db.tokens.markUsed(token)
global.db.saveData()
return res.status(200).json({});
} catch(e) {
console.log(e)
return res.status(e.status).json({ error: 'Error adding new member' });
}
}
authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
@@ -64,8 +89,23 @@ export default 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 = getFileByNumber(path.join(this.DBPath, "images"), path.basename(req.url))
res.sendFile(filePath)
}
get = async (req, res) => {
console.log("get")
let url = req.url
let publicSite = () => {
@@ -132,6 +172,7 @@ export default class Server {
global.db = this.db
this.auth = new AuthHandler()
const app = express();
app.post("/webhook", express.raw({ type: "application/json" }), PaymentsHandler.webhook)
app.use(cors({ origin: '*' }));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
@@ -147,10 +188,10 @@ export default class Server {
const server = http.createServer(app);
global.Socket = new Socket(server);
const PORT = 3005;
const PORT = 10002;
server.listen(PORT, () => {
console.log("\n")
console.log(chalk.yellow("*************** Hyperia ***************"))
console.log(chalk.yellow("*************** parchment.page ********"))
console.log(chalk.yellowBright(`Server is running on port ${PORT}: http://localhost`));
console.log(chalk.yellow("***************************************"))
console.log("\n")
@@ -164,4 +205,6 @@ export default class Server {
Object.preventExtensions(this);
}
}
}
const server = new Server()

68
server/payments.js Normal file
View File

@@ -0,0 +1,68 @@
import Stripe from 'stripe';
import dotenv from 'dotenv';
dotenv.config();
const stripe = new Stripe(process.env.STRIPE_SECRET);
export default class PaymentsHandler {
static async danceTicket(req, res) {
try {
const session = await stripe.checkout.sessions.create({
mode: "payment",
payment_method_types: ["card"],
metadata: {
productId: "austin_winter_ball_2025_ticket"
},
line_items: [
{
price_data: {
currency: "usd",
product_data: {
name: "Hyperia Winter Ball"
},
unit_amount: 3500
},
quantity: 1
}
],
success_url: `${process.env.PROTOCOL + process.env.BASE_URL}/success`,
cancel_url: `${process.env.PROTOCOL + process.env.BASE_URL}/events`
});
res.json({ url: session.url });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Something went wrong." });
}
}
static webhook = (req, res) => {
const sig = req.headers["stripe-signature"];
try {
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.WEBHOOK_SECRET
);
if (event.type === "checkout.session.completed") {
const session = event.data.object;
let toStore = {
"product": session.metadata.productId,
"email": session.customer_details.email,
"name": session.customer_details.name,
"time": global.currentTime(),
"amount": session.amount_total,
}
global.db.payments.add(toStore)
}
res.sendStatus(200);
} catch (err) {
console.error(err);
res.sendStatus(400);
}
}
}

24
server/util.js Normal file
View File

@@ -0,0 +1,24 @@
global.ServerError = class extends Error {
constructor(status, msg) {
super(msg);
this.status = status;
}
}
global.currentTime = function () {
const now = new Date();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const year = now.getFullYear();
let hours = now.getHours();
const ampm = hours >= 12 ? "pm" : "am";
hours = hours % 12 || 12; // convert to 12-hour format
const minutes = String(now.getMinutes()).padStart(2, "0");
const seconds = String(now.getSeconds()).padStart(2, "0");
const ms = String(now.getMilliseconds()).padStart(4, "0"); // 4-digit like "5838"
return `${month}.${day}.${year}-${hours}:${minutes}:${seconds}${ms}${ampm}`;
}

View File

@@ -0,0 +1,43 @@
import { z } from 'zod';
const sendSchema = z.object({
forum: z.string(),
text: z.string(),
})
.strict()
const getSchema = z.object({
forum: z.string(),
number: z.number()
})
.strict()
export default class ForumHandler {
static 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}
} catch(e) {
console.error(e)
}
}
static handleGet(msg) {
let data = global.db.posts.get(msg.forum, msg.number)
return data
}
static 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)
case "GET":
if(!getSchema.safeParse(msg).success) throw new Error("Incorrectly formatted Forum ws message!")
return this.handleGet(msg)
}
}
}

View File

@@ -0,0 +1,40 @@
import { z } from 'zod';
const sendSchema = z.object({
conversation: z.string(),
text: z.string(),
})
.strict()
export default class MessagesHandler {
static handleSend(msg, ws) {
let user = global.db.members.getByEmail(ws.userEmail)
let convo = global.db.conversations.get(msg.conversation)
if(convo.between.includes(`MEMBER-${user.id}`)) {
global.db.messages.add(msg.conversation, msg.text, `MEMBER-${user.id}`)
global.Socket.broadcast({event: "new-message", app: "MESSAGES", msg: {conversationID: convo.id, messages: global.db.messages.getByConversation(`CONVERSATION-${msg.conversation}`)}})
} else {
throw new Error("Can't add to a conversation user is not a part of!")
}
return {success: true}
}
static handleGet(ws) {
let user = global.db.members.getByEmail(ws.userEmail)
let data = global.db.conversations.getByMember(`MEMBER-${user.id}`)
return data
}
static handle(operation, msg, ws) {
switch(operation) {
case "GET":
return this.handleGet(ws)
case "SEND":
if(!sendSchema.safeParse(msg).success) throw new Error("Incorrectly formatted Forum ws message!")
return this.handleSend(msg, ws)
}
}
}

109
server/ws/ws.js Normal file
View File

@@ -0,0 +1,109 @@
import { WebSocket, WebSocketServer } from 'ws';
import { z } from 'zod';
import jwt from 'jsonwebtoken';
import ForumHandler from "./handlers/ForumHandler.js"
import MessagesHandler from "./handlers/MessagesHandler.js"
export default class Socket {
wss;
messageSchema = z.object({
id: z.string(),
app: z.string(),
operation: z.string().optional(),
msg: z.union([
z.object({}).passthrough(), // allows any object
z.array(z.any()) // allows any array
]).optional()
})
.superRefine((data, ctx) => {
if (data.operation !== "GET" && data.msg === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["msg"],
message: "msg is required when operation is not GET"
})
}
})
.strict()
constructor(server) {
this.wss = new WebSocketServer({ server });
this.wss.on('connection', (ws, req) => {
console.log('✅ New WebSocket client connected');
function parseCookies(cookieHeader = "") {
return Object.fromEntries(
cookieHeader.split(";").map(c => {
const [key, ...v] = c.trim().split("=");
return [key, decodeURIComponent(v.join("="))];
})
);
}
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);
ws.userEmail = payload.email;
ws.on('message', (msg) => {
this.handleMessage(msg, ws);
});
ws.on('close', () => {
console.log('Client disconnected');
});
});
console.log('WebSocket server initialized');
}
// 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) => {
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:
console.error("unknown ws message")
}
let response = {
...req
}
response.msg = responseData
if(!this.messageSchema.safeParse(response).success) throw new Error("Socket.handleMessage: Outgoing ws message has incorrect format!")
ws.send(JSON.stringify(response))
} catch (e) {
console.error("Invalid WS message:", e);
}
}
broadcast(event) {
if (!this.wss) return;
let message = JSON.stringify(event)
this.wss.clients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
}
});
}
}

View File

@@ -0,0 +1,63 @@
css(`
email-join-form {
display: flex
}
`)
export default class EmailJoinForm extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.querySelector('#submit-button').addEventListener('click', () => this.submitEmail());
}
isValidEmail(email) {
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,16}$/;
return emailRegex.test(email);
}
showError(message) {
$(this).find('#form-message')
.css('color', 'red')
.text(message);
}
showSuccess(message) {
$(this).find('#form-message')
.css('color', 'green')
.text(message);
}
clearError() {
this.querySelector('#form-message').textContent = '';
}
async submitEmail() {
const email = this.querySelector('#email-input').value.trim();
this.clearError();
if (!this.isValidEmail(email)) {
this.showError('Please enter a valid email address.');
return;
}
const res = await fetch('/api/join', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
if (res.ok) {
this.showSuccess('Email sent.');
} else {
const error = await res.text();
this.showError(error)
}
}
}
customElements.define("email-join-form", EmailJoinForm);

1173
ui/_/code/quill.js Normal file

File diff suppressed because one or more lines are too long

111
ui/_/code/shared.css Normal file
View File

@@ -0,0 +1,111 @@
:root {
--main: var(--brown);
--accent: var(--gold);
--tan: #FFDFB4;
--gold: #F2B36F;
--divider: #bb7c36;
--green: #0857265c;
--red: #BC1C02;
--brown: #812A18;
--darkbrown: #3f0808;
--accent2: var(--green);
}
@media (prefers-color-scheme: dark) {
:root {
--main: var(--brown);
--accent: var(--gold);
--accent2: var(--gold);
}
}
@font-face {
font-family: 'Canterbury';
src: url('/_/fonts/Canterbury/Canterbury.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Bona Nova';
src: url('/_/fonts/BonaNova/BonaNova-Regular.woff') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Bona Nova';
src: url('/_/fonts/BonaNova/BonaNova-Bold.woff') format('truetype');
font-weight: bold;
font-style: normal;
}
body {
margin: 0px;
font-family: 'Bona Nova', sans-serif;
font-size: 16px;
background-color: var(--main);
color: var(--accent);
}
@media (max-width: 480px) {
body, html{
overflow-x: hidden;
}
}
#title {
padding: 5px 10px;
font-size: 1.7rem;
position: fixed; top: 4.5vh; left: 6vw;
cursor: pointer; z-index: 1;
}
a {
cursor: default;
text-decoration: none;
text-underline-offset: 5px;
transition: background .02s, color .2s;
user-select: none;
color: var(--accent);
display: inline-block; /* makes background and padding behave */
padding: 0.2em 0.5em; /* adds breathing room */
}
a:hover {
text-decoration: none;
background: var(--green);
color: var(--tan);
}
a:active {
background: var(--red); /* background color works now */
}
button {
background-color: transparent;
color: var(--accent);
padding: 0.5em;
box-shadow: none;
border: 1px solid var(--accent);
border-radius: 0.3em;
}
input {
background-color: transparent;
border: 1px solid var(--accent2);
padding-left: 1em;
padding-top: 0.5em;
padding-bottom: 0.5em;
border-radius: 0.3em;
}
input::placeholder {
color: var(--accent)
}
input:focus {
outline: 1px solid var(--red);
}

View File

@@ -0,0 +1,62 @@
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

45
ui/_/code/ws/Socket.js Normal file
View File

@@ -0,0 +1,45 @@
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
}));
}
}

8
ui/_/code/zod.js Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="stylesheet" type="text/css"
href="style.css"/>
</head>
<body>
<h1>Generated from: http://font.download</h1><br/>
<h1 style="font-family:'Bona Nova Regular';font-weight:normal;font-size:42px">AaBbCcDdEeFfGgHhŞşIıİi Example</h1>
<h1 style="font-family:'Bona Nova Italic';font-weight:normal;font-size:42px">AaBbCcDdEeFfGgHhŞşIıİi Example</h1>
<h1 style="font-family:'Bona Nova Bold';font-weight:normal;font-size:42px">AaBbCcDdEeFfGgHhŞşIıİi Example</h1>
</body>
</html>

View File

@@ -0,0 +1,24 @@
/* #### Generated By: http://font.download #### */
@font-face {
font-family: 'Bona Nova Regular';
font-style: normal;
font-weight: normal;
src: local('Bona Nova Regular'), url('BonaNova-Regular.woff') format('woff');
}
@font-face {
font-family: 'Bona Nova Italic';
font-style: normal;
font-weight: normal;
src: local('Bona Nova Italic'), url('BonaNova-Italic.woff') format('woff');
}
@font-face {
font-family: 'Bona Nova Bold';
font-style: normal;
font-weight: normal;
src: local('Bona Nova Bold'), url('BonaNova-Bold.woff') format('woff');
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

93
ui/_/fonts/CrimsonText/OFL.txt Executable file
View File

@@ -0,0 +1,93 @@
Copyright 2010 The Crimson Text Project Authors (https://github.com/googlefonts/Crimson)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

4
ui/_/icons/Column.svg Normal file
View File

@@ -0,0 +1,4 @@
<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>

After

Width:  |  Height:  |  Size: 2.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -140 780 780" enable-background="new 0 0 780 500" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect width="780" height="500" fill="#FFF" stroke-width="15px" stroke="#000000"/><path d="m409.41 197.26c30.938 0 56.02 23.58 56.02 52.709v0.033c0 29.129-25.082 52.742-56.02 52.742-30.941 0-56.022-23.613-56.022-52.742v-0.033c0-29.129 25.081-52.709 56.022-52.709z" fill="#F47216"/><path d="m321.43 197.94c8.836 0 16.247 1.785 25.27 6.09v22.753c-8.544-7.863-15.955-11.154-25.757-11.154-19.265 0-34.413 15.015-34.413 34.051 0 20.074 14.681 34.195 35.368 34.195 9.312 0 16.586-3.12 24.802-10.856v22.764c-9.343 4.142-16.912 5.775-25.757 5.775-31.277 0-55.581-22.597-55.581-51.736-2e-3 -28.83 24.949-51.882 56.068-51.882zm-97.113 0.626c11.546 0 22.109 3.721 30.942 10.994l-10.748 13.248c-5.351-5.646-10.411-8.027-16.563-8.027-8.854 0-15.301 4.744-15.301 10.988 0 5.354 3.618 8.188 15.944 12.481 23.364 8.043 30.289 15.176 30.289 30.926 0 19.193-14.976 32.554-36.319 32.554-15.631 0-26.993-5.795-36.457-18.871l13.268-12.03c4.73 8.608 12.622 13.223 22.42 13.223 9.163 0 15.947-5.95 15.947-13.983 0-4.164-2.056-7.733-6.158-10.258-2.066-1.195-6.158-2.978-14.199-5.646-19.292-6.538-25.91-13.527-25.91-27.186-1e-3 -16.227 14.213-28.413 32.845-28.413zm234.72 1.729h22.436l28.084 66.592 28.447-66.592h22.267l-45.493 101.69h-11.054l-44.687-101.69zm-301.21 0.152h20.541v99.143h-20.541v-99.143zm411.73 0h58.253v16.799h-37.726v22.006h36.336v16.791h-36.336v26.762h37.726v16.785h-58.253v-99.143zm115.59 57.377c15.471-2.965 23.983-12.926 23.983-28.105 0-18.562-13.575-29.271-37.266-29.271h-30.454v99.144h20.516v-39.83h2.681l28.43 39.828h25.26l-33.15-41.766zm-17.218-11.736h-6.002v-30.025h6.326c12.791 0 19.744 5.049 19.744 14.697 2e-3 9.967-6.951 15.328-20.068 15.328zm-576.09-45.641h-30.149v99.143h29.992c15.946 0 27.465-3.543 37.573-11.445 12.014-9.359 19.117-23.467 19.117-38.057 1e-3 -29.259-23.221-49.641-56.533-49.641zm23.997 74.479c-6.454 5.484-14.837 7.879-28.108 7.879h-5.514v-65.559h5.513c13.271 0 21.323 2.238 28.108 8.018 7.104 5.956 11.377 15.184 11.377 24.682 1e-3 9.513-4.273 19.024-11.376 24.98z"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -9 58 58" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="57" height="39" rx="3.5" fill="white" stroke="#F3F3F3"/>
<path d="M34.3102 28.9765H23.9591V10.5122H34.3102V28.9765Z" fill="#FF5F00"/>
<path d="M24.6223 19.7429C24.6223 15.9973 26.3891 12.6608 29.1406 10.5107C27.1285 8.93843 24.5892 7.99998 21.8294 7.99998C15.2961 7.99998 10 13.2574 10 19.7429C10 26.2283 15.2961 31.4857 21.8294 31.4857C24.5892 31.4857 27.1285 30.5473 29.1406 28.975C26.3891 26.8249 24.6223 23.4884 24.6223 19.7429" fill="#EB001B"/>
<path d="M48.2706 19.7429C48.2706 26.2283 42.9745 31.4857 36.4412 31.4857C33.6814 31.4857 31.1421 30.5473 29.1293 28.975C31.8815 26.8249 33.6483 23.4884 33.6483 19.7429C33.6483 15.9973 31.8815 12.6608 29.1293 10.5107C31.1421 8.93843 33.6814 7.99998 36.4412 7.99998C42.9745 7.99998 48.2706 13.2574 48.2706 19.7429" fill="#F79E1B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -140 780 780" enable-background="new 0 0 780 500" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M40,0h700c22.092,0,40,17.909,40,40v420c0,22.092-17.908,40-40,40H40c-22.091,0-40-17.908-40-40V40 C0,17.909,17.909,0,40,0z" fill="#0E4595"/><path d="m293.2 348.73l33.361-195.76h53.36l-33.385 195.76h-53.336zm246.11-191.54c-10.57-3.966-27.137-8.222-47.822-8.222-52.725 0-89.865 26.55-90.18 64.603-0.299 28.13 26.514 43.822 46.752 53.186 20.771 9.595 27.752 15.714 27.654 24.283-0.131 13.121-16.586 19.116-31.922 19.116-21.357 0-32.703-2.967-50.227-10.276l-6.876-3.11-7.489 43.823c12.463 5.464 35.51 10.198 59.438 10.443 56.09 0 92.5-26.246 92.916-66.882 0.199-22.269-14.016-39.216-44.801-53.188-18.65-9.055-30.072-15.099-29.951-24.268 0-8.137 9.668-16.839 30.557-16.839 17.449-0.27 30.09 3.535 39.938 7.5l4.781 2.26 7.232-42.429m137.31-4.223h-41.232c-12.773 0-22.332 3.487-27.941 16.234l-79.244 179.4h56.031s9.16-24.123 11.232-29.418c6.125 0 60.555 0.084 68.338 0.084 1.596 6.853 6.49 29.334 6.49 29.334h49.514l-43.188-195.64zm-65.418 126.41c4.412-11.279 21.26-54.723 21.26-54.723-0.316 0.522 4.379-11.334 7.074-18.684l3.605 16.879s10.219 46.729 12.354 56.528h-44.293zm-363.3-126.41l-52.24 133.5-5.567-27.13c-9.725-31.273-40.025-65.155-73.898-82.118l47.766 171.2 56.456-0.064 84.004-195.39h-56.521" fill="#ffffff"/><path d="m146.92 152.96h-86.041l-0.681 4.073c66.938 16.204 111.23 55.363 129.62 102.41l-18.71-89.96c-3.23-12.395-12.597-16.094-24.186-16.527" fill="#F2AE14"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

5
ui/_/icons/hamburger.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg width="86" height="59" viewBox="0 0 86 59" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="86" height="11" rx="3" fill="#0B5538"/>
<rect y="48" width="86" height="11" rx="3" fill="#0B5538"/>
<rect y="24" width="86" height="11" rx="3" fill="#0B5538"/>
</svg>

After

Width:  |  Height:  |  Size: 276 B

5
ui/_/icons/jobs.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 KiB

4
ui/_/icons/letter.svg Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<path d="M0 0 C33 0 66 0 100 0 C100 23.76 100 47.52 100 72 C67 72 34 72 0 72 C0 48.24 0 24.48 0 0 Z M4 4 C3.46383745 8.70053019 3.46383745 8.70053019 5.30664062 10.5546875 C5.91701172 10.99039062 6.52738281 11.42609375 7.15625 11.875 C7.84114502 12.37870117 8.52604004 12.88240234 9.23168945 13.40136719 C9.98023193 13.92891602 10.72877441 14.45646484 11.5 15 C13.06813799 16.14868129 14.63450237 17.29978719 16.19921875 18.453125 C17.40070557 19.33194336 17.40070557 19.33194336 18.62646484 20.22851562 C22.28176453 22.95664302 25.79456149 25.84835522 29.3125 28.75 C31.2591282 30.33484221 33.20813973 31.91671841 35.15722656 33.49853516 C36.36876709 34.48568066 37.57657813 35.4774244 38.78027344 36.47412109 C40.40268905 37.81286849 42.04179632 39.1275644 43.6875 40.4375 C44.59886719 41.17871094 45.51023438 41.91992188 46.44921875 42.68359375 C48.88630843 44.26660079 48.88630843 44.26660079 51.37890625 43.5859375 C54.95082046 41.42468986 58.06328957 38.87595616 61.25 36.1875 C67.05660886 31.37582668 72.93368091 26.68843648 78.9375 22.125 C80.0061731 21.30745239 80.0061731 21.30745239 81.09643555 20.47338867 C84.06177854 18.20933035 87.03509444 15.97490703 90.0703125 13.8046875 C91.30394531 12.91136719 91.30394531 12.91136719 92.5625 12 C93.24441406 11.525625 93.92632812 11.05125 94.62890625 10.5625 C96.54964019 8.72009187 96.54964019 8.72009187 96 4 C65.64 4 35.28 4 4 4 Z M4 15 C4 32.49 4 49.98 4 68 C34.36 68 64.72 68 96 68 C96 50.51 96 33.02 96 15 C91.10723979 18.66957016 86.23777639 22.33985116 81.4375 26.125 C80.5556604 26.81843506 80.5556604 26.81843506 79.65600586 27.52587891 C76.42735841 30.06945531 73.21156659 32.62826175 70.0078125 35.203125 C69.01890015 35.99759033 69.01890015 35.99759033 68.01000977 36.80810547 C66.75119369 37.82090246 65.49376977 38.83543328 64.23803711 39.85205078 C63.07629198 40.7874235 61.90867537 41.71555794 60.73486328 42.63574219 C59.54616335 43.57050931 58.37318371 44.52551784 57.21435547 45.49707031 C54.26370419 47.84496996 52.75924576 48.97890984 48.90625 49.0859375 C45.41094599 47.77989648 43.28715117 46.10681719 40.5 43.625 C39.44697905 42.70771873 38.39222174 41.79242782 37.3359375 40.87890625 C36.7903418 40.40340332 36.24474609 39.92790039 35.68261719 39.43798828 C32.89861809 37.05874355 30.01440497 34.81161154 27.125 32.5625 C26.54911133 32.11293945 25.97322266 31.66337891 25.37988281 31.20019531 C24.19857561 30.27805578 23.01693136 29.35634787 21.83496094 28.43505859 C19.90812651 26.92814897 17.98846374 25.41247563 16.0703125 23.89453125 C15.43077637 23.38913818 14.79124023 22.88374512 14.13232422 22.36303711 C12.90121746 21.38980809 11.671035 20.41540842 10.44189453 19.43969727 C7.37045638 16.99238729 7.37045638 16.99238729 4 15 Z " fill="#000000" transform="translate(0,14)"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 466.583 466.582"
xml:space="preserve">
<g>
<path d="M233.292,0c-85.1,0-154.334,69.234-154.334,154.333c0,34.275,21.887,90.155,66.908,170.834
c31.846,57.063,63.168,104.643,64.484,106.64l22.942,34.775l22.941-34.774c1.317-1.998,32.641-49.577,64.483-106.64
c45.023-80.68,66.908-136.559,66.908-170.834C387.625,69.234,318.391,0,233.292,0z M233.292,233.291c-44.182,0-80-35.817-80-80
s35.818-80,80-80c44.182,0,80,35.817,80,80S277.473,233.291,233.292,233.291z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 878 B

38
ui/_/icons/logo.svg Normal file
View File

@@ -0,0 +1,38 @@
<svg width="64" height="90" viewBox="0 0 64 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.1239 0.570931C29.0573 0.570676 32.396 0.410984 40.1415 0.571653C41.7911 0.571653 41.0611 0.571461 44.5585 0.570931L57.647 0.569493C58.7578 0.569493 62.1339 0.581774 63.09 0.569493H63.0944C63.3274 0.569493 63.4247 0.756732 63.4417 0.792724C63.4679 0.848215 63.4775 0.901709 63.4819 0.928461C63.4922 0.98942 63.4963 1.06338 63.4981 1.13729C63.5022 1.29126 63.4992 1.52411 63.494 1.83975C63.4837 2.47691 63.4631 3.48782 63.4631 4.99379V5.00422C63.3842 7.81995 63.4048 10.6396 63.4163 13.4676C63.4277 16.293 63.4295 19.1256 63.3127 21.9554C63.2297 23.9673 63.0649 28.3358 62.8249 32.7158C62.5856 37.0848 62.27 41.5003 61.8807 43.5896C58.5462 61.4885 50.205 73.5428 40.9305 81.7045C40.0346 82.4931 35.2042 87.0623 32.4118 88.9238L32.2073 89.0603L32.0028 88.9238C30.6096 87.9951 28.4816 86.4219 26.5905 84.9689C24.7088 83.5233 23.0269 82.1702 22.545 81.6883C13.8332 72.9765 6.45283 61.6669 2.53599 43.6C2.16234 41.8763 1.70377 37.3097 1.32695 32.7694C0.94926 28.2189 0.650322 23.6484 0.599649 21.8967C0.429081 16 0.534793 10.0862 0.509995 4.20203C0.508074 3.74572 0.505323 2.95258 0.52656 2.27253C0.537126 1.9341 0.553891 1.61406 0.580927 1.37492C0.594126 1.25817 0.61158 1.14374 0.637093 1.05196C0.649057 1.00893 0.669254 0.946291 0.705504 0.886696C0.730903 0.844942 0.837591 0.684347 1.05295 0.684347C14.1416 0.684347 19.0709 0.656043 21.3958 0.62782C23.7111 0.599711 23.45 0.571163 26.1239 0.570931Z" fill="#AB2C19" stroke="#FFB700"/>
<path d="M39.2138 0.623047L24.6758 0.623065L24.8348 83.272L31.2869 88.2494L32.2087 88.9868L33.3147 88.2494L39.2138 83.272V0.623047Z" fill="#174228"/>
<path d="M24.6758 0.123065C24.6758 0.456398 24.6758 0.789731 24.6758 1.12306C27.9793 1.12306 31.2829 1.12306 34.5864 1.12305C36.1289 1.12305 37.6713 1.12305 39.2138 1.12305L38.7138 0.623047C38.7138 28.1727 38.7138 55.7224 38.7138 83.272L38.8914 82.8899C36.925 84.549 34.9587 86.2081 32.9923 87.8672L33.0374 87.8333C32.8248 87.9751 32.6122 88.1168 32.3996 88.2586C32.2435 88.3626 32.0874 88.4667 31.9313 88.5707L32.521 88.5963C32.2138 88.3505 31.9065 88.1047 31.5993 87.8589C30.3624 86.9047 29.1324 85.9558 27.9025 85.007C26.9817 84.2967 26.061 83.5864 25.1403 82.8762L25.3348 83.2711C25.2882 59.025 25.2415 34.7788 25.1949 10.5327C25.1885 7.2292 25.1821 3.92563 25.1758 0.622102C24.8424 0.622744 24.5091 0.623385 24.1758 0.624027C24.1821 3.92756 24.1885 7.23113 24.1949 10.5347C24.2415 34.7808 24.2882 59.0269 24.3349 83.273L24.3353 83.5182L24.5294 83.6679C25.4502 84.3782 26.3709 85.0885 27.2917 85.7988C28.5216 86.7476 29.7516 87.6964 30.9815 88.6453C31.2818 88.8856 31.5891 89.1314 31.8963 89.3772L32.1818 89.6056L32.486 89.4028C32.6421 89.2987 32.7982 89.1947 32.9543 89.0906C33.1669 88.9489 33.3795 88.8071 33.5921 88.6654L33.6372 88.6315C35.6035 86.9724 37.5699 85.3133 39.5362 83.6542L39.7138 83.5044V83.272C39.7138 55.7224 39.7138 28.1727 39.7138 0.623047V0.123046L39.2138 0.123047C37.6713 0.123049 36.1289 0.123051 34.5864 0.123053C31.2829 0.123057 27.9793 0.123061 24.6758 0.123065ZM24.6758 1.12306L24.6758 0.123065L24.1748 0.123065L24.1758 0.624027L25.1758 0.622102L24.6758 1.12306Z" fill="#170405"/>
<path d="M2.26625 41.793H62.0707L62.8081 34.6035L63.078 27.4141L0.965309 27.4141L1.42096 34.6035L2.26625 41.793Z" fill="#174228" stroke="#170405"/>
<path d="M27.0323 27.356C25.2747 27.1189 24.8903 26.2433 24.3044 25.605C23.7186 24.9666 23.4989 25.003 23.4989 24.0363C23.4989 23.179 24.6706 22.65 24.0664 21.9022C23.4622 21.1544 22.2997 21.8566 23.0595 22.6135C23.2151 22.7686 22.6567 23.0331 22.73 23.3614C22.4919 22.9966 22.1624 22.7047 22.1624 22.4494C22.1624 22.194 22.7116 21.6103 22.199 21.1361C21.6864 20.6619 20.6612 21.9569 21.0639 22.1028C21.4667 22.2488 21.5399 22.3035 21.5399 22.7048C21.5399 23.106 21.5033 23.3614 21.6681 23.6715C21.1555 23.2337 21.1921 23.2155 21.0273 22.8324C20.8625 22.4494 20.3499 22.3034 19.9105 22.9419C19.4711 23.5803 19.3247 23.7262 19.6542 24.018C19.9837 24.3099 20.3133 23.5985 20.771 24.2551C21.2287 24.9118 21.5948 26.0974 21.009 25.9515C20.4231 25.8056 20.057 26.0244 20.295 26.8088C20.533 27.5931 21.1921 27.3377 21.5399 27.1553C21.8878 26.9729 21.9061 26.9182 22.4553 27.1736C23.0046 27.4289 23.5721 27.9032 25.9887 31.9889C28.4054 36.0745 28.1491 27.6477 27.0323 27.356Z" fill="#FE9201"/>
<path d="M32.7512 39.8074C30.8471 41.4949 28.5313 38.8318 27.1763 39.7071C27.9361 39.9261 28.1523 40.5543 26.9743 40.5543C24.5535 40.5543 25.2784 41.6892 23.932 42.6437C24.7476 42.5534 24.9485 42.3395 25.1101 42.1408C25.3239 43.2543 24.4887 42.9663 24.8771 44.6169C25.1101 43.856 25.5114 43.8047 25.5114 43.8047C25.8351 46.6809 27.3308 47.0591 27.1296 48.7702C27.7122 48.2024 27.8028 49.6341 26.6766 49.8534C25.5503 50.0728 24.5017 50.047 23.8544 49.428C23.2071 48.8089 21.809 50.1244 22.767 50.2148C23.7249 50.3051 23.6602 50.6144 24.2169 50.6789C23.6214 50.8338 23.4337 51.2143 22.7993 50.7368C22.165 50.2597 21.3041 51.7496 22.0938 52.0073C22.8835 52.2654 23.1877 51.601 24.1069 51.4528C23.5178 51.8849 23.6473 52.3362 23.0842 52.2009C22.521 52.0656 21.7119 52.2717 22.3786 53.0264C23.0453 53.7807 23.5243 53.1812 23.8544 52.9361C24.1845 52.6909 24.3724 52.751 24.5535 52.4203C24.385 52.7609 23.4725 53.0843 24.0098 53.6067C24.547 54.1292 25.3108 54.1421 25.5438 53.2841C25.7768 52.4265 25.8545 51.917 27.0002 51.917C28.1459 51.917 29.0651 52.4203 29.0651 51.4849C29.0651 50.5499 29.268 50.2361 29.8355 50.5326C29.5655 49.8804 29.9133 49.5202 29.5929 49.1098C29.8401 49.2193 29.9774 49.4291 29.9774 49.4291C30.3985 48.5169 29.7211 47.4407 29.4648 47.2859C29.9133 46.9939 30.1697 47.0031 30.1697 47.0031C29.5106 46.839 28.7874 45.5803 28.6501 44.7504C28.5952 44.413 29.3824 44.7596 30.0049 45.2338C30.6273 45.7079 33.2271 46.3712 34.1072 45.0295C34.9876 43.6882 33.8545 39.0995 32.7512 39.8074Z" fill="#FE9201"/>
<path d="M37.8532 31.3344C36.48 34.1798 36.0223 34.6358 36.1138 36.1864C36.2054 37.7367 38.6404 38.9589 39.3726 40.8012C40.1048 42.6436 41.9907 43.0993 42.1006 40.8193C42.2104 38.5393 38.2924 37.208 38.2924 34.3806C38.2924 31.5534 40.9274 30.1779 41.4413 29.091C41.5505 29.7384 41.3565 29.9702 41.24 30.3313C41.24 30.3313 46.714 28.0148 46.0551 25.4248C46.4946 25.8352 46.6041 26.3551 46.6041 26.6105C46.815 26.346 48.0689 24.3487 46.641 22.689C45.213 21.0291 43.968 22.4519 43.968 23.4004C43.968 24.3489 45.6522 24.3853 45.6522 23.7652C45.6522 23.2933 45.0833 23.3307 44.9015 23.7652C44.5538 23.145 45.5792 22.1601 46.1831 22.8349C46.7874 23.5098 46.8058 24.8049 45.6157 24.8049C45.0342 24.8049 43.6251 24.7913 43.4736 22.9262C42.5747 24.7415 45.1397 24.9764 45.1397 26.2459C45.1397 27.7598 43.6019 28.2158 43.5104 26.8843C43.7847 27.1031 44.6272 26.5012 43.8581 25.6622C43.0894 24.8231 42.8173 24.3382 42.7229 23.7287C41.9074 24.8134 45.0663 27.2308 41.1121 29.0914C42.119 27.9787 42.3841 26.8289 42.2289 25.9905C42.2289 25.9905 41.7529 26.8478 41.167 26.7384C42.192 24.6225 41.4012 23.197 43.492 21.5399C42.6208 21.6038 42.1787 22.164 41.1302 21.777C41.9863 21.6178 42.7609 21.1082 43.5285 20.9015C41.2585 20.1536 40.8006 17.5635 43.3456 16.834C42.1739 18.2385 42.613 19.0958 43.071 19.6248C43.0894 18.4756 43.7847 18.4391 44.0594 18.4756C43.4736 19.2964 43.4186 19.8619 44.6637 20.4821C45.9088 21.1022 45.9818 21.3576 45.9818 21.3576C45.9818 21.3576 46.4762 20.555 46.9887 20.7739C46.8239 20.9928 46.5219 21.166 46.7324 21.5126C46.9429 21.8592 50.6686 26.3553 45.268 29.2922C46.1281 29.128 46.5676 29.4928 46.5676 29.4928C46.5676 29.4928 45.268 29.6387 44.389 30.2772C46.2015 30.6237 46.5779 30.2607 47.3732 29.6752C46.5529 31.6024 43.6933 30.9155 42.522 31.6451C44.0963 31.8458 44.4443 33.1408 45.744 32.5571C43.9333 35.0048 42.467 31.025 40.2335 32.5753C41.6983 32.5206 41.9177 33.0314 42.2657 33.1408C41.5881 33.4691 38.9335 32.8672 39.3365 34.8371C39.9223 34.2352 40.6181 34.1075 40.6181 34.1075C40.6181 34.1075 39.4464 35.3296 40.3069 36.1322C41.167 36.9347 42.7052 38.1021 42.8331 38.9593C43.1443 38.3576 43.5654 37.938 43.5654 37.938C43.5654 37.938 43.5654 39.3608 43.3641 41.1666C43.1628 42.9721 41.1213 45.015 38.9796 43.027C39.1352 42.3977 38.7871 41.3123 38.6043 41.1117C39.4279 42.5345 38.4395 44.2308 38.5678 45.1975C38.3296 44.9785 38.2382 44.3584 38.2382 44.3584C36.6577 46.607 38.706 46.9086 40.197 45.9268C40.0805 46.1849 39.6845 46.7479 40.1236 46.7114C40.5631 46.6749 41.6069 46.5654 41.6069 47.2766C41.6069 47.9881 39.4648 50.1041 39.4648 51.6363C39.4648 53.1686 40.0138 53.0772 39.5562 53.7522C39.0987 54.427 39.9773 55.3026 39.4648 55.6857C38.9519 56.0687 38 55.6307 38.1648 55.2842C38.3296 54.9376 38.5493 54.6095 38.4944 54.2994C38.4395 53.9893 38.7322 53.9344 38.6408 53.5878C38.3753 53.8525 38.2562 54.4815 37.9635 54.5365C37.6704 54.591 37.8901 55.7222 37.2128 55.7767C36.5354 55.8313 35.7298 54.7005 36.2974 54.573C36.865 54.445 36.7734 54.3359 37.0845 53.7884C37.3961 53.2412 37.048 53.7338 36.627 53.9163C36.2059 54.0984 36.3157 54.6095 35.9496 54.6095C35.5834 54.6095 34.8877 53.5513 35.4003 53.2232C35.913 52.8947 36.3157 53.0226 36.6453 52.7306C37.0524 52.6001 37.2136 52.5861 37.3043 52.3475C37.1295 52.3992 36.6636 52.7852 36.1692 52.8582C35.6749 52.9312 35.8763 51.2898 36.334 51.2898C36.7917 51.2898 36.7368 51.9464 37.2313 51.7639C37.7253 51.5814 38.6592 49.2649 38.5309 48.7723C38.403 48.2801 37.7987 48.7543 37.8171 49.0459C37.5789 48.9733 36.2791 48.7723 35.9679 48.5902C36.0594 48.9733 36.5354 49.1919 36.5354 49.1919C35.4186 49.3836 34.668 48.6267 34.6863 48.0611C34.4117 48.9368 34.1737 48.2252 33.4413 49.0094C33.5878 47.66 32.6541 46.9484 32.6541 46.9484C32.6541 46.9484 32.6907 48.1157 31.757 48.7543C31.6106 46.8574 29.8347 45.1426 31.6655 43.9388C30.8416 43.7747 30.8233 43.2276 30.8233 43.2276C30.8233 43.2276 34.4117 42.4795 33.8075 40.145C34.0455 41.2757 33.3132 41.9689 33.3132 41.9689C33.3132 41.9689 32.6724 40.2545 31.757 39.6708C30.8416 39.0868 30.2923 38.3211 30.2923 38.3211C30.2923 38.3211 30.2374 39.3423 30.4571 39.5429C29.5234 39.0507 29.5234 37.5914 29.5234 37.5914C29.5234 37.5914 29.56 38.9047 29.1023 39.1783C29.1023 38.1201 28.4066 36.9894 28.4066 36.9894C28.4066 36.9894 28.5347 38.0475 28.3333 38.2296C28.2601 37.4635 26.5758 35.4937 26.2646 34.7276C25.9533 33.9615 25.2027 31.6268 25.7336 31.189C26.2646 30.7513 30.1458 28.0335 31.4089 28.6354C32.6722 29.2373 38.97 28.7631 37.8536 31.335L37.8532 31.3344Z" fill="#FE9201"/>
<path d="M24.2398 34.9371C23.3702 34.8823 23.3061 34.8276 23.1413 34.6635C23.297 35.1195 22.7478 35.9402 21.8873 35.3566C21.0268 34.7729 20.5416 34.2348 20.148 35.1104C19.7544 35.9859 20.4684 36.2139 20.8528 36.1318C21.2373 36.0497 21.3929 36.4328 21.9147 36.3416C21.3747 36.6608 21.1183 36.8888 20.2304 36.6608C19.3425 36.4328 19.1044 38.2295 20.1297 38.3114C21.155 38.3936 20.6515 37.8007 21.2007 37.5821C21.7499 37.3631 22.1893 37.0073 22.1893 37.0073C22.1893 37.0073 21.695 37.2809 21.6859 37.9195C21.6767 38.5577 20.9077 38.6672 21.1915 39.0137C21.4753 39.3603 22.1985 39.6796 22.647 39.3695C23.0955 39.0594 22.8301 38.5304 22.7385 38.2295C22.647 37.9283 23.0406 37.5364 23.5075 37.5821C23.9743 37.6274 23.9286 37.9832 23.5166 38.2752C23.1047 38.5669 24.6975 38.968 25.0545 38.6399C25.4115 38.3114 24.6609 37.4907 25.503 37.2263C26.3452 36.9618 26.986 37.2355 26.986 37.2355C26.1604 36.6308 26.8892 36.6307 27.7458 36.3963C28.4389 36.2066 28.771 35.7123 30.016 36.2595C30.9863 36.6425 30.8764 36.4784 30.9497 35.8035C31.0229 35.1286 31.6637 35.1286 31.8102 34.3078C31.9566 33.487 32.4327 33.2499 32.4327 31.0428C32.4327 28.8358 29.6302 29.8102 28.4047 31.061C27.1795 32.3119 27.4711 33.3048 26.7021 33.8062C25.4847 34.5999 25.1095 34.9918 24.2398 34.9371Z" fill="#FE9201"/>
<path d="M32.5568 17.6998C32.738 17.0807 31.793 15.9844 31.793 15.9844C31.793 15.9844 33.1005 16.1004 33.9678 17.0807C34.8222 16.41 35.9356 16.2037 35.9356 16.2037C35.9356 16.2037 35.6508 16.6164 35.5602 16.8615C37.0231 16.8873 36.8418 17.1323 37.2691 17.1323C36.6347 17.4934 36.4664 17.5966 36.4664 17.5966C38.1882 18.0609 40.0394 20.2148 40.0394 20.2148C40.0394 20.2148 39.3145 19.9569 38.0717 20.1891C40.7513 20.9887 40.1688 22.2591 40.9843 23.0136C39.4827 22.6654 39.0295 22.2785 38.499 22.6138C39.5992 22.8718 40.0781 23.9165 40.0781 23.9165C40.0781 23.9165 39.8842 23.7746 39.7677 23.7488C39.9616 23.981 41.1399 27.386 39.7935 29.0883C39.91 27.6437 38.9389 27.2439 38.9389 27.2439C38.9389 27.2439 39.1851 30.0814 38.68 30.7779C38.6218 30.5522 38.4861 30.2684 38.3954 30.1846C38.3954 30.1846 38.4861 31.7581 37.6315 32.3257C37.567 30.5716 37.1913 30.4039 37.0102 30.3007C35.3854 32.6546 35.0554 32.7448 34.421 33.422C34.6929 32.1451 34.6282 31.5776 34.4857 31.3455C33.2559 32.5965 34.2915 32.9448 32.6345 34.3506C32.6992 32.8287 32.3497 32.4031 32.3497 32.4031C32.3497 32.4031 32.4532 33.3317 31.6895 33.9508C31.56 32.2225 31.314 31.8356 31.4823 30.7908C30.7833 31.655 30.6538 31.9903 30.6538 31.9903C30.5714 31.5617 30.848 30.6877 30.6926 30.3781C30.3561 31.6163 29.4499 31.8614 29.4499 31.8614C29.4499 31.8614 29.9159 30.3652 29.6311 29.8106C28.8155 30.8682 29.2022 31.7364 27.6735 32.311C27.9573 31.6908 27.9756 31.1436 27.9756 31.1436C27.9756 31.1436 26.9595 32.5754 26.7582 33.3598C26.5365 32.5959 26.8497 31.8641 26.8497 31.8641C26.6555 31.9995 25.9251 32.767 25.7695 33.4966C25.3759 32.2654 25.559 31.5905 25.559 31.5905C25.559 31.5905 25.3114 31.6968 25.0006 31.9644C25.3485 30.5964 25.7146 30.2316 25.9251 29.8759C25.3301 30.1951 25.1013 30.4869 24.8816 30.6693C24.7534 29.9488 25.2166 28.2887 26.7442 27.7857C26.0063 27.0118 25.9027 26.4959 25.9157 25.9671C26.6277 26.8828 27.3138 27.1279 27.7151 26.741C28.1164 26.354 28.453 26.3798 28.9579 25.8381C29.4628 25.2964 30.1748 25.3996 30.7056 24.9353C31.2363 24.471 32.2963 23.4736 32.1315 22.744C31.9829 23.084 31.7342 22.8902 31.7948 23.2786C31.5307 23.1835 31.4851 23.3029 31.5256 23.5528C31.2363 23.4484 31.3553 23.699 31.2252 23.6468C30.6577 23.4279 30.319 24.0573 30.1542 24.4403C29.9895 24.8233 29.7606 24.7869 29.2571 24.5862C28.7537 24.3855 28.9001 24.1849 29.0832 24.0755C29.2663 23.9661 28.9367 23.5556 28.9367 23.4188C29.1748 23.583 29.6141 23.4644 29.7332 23.3732C28.5889 22.6071 27.3989 22.8533 27.6735 23.9752C27.0602 23.811 27.1884 23.1999 27.1884 23.1999C27.1884 23.1999 25.3667 23.583 25.4217 21.6769C26.0258 22.7439 27.225 22.7713 27.8291 22.5524C28.4333 22.3335 29.3445 22.6287 30.056 23.0474C30.2445 23.1582 30.2825 22.9214 30.2427 22.8298C29.93 22.111 28.9648 21.2195 28.6853 22.2216C28.4068 21.6681 28.1587 22.0964 27.8932 21.8958C27.6278 21.6951 26.8771 21.0567 27.2525 20.473C27.6278 19.8894 27.8108 20.1265 28.6439 19.4516C29.4768 18.7767 29.7175 18.5497 30.5021 18.4484C30.7319 18.4187 30.7562 17.8954 30.6852 17.7553C31.1458 17.8334 31.6006 17.9742 31.6006 17.9742C31.6006 17.9742 31.5113 17.1156 30.724 17.3527C31.2584 16.7205 32.0575 17.179 32.5568 17.6998Z" fill="#FE9201"/>
<path d="M32.9805 18.5051C33.667 18.8425 34.7838 17.2374 35.6626 18.6145C36.5414 19.9917 34.8662 20.2197 34.5916 20.7486C34.738 20.2926 34.143 19.7089 33.8226 20.0008C34.1522 19.6998 34.4268 19.8002 34.4268 19.8002" fill="#FE9201"/>
<path d="M32.363 20.0176C32.5552 20.4097 31.8503 20.3733 31.7771 20.7745C31.7039 21.1759 32.7474 21.5406 32.7474 21.5406" fill="#FE9201"/>
<path d="M32.3203 21.7949C32.4851 21.9043 32.6773 22.2053 32.6773 22.2053Z" fill="#FE9201"/>
<path d="M30.4413 20.2004C30.1371 20.2455 29.8329 19.6522 29.6387 19.5297C29.7811 19.2847 29.7875 19.1041 30.0853 19.1105" fill="#FE9201"/>
<path d="M29.8359 19.7093C30.1725 19.4062 30.5788 19.3967 30.8118 19.616Z" fill="#FE9201"/>
<path d="M38.6214 56.6091C37.9353 56.035 38.2719 55.0034 38.6926 55.3968C39.1132 55.7902 38.0713 55.8868 38.6214 56.6091Z" fill="#FE9201"/>
<path d="M36.1497 56.1741C35.5866 55.3486 36.2856 54.4973 36.6157 54.9748C36.9459 55.4519 35.8972 55.7487 36.1497 56.1741Z" fill="#FE9201"/>
<path d="M34.7425 54.6252C34.4836 53.6449 35.2409 53.219 35.5258 53.6578C35.8106 54.0962 34.6001 54.2831 34.7425 54.6252Z" fill="#FE9201"/>
<path d="M34.9423 52.7162C34.4439 51.4586 36.3663 51.1039 36.1139 51.8262C35.8614 52.5485 35.0782 51.8712 34.9423 52.7162Z" fill="#FE9201"/>
<path d="M27.7217 20.0254C27.5244 20.5588 27.4921 21.1498 27.5342 21.5732C27.2582 21.281 27.0015 20.8642 27.2539 20.4717C27.4226 20.2094 27.5542 20.1134 27.7217 20.0254Z" fill="#FE9201"/>
<path d="M27.7217 20.0254C27.5244 20.5588 27.4921 21.1498 27.5342 21.5732C27.2582 21.281 27.0015 20.8642 27.2539 20.4717C27.4226 20.2094 27.5542 20.1134 27.7217 20.0254Z" stroke="#EFC88F"/>
<path d="M27.3797 20.6191C27.364 20.7201 27.3535 20.82 27.3458 20.9176C27.3238 20.8156 27.331 20.7168 27.3797 20.6191Z" fill="#FE9201"/>
<path d="M21.8897 49.9697C21.5791 49.0218 23.2749 48.7508 22.9901 49.5438C22.7053 50.3373 22.1616 49.17 21.8897 49.9697Z" fill="#FE9201"/>
<path d="M21.389 51.8071C20.3275 51.0203 22.1981 49.9176 22.3341 50.8592C22.47 51.8008 21.033 50.8721 21.389 51.8071Z" fill="#FE9201"/>
<path d="M22.0468 54.0084C21.255 53.2331 22.2757 52.2162 22.6784 52.6631C23.0812 53.1099 21.9873 53.3971 22.0468 54.0084Z" fill="#FE9201"/>
<path d="M24.004 54.4377C23.4822 53.7397 24.0132 52.9555 24.3564 53.3113C24.6997 53.6667 23.9491 53.9543 24.004 54.4377Z" fill="#FE9201"/>
<path d="M20.3711 21.4018C20.7007 20.327 22.2887 20.8815 21.7944 21.3831C21.3001 21.8848 21.314 20.7452 20.3711 21.4018Z" fill="#FE9201"/>
<path d="M22.8242 21.4763C23.465 21.1345 23.9375 21.8697 23.4974 22.0309C23.0572 22.1921 23.4197 21.6182 22.8242 21.4763Z" fill="#FE9201"/>
<path d="M18.9355 23.1036C19.1168 21.9105 20.5926 22.3297 20.2107 22.852C19.8288 23.3744 19.7123 22.349 18.9355 23.1036Z" fill="#FE9201"/>
<path d="M19.791 26.1992C19.9375 27.4396 20.917 27.0565 20.8163 26.6735C20.7156 26.2904 20.3952 26.7282 19.791 26.1992Z" fill="#FE9201"/>
<path d="M19.6192 36.1738C19.0425 35.1159 20.3699 34.5139 20.4797 34.9973C20.5895 35.4807 19.7382 35.1797 19.6192 36.1738Z" fill="#FE9201"/>
<path d="M19.7018 38.9021C18.8505 38.0265 19.6917 37.1741 20.0771 37.4156C20.6532 37.7762 19.7476 38.0538 19.7018 38.9021Z" fill="#FE9201"/>
<path d="M22.1387 40.2003C21.3881 40.1092 21.2952 38.9711 21.8184 38.9965C22.569 39.033 21.6993 39.5982 22.1387 40.2003Z" fill="#FE9201"/>
<path d="M23.2754 38.9215C24.0535 39.6327 24.6546 38.4647 24.2457 38.3105C23.6415 38.0823 24.1084 38.9034 23.2754 38.9215Z" fill="#FE9201"/>
<path d="M31.1973 18.6157C31.508 18.8478 31.8446 18.7317 32.0646 18.5898Z" fill="#FE9201"/>
<path d="M31.8965 19.3105C32.1166 19.5427 32.259 19.6072 32.492 19.5169Z" fill="#FE9201"/>
<path d="M33.2891 19.0358C33.582 19.1361 33.8474 19.1635 33.9207 19.3824C33.9573 19.1726 34.2411 18.9355 34.4699 19.1361C34.36 18.8625 34.7354 18.6801 35.1198 18.826" fill="#FE9201"/>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB

29
ui/_/icons/logo2.svg Normal file
View File

@@ -0,0 +1,29 @@
<svg width="115" height="161" viewBox="0 0 115 161" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M46.9604 1.12722C52.2219 1.12677 58.2104 0.840336 72.1031 1.12852C75.0618 1.12852 73.7524 1.12818 80.0255 1.12722L103.502 1.12465C105.494 1.12465 111.55 1.14667 113.265 1.12465H113.272C113.69 1.12465 113.865 1.46049 113.895 1.52504C113.942 1.62458 113.96 1.72053 113.968 1.76851C113.986 1.87785 113.993 2.01051 113.997 2.14308C114.004 2.41925 113.999 2.8369 113.989 3.40304C113.971 4.54588 113.934 6.35911 113.934 9.06029V9.079C113.792 14.1294 113.829 19.1869 113.85 24.2593C113.87 29.3271 113.874 34.4079 113.664 39.4834C113.515 43.0922 113.22 50.9277 112.789 58.7839C112.36 66.6204 111.794 74.5402 111.095 78.2878C105.115 110.392 90.1533 132.013 73.5183 146.653C71.9113 148.067 63.2473 156.263 58.2387 159.602L57.8719 159.846L57.505 159.602C55.006 157.936 51.1892 155.114 47.7973 152.508C44.4221 149.915 41.4054 147.488 40.5411 146.623C24.9152 130.998 11.6773 110.712 4.65186 78.3063C3.98166 75.2147 3.15915 67.0238 2.48325 58.8801C1.80582 50.718 1.26962 42.5202 1.17874 39.3782C0.872797 28.8016 1.06241 18.1943 1.01793 7.64014C1.01448 6.82169 1.00955 5.39907 1.04764 4.17929C1.06659 3.57228 1.09666 2.99823 1.14515 2.56931C1.16883 2.3599 1.20014 2.15464 1.2459 1.99002C1.26736 1.91284 1.30358 1.80049 1.3686 1.6936C1.41416 1.6187 1.60552 1.33065 1.9918 1.33065C25.4683 1.33065 34.3097 1.27989 38.4797 1.22926C42.6326 1.17885 42.1643 1.12764 46.9604 1.12722Z" fill="#BD1C03" stroke="#EEC88E"/>
<path d="M70.4394 1.2207L44.3633 1.22074L44.6486 149.464L56.2214 158.392L57.8746 159.714L59.8586 158.392L70.4394 149.464V1.2207Z" fill="#233779"/>
<path d="M44.3633 0.720735C44.3633 1.05407 44.3633 1.3874 44.3633 1.72074C50.2887 1.72073 56.2141 1.72072 62.1395 1.72071C64.9062 1.72071 67.6728 1.72071 70.4394 1.7207L69.9394 1.2207C69.9394 50.6352 69.9394 100.05 69.9394 149.464L70.117 149.082C66.59 152.058 63.0631 155.034 59.5361 158.01L59.5812 157.976C59.1999 158.23 58.8185 158.484 58.4372 158.738C58.1572 158.925 57.8773 159.112 57.5973 159.298L58.187 159.324C57.6359 158.883 57.0848 158.442 56.5337 158.001C54.3207 156.294 52.1146 154.592 49.9085 152.89C48.257 151.616 46.6055 150.342 44.954 149.068L45.1486 149.463C45.0649 105.974 44.9812 62.4851 44.8975 18.996C44.8861 13.0706 44.8747 7.14518 44.8633 1.21977C44.5299 1.22041 44.1966 1.22106 43.8633 1.2217C43.8747 7.14711 43.8861 13.0725 43.8975 18.9979C43.9812 62.487 44.0649 105.976 44.1486 149.465L44.1491 149.71L44.3432 149.86C45.9947 151.134 47.6462 152.408 49.2977 153.682C51.5038 155.384 53.7099 157.086 55.916 158.788C56.4601 159.223 57.0112 159.664 57.5623 160.105L57.8478 160.333L58.152 160.13C58.432 159.944 58.7119 159.757 58.9919 159.57C59.3732 159.316 59.7546 159.062 60.1359 158.808L60.181 158.774C63.7079 155.798 67.2349 152.822 70.7618 149.846L70.9394 149.696V149.464C70.9394 100.05 70.9394 50.6352 70.9394 1.2207V0.720703L70.4394 0.720703C67.6728 0.720707 64.9062 0.72071 62.1395 0.720713C56.2141 0.720721 50.2887 0.720728 44.3633 0.720735ZM44.3633 1.72074L44.3633 0.720735L43.8623 0.720736L43.8633 1.2217L44.8633 1.21977L44.3633 1.72074Z" fill="#EEC88E"/>
<path d="M4.17083 75.0672H111.439L112.762 62.1718L113.246 49.2764L1.83739 49.2764L2.65467 62.1718L4.17083 75.0672Z" fill="#223778" stroke="#EEC88E"/>
<path d="M48.595 49.1708C45.4425 48.7454 44.7529 47.1751 43.702 46.03C42.6512 44.885 42.2572 44.9504 42.2572 43.2164C42.2572 41.6786 44.3588 40.7299 43.2752 39.3885C42.1915 38.0472 40.1063 39.3067 41.4691 40.6645C41.7482 40.9426 40.7466 41.417 40.878 42.0058C40.4511 41.3515 39.8601 40.828 39.8601 40.3701C39.8601 39.912 40.8452 38.8651 39.9257 38.0144C39.0063 37.1639 37.1673 39.4867 37.8897 39.7484C38.6122 40.0102 38.7435 40.1083 38.7435 40.8281C38.7435 41.5478 38.6779 42.0059 38.9734 42.5621C38.0539 41.7768 38.1196 41.7442 37.8241 41.0571C37.5285 40.37 36.609 40.1082 35.8209 41.2534C35.0328 42.3984 34.7701 42.6601 35.3612 43.1836C35.9522 43.707 36.5433 42.4311 37.3643 43.6089C38.1853 44.7868 38.842 46.9133 37.7912 46.6516C36.7404 46.3898 36.0836 46.7824 36.5105 48.1892C36.9374 49.596 38.1196 49.138 38.7435 48.8108C39.3675 48.4837 39.4003 48.3855 40.3854 48.8435C41.3706 49.3015 42.3886 50.1522 46.7231 57.4805C51.0578 64.8087 50.598 49.6939 48.595 49.1708Z" fill="#EFC88F"/>
<path d="M58.8551 71.5038C55.4399 74.5306 51.2861 69.754 48.8557 71.3239C50.2184 71.7167 50.6064 72.8436 48.4934 72.8436C44.1513 72.8436 45.4516 74.8791 43.0366 76.5912C44.4995 76.4292 44.8598 76.0456 45.1497 75.6892C45.5331 77.6863 44.0351 77.1698 44.7317 80.1305C45.1497 78.7656 45.8695 78.6736 45.8695 78.6736C46.45 83.8325 49.1329 84.511 48.772 87.5801C49.817 86.5617 49.9795 89.1295 47.9594 89.523C45.9392 89.9165 44.0584 89.8702 42.8974 88.7598C41.7364 87.6495 39.2287 90.009 40.9469 90.1711C42.6652 90.3331 42.5491 90.8879 43.5476 91.0036C42.4795 91.2814 42.1428 91.9638 41.005 91.1075C39.8672 90.2517 38.323 92.9241 39.7394 93.3863C41.1559 93.8492 41.7016 92.6575 43.3502 92.3917C42.2937 93.1668 42.5258 93.9762 41.5158 93.7335C40.5058 93.4908 39.0545 93.8605 40.2503 95.2141C41.4461 96.5672 42.3052 95.4919 42.8974 95.0521C43.4895 94.6124 43.8266 94.7202 44.1513 94.127C43.8491 94.738 42.2125 95.318 43.176 96.255C44.1397 97.1921 45.5097 97.2152 45.9276 95.6764C46.3456 94.1382 46.4849 93.2243 48.5399 93.2243C50.5949 93.2243 52.2435 94.127 52.2435 92.4492C52.2435 90.7722 52.6075 90.2094 53.6255 90.7411C53.1411 89.5712 53.765 88.9252 53.1903 88.1891C53.6337 88.3855 53.8799 88.7618 53.8799 88.7618C54.6353 87.1258 53.4202 85.1954 52.9605 84.9177C53.765 84.3939 54.2247 84.4104 54.2247 84.4104C53.0426 84.1162 51.7455 81.8585 51.4992 80.3699C51.4007 79.7648 52.8127 80.3864 53.9292 81.2369C55.0457 82.0873 59.7087 83.277 61.2874 80.8705C62.8664 78.4647 60.8341 70.2341 58.8551 71.5038Z" fill="#EFC88F"/>
<path d="M68.0034 56.3075C65.5403 61.4112 64.7193 62.2292 64.8835 65.0103C65.0477 67.791 69.4153 69.9832 70.7286 73.2877C72.042 76.5923 75.4245 77.4096 75.6216 73.3201C75.8187 69.2306 68.791 66.8427 68.791 61.7713C68.791 56.7003 73.5174 54.2332 74.4392 52.2836C74.635 53.4448 74.2871 53.8606 74.0781 54.5083C74.0781 54.5083 83.8965 50.3534 82.7147 45.7078C83.503 46.4439 83.6994 47.3764 83.6994 47.8345C84.0777 47.36 86.3268 43.7776 83.7656 40.8007C81.2043 37.8235 78.9711 40.3754 78.9711 42.0766C78.9711 43.7779 81.9919 43.8433 81.9919 42.731C81.9919 41.8847 80.9716 41.9517 80.6455 42.731C80.0219 41.6186 81.861 39.8519 82.9442 41.0625C84.0281 42.273 84.0612 44.5958 81.9265 44.5958C80.8836 44.5958 78.3561 44.5715 78.0843 41.2261C76.4721 44.4822 81.0727 44.9034 81.0727 47.1805C81.0727 49.8959 78.3144 50.7138 78.1504 48.3255C78.6424 48.7181 80.1535 47.6385 78.774 46.1335C77.3952 44.6285 76.9072 43.7587 76.7379 42.6656C75.2751 44.6111 80.9411 48.9472 73.8487 52.2843C75.6547 50.2886 76.1302 48.2261 75.8518 46.7225C75.8518 46.7225 74.998 48.2602 73.9472 48.0639C75.7856 44.2687 74.3671 41.7119 78.1174 38.7396C76.5547 38.8542 75.7618 39.8589 73.8811 39.1649C75.4166 38.8794 76.806 37.9653 78.1828 37.5945C74.1112 36.2531 73.2899 31.6073 77.8548 30.2988C75.7532 32.8181 76.5408 34.3557 77.3622 35.3045C77.3952 33.2433 78.6424 33.1779 79.1351 33.2433C78.0843 34.7156 77.9858 35.7298 80.219 36.8422C82.4522 37.9545 82.5831 38.4126 82.5831 38.4126C82.5831 38.4126 83.47 36.9731 84.3892 37.3656C84.0936 37.7583 83.552 38.069 83.9296 38.6906C84.3072 39.3123 90.9896 47.3768 81.3029 52.6444C82.8457 52.35 83.634 53.0044 83.634 53.0044C83.634 53.0044 81.3029 53.2661 79.7263 54.4112C82.9773 55.0328 83.6525 54.3818 85.0789 53.3315C83.6075 56.7882 78.4784 55.5562 76.3775 56.8649C79.2012 57.2248 79.8255 59.5476 82.1566 58.5007C78.9089 62.891 76.2789 55.7525 72.2728 58.5333C74.9001 58.4352 75.2936 59.3513 75.9179 59.5476C74.7024 60.1365 69.941 59.0568 70.6638 62.5902C71.7146 61.5105 72.9625 61.2815 72.9625 61.2815C72.9625 61.2815 70.8609 63.4735 72.4044 64.9131C73.9472 66.3526 76.7062 68.4463 76.9356 69.9839C77.4938 68.9046 78.249 68.1521 78.249 68.1521C78.249 68.1521 78.249 70.704 77.8879 73.9431C77.5268 77.1815 73.8652 80.8458 70.0237 77.28C70.3028 76.1512 69.6785 74.2043 69.3505 73.8445C70.8278 76.3965 69.0549 79.4392 69.285 81.1731C68.8578 80.7803 68.6938 79.668 68.6938 79.668C65.859 83.7013 69.533 84.2422 72.2073 82.4812C71.9983 82.9441 71.2881 83.9539 72.0757 83.8884C72.864 83.8229 74.7361 83.6265 74.7361 84.9022C74.7361 86.1785 70.894 89.9737 70.894 92.7221C70.894 95.4705 71.8786 95.3065 71.058 96.5173C70.2373 97.7275 71.8132 99.2981 70.894 99.9852C69.9741 100.672 68.2666 99.8866 68.5622 99.265C68.8578 98.6434 69.2519 98.0548 69.1534 97.4987C69.0549 96.9425 69.58 96.844 69.416 96.2224C68.9398 96.6972 68.7262 97.8254 68.2011 97.9239C67.6754 98.0218 68.0695 100.051 66.8547 100.149C65.6397 100.246 64.1948 98.2182 65.2128 97.9894C66.2308 97.7599 66.0666 97.5641 66.6246 96.5821C67.1834 95.6007 66.5591 96.4842 65.8039 96.8116C65.0486 97.1383 65.2456 98.0548 64.5889 98.0548C63.9321 98.0548 62.6843 96.1569 63.6038 95.5683C64.5232 94.9791 65.2456 95.2086 65.8367 94.6848C66.5671 94.4507 66.8561 94.4256 67.0187 93.9977C66.7053 94.0903 65.8696 94.7827 64.9829 94.9136C64.0962 95.0446 64.4575 92.1005 65.2784 92.1005C66.0994 92.1005 66.0009 93.2782 66.8878 92.9509C67.7739 92.6236 69.449 88.4686 69.2189 87.5851C68.9894 86.7023 67.9055 87.5527 67.9386 88.0758C67.5114 87.9455 65.18 87.5851 64.6217 87.2584C64.7859 87.9455 65.6396 88.3377 65.6396 88.3377C63.6365 88.6815 62.2902 87.3239 62.323 86.3094C61.8304 87.88 61.4035 86.6037 60.09 88.0103C60.3527 85.5899 58.678 84.3136 58.678 84.3136C58.678 84.3136 58.7437 86.4073 57.0689 87.5527C56.8062 84.1503 53.6209 81.0746 56.9047 78.9154C55.427 78.6211 55.3942 77.6398 55.3942 77.6398C55.3942 77.6398 61.8304 76.298 60.7467 72.1106C61.1737 74.1388 59.8601 75.3821 59.8601 75.3821C59.8601 75.3821 58.7108 72.307 57.0688 71.2602C55.427 70.2127 54.4418 68.8391 54.4418 68.8391C54.4418 68.8391 54.3433 70.671 54.7373 71.0307C53.0625 70.1479 53.0625 67.5304 53.0625 67.5304C53.0625 67.5304 53.1282 69.886 52.3073 70.3767C52.3073 68.4787 51.0594 66.4507 51.0594 66.4507C51.0594 66.4507 51.2893 68.3485 50.9281 68.6751C50.7967 67.301 47.7756 63.7679 47.2174 62.3938C46.6591 61.0196 45.3127 56.8319 46.2651 56.0468C47.2174 55.2615 54.1789 50.3868 56.4446 51.4665C58.7104 52.5461 70.0065 51.6955 68.0041 56.3085L68.0034 56.3075Z" fill="#EFC88F"/>
<path d="M43.5826 62.7664C42.0228 62.6682 41.9078 62.57 41.6123 62.2756C41.8914 63.0935 40.9063 64.5657 39.3629 63.5188C37.8195 62.4719 36.9493 61.5067 36.2433 63.0772C35.5373 64.6476 36.8179 65.0565 37.5075 64.9093C38.1971 64.7621 38.4763 65.4491 39.4121 65.2855C38.4435 65.8581 37.9837 66.267 36.391 65.8581C34.7984 65.4491 34.3715 68.6719 36.2104 68.8187C38.0494 68.9661 37.1463 67.9028 38.1315 67.5106C39.1166 67.1178 39.9047 66.4796 39.9047 66.4796C39.9047 66.4796 39.0181 66.9703 39.0017 68.1157C38.9852 69.2604 37.606 69.4568 38.115 70.0785C38.624 70.7001 39.9211 71.2728 40.7255 70.7166C41.5301 70.1605 41.0539 69.2115 40.8897 68.6719C40.7255 68.1316 41.4316 67.4286 42.269 67.5106C43.1063 67.592 43.0243 68.2301 42.2854 68.7539C41.5465 69.277 44.4034 69.9965 45.0438 69.4079C45.6841 68.8187 44.3378 67.3466 45.8483 66.8725C47.3589 66.398 48.5082 66.889 48.5082 66.889C47.0274 65.8044 48.3347 65.8041 49.871 65.3837C51.1143 65.0435 51.71 64.1568 53.9429 65.1383C55.6833 65.8254 55.4863 65.5309 55.6177 64.3204C55.7491 63.1099 56.8984 63.1099 57.1612 61.6377C57.4238 60.1655 58.2776 59.7401 58.2776 55.7814C58.2776 51.8227 53.2509 53.5706 51.053 55.8141C48.8553 58.0578 49.3784 59.8387 47.999 60.738C45.8155 62.1615 45.1424 62.8645 43.5826 62.7664Z" fill="#EFC88F"/>
<path d="M58.5022 31.8522C58.8273 30.7418 57.1322 28.7754 57.1322 28.7754C57.1322 28.7754 59.4774 28.9836 61.0331 30.7418C62.5656 29.5389 64.5625 29.1687 64.5625 29.1687C64.5625 29.1687 64.0518 29.909 63.8892 30.3486C66.5133 30.3949 66.188 30.8344 66.9543 30.8344C65.8165 31.4821 65.5146 31.6672 65.5146 31.6672C68.603 32.5 71.9234 36.3634 71.9234 36.3634C71.9234 36.3634 70.6233 35.9007 68.394 36.3171C73.2003 37.7514 72.1555 40.0301 73.6183 41.3835C70.9248 40.7589 70.1121 40.0648 69.1604 40.6663C71.1338 41.129 71.9928 43.0028 71.9928 43.0028C71.9928 43.0028 71.645 42.7484 71.436 42.7021C71.7838 43.1185 73.8974 49.2259 71.4823 52.2793C71.6913 49.6882 69.9494 48.971 69.9494 48.971C69.9494 48.971 70.3911 54.0606 69.4852 55.3098C69.3807 54.905 69.1373 54.396 68.9746 54.2457C68.9746 54.2457 69.1373 57.068 67.6044 58.086C67.4887 54.9397 66.8148 54.639 66.4901 54.454C63.5758 58.6759 62.9837 58.8378 61.846 60.0524C62.3336 57.7621 62.2175 56.7442 61.962 56.3278C59.7561 58.5718 61.6137 59.1964 58.6416 61.718C58.7577 58.9882 58.1307 58.2248 58.1307 58.2248C58.1307 58.2248 58.3165 59.8905 56.9465 61.0009C56.7143 57.9009 56.2731 57.2069 56.575 55.333C55.3211 56.8831 55.0889 57.4845 55.0889 57.4845C54.9411 56.7157 55.4372 55.1479 55.1586 54.5927C54.5549 56.8136 52.9294 57.2532 52.9294 57.2532C52.9294 57.2532 53.7653 54.5696 53.2545 53.5749C51.7917 55.4718 52.4853 57.0289 49.7433 58.0596C50.2524 56.9472 50.2852 55.9657 50.2852 55.9657C50.2852 55.9657 48.4627 58.5339 48.1015 59.9408C47.7039 58.5707 48.2657 57.258 48.2657 57.258C47.9174 57.5009 46.6073 58.8775 46.3282 60.1862C45.6222 57.9778 45.9506 56.7673 45.9506 56.7673C45.9506 56.7673 45.5065 56.958 44.949 57.438C45.573 54.9842 46.2297 54.3299 46.6073 53.6919C45.54 54.2644 45.1296 54.7879 44.7356 55.1151C44.5057 53.8228 45.3365 50.8451 48.0765 49.9428C46.7529 48.5547 46.5671 47.6294 46.5903 46.6809C47.8675 48.3234 49.0981 48.7629 49.8179 48.069C50.5378 47.3749 51.1415 47.4212 52.047 46.4496C52.9526 45.4779 54.2297 45.6629 55.1818 44.8302C56.1338 43.9974 58.0349 42.2084 57.7394 40.8998C57.4729 41.5097 57.0267 41.162 57.1355 41.8586C56.6617 41.688 56.5799 41.9023 56.6526 42.3505C56.1336 42.1633 56.3471 42.6127 56.1139 42.5192C55.0959 42.1266 54.4884 43.2553 54.1929 43.9424C53.8973 44.6294 53.4869 44.564 52.5838 44.204C51.6807 43.8442 51.9434 43.4843 52.2718 43.288C52.6002 43.0917 52.0091 42.3556 52.0091 42.1102C52.436 42.4046 53.2241 42.192 53.4376 42.0284C51.3852 40.6542 49.2507 41.0959 49.7433 43.1081C48.6432 42.8136 48.8731 41.7175 48.8731 41.7175C48.8731 41.7175 45.6057 42.4046 45.7042 38.9858C46.7879 40.8996 48.9388 40.9487 50.0224 40.5562C51.1061 40.1635 52.7405 40.6931 54.0167 41.444C54.3547 41.6428 54.423 41.2181 54.3515 41.0537C53.7907 39.7644 52.0594 38.1654 51.5582 39.9628C51.0585 38.97 50.6135 39.7383 50.1374 39.3784C49.6612 39.0185 48.3148 37.8734 48.9881 36.8265C49.6612 35.7796 49.9896 36.2049 51.4838 34.9943C52.9778 33.7838 53.4095 33.3767 54.8169 33.195C55.2291 33.1417 55.2726 32.2031 55.1452 31.9518C55.9713 32.0919 56.7871 32.3444 56.7871 32.3444C56.7871 32.3444 56.6269 30.8043 55.2148 31.2296C56.1733 30.0958 57.6067 30.9181 58.5022 31.8522Z" fill="#EFC88F"/>
<path d="M69.3805 101.639C68.1498 100.61 68.7536 98.7595 69.5081 99.4651C70.2627 100.171 68.3938 100.344 69.3805 101.639Z" fill="#9C0001"/>
<path d="M64.9431 100.86C63.933 99.3797 65.1869 97.8528 65.779 98.7092C66.3711 99.5649 64.4903 100.097 64.9431 100.86Z" fill="#9C0001"/>
<path d="M62.4194 98.0807C61.955 96.3223 63.3134 95.5585 63.8243 96.3454C64.3351 97.1317 62.164 97.467 62.4194 98.0807Z" fill="#9C0001"/>
<path d="M62.7776 94.6558C61.8836 92.4001 65.3318 91.7639 64.879 93.0594C64.4262 94.3549 63.0214 93.1401 62.7776 94.6558Z" fill="#9C0001"/>
<path d="M49.8291 36.0225C49.4752 36.9792 49.4167 38.0393 49.4922 38.7988C48.9973 38.2747 48.537 37.5271 48.9893 36.8232C49.2919 36.3526 49.5286 36.1804 49.8291 36.0225Z" fill="black"/>
<path d="M49.8291 36.0225C49.4752 36.9792 49.4167 38.0393 49.4922 38.7988C48.9973 38.2747 48.537 37.5271 48.9893 36.8232C49.2919 36.3526 49.5286 36.1804 49.8291 36.0225Z" stroke="#EFC88F"/>
<path d="M39.3693 89.7329C38.812 88.0327 41.8538 87.5467 41.343 88.9691C40.8321 90.3922 39.8569 88.2986 39.3693 89.7329Z" fill="#9C0001"/>
<path d="M38.4649 93.0275C36.5609 91.6163 39.9162 89.6383 40.1601 91.3273C40.4038 93.0162 37.8265 91.3504 38.4649 93.0275Z" fill="#9C0001"/>
<path d="M39.6464 96.9784C38.2262 95.5877 40.0569 93.7638 40.7793 94.5653C41.5017 95.3668 39.5397 95.882 39.6464 96.9784Z" fill="#9C0001"/>
<path d="M43.1582 97.7486C42.2223 96.4968 43.1746 95.0902 43.7903 95.7283C44.406 96.3658 43.0596 96.8816 43.1582 97.7486Z" fill="#9C0001"/>
<path d="M36.6484 38.4928C37.2397 36.565 40.088 37.5596 39.2014 38.4593C38.3147 39.359 38.3397 37.3151 36.6484 38.4928Z" fill="#9C0001"/>
<path d="M41.0391 38.6255C42.1884 38.0125 43.0359 39.3311 42.2465 39.6203C41.457 39.9094 42.1072 38.88 41.0391 38.6255Z" fill="#9C0001"/>
<path d="M34.0664 41.5423C34.3915 39.4024 37.0386 40.1542 36.3536 41.0911C35.6685 42.0281 35.4596 40.1889 34.0664 41.5423Z" fill="#9C0001"/>
<path d="M35.6055 47.0957C35.8681 49.3205 37.625 48.6334 37.4444 47.9463C37.2638 47.2593 36.6891 48.0445 35.6055 47.0957Z" fill="#9C0001"/>
<path d="M35.2945 64.9888C34.26 63.0913 36.6409 62.0116 36.8379 62.8787C37.0349 63.7457 35.508 63.2058 35.2945 64.9888Z" fill="#9C0001"/>
<path d="M35.4449 69.8825C33.9179 68.3119 35.4267 66.783 36.1181 67.2161C37.1514 67.8629 35.527 68.3609 35.4449 69.8825Z" fill="#9C0001"/>
<path d="M39.8151 72.2087C38.4688 72.0454 38.3021 70.004 39.2405 70.0496C40.5869 70.1151 39.027 71.1288 39.8151 72.2087Z" fill="#9C0001"/>
<path d="M41.8594 69.9137C43.255 71.1893 44.3331 69.0943 43.5998 68.8179C42.5161 68.4086 43.3535 69.8813 41.8594 69.9137Z" fill="#9C0001"/>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 102 KiB

3
ui/_/icons/profile.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="88" height="124" viewBox="0 0 88 124" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.998" d="M42.4512 2.00195C52.1659 2.22835 61.398 4.45649 70.166 8.69238L70.3818 8.79688L70.6162 8.84668C72.5686 9.26638 74.537 9.47923 76.5195 9.48535C76.5062 11.0178 76.3492 12.5388 76.0479 14.0508C74.4195 16.4705 72.6152 18.7571 70.6279 20.9072L69.8799 21.7168L70.1641 22.7812C71.785 28.8491 72.6458 35.0483 72.7471 41.3818L72.7549 41.8369L72.958 42.2432C74.3564 45.0408 75.7681 47.8651 77.1934 50.7148C76.401 51.7697 75.5014 52.7412 74.4893 53.6279L73.8906 54.1533L73.8164 54.9463C73.2326 61.1851 71.2275 66.786 67.8037 71.7871C66.3409 71.9333 64.8778 72.0802 63.415 72.2266C61.648 72.4034 59.8809 72.5803 58.1143 72.7568L56.9131 72.877L56.46 73.9951C55.1158 77.3095 54.9178 80.7587 55.8496 84.2715L55.9658 84.708L56.2578 85.0527C58.3856 87.5622 60.8538 89.6502 63.6553 91.3105L63.7783 91.3838L63.9102 91.4385C70.6068 94.2094 77.192 97.0352 83.665 99.9141C72.8406 106.409 62.2808 113.347 51.9873 120.731C49.5114 121.96 46.9641 122.264 44.2627 121.648C30.7653 112.891 16.9807 104.63 2.91113 96.8623C2.98649 96.7878 3.06425 96.7148 3.14453 96.6436C9.6346 94.2535 15.6931 91.0299 21.3154 86.9707L21.5225 86.8213L21.6855 86.625C23.5522 84.3704 24.9161 81.8385 25.7686 79.041L25.8184 78.877L25.8398 78.7061C26.5676 72.8871 26.5676 67.0651 25.8398 61.2461L25.7744 60.7236L25.4609 60.3018C16.1326 47.7366 13.0625 33.9256 16.21 18.7246C21.8795 8.29751 30.5529 2.76795 42.4512 2.00195Z" fill="#FE9201" fill-opacity="0.36" stroke="#FE9201" stroke-width="4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

4
ui/_/icons/x.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="241" height="241" viewBox="0 0 241 241" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="26.1953" y="0.000366211" width="303" height="37" transform="rotate(45 26.1953 0.000366211)" fill="#0B5538"/>
<rect y="214.253" width="303" height="37" transform="rotate(-45 0 214.253)" fill="#0B5538"/>
</svg>

After

Width:  |  Height:  |  Size: 318 B

576
ui/_/images/castle-dark.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 553 KiB

3094
ui/_/images/castle-dark2.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.6 MiB

3094
ui/_/images/castle-dark3.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.6 MiB

578
ui/_/images/castle.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 553 KiB

3
ui/_/images/divider.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="4004" height="80" viewBox="0 0 4004 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1978.59 16.9981C1958.34 34.6594 1926.21 39.7642 1911.05 27.2249C1905.12 22.3182 1901.18 22.3387 1895.3 27.3069C1889.42 32.2751 1668.94 35.0689 943.815 38.8441C319.093 42.0967 0.1788 45.4029 0.198794 49.2432C0.218788 53.0834 319.151 53.0688 943.873 49.8162C1617.81 46.3074 1889.5 46.5388 1894.77 50.9003C1900.04 55.2618 1905.95 55.231 1915.11 50.7944C1938.02 40.2513 1957.73 43.9891 1980.14 61.9768L2001.88 79.4194L2023.45 61.7513C2046.32 42.9792 2077.13 37.8812 2092.29 50.4205C2098.22 55.3272 2102.16 55.3067 2108.04 50.3385C2113.92 45.3703 2334.4 42.5766 3059.53 38.8013C3684.25 35.5487 4003.16 32.2425 4003.14 28.4023C4003.12 24.562 3684.19 24.5767 3059.47 27.8292C2334.35 31.6045 2113.85 31.1066 2107.92 26.2C2101.98 21.2933 2098.05 21.3138 2092.17 26.282C2077.14 38.9784 2044.96 34.2084 2024.53 16.759C2013.98 7.48739 2003.44 0.410206 2001.47 0.420455C1999.5 0.430705 1989.04 7.61722 1978.59 16.9981Z" fill="#FFDFB4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
ui/_/images/fabric.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
ui/_/images/fabric.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

BIN
ui/_/images/knight.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

BIN
ui/_/images/knight.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

BIN
ui/_/images/the_return.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

View File

@@ -0,0 +1,104 @@
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(() => {
HStack(() => {
VStack(() => {
img("/_/icons/logo.svg", "2em")
.padding(0.8, em)
.borderRadius(12, px)
.marginHorizontal(1, em)
.onHover(function (hovering) {
if(hovering) {
this.style.background = "var(--darkbrown)"
} else {
this.style.background = ""
}
})
.opacity(0)
img("/_/icons/place/austin.svg", "2em")
.padding(0.8, em)
.borderRadius(12, px)
.marginHorizontal(1, em)
.onHover(function (hovering) {
if(hovering) {
this.style.background = "var(--darkbrown)"
} else {
this.style.background = ""
}
})
.opacity(0)
})
.height(100, vh)
.paddingLeft(2, em)
.paddingRight(2, em)
.gap(1, em)
.marginTop(20, vh)
VStack(() => {
ForumPanel()
input("Message Hyperia", "98%")
.paddingVertical(1, em)
.paddingLeft(2, pct)
.color("var(--accent)")
.background("var(--darkbrown)")
.marginBottom(6, em)
.border("none")
.fontSize(1, em)
.onKeyDown(function (e) {
if (e.key === "Enter") {
window.Socket.send({app: "FORUM", operation: "SEND", msg: {forum: "HY", text: this.value }})
this.value = ""
}
})
})
.gap(0.5, em)
.width(100, pct)
.height(100, vh)
.alignHorizontal("center")
.alignVertical("end")
})
.width(100, "%")
.height(87, vh)
.x(0).y(0, vh)
})
.width(100, pct)
.height(100, pct)
}
}
register(Forum)

View File

@@ -0,0 +1,90 @@
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 () => {
console.log("appear")
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) {
console.log("rerendering", res.msg)
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)

View File

@@ -0,0 +1,101 @@
import "./JobsSidebar.js"
import "./JobsGrid.js"
css(`
jobs- {
font-family: 'Bona';
}
jobs- input::placeholder {
font-family: 'Bona Nova';
font-size: 0.9em;
color: var(--accent);
}
input[type="checkbox"] {
appearance: none; /* remove default style */
-webkit-appearance: none;
width: 1em;
height: 1em;
border: 1px solid var(--accent);
}
input[type="checkbox"]:checked {
background-color: var(--red);
}
`)
class Jobs extends Shadow {
jobs = [
{
title: "Austin Chapter Lead",
salary: "1% of Local Revenue",
company: "Hyperia",
city: "Austin",
state: "TX"
}
]
render() {
ZStack(() => {
HStack(() => {
JobsSidebar()
JobsGrid(this.jobs)
})
.width(100, "%")
.x(0).y(13, vh)
HStack(() => {
input("Search jobs... (Coming Soon!)", "45vw")
.attr({
"type": "text",
"disabled": "true"
})
.fontSize(1.1, em)
.paddingLeft(1.3, em)
.background("transparent")
.border("0.5px solid var(--divider)")
.outline("none")
.color("var(--accent)")
.opacity(0.5)
.borderRadius(10, px)
.background("grey")
.cursor("not-allowed")
button("+ Add Job")
.width(7, em)
.marginLeft(1, em)
.borderRadius(10, px)
.background("transparent")
.border("0.3px solid var(--accent2)")
.color("var(--accent)")
.fontFamily("Bona Nova")
.onHover(function (hovering) {
if(hovering) {
this.style.background = "var(--green)"
} else {
this.style.background = "transparent"
}
})
.onClick((clicking) => {
console.log(this, "clicked")
})
})
.x(55, vw).y(4, vh)
.position("absolute")
.transform("translateX(-50%)")
})
.width(100, "%")
.height(100, "%")
}
connectedCallback() {
// Optional additional logic
}
}
register(Jobs)

View File

@@ -0,0 +1,60 @@
class JobsGrid extends Shadow {
jobs;
constructor(jobs) {
super()
this.jobs = jobs
}
boldUntilFirstSpace(text) {
const index = text.indexOf(' ');
if (index === -1) {
// No spaces — bold the whole thing
return `<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)

View File

@@ -0,0 +1,26 @@
class JobsSidebar extends Shadow {
render() {
VStack(() => {
h3("Location")
.color("var(--accent2)")
.marginBottom(0, em)
HStack(() => {
input("Location", "100%")
.paddingLeft(3, em)
.paddingVertical(0.75, em)
.backgroundImage("/_/icons/locationPin.svg")
.backgroundRepeat("no-repeat")
.backgroundSize("18px 18px")
.backgroundPosition("10px center")
})
})
.paddingTop(1, em)
.paddingLeft(3, em)
.paddingRight(3, em)
.gap(1, em)
.minWidth(10, vw)
}
}
register(JobsSidebar)

View File

@@ -0,0 +1,105 @@
import "./MarketSidebar.js"
import "./MarketGrid.js"
css(`
market- {
font-family: 'Bona';
}
market- input::placeholder {
font-family: 'Bona Nova';
font-size: 0.9em;
color: var(--accent);
}
input[type="checkbox"] {
appearance: none; /* remove default style */
-webkit-appearance: none;
width: 1em;
height: 1em;
border: 1px solid var(--accent);
}
input[type="checkbox"]:checked {
background-color: var(--red);
}
`)
class Market extends Shadow {
listings = [
{
title: "Shield Lapel Pin",
stars: "5",
reviews: 1,
price: "$12",
company: "Hyperia",
type: "new",
image: "/db/images/1",
madeIn: "America"
}
]
render() {
ZStack(() => {
HStack(() => {
MarketSidebar()
MarketGrid(this.listings)
})
.width(100, "%")
.x(0).y(13, vh)
HStack(() => {
input("Search for products... (Coming Soon!)", "45vw")
.attr({
"type": "text",
"disabled": "true"
})
.fontSize(1.1, em)
.paddingLeft(1.3, em)
.background("transparent")
.border("0.5px solid var(--divider)")
.outline("none")
.color("var(--accent)")
.opacity(0.5)
.borderRadius(10, px)
.background("grey")
.cursor("not-allowed")
button("+ Add Item")
.width(7, em)
.marginLeft(1, em)
.borderRadius(10, px)
.background("transparent")
.border("0.5px solid var(--accent2)")
.color("var(--accent)")
.fontFamily("Bona Nova")
.onHover(function (hovering) {
if(hovering) {
this.style.background = "var(--green)"
} else {
this.style.background = "transparent"
}
})
.onClick((clicking) => {
console.log(this, "clicked")
})
})
.x(55, vw).y(4, vh)
.position("absolute")
.transform("translateX(-50%)")
})
.width(100, "%")
.height(100, "%")
}
connectedCallback() {
// Optional additional logic
}
}
register(Market)

View File

@@ -0,0 +1,140 @@
class MarketGrid extends Shadow {
listings;
constructor(listings) {
super()
this.listings = listings
}
boldUntilFirstSpace(text) {
if(!text) return
const index = text.indexOf(' ');
if (index === -1) {
// No spaces — bold the whole thing
return `<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 hyperiaMade = params.get("hyperia-made") === "true";
const americaMade = params.get("america-made") === "true";
const newItem = params.get("new") === "true";
const usedItem = params.get("used") === "true";
let filtered = this.listings;
if (hyperiaMade) {
filtered = filtered.filter(item => item.madeIn === "Hyperia");
}
if (americaMade) {
filtered = filtered.filter(item => item.madeIn === "America");
}
if (newItem) {
filtered = filtered.filter(item => item.type === "new");
}
if (usedItem) {
filtered = filtered.filter(item => item.type === "used");
}
for (let i = 0; i < filtered.length; i++) {
const rating = filtered[i].stars
const percent = (rating / 5)
VStack(() => {
img(filtered[i].image)
.marginBottom(0.5, em)
p(filtered[i].company)
.marginBottom(0.5, em)
p(filtered[i].title)
.fontSize(1.2, em)
.fontWeight("bold")
.marginBottom(0.5, em)
HStack(() => {
p(filtered[i].stars)
.marginRight(0.2, em)
ZStack(() => {
div("★★★★★") // Empty stars (background)
.color("#ccc")
div("★★★★★") // Filled stars (foreground, clipped by width)
.color("#ffa500")
.position("absolute")
.top(0)
.left(0)
.whiteSpace("nowrap")
.overflow("hidden")
.width(percent * 5, em)
})
.display("inline-block")
.position("relative")
.fontSize(1.2, em)
.lineHeight(1)
p(filtered[i].reviews)
.marginLeft(0.2, em)
})
.marginBottom(0.5, em)
p(filtered[i].price)
.fontSize(1.75, em)
.marginBottom(0.5, em)
button("Coming Soon!")
.onClick((finished) => {
if(finished) {
}
})
.onHover(function (hovering) {
if(hovering) {
this.style.backgroundColor = "var(--green)"
} else {
this.style.backgroundColor = ""
}
})
})
.padding(1, em)
.border("1px solid var(--accent2)")
.borderRadius(5, "px")
}
})
.display("grid")
.gridTemplateColumns("repeat(auto-fill, minmax(250px, 1fr))")
.gap(1, em)
} else {
p("No Listings!")
}
})
.onQueryChanged(() => {
console.log("query did change yup")
this.rerender()
})
.height(100, vh)
.paddingLeft(2, em)
.paddingRight(2, em)
.gap(0, em)
.width(100, "%")
}
}
register(MarketGrid)

View File

@@ -0,0 +1,85 @@
class MarketSidebar extends Shadow {
handleChecked(e) {
let checked = e.target.checked
let label = $(`label[for="${e.target.id}"]`).innerText
if(checked) {
window.setQuery(label.toLowerCase(), true)
} else {
window.setQuery(label.toLowerCase(), null)
}
}
render() {
VStack(() => {
p("Make")
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "hyperia-check"
})
.onChange(this.handleChecked)
label("Hyperia-Made")
.attr({
"for": "hyperia-check"
})
.marginLeft(0.5, em)
})
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "america-check"
})
.onChange(this.handleChecked)
label("America-Made")
.attr({
"for": "america-check"
})
.marginLeft(0.5, em)
})
p("Condition")
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "new-check"
})
.onChange(this.handleChecked)
label("New")
.attr({
"for": "new-check"
})
.marginLeft(0.5, em)
})
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "used-check"
})
.onChange(this.handleChecked)
label("Used")
.attr({
"for": "used-check"
})
.marginLeft(0.5, em)
})
})
.paddingTop(12, vh)
.paddingLeft(3, em)
.paddingRight(3, em)
.gap(1, em)
.minWidth(10, vw)
.userSelect('none')
}
}
register(MarketSidebar)

View File

@@ -0,0 +1,188 @@
import "./MessagesSidebar.js"
import "./MessagesPanel.js"
css(`
messages- {
font-family: 'Bona';
}
messages- input::placeholder {
font-family: 'Bona Nova';
font-size: 0.9em;
color: var(--accent);
}
input[type="checkbox"] {
appearance: none; /* remove default style */
-webkit-appearance: none;
width: 1em;
height: 1em;
border: 1px solid var(--accent);
}
input[type="checkbox"]:checked {
background-color: var(--red);
}
`)
class Messages extends Shadow {
conversations = []
selectedConvoID = null
onConversationSelect(i) {
console.log("convo selected: ", i)
this.selectedConvoID = i
this.$("messagessidebar-").rerender()
this.$("messagespanel-").rerender()
}
getConvoFromID(id) {
for(let i=0; i<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)
.alignHorizontal("center")
.alignVertical("end")
})
.onAppear(async () => {
let res = await Socket.send({app: "MESSAGES", operation: "GET"})
if(!res) console.error("failed to get messages")
if(res.msg.length > 0 && this.conversations.length === 0) {
this.conversations = res.msg
this.selectedConvoID = this.conversations[0].id
this.rerender()
}
window.addEventListener("new-message", (e) => {
let convoID = e.detail.conversationID
let messages = e.detail.messages
let convo = this.getConvoFromID(convoID)
convo.messages = messages
this.rerender()
})
})
.width(100, "%")
.height(87, vh)
.x(0).y(13, vh)
VStack(() => {
p("Add Message")
input("enter email...")
.color("var(--accent)")
.onKeyDown(function (e) {
if (e.key === "Enter") {
window.Socket.send({app: "MESSAGES", operation: "ADDCONVERSATION", msg: {email: this.value }})
this.value = ""
}
})
p("x")
.onClick(function (done) {
if(done) {
this.parentElement.style.display = "none"
}
})
.xRight(2, em).y(2, em)
.fontSize(1.4, em)
.cursor("pointer")
})
.gap(1, em)
.alignVertical("center")
.alignHorizontal("center")
.backgroundColor("black")
.border("1px solid var(--accent)")
.position("fixed")
.x(50, vw).y(50, vh)
.center()
.width(60, vw)
.height(60, vh)
.display("none")
.attr({id: "addPanel"})
HStack(() => {
input("Search messages... (Coming Soon!)", "45vw")
.attr({
"type": "text",
"disabled": "true"
})
.fontSize(1.1, em)
.paddingLeft(1.3, em)
.background("transparent")
.border("0.5px solid var(--divider)")
.outline("none")
.color("var(--accent)")
.opacity(0.5)
.borderRadius(10, px)
.background("grey")
.cursor("not-allowed")
button("+ New Message")
.width(13, em)
.marginLeft(1, em)
.borderRadius(10, px)
.background("transparent")
.border("0.5px solid var(--divider)")
.color("var(--accent)")
.fontFamily("Bona Nova")
.onHover(function (hovering) {
if(hovering) {
this.style.background = "var(--green)"
} else {
this.style.background = "transparent"
}
})
.onClick((done) => {
console.log("click")
if(done) {
this.$("#addPanel").style.display = "flex"
}
console.log(this, "clicked")
})
})
.x(55, vw).y(4, vh)
.position("absolute")
.transform("translateX(-50%)")
})
.width(100, "%")
.height(100, "%")
}
}
register(Messages)

View File

@@ -0,0 +1,56 @@
import "../../components/LoadingCircle.js"
class MessagesPanel extends Shadow {
messages
constructor(messages) {
super()
this.messages = messages
}
render() {
VStack(() => {
if(this.messages) {
for(let i=0; i<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)

View File

@@ -0,0 +1,73 @@
class MessagesSidebar extends Shadow {
conversations = []
selectedConvoID
onSelect
constructor(conversations, selectedConvoID, onSelect) {
super()
this.conversations = conversations
this.selectedConvoID = selectedConvoID
this.onSelect = onSelect
}
render() {
VStack(() => {
this.conversations.forEach((convo, i) => {
VStack(() => {
HStack(() => {
p(this.makeConvoTitle(convo.between))
.textAlign("left")
.marginLeft(0.5, inches)
.paddingTop(0.2, inches)
.width(100, pct)
.marginTop(0)
.fontSize(1, em)
.fontWeight("bold")
p(util.formatTime(convo.messages.last.time))
.paddingTop(0.2, inches)
.fontSize(0.8, em)
.marginRight(0.1, inches)
.color("var(--divider")
})
.justifyContent("space-between")
.marginBottom(0)
p(convo.messages.last.text)
.fontSize(0.8, em)
.textAlign("left")
.marginLeft(0.5, inches)
.marginBottom(2, em)
.color("var(--divider)")
})
.background(convo.id === this.selectedConvoID ? "var(--darkbrown)" : "")
.onClick(() => {
this.onSelect(i)
})
})
})
.minWidth(15, vw)
.height(100, vh)
.gap(0, em)
}
makeConvoTitle(members) {
let membersString = ""
for(let i=0; i<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)

View File

@@ -0,0 +1,153 @@
css(`
tasks- {
font-family: 'Bona';
}
tasks- input::placeholder {
font-family: 'Bona Nova';
font-size: 0.9em;
color: var(--accent);
}
input[type="checkbox"] {
appearance: none; /* remove default style */
-webkit-appearance: none;
width: 1em;
height: 1em;
border: 1px solid var(--accent);
}
input[type="checkbox"]:checked {
background-color: var(--red);
}
`)
class Tasks extends Shadow {
projects = [
{
"title": "Blockcatcher",
"tasks": {}
}
]
columns = [
{
"title": "backlog",
"tasks": {}
}
]
render() {
ZStack(() => {
HStack(() => {
VStack(() => {
h3("Projects")
.marginTop(0)
.marginBottom(1, em)
.marginLeft(0.4, em)
if (this.projects.length >= 1) {
for(let i = 0; i < this.projects.length; i++) {
p(this.projects[i].title)
}
} else {
p("No Projects!")
}
})
.height(100, vh)
.paddingLeft(2, em)
.paddingRight(2, em)
.paddingTop(2, em)
.gap(0, em)
.borderRight("0.5px solid var(--accent2)")
HStack(() => {
if (this.columns.length >= 1) {
for(let i = 0; i < this.columns.length; i++) {
p(this.columns[i].name)
}
} else {
p("No Conversations!")
}
})
.height(100, vh)
.paddingLeft(2, em)
.paddingRight(2, em)
.paddingTop(2, em)
.gap(0, em)
.borderRight("0.5px solid var(--accent2)")
})
.width(100, "%")
.x(0).y(13, vh)
.borderTop("0.5px solid var(--accent2)")
p("0 Items")
.position("absolute")
.x(50, vw).y(50, vh)
.transform("translate(-50%, -50%)")
HStack(() => {
input("Search tasks...", "45vw")
.attr({
"type": "text"
})
.fontSize(1.1, em)
.paddingLeft(1.3, em)
.background("transparent")
.border("0.5px solid var(--accent2)")
.outline("none")
.color("var(--accent)")
.borderRadius(10, px)
button("Search")
.marginLeft(2, em)
.borderRadius(10, px)
.background("transparent")
.border("0.5px solid var(--accent2)")
.color("var(--accent)")
.fontFamily("Bona Nova")
.onHover(function (hovering) {
if(hovering) {
this.style.background = "var(--green)"
} else {
this.style.background = "transparent"
}
})
button("+ New Task")
.width(9, em)
.marginLeft(1, em)
.borderRadius(10, px)
.background("transparent")
.border("0.5px solid var(--accent2)")
.color("var(--accent)")
.fontFamily("Bona Nova")
.onHover(function (hovering) {
if(hovering) {
this.style.background = "var(--green)"
} else {
this.style.background = "transparent"
}
})
.onClick((clicking) => {
console.log(this, "clicked")
})
})
.x(55, vw).y(4, vh)
.position("absolute")
.transform("translateX(-50%)")
})
.width(100, "%")
.height(100, "%")
}
connectedCallback() {
// Optional additional logic
}
}
register(Tasks)

View File

@@ -0,0 +1,133 @@
css(`
app-menu {
color: var(--tan);
transform: translateX(-50%);
transition: transform .3s;
display: flex; gap: 2em; position: fixed; left: 50vw; bottom: 2em;
}
app-menu.minimized {
color: var(--accent);
transform: translate(-50%, 65%);
border: 0.2px solid var(--accent);
padding-top: 0.5em;
padding-left: 2em;
padding-right: 2em;
padding-bottom: 4em;
bottom: 1em;
border-radius: 12px;
}
app-menu p {
cursor: default;
transition: transform .3s, text-decoration .3s;
padding: 0.5em;
border-radius: 5px;
text-underline-offset: 5px;
}
app-menu p:hover {
text-decoration: underline;
transform: translateY(-5%)
}
app-menu p.touched {
text-decoration: underline;
transform: translateY(0%)
}
app-menu p.selected {
text-decoration: underline;
transform: translateY(-10%)
}
#divider.minimized {
display: none;
}
`)
register(
class AppMenu extends Shadow {
selected;
constructor(selected) {
super()
this.selected = selected
}
render() {
VStack(() => {
HStack(() => {
p("Forum")
p("Messages")
p("Market")
p("Jobs")
})
.justifyContent("center")
.gap(1.5, em)
.paddingRight(2, em)
img("/_/images/divider.svg", "40vw")
.attr({
"id": "divider",
})
})
.gap(0.5, em)
.onNavigate(() => {
if(window.location.pathname === "/") {
this.styleMaximized()
$("app-window").close()
} else {
this.styleMinimized()
$("app-window").open(this.selected)
}
})
.onAppear(() => {
Array.from(this.querySelectorAll("p")).forEach((el) => {
el.addEventListener("mousedown", (e) => {
el.classList.add("touched")
})
})
window.addEventListener("mouseup", (e) => {
let target = e.target
if(!target.matches("app-menu p")) {
return
}
target.classList.remove("touched")
if(target.classList.contains("selected")) {
this.selected = ""
window.navigateTo("/")
} else {
this.selected = target.innerText
window.navigateTo("/app/" + target.innerText.toLowerCase())
}
})
})
if(this.selected) {
this.styleMinimized()
}
}
styleMaximized() {
$$("app-menu p").forEach((el) => {
el.classList.remove("selected")
})
this.classList.remove("minimized")
$("#divider").style.display = ""
}
styleMinimized() {
$$("app-menu p").forEach((el) => {
if(el.innerText !== this.selected) {
el.classList.remove("selected")
} else {
el.classList.add("selected")
}
})
this.classList.add("minimized")
$("#divider").style.display = "none"
}
}
, "app-menu")

View File

@@ -0,0 +1,54 @@
import "../apps/Forum/Forum.js"
import "../apps/Tasks/Tasks.js"
import "../apps/Messages/Messages.js"
import "../apps/Market/Market.js"
import "../apps/Jobs/Jobs.js"
class AppWindow extends Shadow {
app;
constructor(app) {
super()
this.app = app
}
render() {
ZStack(() => {
switch(this.app) {
case "Forum":
Forum()
break;
case "Messages":
Messages()
break;
case "Market":
Market()
break;
case "Jobs":
Jobs()
break;
}
})
.position("fixed")
.display(this.app ? 'block' : 'none')
.width(100, "vw")
.height(100, "vh")
.background("#591d10")
.x(0)
.y(0)
// .backgroundImage("/_/images/fabric.png")
// .backgroundSize("33vw auto")
}
open(app) {
this.app = app
this.rerender()
}
close() {
this.style.display = "none"
}
}
register(AppWindow, "app-window")

View File

@@ -0,0 +1,91 @@
import "./AppWindow.js"
import "./AppMenu.js"
import "./ProfileButton.js"
import "./InputBox.js"
import "./Sidebar.js"
class Home extends Shadow {
render() {
ZStack(() => {
img("/_/icons/logo.svg", "2.5em")
.position("fixed")
.left(3, em)
.top(3, vh)
.zIndex(3)
.onClick(() => {
window.navigateTo("/")
})
div()
.width(100, vw)
.height(100, vh)
.margin(0)
.backgroundImage("/_/images/the_return.webp")
.backgroundSize("cover")
.backgroundPosition("48% 65%")
.backgroundRepeat("no-repeat")
switch(window.location.pathname) {
case "/":
AppWindow()
AppMenu()
break
case "/app/jobs":
AppWindow("Jobs")
AppMenu("Jobs")
break;
case "/app/messages":
AppWindow("Messages")
AppMenu("Messages")
break;
case "/app/market":
AppWindow("Market")
AppMenu("Market")
break;
case "/app/forum":
AppWindow("Forum")
AppMenu("Forum")
break;
default:
throw new Error("Unknown route!")
}
HStack(() => {
ProfileButton()
.zIndex(1)
.cursor("default")
a("/signout", "Sign Out")
.background("transparent")
.border(window.location.pathname === "/" ? "1px solid var(--tan)" : "0.5px solid #bb7c36")
.color(window.location.pathname === "/" ? "var(--tan)" : "var(--accent)")
.borderRadius(5, px)
.onHover(function (hovering) {
if(hovering) {
this.style.background = "var(--green)"
} else {
this.style.background = ""
}
})
.onNavigate(function () {
if(window.location.pathname === "/") {
this.style.border = "1px solid var(--tan)"
this.style.color = "var(--tan)"
} else {
this.style.border = "0.5px solid #bb7c36"
this.style.color = "var(--accent)"
}
})
})
.gap(1, em)
.xRight(2, em).y(2.3, em)
.position("fixed")
.alignVertical("center")
})
}
}
register(Home)

View File

@@ -0,0 +1,54 @@
css(`
input-box {
display: block;
width: 60vw;
position: fixed;
left: 20vw;
bottom: 2vw;
}
.input {
width: 100%;
background-color: var(--accent2);
opacity: 0.5;
border-radius: 12px;
border: none;
resize: none;
color: var(--orange);
padding: 1em;
height: 1em;
outline: none;
transition: opacity .1s, scale .1s
}
.input:focus {
opacity: 80%;
scale: 1.02
}
`)
export default class InputBox extends HTMLElement {
hovered = false
connectedCallback() {
this.render()
this.addListeners()
}
render() {
this.innerHTML = /* html */`
<textarea class="input"></textarea>
`
}
addListeners() {
this.$("textarea").addEventListener("keydown", (e) => {
if(e.key === "Enter") {
e.preventDefault()
e.target.blur()
}
})
}
}
customElements.define("input-box", InputBox)

View File

@@ -0,0 +1,25 @@
class LoadingCircle extends Shadow {
render() {
div()
.borderRadius(100, pct)
.width(2, em).height(2, em)
.x(45, pct).y(50, pct)
.center()
.backgroundColor("var(--accent")
.transition("transform 1.75s ease-in-out")
.onAppear(function () {
let growing = true;
setInterval(() => {
if (growing) {
this.style.transform = "scale(1.5)";
} else {
this.style.transform = "scale(0.7)";
}
growing = !growing;
}, 750);
});
}
}
register(LoadingCircle)

View File

@@ -0,0 +1,43 @@
import "./ProfileMenu.js"
class ProfileButton extends Shadow {
async render() {
ZStack(async () => {
img("/_/icons/profile.svg", "1.5em", "1.5em")
.backgroundColor("var(--accent)")
.padding(0.2, em)
.borderRadius(5, px)
ProfileMenu()
})
.display("block")
.onAppear(() => {
window.addEventListener("mousedown", (e) => { // bad - adding every time it renders
if(!e.target.closest("profilebutton-")) {
this.$("profile-menu").style.display = "none"
}
})
})
.onHover((hovering, e) => {
console.log(hovering)
console.log(e.target)
if(hovering && !e.target.closest("profile-menu")) {
this.$("img").backgroundColor("var(--accent)")
this.$("img").style.outline = "1px solid black"
} else if(!e.target.closest("profile-menu")) {
this.$("img").backgroundColor("")
this.$("img").style.outline = ""
}
})
.onClick((done) => {
console.log(done)
if(done) {
this.$("profile-menu").style.display = ""
}
})
}
}
register(ProfileButton)

View File

@@ -0,0 +1,68 @@
class ProfileMenu extends Shadow {
render() {
VStack(() => {
h2("Profile")
HStack(() => {
p("Email: ")
.fontWeight("bold")
p(window.profile?.email)
})
.gap(1, em)
HStack(() => {
p("Name: ")
.fontWeight("bold")
p(window.profile?.name)
})
.gap(1, em)
p("X")
.onClick(() => {
this.style.display = "none"
})
.xRight(2, em).y(1, em)
})
.paddingLeft(1, em)
.color("var(--accent)")
.position("fixed")
.border("1px solid var(--accent)")
.x(50, vw).y(47, vh)
.width(70, vw)
.height(70, vh)
.backgroundColor("black")
.center()
.display("none")
.onAppear(async () => {
if(!window.profile) {
window.profile = await this.fetchProfile()
this.rerender()
}
})
}
async fetchProfile() {
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(profile);
return profile
} catch (err) {
console.error(err);
}
}
}
register(ProfileMenu, "profile-menu")

View File

@@ -0,0 +1,39 @@
css(`
side-bar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 16vw;
border-right: 0.5px solid var(--accent2);
display: flex;
flex-direction: column;
padding-top: 13vh;
}
side-bar button {
color: var(--darkbrown);
margin: 1.5em;
background-color: color-mix(in srgb, var(--accent2) 35%, var(--orange) 65%);
border: 1px solid var(--orange);
border-radius: 12px;
padding: 0.5em;
font-weight: bold;
}
`)
class Sidebar extends HTMLElement {
connectedCallback() {
this.render()
}
render() {
this.innerHTML = /* html */ `
<span id="title" style="position: absolute; left: 50%; transform: translateX(-50%) " class="link" onclick='window.location.href="/"'>hyperia</span>
<button>Main</button>
`
}
}
customElements.define("side-bar", Sidebar)

14
ui/desktop/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hyperia</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/_/icons/logo.svg">
<link rel="stylesheet" href="/_/code/shared.css">
<script src="/_/code/quill.js"></script>
<script src="/_/code/zod.js"></script>
<script type="module" src="75820185/index.js"></script>
</head>
<body>
</body>
</html>

8
ui/desktop/index.js Normal file
View File

@@ -0,0 +1,8 @@
import Socket from "./ws/Socket.js"
import "./components/Home.js"
import util from "./util.js"
window.util = util
window.Socket = new Socket()
Home()

9
ui/desktop/util.js Normal file
View File

@@ -0,0 +1,9 @@
export default class util {
static formatTime(str) {
const match = str.match(/-(\d+:\d+):\d+.*(am|pm)/i);
if (!match) return null;
const [_, hourMin, ampm] = match;
return hourMin + ampm.toLowerCase();
}
}

View File

@@ -0,0 +1,62 @@
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

45
ui/desktop/ws/Socket.js Normal file
View File

@@ -0,0 +1,45 @@
import Connection from "./Connection.js";
export default class Socket {
connection;
disabled = true;
requestID = 1;
pending = new Map();
constructor() {
this.connection = new Connection(this.receive);
}
isOpen() {
if(this.connection.checkOpen()) {
return true;
} else {
return false;
}
}
send(msg) {
return new Promise(resolve => {
const id = (++this.requestID).toString();
this.pending.set(id, resolve);
this.connection.send(JSON.stringify({ id, ...msg }));
});
}
receive = (event) => {
const msg = JSON.parse(event.data);
if (msg.id && this.pending.has(msg.id)) {
this.pending.get(msg.id)(msg);
this.pending.delete(msg.id);
return;
} else {
this.onBroadcast(msg)
}
}
onBroadcast(msg) {
window.dispatchEvent(new CustomEvent(msg.event, {
detail: msg.msg
}));
}
}

View File

@@ -0,0 +1,66 @@
import './ForumPanel.js'
css(`
forum- {
font-family: 'Bona';
}
forum- input::placeholder {
font-family: 'Bona Nova';
font-size: 0.9em;
color: var(--accent);
}
input[type="checkbox"] {
appearance: none; /* remove default style */
-webkit-appearance: none;
width: 1em;
height: 1em;
border: 1px solid var(--accent);
}
input[type="checkbox"]:checked {
background-color: var(--red);
}
`)
class Forum extends Shadow {
selectedForum = "HY"
render() {
ZStack(() => {
VStack(() => {
ForumPanel()
input("Message Hyperia", "98%")
.paddingVertical(1, em)
.paddingLeft(2, pct)
.color("var(--accent)")
.background("var(--darkbrown)")
.marginBottom(6, em)
.border("none")
.fontSize(1, em)
.onKeyDown(function (e) {
if (e.key === "Enter") {
window.Socket.send({app: "FORUM", operation: "SEND", msg: {forum: "HY", text: this.value }})
this.value = ""
}
})
})
.gap(0.5, em)
.width(100, pct)
.height(100, vh)
.alignHorizontal("center")
.alignVertical("end")
})
.onAppear(() => document.body.style.backgroundColor = "var(--darkbrown)")
.width(100, pct)
.height(100, pct)
}
}
register(Forum)

View File

@@ -0,0 +1,88 @@
import "../../components/LoadingCircle.js"
class ForumPanel extends Shadow {
forums = [
"HY"
]
messages = []
render() {
VStack(() => {
if(this.messages.length > 0) {
let previousDate = null
for(let i=0; i<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)

101
ui/mobile/apps/Jobs/Jobs.js Normal file
View File

@@ -0,0 +1,101 @@
import "./JobsSidebar.js"
import "./JobsGrid.js"
css(`
jobs- {
font-family: 'Bona';
}
jobs- input::placeholder {
font-family: 'Bona Nova';
font-size: 0.9em;
color: var(--accent);
}
input[type="checkbox"] {
appearance: none; /* remove default style */
-webkit-appearance: none;
width: 1em;
height: 1em;
border: 1px solid var(--accent);
}
input[type="checkbox"]:checked {
background-color: var(--red);
}
`)
class Jobs extends Shadow {
jobs = [
{
title: "Austin Chapter Lead",
salary: "1% of Local Revenue",
company: "Hyperia",
city: "Austin",
state: "TX"
}
]
render() {
ZStack(() => {
HStack(() => {
JobsSidebar()
JobsGrid(this.jobs)
})
.width(100, "%")
.x(0).y(13, vh)
HStack(() => {
input("Search jobs... (Coming Soon!)", "45vw")
.attr({
"type": "text",
"disabled": "true"
})
.fontSize(1.1, em)
.paddingLeft(1.3, em)
.background("transparent")
.border("0.5px solid var(--divider)")
.outline("none")
.color("var(--accent)")
.opacity(0.5)
.borderRadius(10, px)
.background("grey")
.cursor("not-allowed")
button("+ Add Job")
.width(7, em)
.marginLeft(1, em)
.borderRadius(10, px)
.background("transparent")
.border("0.3px solid var(--accent2)")
.color("var(--accent)")
.fontFamily("Bona Nova")
.onHover(function (hovering) {
if(hovering) {
this.style.background = "var(--green)"
} else {
this.style.background = "transparent"
}
})
.onClick((clicking) => {
console.log(this, "clicked")
})
})
.x(55, vw).y(4, vh)
.position("absolute")
.transform("translateX(-50%)")
})
.width(100, "%")
.height(100, "%")
}
connectedCallback() {
// Optional additional logic
}
}
register(Jobs)

View File

@@ -0,0 +1,60 @@
class JobsGrid extends Shadow {
jobs;
constructor(jobs) {
super()
this.jobs = jobs
}
boldUntilFirstSpace(text) {
const index = text.indexOf(' ');
if (index === -1) {
// No spaces — bold the whole thing
return `<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)

View File

@@ -0,0 +1,26 @@
class JobsSidebar extends Shadow {
render() {
VStack(() => {
h3("Location")
.color("var(--accent2)")
.marginBottom(0, em)
HStack(() => {
input("Location", "100%")
.paddingLeft(3, em)
.paddingVertical(0.75, em)
.backgroundImage("/_/icons/locationPin.svg")
.backgroundRepeat("no-repeat")
.backgroundSize("18px 18px")
.backgroundPosition("10px center")
})
})
.paddingTop(1, em)
.paddingLeft(3, em)
.paddingRight(3, em)
.gap(1, em)
.minWidth(10, vw)
}
}
register(JobsSidebar)

View File

@@ -0,0 +1,105 @@
import "./MarketSidebar.js"
import "./MarketGrid.js"
css(`
market- {
font-family: 'Bona';
}
market- input::placeholder {
font-family: 'Bona Nova';
font-size: 0.9em;
color: var(--accent);
}
input[type="checkbox"] {
appearance: none; /* remove default style */
-webkit-appearance: none;
width: 1em;
height: 1em;
border: 1px solid var(--accent);
}
input[type="checkbox"]:checked {
background-color: var(--red);
}
`)
class Market extends Shadow {
listings = [
{
title: "Shield Lapel Pin",
stars: "5",
reviews: 1,
price: "$12",
company: "Hyperia",
type: "new",
image: "/db/images/1",
madeIn: "America"
}
]
render() {
ZStack(() => {
HStack(() => {
MarketSidebar()
MarketGrid(this.listings)
})
.width(100, "%")
.x(0).y(13, vh)
HStack(() => {
input("Search for products... (Coming Soon!)", "45vw")
.attr({
"type": "text",
"disabled": "true"
})
.fontSize(1.1, em)
.paddingLeft(1.3, em)
.background("transparent")
.border("0.5px solid var(--divider)")
.outline("none")
.color("var(--accent)")
.opacity(0.5)
.borderRadius(10, px)
.background("grey")
.cursor("not-allowed")
button("+ Add Item")
.width(7, em)
.marginLeft(1, em)
.borderRadius(10, px)
.background("transparent")
.border("0.5px solid var(--accent2)")
.color("var(--accent)")
.fontFamily("Bona Nova")
.onHover(function (hovering) {
if(hovering) {
this.style.background = "var(--green)"
} else {
this.style.background = "transparent"
}
})
.onClick((clicking) => {
console.log(this, "clicked")
})
})
.x(55, vw).y(4, vh)
.position("absolute")
.transform("translateX(-50%)")
})
.width(100, "%")
.height(100, "%")
}
connectedCallback() {
// Optional additional logic
}
}
register(Market)

View File

@@ -0,0 +1,140 @@
class MarketGrid extends Shadow {
listings;
constructor(listings) {
super()
this.listings = listings
}
boldUntilFirstSpace(text) {
if(!text) return
const index = text.indexOf(' ');
if (index === -1) {
// No spaces — bold the whole thing
return `<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 hyperiaMade = params.get("hyperia-made") === "true";
const americaMade = params.get("america-made") === "true";
const newItem = params.get("new") === "true";
const usedItem = params.get("used") === "true";
let filtered = this.listings;
if (hyperiaMade) {
filtered = filtered.filter(item => item.madeIn === "Hyperia");
}
if (americaMade) {
filtered = filtered.filter(item => item.madeIn === "America");
}
if (newItem) {
filtered = filtered.filter(item => item.type === "new");
}
if (usedItem) {
filtered = filtered.filter(item => item.type === "used");
}
for (let i = 0; i < filtered.length; i++) {
const rating = filtered[i].stars
const percent = (rating / 5)
VStack(() => {
img(filtered[i].image)
.marginBottom(0.5, em)
p(filtered[i].company)
.marginBottom(0.5, em)
p(filtered[i].title)
.fontSize(1.2, em)
.fontWeight("bold")
.marginBottom(0.5, em)
HStack(() => {
p(filtered[i].stars)
.marginRight(0.2, em)
ZStack(() => {
div("★★★★★") // Empty stars (background)
.color("#ccc")
div("★★★★★") // Filled stars (foreground, clipped by width)
.color("#ffa500")
.position("absolute")
.top(0)
.left(0)
.whiteSpace("nowrap")
.overflow("hidden")
.width(percent * 5, em)
})
.display("inline-block")
.position("relative")
.fontSize(1.2, em)
.lineHeight(1)
p(filtered[i].reviews)
.marginLeft(0.2, em)
})
.marginBottom(0.5, em)
p(filtered[i].price)
.fontSize(1.75, em)
.marginBottom(0.5, em)
button("Coming Soon!")
.onClick((finished) => {
if(finished) {
}
})
.onHover(function (hovering) {
if(hovering) {
this.style.backgroundColor = "var(--green)"
} else {
this.style.backgroundColor = ""
}
})
})
.padding(1, em)
.border("1px solid var(--accent2)")
.borderRadius(5, "px")
}
})
.display("grid")
.gridTemplateColumns("repeat(auto-fill, minmax(250px, 1fr))")
.gap(1, em)
} else {
p("No Listings!")
}
})
.onQueryChanged(() => {
console.log("query did change yup")
this.rerender()
})
.height(100, vh)
.paddingLeft(2, em)
.paddingRight(2, em)
.gap(0, em)
.width(100, "%")
}
}
register(MarketGrid)

View File

@@ -0,0 +1,85 @@
class MarketSidebar extends Shadow {
handleChecked(e) {
let checked = e.target.checked
let label = $(`label[for="${e.target.id}"]`).innerText
if(checked) {
window.setQuery(label.toLowerCase(), true)
} else {
window.setQuery(label.toLowerCase(), null)
}
}
render() {
VStack(() => {
p("Make")
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "hyperia-check"
})
.onChange(this.handleChecked)
label("Hyperia-Made")
.attr({
"for": "hyperia-check"
})
.marginLeft(0.5, em)
})
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "america-check"
})
.onChange(this.handleChecked)
label("America-Made")
.attr({
"for": "america-check"
})
.marginLeft(0.5, em)
})
p("Condition")
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "new-check"
})
.onChange(this.handleChecked)
label("New")
.attr({
"for": "new-check"
})
.marginLeft(0.5, em)
})
HStack(() => {
input()
.attr({
"type": "checkbox",
"id": "used-check"
})
.onChange(this.handleChecked)
label("Used")
.attr({
"for": "used-check"
})
.marginLeft(0.5, em)
})
})
.paddingTop(12, vh)
.paddingLeft(3, em)
.paddingRight(3, em)
.gap(1, em)
.minWidth(10, vw)
.userSelect('none')
}
}
register(MarketSidebar)

View File

@@ -0,0 +1,188 @@
import "./MessagesSidebar.js"
import "./MessagesPanel.js"
css(`
messages- {
font-family: 'Bona';
}
messages- input::placeholder {
font-family: 'Bona Nova';
font-size: 0.9em;
color: var(--accent);
}
input[type="checkbox"] {
appearance: none; /* remove default style */
-webkit-appearance: none;
width: 1em;
height: 1em;
border: 1px solid var(--accent);
}
input[type="checkbox"]:checked {
background-color: var(--red);
}
`)
class Messages extends Shadow {
conversations = []
selectedConvoID = null
onConversationSelect(i) {
console.log("convo selected: ", i)
this.selectedConvoID = i
this.$("messagessidebar-").rerender()
this.$("messagespanel-").rerender()
}
getConvoFromID(id) {
for(let i=0; i<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)
.alignHorizontal("center")
.alignVertical("end")
})
.onAppear(async () => {
let res = await Socket.send({app: "MESSAGES", operation: "GET"})
if(!res) console.error("failed to get messages")
if(res.msg.length > 0 && this.conversations.length === 0) {
this.conversations = res.msg
this.selectedConvoID = this.conversations[0].id
this.rerender()
}
window.addEventListener("new-message", (e) => {
let convoID = e.detail.conversationID
let messages = e.detail.messages
let convo = this.getConvoFromID(convoID)
convo.messages = messages
this.rerender()
})
})
.width(100, "%")
.height(87, vh)
.x(0).y(13, vh)
VStack(() => {
p("Add Message")
input("enter email...")
.color("var(--accent)")
.onKeyDown(function (e) {
if (e.key === "Enter") {
window.Socket.send({app: "MESSAGES", operation: "ADDCONVERSATION", msg: {email: this.value }})
this.value = ""
}
})
p("x")
.onClick(function (done) {
if(done) {
this.parentElement.style.display = "none"
}
})
.xRight(2, em).y(2, em)
.fontSize(1.4, em)
.cursor("pointer")
})
.gap(1, em)
.alignVertical("center")
.alignHorizontal("center")
.backgroundColor("black")
.border("1px solid var(--accent)")
.position("fixed")
.x(50, vw).y(50, vh)
.center()
.width(60, vw)
.height(60, vh)
.display("none")
.attr({id: "addPanel"})
HStack(() => {
input("Search messages... (Coming Soon!)", "45vw")
.attr({
"type": "text",
"disabled": "true"
})
.fontSize(1.1, em)
.paddingLeft(1.3, em)
.background("transparent")
.border("0.5px solid var(--divider)")
.outline("none")
.color("var(--accent)")
.opacity(0.5)
.borderRadius(10, px)
.background("grey")
.cursor("not-allowed")
button("+ New Message")
.width(13, em)
.marginLeft(1, em)
.borderRadius(10, px)
.background("transparent")
.border("0.5px solid var(--divider)")
.color("var(--accent)")
.fontFamily("Bona Nova")
.onHover(function (hovering) {
if(hovering) {
this.style.background = "var(--green)"
} else {
this.style.background = "transparent"
}
})
.onClick((done) => {
console.log("click")
if(done) {
this.$("#addPanel").style.display = "flex"
}
console.log(this, "clicked")
})
})
.x(55, vw).y(4, vh)
.position("absolute")
.transform("translateX(-50%)")
})
.width(100, "%")
.height(100, "%")
}
}
register(Messages)

View File

@@ -0,0 +1,56 @@
import "../../components/LoadingCircle.js"
class MessagesPanel extends Shadow {
messages
constructor(messages) {
super()
this.messages = messages
}
render() {
VStack(() => {
if(this.messages) {
for(let i=0; i<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)

Some files were not shown because too many files have changed in this diff Show More