Compare commits

...

35 Commits

Author SHA1 Message Date
metacryst
2faa9e740e xcode changes for release 2026-03-27 06:03:28 -05:00
metacryst
a589977015 notifications setting 2026-03-26 07:44:51 -05:00
metacryst
06e2fabe81 push notifications working 2026-03-26 05:56:14 -05:00
metacryst
d107d68bcc push notifications working, server functions coming from backend 2026-03-26 02:32:59 -05:00
metacryst
472e69d3c0 adding error handling for signup, moving delete account button in sidebar to bottom 2026-03-25 23:41:45 -05:00
1c61a4d202 Delete account in sidebar 2026-03-24 16:39:53 -04:00
40b0855ca5 EnterCode Signup working
- After fetching join code in EnterCode(), it sets networkId attribute on "signup-" (Signup.js)
- Signup.js then includes the attribute in the call body
- Modified all calls to /signout, /profile, /login, /signup to be prefixed by '/auth'
- Added '/auth' to vite config file
- Modified final "else if" statement in .attr in quill.js to return `this.getAttribute(arg1)` instead of `this.getAttribute("")`
2026-03-24 15:48:23 -04:00
124066da59 Apps load from server
- Deleted /Jobs, /Announcements, /Events, /People
- Commented out Forum and ForumPanel
- Deleted /components/SearchBar, /components/LoadingCircle, /components/AddButton
2026-03-24 14:51:44 -04:00
metacryst
f3aceb69af accepting code for auth page 2026-03-24 10:39:57 -05:00
metacryst
a87d521a4f improving authpage 2026-03-24 06:07:29 -05:00
metacryst
c5b71add07 custom app rendering 2026-03-24 04:16:00 -05:00
metacryst
881c9408b6 new announcements page, adding searchbar and message input. 2026-03-23 05:26:55 -05:00
metacryst
35f0fe3654 smaller font for header, announcements full size always 2026-03-21 06:54:53 -05:00
metacryst
21b7b0a252 Sidebar fully functional 2026-03-21 03:10:50 -05:00
metacryst
1c6f12c210 preliminary sidebar fixes 2026-03-20 17:16:05 -05:00
56f7c7d3a3 Sidebar + Profile changes
- Sidebar displays profile photo and name, can be tapped on
- Profile now has "Tap to edit" button on photo
2026-03-20 14:13:14 -04:00
c7ddb02ac1 Sidebar content moved to new draggable sidebar 2026-03-20 13:43:11 -04:00
63fbab34ce Announcements page
- Updated handlers.js
- Repurposed Forum and ForumPanel for Announcements
- Adjusted styling
- Commented out long drag logs
2026-03-20 12:36:23 -04:00
metacryst
8fad5d7717 fix events icon, misc errors, new sidebar started 2026-03-20 09:27:56 -05:00
metacryst
41a9c9d269 adding state() to quill, after-request error handling to login 2026-03-20 00:36:33 -05:00
dd1ec2c374 Upload image
- Image upload works on profile
- added multer into package.json for handling image files
- files are saved under /db/images/users/user-id/profile.ext
2026-03-19 20:25:23 -04:00
58589c56dd Add event + add job form
- Modified handlers to catch errors
- Added placeholder "No location added", etc. messages to Job/Event cards
- Added EventForm.js and JobForm.js for adding
- EventForm and JobForm are animated to slide up from bottom
- Modified openProfile/closeProfile logic
- Fixed SidebarItem().onClick() firing twice bug (switched to .onTap)
- Profile is now animated to slide up from the bottom
2026-03-19 15:32:51 -04:00
metacryst
8dd2312aa0 fixing logout, people not scrolling, sidebar being too short 2026-03-19 08:54:53 -05:00
metacryst
5a56dfa051 improving styling, fixing bugs with profile, login error handling 2026-03-19 07:41:18 -05:00
3a5214ed45 Added missing Profile element
- User's "joined" info
2026-03-19 00:19:41 -04:00
72f0518f9d Profile + Edit Bio + Logout + styling
- Added handler for editing bio to handlers.js
- Added openProfile() and closeProfile() buttons in AppWindowContainer (Profile page is global)
- Added Logout and Profile functionality to Sidebar.js
  - SidebarItem(text).onClick() fires twice, unable to resolve
- Adjust Login page styling
- Added onLogout() to index.js (removes auth_token)
- Added Profile.js, displays user's profile picture (placeholder), name, and bio. User can edit bio.
- Added removeAuthToken() to util.js
- Added /signout to vite config
2026-03-19 00:14:29 -04:00
ede464fb0d Search in Events/Jobs
- SearchBar now dispatches 'jobsearch' or 'eventsearch' event whenever the user submits a search query
- Jobs/Events will then receive searchText to do general search
- Fixed bug where Jobs/Events wouldn't scroll anymore
2026-03-18 20:11:08 -04:00
2082e0c7bc Signup/Login + styling adjustments
- Modified SearchBar styling
- Modified TopBar to display blank circle if the user has no networks (previously missing image icon)
- Refactored Login into AuthPage.js
- AuthPage contains a tab selector for switching between Signup and Login
- Both Login/Signup send the request and either receive an auth_token or an error message
- If auth_token, user will be logged in as usual, in both cases
- Signup validates user input before sending request
- Added /signup target in vite config file
2026-03-18 17:36:03 -04:00
metacryst
d1e4814593 successfully connecting to prod 2026-03-17 07:00:27 -05:00
metacryst
530ea7da89 app icon, styling, light mode, top bar component 2026-03-17 05:33:28 -05:00
metacryst
5903bafee5 better styling 2026-03-16 23:55:56 -05:00
8452841460 Styled Jobs and Events like Figma mockups
- Modified some darkmode css values to match those on the figma
- Fixed misnamed calls to var(--darkaccent) from var(--accentdark)
- Commented out trash/delete button on EventCard and JobCard for now
- Hid scrollbar on Events/Jobs
- Fixed mis-centered People list
- Fixed colors in searchbar in events/jobs
2026-03-16 21:40:03 -04:00
metacryst
69b359d9a1 making login work for prod 2026-03-16 07:56:40 -05:00
metacryst
a626abe1c3 improve styling, fix bottom bar underline bug 2026-03-16 01:09:48 -05:00
834d5e763e Connected DB to events/jobs + more
- Modified handlers.js to be the same as on frm.so
- Added --trash-src to shared.css
- Modified Event and Job cards to include a trash icon for deleting
- Deleting works, it just does not smoothly re-render yet
- Adjusted visual bug on Events/Jobs where the contents of the AppWindow would overflow vertically. They now scroll and the title/search bar remain fixed.
- Refactored part of People.js into PeopleCard.js
2026-03-15 19:45:04 -04:00
59 changed files with 1882 additions and 1294 deletions

View File

@@ -2,13 +2,13 @@
"appId": "so.forum.app",
"appName": "Forum",
"webDir": "dist",
"ios": {
"allowsBackForwardNavigationGestures": true
},
"server": {
"url": "http://sam.local:5173",
"cleartext": true
},
"ios": {
"allowsBackForwardNavigationGestures": true
},
"plugins": {
"SplashScreen": {
"launchAutoHide": true

View File

@@ -18,6 +18,7 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
22352DD22F74F93C0052EF07 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -73,6 +74,7 @@
504EC3061FED79650016851F /* App */ = {
isa = PBXGroup;
children = (
22352DD22F74F93C0052EF07 /* App.entitlements */,
50379B222058CBB4000EE86E /* capacitor.config.json */,
504EC3071FED79650016851F /* AppDelegate.swift */,
504EC30B1FED79650016851F /* Main.storyboard */,
@@ -361,15 +363,16 @@
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 53DK57C7ZF;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.0.5;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = so.hyperia.app;
PRODUCT_BUNDLE_IDENTIFIER = russell.sam.forum;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
@@ -382,14 +385,15 @@
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 53DK57C7ZF;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = so.hyperia.app;
MARKETING_VERSION = 1.0.5;
PRODUCT_BUNDLE_IDENTIFIER = russell.sam.forum;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
SWIFT_VERSION = 5.0;

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1640"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "504EC3031FED79650016851F"
BuildableName = "App.app"
BlueprintName = "App"
ReferencedContainer = "container:App.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "504EC3031FED79650016851F"
BuildableName = "App.app"
BlueprintName = "App"
ReferencedContainer = "container:App.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "504EC3031FED79650016851F"
BuildableName = "App.app"
BlueprintName = "App"
ReferencedContainer = "container:App.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>

View File

@@ -74,4 +74,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
}
}

View File

@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "\\.png",
"filename" : "Group 73 (6).png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,17 +1,17 @@
{
"images" : [
{
"filename" : "splash.png",
"filename" : "Group 74.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "splash 1.png",
"filename" : "Group 75.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "splash 2.png",
"filename" : "Group 76.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -24,11 +24,15 @@
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@@ -40,17 +44,20 @@
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIRequiresFullScreen</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>NSPhotoLibraryUsageDescription</key>
<string>Access your photos to set a profile picture and share with others</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Used to find keep local information relevant.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Used to find forums and communities near you</string>
</dict>
</plist>

View File

@@ -15,6 +15,8 @@ def capacitor_pods
pod 'CapacitorGeolocation', :path => '../../node_modules/@capacitor/geolocation'
pod 'CapacitorGoogleMaps', :path => '../../node_modules/@capacitor/google-maps'
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'
end

View File

@@ -13,6 +13,10 @@ PODS:
- GoogleMaps (~> 8.4)
- CapacitorHaptics (7.0.3):
- Capacitor
- CapacitorPreferences (7.0.4):
- Capacitor
- CapacitorPushNotifications (7.0.6):
- Capacitor
- CapacitorSplashScreen (7.0.3):
- Capacitor
- Google-Maps-iOS-Utils (5.0.0):
@@ -31,6 +35,8 @@ DEPENDENCIES:
- "CapacitorGeolocation (from `../../node_modules/@capacitor/geolocation`)"
- "CapacitorGoogleMaps (from `../../node_modules/@capacitor/google-maps`)"
- "CapacitorHaptics (from `../../node_modules/@capacitor/haptics`)"
- "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)"
- "CapacitorPushNotifications (from `../../node_modules/@capacitor/push-notifications`)"
- "CapacitorSplashScreen (from `../../node_modules/@capacitor/splash-screen`)"
SPEC REPOS:
@@ -52,6 +58,10 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@capacitor/google-maps"
CapacitorHaptics:
:path: "../../node_modules/@capacitor/haptics"
CapacitorPreferences:
:path: "../../node_modules/@capacitor/preferences"
CapacitorPushNotifications:
:path: "../../node_modules/@capacitor/push-notifications"
CapacitorSplashScreen:
:path: "../../node_modules/@capacitor/splash-screen"
@@ -62,11 +72,13 @@ SPEC CHECKSUMS:
CapacitorGeolocation: b96474c3259dd4a294227ea8ec19140b1837cceb
CapacitorGoogleMaps: 20b5445a532f80dbb120fa99941fd094bcc88af6
CapacitorHaptics: d17da7dd984cae34111b3f097ccd3e21f9feec62
CapacitorPreferences: d82a7e3b95fcab43a553268b803356522910d153
CapacitorPushNotifications: c6158ba6f3777f281a675aa43e4011e9723e822b
CapacitorSplashScreen: d06ae8804808e9f649a08e7bb7f283c77b688084
Google-Maps-iOS-Utils: 66d6de12be1ce6d3742a54661e7a79cb317a9321
GoogleMaps: 8939898920281c649150e0af74aa291c60f2e77d
IONGeolocationLib: 20f9d0248a0b5264511fb57a37e25dd2badf797a
PODFILE CHECKSUM: 5fb01647092bb2f97342e333eedc70aeba99b283
PODFILE CHECKSUM: 7fad0e16088b635c7bc1980fea245d4a60cc4bbe
COCOAPODS: 1.15.2

View File

@@ -19,6 +19,8 @@
"@capacitor/google-maps": "^7.2.0",
"@capacitor/haptics": "^7.0.3",
"@capacitor/ios": "^7.4.4",
"@capacitor/preferences": "^7.0.4",
"@capacitor/push-notifications": "^7.0.6",
"@capacitor/splash-screen": "^7.0.3"
},
"devDependencies": {

View File

@@ -1,21 +1,54 @@
### Run Web App
## Run in Browser
```npm run start```
### Build and Run iOS App
### Browser: Dev Frontend and Dev Backend (localhost)
This option should be at the top level of capacitor.config.json
"server": {
"url": "http://sam.local:5173",
"cleartext": true
},
### Browser: Prod Frontend and Prod Backend
Run:
vite build
npx serve dist
If you need to login again:
run localStorage.clear() in the browser dev tools console and then refresh the page.
## Run On Device
https://capacitorjs.com/docs/ios#adding-the-ios-platform
To Install:
One-Time Install:
npm install @capacitor/ios
npx cap add ios
To Open XCode:
npx cap open ios
To Rerun:
Run this command to rebuild for iOS
npm run build && npx cap copy ios
If getting black screen:
npx cap sync iOS
### iOS: Dev Frontend and Dev Backend (localhost)
This option should be at the top level of capacitor.config.json
"server": {
"url": "http://sam.local:5173",
"cleartext": true
},
### iOS: Dev Frontend with Prod Backend (frm.so)
Add "https://frm.so" to VITE_API_URL in .env.development
### iOS: Prod Frontend and Prod Backend (frm.so)
Remove the "server" object from capacitor.config
### Various Commands
npx cap config - this will list the full configuration currently being used
### Architecture
In Development, API routes are routed using the vite.config.js.
@@ -24,3 +57,8 @@ In Development, API routes are routed using the vite.config.js.
Background Color:
In src/manifest.json, "#31d53d" refers to the green color which is visible in the background in the web version. This is not visible in the built version.
Test Push Notifications:
https://icloud.developer.apple.com/dashboard/notifications/teams/53DK57C7ZF/app/russell.sam.forum/notifications/create?notificationId=8bb87cf2-9590-4a63-b7e1-e4c7f2a2c879&environment=DEVELOPMENT&notificationType=push
Note: Even if built in "production" mode, the tokens will still be considered "development" by Apple until the app is actually deployed

View File

@@ -1 +1 @@
VITE_API_URL=
VITE_API_URL=https://frm.so

View File

@@ -0,0 +1,106 @@
import { Preferences } from '@capacitor/preferences';
import "./Login.js";
import "./Signup.js"
import "./EnterCode.js"
class AuthPage extends Shadow {
inputStyles(el) {
return el
.background("var(--main)")
.color("var(--text)")
.border("1px solid var(--accent)")
.fontSize(0.9, rem)
.backgroundColor("var(--searchbackground)")
.borderRadius(12, px)
.outline("none")
.onTouch((start) => {
if(start) {
this.style.backgroundColor = "var(--accent)"
} else {
this.style.backgroundColor = "var(--searchbackground)"
}
})
}
render() {
VStack(() => {
img(window.matchMedia('(prefers-color-scheme: dark)').matches ? "/_/icons/columnwhite.svg" : "/_/icons/logo.svg", window.isMobile() ? "5vmax" : "3vmax")
.marginTop(5, em)
.marginLeft(2, em)
.onClick((done) => {
window.navigateTo("/")
})
HStack(() => {
p("Login")
.state(this, "selected", function (selected) {
if(selected === "1") {
this.fontWeight("bold")
this.background("var(--loginButton)")
} else {
this.fontWeight("normal")
this.background("transparent")
}
})
.padding(0.75, em)
.borderRadius(12, px)
.onTap(() => {
this.attr("selected", "1")
})
p("Enter Code")
.state(this, "selected", function (selected) {
if(selected === "2") {
this.fontWeight("bold")
this.background("var(--loginButton)")
} else {
this.fontWeight("normal")
this.background("transparent")
}
})
.padding(0.75, em)
.borderRadius(12, px)
.onTap(() => {
this.attr("selected", "2")
})
})
.fontFamily("Arial")
.padding(0.25, em)
.borderRadius(12, px)
.background("var(--loginBackground)")
.color("var(--text)")
.horizontalAlign("center")
.margin("auto")
.marginTop(7.5, em)
.marginBottom(2, em)
.gap(0.5, em)
ZStack(() => {
Login()
.state(this, "selected", function (selected) {
if(selected === "1") {
this.display("")
} else {
this.display("none")
}
})
EnterCode()
.state(this, "selected", function (selected) {
if(selected === "2") {
this.display("flex")
} else {
this.display("none")
}
})
})
})
.attr("selected", "1")
.width(100, vw)
.height(100, vh)
.margin(0)
}
}
register(AuthPage)

View File

@@ -0,0 +1,101 @@
import util from "../../util.js"
import "./Signup.js"
class EnterCode extends Shadow {
inputStyles(el) {
return el
.background("var(--main)")
.color("var(--text)")
.border("1px solid var(--accent)")
.fontSize(0.9, rem)
.backgroundColor("var(--searchbackground)")
.borderRadius(12, px)
.outline("none")
.onTouch((start) => {
if (start) {
this.style.backgroundColor = "var(--accent)"
} else {
this.style.backgroundColor = "var(--searchbackground)"
}
})
}
render() {
VStack(() => {
VStack(() => {
p("Enter the code given to you by your organization.")
.color("#614945")
.maxWidth(70, vw)
.marginTop(3, em)
input("Code", "70vw")
.attr({ name: "firstName", type: "text" })
.margin("auto")
.marginVertical(1, em)
.padding(1, em)
.styles(this.inputStyles)
button("==>")
.padding(1, em)
.fontSize(0.9, rem)
.borderRadius(12, px)
.background("var(--searchbackground)")
.color("var(--text)")
.border("1px solid var(--accent)")
.boxSizing("border-box")
.onTouch(function (start) {
if (start) {
this.style.backgroundColor = "var(--accent)"
} else {
this.style.backgroundColor = "var(--searchbackground)"
}
})
.onClick((done) => {
if (done) this.submit()
})
})
.state(this, "codeaccepted", function (accepted) {
if(!accepted) {
this.display("flex")
} else {
this.display("none")
}
})
Signup()
.state(this, "codeaccepted", function (accepted) {
if(accepted) {
this.display("")
} else {
this.display("none")
}
})
})
.horizontalAlign("center")
.display("flex")
this.style.display = "flex"
}
async submit() {
console.log("submit")
const res = await fetch(`${util.HOST}/auth/joincode`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', "Accept": "application/json", "X-Client": "mobile" },
body: JSON.stringify({ code: this.$("input").value })
});
if (res.ok) {
console.log("got join code succ")
this.attr("codeaccepted", "true")
let { networkId } = await res.json()
$("signup-").attr("networkid", networkId)
} else {
const { error } = await res.json();
console.error(error)
}
}
}
register(EnterCode)

147
src/Home/AuthPage/Login.js Normal file
View File

@@ -0,0 +1,147 @@
import { Preferences } from '@capacitor/preferences';
import util from "../../util.js"
class Login extends Shadow {
inputStyles(el) {
return el
.background("var(--main)")
.color("var(--text)")
.border("1px solid var(--accent)")
.fontSize(0.9, rem)
.backgroundColor("var(--searchbackground)")
.borderRadius(12, px)
.outline("none")
.onTouch((start) => {
if (start) {
this.style.backgroundColor = "var(--accent)"
} else {
this.style.backgroundColor = "var(--searchbackground)"
}
})
}
render() {
form(() => {
VStack(() => {
input("Email", "70vw")
.attr({ name: "email", type: "email" })
.margin("auto")
.marginVertical(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Password", "70vw")
.attr({ name: "password", type: "password" })
.margin("auto")
.marginVertical(1, em)
.padding(1, em)
.styles(this.inputStyles)
HStack(() => {
button("==>")
.padding(1, em)
.fontSize(0.9, rem)
.borderRadius(12, px)
.background("var(--searchbackground)")
.color("var(--text)")
.border("1px solid var(--accent)")
.boxSizing("border-box")
.onTouch(function (start) {
if (start) {
this.style.backgroundColor = "var(--accent)"
} else {
this.style.backgroundColor = "var(--searchbackground)"
}
})
})
.width(70, vw)
.margin("auto")
.fontSize(0.9, rem)
.paddingLeft(0, em)
.paddingRight(2, em)
.marginVertical(1, em)
.border("1px solid transparent")
p("")
.state("errortype", function (type) {
const messages = {
email: "Please enter a valid email.",
password: "Please enter a valid password.",
emailwrong: "Could not find an account with this email.",
passwordwrong: "Incorrect password.",
};
if(messages[type]) {
this.display("")
this.innerText = messages[type]
} else {
this.display("none")
this.innerText = ""
}
})
.margin("auto")
.marginTop(1, em)
.color("var(--text)")
.fontFamily("Arial")
.opacity(.7)
.padding(0.5, em)
.backgroundColor("var(--darkred)")
.display("none")
})
})
.height(100, pct)
.onSubmit(async (e) => {
e.preventDefault();
const data = {
email: e.target.$('[name="email"]').value,
password: e.target.$('[name="password"]').value,
};
await this.requestLogin(data);
})
}
isValidEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
isValidPassword = (password) => password.length >= 7;
async requestLogin(data) {
const emailValid = this.isValidEmail(data.email.trim() || "");
const passValid = this.isValidPassword(data.password.trim() || "");
if (!emailValid || !passValid) {
console.log("invalid", emailValid)
const errorType = !emailValid ? 'email' : 'password';
this.$("p").attr({ errorType });
return;
} else {
this.$("p").attr({ errorType: "" });
}
const res = await fetch(`${util.HOST}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Client": "mobile" },
body: JSON.stringify({
email: data["email"],
password: data["password"]
})
});
if (res.ok) {
const { token } = await res.json();
await Preferences.set({ key: 'auth_token', value: token });
global.renderHome();
} else {
const { error } = await res.json();
this.errorMessage = error;
console.error(error)
if(error.includes("email")) {
this.$("p").attr({ errorType: "emailwrong" });
} else {
this.$("p").attr({ errorType: "passwordwrong" });
}
}
}
}
register(Login)

210
src/Home/AuthPage/Signup.js Normal file
View File

@@ -0,0 +1,210 @@
import { Preferences } from '@capacitor/preferences';
import util from "../../util.js"
class Signup extends Shadow {
inputStyles(el) {
return el
.background("var(--main)")
.color("var(--text)")
.border("1px solid var(--accent)")
.fontSize(0.9, rem)
.backgroundColor("var(--searchbackground)")
.borderRadius(12, px)
.outline("none")
.onTouch((start) => {
if (start) {
this.style.backgroundColor = "var(--accent)"
} else {
this.style.backgroundColor = "var(--searchbackground)"
}
})
}
render() {
form(() => {
VStack(() => {
input("First Name", "70vw")
.attr({ name: "firstName", type: "text" })
.margin("auto")
.marginVertical(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Last Name", "70vw")
.attr({ name: "lastName", type: "text" })
.margin("auto")
.marginVertical(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Email", "70vw")
.attr({ name: "email", type: "email" })
.margin("auto")
.marginVertical(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Password", "70vw")
.attr({ name: "password", type: "password" })
.margin("auto")
.marginVertical(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Confirm Password", "70vw")
.attr({ name: "confirmPassword", type: "password" })
.margin("auto")
.marginVertical(1, em)
.padding(1, em)
.styles(this.inputStyles)
p("")
.state("errormessage", function (msg) {
if(msg) {
this.display("")
this.innerText = msg
} else {
this.display("none")
this.innerText = ""
}
})
.margin("auto")
.marginTop(1, em)
.color("var(--text)")
.fontFamily("Arial")
.opacity(.7)
.padding(0.5, em)
.backgroundColor("var(--darkred)")
.display("none")
HStack(() => {
button("==>")
.padding(1, em)
.fontSize(0.9, rem)
.borderRadius(12, px)
.background("var(--searchbackground)")
.color("var(--text)")
.border("1px solid var(--accent)")
.boxSizing("border-box")
.onTouch(function (start) {
if (start) {
this.style.backgroundColor = "var(--accent)"
} else {
this.style.backgroundColor = "var(--searchbackground)"
}
})
})
.width(70, vw)
.margin("auto")
.fontSize(0.9, rem)
.paddingLeft(0, em)
.paddingRight(2, em)
.marginVertical(1, em)
.marginBottom(10, em)
.border("1px solid transparent")
})
})
.height(100, pct)
.onSubmit(async (e) => {
e.preventDefault();
const data = new FormData(e.target);
if (this.verifyInput(data)) {
this.errorMessage = "";
await this.requestSignup(data);
} else {
console.log(this.errorMessage)
}
})
}
async requestSignup(data) {
const networkId = this.attr("networkid");
const res = await fetch(`${util.HOST}/auth/signup`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Client": "mobile" },
body: JSON.stringify({
networkId: networkId,
firstName: data.get("firstName"),
lastName: data.get("lastName"),
email: data.get("email"),
password: data.get("password")
})
});
if (res.ok) {
const { token } = await res.json();
await Preferences.set({ key: 'auth_token', value: token });
global.renderHome();
} else {
const { error } = await res.json();
console.error(error)
this.$("p").attr("errormessage", error)
}
}
verifyInput(data) {
const firstName = data.get("firstName");
const lastName = data.get("lastName");
const email = data.get("email");
const password = data.get("password");
const confirmPassword = data.get("confirmPassword");
if (!firstName || firstName.trim() === "") {
this.$("p").attr("errormessage", "First name is required.")
return false
} else if (firstName.trim().length < 2) {
this.$("p").attr("errormessage", "First name must be at least 2 characters.")
return false
} else if (!/^[a-zA-Z\s'-]+$/.test(firstName.trim())) {
this.$("p").attr("errormessage", "First name contains invalid characters.")
return false
}
if (!lastName || lastName.trim() === "") {
this.$("p").attr("errormessage", "Last name is required.")
return false
} else if (lastName.trim().length < 2) {
this.$("p").attr("errormessage", "Last name must be at least 2 characters.")
return false
} else if (!/^[a-zA-Z\s'-]+$/.test(lastName.trim())) {
this.$("p").attr("errormessage", "Last name contains invalid characters.")
return false
}
if (!email || email.trim() === "") {
this.$("p").attr("errormessage", "Email is required.")
return false
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
this.$("p").attr("errormessage", "Please enter a valid email address.")
return false
}
let passwordError = `Password must be at least 7 characters and include an uppercase letter, a lowercase letter, a number, and a special character (!@#$%^&*(),.?":{}|<>)`
if (!password) {
this.$("p").attr("errormessage", "Password is required.")
return false
} else if (password.length < 7) {
this.$("p").attr("errormessage", passwordError)
return false
} else if (!/[A-Z]/.test(password)) {
this.$("p").attr("errormessage", passwordError)
return false
} else if (!/[a-z]/.test(password)) {
this.$("p").attr("errormessage", passwordError)
return false
} else if (!/[0-9]/.test(password)) {
this.$("p").attr("errormessage", passwordError)
return false
} else if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
this.$("p").attr("errormessage", passwordError)
return false
}
if (!confirmPassword) {
this.$("p").attr("errormessage", "Please confirm your password.")
return false
} else if (confirmPassword !== password) {
this.$("p").attr("errormessage", "Passwords do not match.")
return false
}
return true;
}
}
register(Signup)

View File

@@ -1,41 +1,152 @@
import "../components/Sidebar.js"
import "../components/AppMenu.js"
import "../components/AppWindow.js"
import "../components/AppWindowContainer.js"
css(`
#homeContainer {
-webkit-user-select: none;
}
`)
/*
Sidebar Functionality Checklist:
- Open on Top left network logo touch (WITH transition)
- Follow finger on swipe from left side of the screen
- Open if finger swipw travels far enough to the right (WITH velocity-based transition)
- Re-close if not opened enough of the way (WITH transition)
- Close on touch of home contents (WITH transition)
- Follow finger on swipe beginning near or anywhere on right of divider between sidebar and home contents
- Close if finger swipe travels far enough to the left (WITH velocity-based transition)
- Re-open if not closed enough of the way (WITH transition)
*/
class Home extends Shadow {
dragStartX = null
sidebarOpen = false
SIDEBAR_FULL_OPEN = (window.outerWidth * 5) / 6
SIDEBAR_START_THRESHOLD = window.outerWidth / 10
SIDEBAR_CLOSE_DECISION = (window.outerWidth * 2) / 3
SIDEBAR_OPEN_DECISION = (window.outerWidth / 3)
constructor() {
super()
}
render() {
ZStack(() => {
Sidebar(this.SIDEBAR_FULL_OPEN)
img("/_/icons/hamburger.svg", "3em")
.position("absolute")
.zIndex(2)
.left(1.5, em)
.top(1, em)
.onTouch(function (start) {
if(start) {
this.style.scale = "0.8"
} else if(start === false) {
this.style.scale = ""
$("sidebar-").toggle()
}
})
Sidebar()
ZStack(() => {
VStack(() => {
AppWindow()
AppWindowContainer()
AppMenu()
})
.height(100, pct)
.minHeight(0)
})
.left(0, px)
.backgroundColor("var(--main)")
.overflowX("hidden")
.height(window.visualViewport.height - 20, px)
.height(window.visualViewport.height, px)
.position("fixed")
.top(20, px)
.borderLeft("1px solid var(--accent)")
.onTouch((start, e) => {
if(this.sidebarOpen) {
this.sidebarOpenTouch(start, e)
} else {
this.sidebarClosedTouch(start, e)
}
})
.attr({ id: "homeContainer" })
})
.userSelect("none")
}
openSidebar(duration = 200) {
const home = this.$("#homeContainer");
home.style.transition = `left ${duration}ms`;
home.style.left = `${this.SIDEBAR_FULL_OPEN}px`;
this.sidebarOpen = true;
this.dragStartX = null
setTimeout(() => home.style.transition = "", duration);
}
closeSidebar(duration = 200) {
const home = this.$("#homeContainer");
home.style.transition = `left ${duration}ms`;
home.style.left = "0px";
this.sidebarOpen = false;
this.dragStartX = null
setTimeout(() => home.style.transition = "", duration);
}
sidebarOpenTouch(start, e) {
if(start) {
let amount = e.targetTouches[0].clientX
if(amount > (this.SIDEBAR_FULL_OPEN - this.SIDEBAR_START_THRESHOLD)) {
this.dragStartX = e.touches[0].clientX
this.dragStartTime = Date.now(); // ⬅ track start time
document.addEventListener("touchmove", this.moveSidebar)
}
} else {
if(!this.dragStartX) return;
let endX = e.changedTouches[0].clientX
let duration = this.getDuration(this.dragStartX, endX);
if(Math.abs(this.dragStartX - endX) < 5) { // 2 conditions are separated so this one doesn't close instantly
this.closeSidebar()
} else if(endX < this.SIDEBAR_CLOSE_DECISION) {
this.closeSidebar(duration)
} else {
this.openSidebar(duration)
}
document.removeEventListener("touchmove", this.moveSidebar)
}
}
sidebarClosedTouch(start, e) {
if(start) {
let amount = e.targetTouches[0].clientX
if(amount < this.SIDEBAR_START_THRESHOLD) {
this.dragStartX = e.touches[0].clientX
this.dragStartTime = Date.now();
document.addEventListener("touchmove", this.moveSidebar)
}
} else {
if(!this.dragStartX) return;
let endX = e.changedTouches[0].clientX
let duration = this.getDuration(this.dragStartX, endX);
if(endX > this.SIDEBAR_OPEN_DECISION) {
this.openSidebar(duration)
} else {
this.closeSidebar(duration)
}
document.removeEventListener("touchmove", this.moveSidebar)
}
}
moveSidebar = (e) => {
let amount = e.targetTouches[0].clientX
if(e.targetTouches[0] && amount < this.SIDEBAR_FULL_OPEN) {
this.$("#homeContainer").style.left = `${amount}px`
}
}
getDuration(startX, endX) {
const distance = Math.abs(endX - startX);
const elapsed = Date.now() - this.dragStartTime;
const velocity = distance / elapsed; // px per ms
const duration = Math.round(distance / velocity); // time to cover remaining distance
return Math.min(Math.max(duration, 50), 200); // clamp between 50ms and 200ms
}
}

View File

@@ -1,63 +0,0 @@
const env = import.meta.env
class Login extends Shadow {
inputStyles(el) {
return el
.background("var(--main)")
.color("var(--text)")
.border("1px solid var(--accent)")
.fontSize(0.9, rem)
.backgroundColor("var(--accentdark)")
.borderRadius(12, px)
.outline("none")
.onTouch((start) => {
if(start) {
this.style.backgroundColor = "var(--accent)"
} else {
this.style.backgroundColor = "var(--accentdark)"
}
})
}
render() {
ZStack(() => {
img(window.matchMedia('(prefers-color-scheme: dark)') ? "/_/icons/columnwhite.svg" : "/_/icons/logo.svg", window.isMobile() ? "5vmax" : "3vmax")
.position("absolute")
.top(5, em)
.left(2, em)
.onClick((done) => {
window.navigateTo("/")
})
form(() => {
input("Email", "70vw")
.attr({name: "email", type: "email"})
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Password", "70vw")
.attr({name: "password", type: "password"})
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
button("==>")
.margin(1, em)
.padding(1, em)
.fontSize(0.9, rem)
.borderRadius(12, px)
.background("var(--accent)")
.color("var(--text)")
.border("1px solid var(--accent)")
})
.attr({action: `${env.VITE_API_URL}/login`, method: "POST"})
.x(50, vw).y(50, vh)
.center()
})
.background("var(--main)")
.width(100, vw)
.height(100, pct)
.margin(0)
}
}
register(Login)

222
src/Profile/Profile.js Normal file
View File

@@ -0,0 +1,222 @@
import util from "../util";
css(`
profile- textarea::-webkit-scrollbar {
display: none;
width: 0px;
height: 0px;
}
profile- textarea::-webkit-scrollbar-thumb {
background: transparent;
}
profile- textarea::-webkit-scrollbar-track {
background: transparent;
}
`)
class Profile extends Shadow {
constructor() {
super()
this.profile = global.profile
this.bioText = global.profile.bio ?? ""
}
render() {
ZStack(() => {
div("➩")
.width(3, rem)
.height(3, rem)
.borderRadius(50, pct)
.border("1.5px solid var(--divider)")
.position("absolute")
.fontSize(2, em)
.transform("rotate(180deg)")
.top(3, rem)
.left(2, rem)
.zIndex(1001)
.display("flex")
.alignItems("center")
.justifyContent("center")
.transition("scale .2s")
.state("touched", function (touched) {
if(touched) {
this.scale("1.5")
this.color("var(--darkaccent)")
this.backgroundColor("var(--divider)")
} else {
this.scale("")
this.color("var(--divider)")
this.backgroundColor("var(--darkaccent)")
}
})
.onTouch(function (start) {
if(start) {
this.attr({touched: "true"})
} else {
this.attr({touched: ""})
}
})
.onClick((done) => {
if(done)
$("appwindowcontainer-").closeProfile()
})
form(() => {
input("Image Upload", "0px", "0px")
.attr({ name: "image-upload", type: "file" })
.display("none")
.visibility("hidden")
.onChange((e) => {
this.handleUpload(e.target.files[0]);
})
VStack(() => {
HStack(() => {
if (global.profile.image_path) {
img(`${util.HOST}${global.profile.image_path}`, "10em", "10em")
.borderRadius(100, pct)
}
})
.boxSizing("border-box")
.height(10, em)
.width(10, em)
.border("1px solid var(--accent)")
.borderRadius(100, pct)
.background("var(--darkaccent)")
.onTap(() => {
const inputSelector = this.$('[name="image-upload"]');
inputSelector.click()
})
p("Tap to edit")
.color("var(--headertext)")
.opacity(0.5)
.marginTop(0.5, em)
h1(this.profile.first_name + " " + this.profile.last_name)
.color("var(--headertext")
.width(70, pct)
.marginVertical(0.25, em)
.textAlign("center")
p("Joined " + this.convertDate(this.profile.created))
.color("var(--headertext)")
.marginBottom(0.5, em)
h2("Bio")
.color("var(--headertext")
.margin(0)
.paddingVertical(0.9, em)
.borderTop("2px solid var(--divider)")
.width(70, pct)
.textAlign("center")
textarea(this.bioText ? this.bioText : "Tap to start typing...")
.attr({ name: "bioinput" })
.padding(1, em)
.width(90, pct)
.height(15, em)
.boxSizing("border-box")
.background("var(--searchbackground)")
.color("var(--darktext)")
.border("1px solid color-mix(in srgb, var(--accent) 60%, transparent)")
.borderRadius(12, px)
.fontFamily("Arial")
.fontSize(1.1, em)
.outline("none")
.onAppear((e) => {
if (this.bioText) {
$("profile- textarea").innerText = this.bioText
}
})
.lineHeight(1.2, em)
button("Save Bio")
.padding(1, em)
.fontSize(1.1, em)
.borderRadius(12, px)
.background("var(--searchbackground)")
.color("var(--text)")
.border("1px solid var(--accent)")
.boxSizing("border-box")
.marginVertical(0.75, em)
})
.horizontalAlign("center")
.marginTop(5, em)
})
.onSubmit(async (e) => {
e.preventDefault();
const newBio = new FormData(e.target).get("bioinput");
console.log(this.profile)
if (newBio.trim() !== this.profile.bio?.trim()) {
const result = await server.editBio(newBio, this.profile.id)
const { bio, updated_at } = result.data
global.profile.bio = bio
global.profile.updated_at = updated_at
this.profile = global.profile
}
})
})
.backgroundColor("var(--main)")
.overflowX("hidden")
.height(window.visualViewport.height - 20, px)
.boxSizing("border-box")
.width(100, pct)
.position("fixed")
.top(100, vh)
.zIndex(5)
.transition("top .4s ")
.pointerEvents("none")
}
async handleUpload(file) {
try {
const body = new FormData();
body.append('image', file);
const res = await util.authFetch(`${util.HOST}/profile/upload-image`, {
method: "POST",
credentials: "include",
headers: {
"Accept": "application/json"
},
body: body
});
if(res.status === 401) {
return res.status
}
if (!res.ok) return res.status;
const data = await res.json()
global.profile = data.member
console.log(global.profile)
} catch (err) { // Network error / Error reaching server
console.error(err);
}
}
convertDate(rawDate) {
const parsed = new Date(rawDate);
if (isNaN(parsed.getTime())) return rawDate;
const month = parsed.toLocaleString("en-US", { month: "long", timeZone: "UTC" });
const day = parsed.getUTCDate();
const year = parsed.getUTCFullYear();
const ordinal = (n) => {
const mod100 = n % 100;
if (mod100 >= 11 && mod100 <= 13) return `${n}th`;
switch (n % 10) {
case 1: return `${n}st`;
case 2: return `${n}nd`;
case 3: return `${n}rd`;
default: return `${n}th`;
}
};
return `${month} ${ordinal(day)}, ${year}`;
}
}
register(Profile)

View File

@@ -1,34 +0,0 @@
export const IS_NODE =
typeof process !== "undefined" &&
process.versions?.node != null
async function bridgeSend(name, args) {
// Example browser implementation: send function call to server
const res = await global.Socket.send({
name: name,
args: args
})
return res
}
/**
* Wraps an object of functions so that:
* - Node calls the real function
* - Browser calls bridgeSend
*/
export function createBridge(funcs) {
return new Proxy(funcs, {
get(target, prop) {
const orig = target[prop]
return function (...args) {
if (IS_NODE) {
return orig(...args)
} else {
return bridgeSend(prop, args)
}
}
}
})
}

View File

@@ -1,47 +0,0 @@
const handlers = {
async getStripeProfile(networkId) {
return global.payments.getProfile(networkId)
},
async addEvent(newEvent, networkId, creatorId) {
return await global.db.events.add(newEvent, networkId, creatorId)
},
async editEvent(id, updatedEvent, networkId, userId) {
return await global.db.events.add(id, updatedEvent, networkId, userId);
},
async deleteEvent(id, networkId, userId) {
return await global.db.events.add(id, networkId, userId);
},
async getEvent(id) {
return global.db.events.getById(id)
},
async getEvents(networkId) {
return global.db.events.getByNetwork(networkId)
},
async addJob(newJob, networkId, creatorId) {
return await global.db.jobs.add(newJob, networkId, creatorId);
},
async editJob(id, updatedJob, networkId, userId) {
return await global.db.jobs.add(id, updatedJob, networkId, userId);
},
async deleteJob(id, networkId, userId) {
return await global.db.jobs.add(id, networkId, userId);
},
async getJob(id) {
return await global.db.jobs.getById(id)
},
async getJobs(networkId) {
return global.db.jobs.getByNetwork(networkId)
},
}
export default handlers

View File

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

View File

@@ -1,6 +1,7 @@
:root {
--main: #FFE9C8;
--accent: #60320c;
--accent: #570b0b;
--darkaccent: #dfc9ac;
--text: #340000;
--yellow: #f1f3c3;
--bone: #fff2e7;
@@ -9,10 +10,19 @@
--green: #0857265c;
--red: #ff0000;
--quillred: #DE3F3F;
--darkred: #6b2c1d;
--brown: #812A18;
--sidebar: #698b6f;
--divider: #523636;
--darkbrown: #3f0808;
--darkgrey: #5c4646;
--headertext: #433c36e2;
--searchbackground: #ffeed8;
--loginButton: var(--main);
--loginBackground: #d96b6b;
--home-src: /_/icons/home.svg;
--home-selected-src: /_/icons/homelightselected.svg;
--people-src: /_/icons/people.svg;
@@ -27,6 +37,7 @@
--column-src: /_/icons/column2.svg;
--nodes-src: /_/icons/nodes.svg;
--forum-src: /_/icons/forum.svg;
--trash-src: /_/icons/trash.svg;
--pin-src: /_/icons/pin.svg;
--time-src: /_/icons/time.svg;
@@ -37,10 +48,20 @@
@media (prefers-color-scheme: dark) {
:root {
--main: rgb(69, 20, 13);
--accent: rgb(106, 44, 28);
--accentdark: rgb(37, 2, 5);
--text: rgb(255, 225, 181);
--main: #2A150E;
--accent: #3D2622;
--darkaccent: #240609;
--text: #FADFB6;
--sidebar: #240609;
--divider: #523636;
--darktext: #62473E;
--headertext: #ffd8bb;
--darkred: #6b2c1d;
--searchbackground: #260F0C;
--loginButton: var(--darkaccent);
--loginBackground: var(--accent);
--home-src: /_/icons/homelight.svg;
--home-selected-src: /_/icons/homelightselected.svg;
@@ -56,20 +77,28 @@
--column-src: /_/icons/column2.svg;
--nodes-src: /_/icons/nodes.svg;
--forum-src: /_/icons/forum.svg;
--trash-src: /_/icons/trash.svg;
--pin-src: /_/icons/pinlight.svg;
--time-src: /_/icons/timelight.svg;
}
}
input {
outline: none;
caret-color: var(--text); /* hide real caret */
}
input::placeholder {
font-family: Arial;
color: #5C504D;
}
html,
body {
padding: 0;
margin: 0;
font-family: Arial;
}
body {

View File

@@ -1,3 +1,6 @@
import { Preferences } from '@capacitor/preferences';
import util from "../../../util.js"
class Connection {
connectionTries = 0;
ws;
@@ -8,10 +11,14 @@ class Connection {
}
init = async () => {
const { value: token } = await Preferences.get({ key: 'auth_token' });
return new Promise((resolve, reject) => {
const url = window.location.hostname.includes("local")
? "ws://" + window.location.host + "/ws"
: "wss://" + window.location.hostname + window.location.pathname + "/ws";
let url = ""
if(util.HOST) {
url = "wss://" + util.HOST.replace(/^https?:\/\//, '') + "/ws" + `?token=${token}`;
} else {
url = "ws://" + window.location.host + "/ws"
}
this.ws = new WebSocket(url);

View File

@@ -1,72 +0,0 @@
import util from "../../util"
class EventCard extends Shadow {
constructor(event) {
super()
this.event = event
}
render() {
VStack(() => {
h3(this.event.title)
.color("var(--brown)")
.fontSize(1.2, em)
.fontWeight("bold")
.marginVertical(0, em)
HStack(() => {
img(util.cssVariable("pin-src"), "1.3em")
p(this.event.location)
})
.gap(0.3, em)
.verticalAlign("center")
HStack(() => {
img(util.cssVariable("time-src"), "1.2em")
p(this.convertDate(this.event.time_start))
})
.gap(0.4, em)
.verticalAlign("center")
p("<b>Description: </b>" + " " + this.event.description)
})
.maxHeight(12.5, em)
.padding(0.75, em)
.gap(0, em)
.borderBottom("1px solid var(--divider)")
.verticalAlign("center")
.gap(0.5, em)
}
convertDate(rawDate) {
const parsed = new Date(rawDate);
if (isNaN(parsed.getTime())) return rawDate;
const month = parsed.toLocaleString("en-US", { month: "long", timeZone: "UTC" });
const day = parsed.getUTCDate();
const year = parsed.getUTCFullYear();
const hours24 = parsed.getUTCHours();
const minutes = parsed.getUTCMinutes();
const hours12 = hours24 % 12 || 12;
const ampm = hours24 >= 12 ? "PM" : "AM";
const paddedMinutes = String(minutes).padStart(2, "0");
const ordinal = (n) => {
const mod100 = n % 100;
if (mod100 >= 11 && mod100 <= 13) return `${n}th`;
switch (n % 10) {
case 1: return `${n}st`;
case 2: return `${n}nd`;
case 3: return `${n}rd`;
default: return `${n}th`;
}
};
return `${month} ${ordinal(day)}, ${year} at ${hours12}:${paddedMinutes} ${ampm}`;
}
}
register(EventCard)

View File

@@ -1,142 +0,0 @@
import "../../components/LoadingCircle.js"
import "./EventCard.js"
import server from "../../_/code/bridge/serverFunctions.js"
import "../../components/SearchBar.js"
css(`
events- {
font-family: 'Arial';
}
events- h1 {
font-family: 'Bona';
}
events- p {
color: var(--accent);
}
events- p b {
color: var(--darkbrown);
}
`)
class Events extends Shadow {
events = [
{
id: 1,
network_id: 2,
creator_id: 1,
title: "Austin Chapter Lead",
description: "This is the descriptio lets try a longer one agaiin just to see what it would look like? mayeb even longer bc that was pretty short now that i see it. maybe 2 sentences if possible or more.",
location: "1234 location",
time_start: "2026-01-13 13:41:41.0722",
created: "2026-01-13 12:00:00.0000",
updated_at: "2026-01-13 13:41:41.0722"
}
]
constructor() {
super()
// this.events = global.currentNetwork.data.events;
}
render() {
VStack(() => {
h1("Events")
.color("var(--quillred)")
.textAlign("center")
.marginBottom(0.25, em)
h3(global.currentNetwork.name)
.color("var(--quillred)")
.textAlign("center")
.margin(0)
.fontFamily("Bona")
SearchBar()
if (this.events == "" || this.events == []) {
LoadingCircle()
} else if (this.events.length > 0) {
for (let i = 0; i < this.events.length; i++) {
EventCard(this.events[i])
.borderTop(i == 0 ? "1px solid var(--divider)" : "")
}
} else {
h2("No Events")
.color("var(--brown)")
.fontWeight("bold")
.marginTop(7.5, em)
.marginBottom(0.5, em)
.textAlign("center")
}
})
.boxSizing("border-box")
.paddingVertical(1, em)
.height(100, pct)
.width(100, pct)
}
async getEvents(networkId) {
const fetchedEvents = await server.getEvents(networkId)
if (this.checkForUpdates(this.events, fetchedEvents)) {
this.events = fetchedEvents
this.rerender()
}
}
connectedCallback() {
this.getEvents(global.currentNetwork.id)
}
checkForUpdates(currentEvents, fetchedEvents) {
if (currentEvents.length !== fetchedEvents.length) return true;
const currentMap = new Map(currentEvents.map(event => [event.id, event]));
for (const fetchedEvent of fetchedEvents) {
const currentEvent = currentMap.get(fetchedEvent.id);
// new event added
if (!currentEvent) return true;
// existing event changed
if (currentEvent.updated_at !== fetchedEvent.updated_at) {
return true;
}
}
return false;
}
convertDate(rawDate) {
const parsed = new Date(rawDate);
if (isNaN(parsed.getTime())) return rawDate;
const month = parsed.toLocaleString("en-US", { month: "long", timeZone: "UTC" });
const day = parsed.getUTCDate();
const year = parsed.getUTCFullYear();
const hours24 = parsed.getUTCHours();
const minutes = parsed.getUTCMinutes();
const hours12 = hours24 % 12 || 12;
const ampm = hours24 >= 12 ? "PM" : "AM";
const paddedMinutes = String(minutes).padStart(2, "0");
const ordinal = (n) => {
const mod100 = n % 100;
if (mod100 >= 11 && mod100 <= 13) return `${n}th`;
switch (n % 10) {
case 1: return `${n}st`;
case 2: return `${n}nd`;
case 3: return `${n}rd`;
default: return `${n}th`;
}
};
return `${month} ${ordinal(day)}, ${year} at ${hours12}:${paddedMinutes} ${ampm}`;
}
}
register(Events)

View File

@@ -1,83 +1,83 @@
import './ForumPanel.js'
// import './ForumPanel.js'
css(`
forum- {
font-family: 'Bona';
}
// css(`
// forum- {
// font-family: 'Bona';
// }
forum- input::placeholder {
font-family: 'Bona Nova';
font-size: 0.9em;
color: var(--accent);
}
// forum- input::placeholder {
// font-family: 'Bona Nova';
// font-size: 0.9em;
// color: var(--accent);
// }
input::placeholder {
font-family: Arial;
}
// input::placeholder {
// font-family: Arial;
// }
input[type="checkbox"] {
appearance: none; /* remove default style */
-webkit-appearance: none;
width: 1em;
height: 1em;
border: 1px solid 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);
}
`)
// input[type="checkbox"]:checked {
// background-color: var(--red);
// }
// `)
class Forum extends Shadow {
render() {
ZStack(() => {
VStack(() => {
// class Forum extends Shadow {
// render() {
// ZStack(() => {
// VStack(() => {
ForumPanel()
// ForumPanel()
input("Message", "70%")
.paddingVertical(0.75, em)
.boxSizing("border-box")
.paddingHorizontal(2, em)
.color("var(--accent)")
.background("black")
.marginBottom(1, em)
.border("0.5px solid #6f5e4e")
.borderRadius(100, px)
.fontFamily("Arial")
.fontSize(1, em)
.onKeyDown(async function(e) {
if (e.key === "Enter") {
let msg = {
forum: global.currentNetwork.abbreviation,
text: this.value
}
await global.Socket.send({
app: "FORUM",
operation: "SEND",
msg: msg
})
this.value = ""
}
})
})
.gap(0.5, em)
.boxSizing("border-box")
.width(100, pct)
.height(100, pct)
.horizontalAlign("center")
.verticalAlign("end")
.minHeight(0)
})
.backgroundColor("var(--main)")
.boxSizing("border-box")
.paddingVertical(1, em)
.width(100, pct)
.minHeight(0)
.flex("1 1 auto")
}
// input("Message", "70%")
// .paddingVertical(0.75, em)
// .boxSizing("border-box")
// .paddingHorizontal(2, em)
// .color("var(--accent)")
// .background("black")
// .marginBottom(1, em)
// .border("0.5px solid #6f5e4e")
// .borderRadius(100, px)
// .fontFamily("Arial")
// .fontSize(1, em)
// .onKeyDown(async function(e) {
// if (e.key === "Enter") {
// let msg = {
// forum: global.currentNetwork.abbreviation,
// text: this.value
// }
// await global.Socket.send({
// app: "FORUM",
// operation: "SEND",
// msg: msg
// })
// this.value = ""
// }
// })
// })
// .gap(0.5, em)
// .boxSizing("border-box")
// .width(100, pct)
// .height(100, pct)
// .horizontalAlign("center")
// .verticalAlign("end")
// .minHeight(0)
// })
// .backgroundColor("var(--main)")
// .boxSizing("border-box")
// .paddingVertical(1, em)
// .width(100, pct)
// .minHeight(0)
// .flex("1 1 auto")
// }
}
// }
register(Forum)
// register(Forum)

View File

@@ -1,193 +1,193 @@
import "../../components/LoadingCircle.js"
// import "../../components/LoadingCircle.js"
css(`
forumpanel- {
scrollbar-width: none;
-ms-overflow-style: none;
}
// css(`
// forumpanel- {
// scrollbar-width: none;
// -ms-overflow-style: none;
// }
forumpanel-::-webkit-scrollbar {
display: none;
width: 0px;
height: 0px;
}
// forumpanel-::-webkit-scrollbar {
// display: none;
// width: 0px;
// height: 0px;
// }
forumpanel-::-webkit-scrollbar-thumb {
background: transparent;
}
// forumpanel-::-webkit-scrollbar-thumb {
// background: transparent;
// }
forumpanel-::-webkit-scrollbar-track {
background: transparent;
}
`)
// forumpanel-::-webkit-scrollbar-track {
// background: transparent;
// }
// `)
class ForumPanel extends Shadow {
messages = []
isSending = false
// class ForumPanel extends Shadow {
// messages = []
// isSending = false
render() {
VStack(() => {
if(this.messages.length > 0) {
let previousDate = null
// 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 isMe = message.authorId === global.profile.id
const dateParts = this.parseDate(message.time);
const { date, time } = dateParts;
// for(let i=0; i<this.messages.length; i++) {
// let message = this.messages[i]
// const isMe = message.authorId === global.profile.id
// const dateParts = this.parseDate(message.time);
// const { date, time } = dateParts;
if (previousDate !== date) {
previousDate = date;
// if (previousDate !== date) {
// previousDate = date;
p(date)
.textAlign("center")
.opacity(0.6)
.fontWeight("bold")
.paddingTop(1, em)
.paddingBottom(0.5, em)
.color("var(--quillred)")
.borderTop(`1px solid var(--${i == 0 ? "transparent" : "divider"})`)
}
// p(date)
// .textAlign("center")
// .opacity(0.6)
// .fontWeight("bold")
// .paddingTop(1, em)
// .paddingBottom(0.5, em)
// .color("var(--quillred)")
// .borderTop(`1px solid var(--${i == 0 ? "transparent" : "divider"})`)
// }
VStack(() => {
HStack(() => {
h3(isMe ? "Me" : message.sentBy)
.color(isMe ? "var(--quillred)" : "var(--brown")
.margin(0)
// VStack(() => {
// HStack(() => {
// h3(isMe ? "Me" : message.sentBy)
// .color(isMe ? "var(--quillred)" : "var(--brown")
// .margin(0)
h3(`${date} ${time}`)
.opacity(0.5)
.color("var(--brown)")
.margin(0)
.marginLeft(0.5, em)
.fontSize(1, em)
// h3(`${date} ${time}`)
// .opacity(0.5)
// .color("var(--brown)")
// .margin(0)
// .marginLeft(0.5, em)
// .fontSize(1, em)
if (message.edited) {
p("(edited)")
.color("var(--brown)")
.letterSpacing(0.8, "px")
.opacity(0.8)
.fontWeight("bold")
.paddingLeft(0.25, em)
.fontSize(0.9, em)
}
})
.verticalAlign("center")
.marginBottom(0.1, em)
// if (message.edited) {
// p("(edited)")
// .color("var(--brown)")
// .letterSpacing(0.8, "px")
// .opacity(0.8)
// .fontWeight("bold")
// .paddingLeft(0.25, em)
// .fontSize(0.9, em)
// }
// })
// .verticalAlign("center")
// .marginBottom(0.1, em)
p(message.text)
.color("var(--accent)")
.borderLeft("1.5px solid var(--divider)")
.borderBottomLeftRadius("7.5px")
.paddingLeft(0.5, em)
.marginHorizontal(0.2, em)
.paddingVertical(0.2, em)
.boxSizing("border-box")
})
.marginBottom(0.05, em)
.onClick(async (finished, e) => {
if (finished) {
console.log(message.id)
let msg = {
forum: global.currentNetwork.abbreviation,
id: message.id,
text: "EDITED TEXT TEST!"
}
await global.Socket.send({
app: "FORUM",
operation: "PUT",
msg: msg
})
}
})
}
} else {
LoadingCircle()
}
})
.gap(1, em)
.fontSize(1.1, em)
.boxSizing("border-box")
.flex("1 1 auto")
.minHeight(0)
.overflowY("auto")
.width(100, pct)
.paddingBottom(2, em)
.paddingHorizontal(4, pct)
.backgroundColor("var(--main)")
.onAppear(async () => {
requestAnimationFrame(() => {
this.scrollTo({ top: 0, behavior: "smooth" });
});
if (!this.isSending) {
this.isSending = true
let res = await global.Socket.send({
app: "FORUM",
operation: "GET",
msg: {
forum: global.currentNetwork.abbreviation,
by: "network",
authorId: -999 // default
}
})
if(!res) console.error("failed to get messages")
if(res.msg.length > 0 && this.messages.length === 0) {
this.messages = res.msg.reverse()
this.rerender()
}
this.isSending = false
}
})
.onEvent("new-post", this.onNewPost)
.onEvent("deleted-post", this.onDeletedPost)
.onEvent("edited-post", this.onEditedPost)
}
// p(message.text)
// .color("var(--accent)")
// .borderLeft("1.5px solid var(--divider)")
// .borderBottomLeftRadius("7.5px")
// .paddingLeft(0.5, em)
// .marginHorizontal(0.2, em)
// .paddingVertical(0.2, em)
// .boxSizing("border-box")
// })
// .marginBottom(0.05, em)
// .onClick(async (finished, e) => {
// if (finished) {
// console.log(message.id)
// let msg = {
// forum: global.currentNetwork.abbreviation,
// id: message.id,
// text: "EDITED TEXT TEST!"
// }
// await global.Socket.send({
// app: "FORUM",
// operation: "PUT",
// msg: msg
// })
// }
// })
// }
// } else {
// LoadingCircle()
// }
// })
// .gap(1, em)
// .fontSize(1.1, em)
// .boxSizing("border-box")
// .flex("1 1 auto")
// .minHeight(0)
// .overflowY("auto")
// .width(100, pct)
// .paddingBottom(2, em)
// .paddingHorizontal(4, pct)
// .backgroundColor("var(--main)")
// .onAppear(async () => {
// requestAnimationFrame(() => {
// this.scrollTo({ top: 0, behavior: "smooth" });
// });
// if (!this.isSending) {
// this.isSending = true
// let res = await global.Socket.send({
// app: "FORUM",
// operation: "GET",
// msg: {
// forum: global.currentNetwork.abbreviation,
// by: "network",
// authorId: -999 // default
// }
// })
// if(!res) console.error("failed to get messages")
// if(res.msg.length > 0 && this.messages.length === 0) {
// this.messages = res.msg.reverse()
// this.rerender()
// }
// this.isSending = false
// }
// })
// .onEvent("new-post", this.onNewPost)
// .onEvent("deleted-post", this.onDeletedPost)
// .onEvent("edited-post", this.onEditedPost)
// }
onNewPost = (e) => {
let newPost = e.detail
if (this.messages && !this.messages.some(post => post.id === newPost.id)) {
this.messages.unshift(newPost)
this.rerender()
}
}
// onNewPost = (e) => {
// let newPost = e.detail
// if (this.messages && !this.messages.some(post => post.id === newPost.id)) {
// this.messages.unshift(newPost)
// this.rerender()
// }
// }
onDeletedPost = (e) => {
let deletedId = e.detail
const i = this.messages.findIndex(post => post.id === deletedId)
if (i !== -1) this.messages.splice(i, 1);
this.rerender()
}
// onDeletedPost = (e) => {
// let deletedId = e.detail
// const i = this.messages.findIndex(post => post.id === deletedId)
// if (i !== -1) this.messages.splice(i, 1);
// this.rerender()
// }
onEditedPost = (e) => {
let editedPost = e.detail
const i = this.messages.findIndex(post => post.id === editedPost.id)
if (i !== -1) {
this.messages.splice(i, 1)
this.messages.unshift(editedPost)
}
// onEditedPost = (e) => {
// let editedPost = e.detail
// const i = this.messages.findIndex(post => post.id === editedPost.id)
// if (i !== -1) {
// this.messages.splice(i, 1)
// this.messages.unshift(editedPost)
// }
this.rerender()
}
// 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;
// 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()}`;
// const [, mm, dd, yyyy, hh, min, ampm] = match;
// const date = `${mm}/${dd}/${yyyy}`;
// const time = `${hh}:${min}${ampm.toLowerCase()}`;
return { date, time };
}
// return { date, time };
// }
formatTime(str) {
const match = str.match(/-(\d+:\d+):\d+.*(am|pm)/i);
if (!match) return null;
// formatTime(str) {
// const match = str.match(/-(\d+:\d+):\d+.*(am|pm)/i);
// if (!match) return null;
const [_, hourMin, ampm] = match;
return hourMin + ampm.toLowerCase();
}
}
// const [_, hourMin, ampm] = match;
// return hourMin + ampm.toLowerCase();
// }
// }
register(ForumPanel)
// register(ForumPanel)

View File

@@ -1,39 +0,0 @@
import util from "../../util.js"
class JobCard extends Shadow {
constructor(job) {
super()
this.job = job
}
render() {
VStack(() => {
h3(this.job.title)
.color("var(--brown)")
.fontSize(1.2, em)
.fontWeight("bold")
.marginVertical(0, em)
HStack(() => {
img(util.cssVariable("pin-src"), "1.3em")
p(this.job.location)
})
.gap(0.3, em)
.verticalAlign("center")
p("<b>Company: </b>" + " " + this.job.company)
p("<b>Salary: </b>" + " " + this.job.salary)
p("<b>Description: </b>" + " " + this.job.description)
})
.width(100, pct)
.maxHeight(12.5, em)
.padding(0.75, em)
.gap(0, em)
.borderBottom("1px solid var(--divider)")
.textAlign("left")
.gap(0.5, em)
.boxSizing("border-box")
}
}
register(JobCard)

View File

@@ -1,78 +0,0 @@
import util from "../../util.js"
class JobForm extends Shadow {
inputStyles(el) {
return el
.background("var(--main)")
.color("var(--text)")
.border("1px solid var(--accent)")
.fontSize(0.9, rem)
.backgroundColor("var(--accentdark)")
.borderRadius(12, px)
.outline("none")
.onTouch((start) => {
if (start) {
this.style.backgroundColor = "var(--accent)"
} else {
this.style.backgroundColor = "var(--accentdark)"
}
})
}
render() {
form(() => {
VStack(() => {
input("Title", "70vw")
.attr({ name: "title", type: "text" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Location", "70vw")
.attr({ name: "location", type: "text" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Company", "70vw")
.attr({ name: "company", type: "text" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("salary", "70vw")
.attr({ name: "salary", type: "number" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
input("Description", "70vw")
.attr({ name: "description", type: "text" })
.margin(1, em)
.padding(1, em)
.styles(this.inputStyles)
button("==>")
.margin(1, em)
.padding(1, em)
.fontSize(0.9, rem)
.borderRadius(12, px)
.background("var(--accent)")
.color("var(--text)")
.border("1px solid var(--accent)")
})
})
.position("absolute")
.height(90, pct)
.width(95, pct)
.top(50, pct).left(50, pct)
.center()
.background("var(--main)")
.zIndex(100)
.borderTopLeftRadius("10px")
.borderTopRightRadius("10px")
.boxSizing("border-box")
.transform(`translate(-50%, -45%)`)
}
tryThis() {
console.log("hello2")
}
}
register(JobForm)

View File

@@ -1,112 +0,0 @@
import "./JobsSidebar.js"
import "./JobsGrid.js"
import "./JobCard.js"
import "./JobForm.js"
import "../../components/SearchBar.js"
import server from "../../_/code/bridge/serverFunctions.js"
css(`
jobs- {
font-family: 'Arial';
}
jobs- h1 {
font-family: 'Bona';
}
jobs- p {
color: var(--accent);
}
jobs- p b {
color: var(--darkbrown);
}
`)
class Jobs extends Shadow {
jobs = [
{
id: 1,
network_id: 2,
creator_id: 1,
title: "Austin Chapter Lead",
description: "This is the description",
salary: 50000.00,
company: "Hyperia",
location: "1234 location",
created: "2026-03-12 13:41:41.0722",
updated_at: "2026-03-12 13:41:41.0722"
}
]
render() {
VStack(() => {
h1("Jobs")
.color("var(--quillred)")
.textAlign("center")
.marginBottom(0.25, em)
h3(global.currentNetwork.name)
.color("var(--quillred)")
.textAlign("center")
.margin(0)
.fontFamily("Bona")
// JobForm()
SearchBar()
if (this.jobs == "" || this.jobs == []) {
LoadingCircle()
} else if (this.jobs.length > 0) {
for (let i = 0; i < this.jobs.length; i++) {
JobCard(this.jobs[i])
.borderTop(i == 0 ? "1px solid var(--divider)" : "")
}
} else {
h2("No Jobs")
.color("var(--brown)")
.fontWeight("bold")
.marginTop(7.5, em)
.marginBottom(0.5, em)
.textAlign("center")
}
})
.boxSizing("border-box")
.paddingVertical(1, em)
.height(100, pct)
.width(100, pct)
}
async getJobs(networkId) {
const fetchedJobs = await server.getJobs(networkId)
if (this.checkForUpdates(this.jobs, fetchedJobs)) {
this.jobs = fetchedJobs
this.rerender()
}
}
connectedCallback() {
this.getJobs(global.currentNetwork.id)
}
checkForUpdates(currentJobs, fetchedJobs) {
if (currentJobs.length !== fetchedJobs.length) return true;
const currentMap = new Map(currentJobs.map(job => [job.id, job]));
for (const fetchedJob of fetchedJobs) {
const currentJob = currentMap.get(fetchedJob.id);
// new job added
if (!currentJob) return true;
// existing job changed
if (currentJob.updated_at !== fetchedJob.updated_at) {
return true;
}
}
return false;
}
}
register(Jobs)

View File

@@ -1,60 +0,0 @@
class JobsGrid extends Shadow {
jobs;
constructor(jobs) {
super()
this.jobs = jobs
}
boldUntilFirstSpace(text) {
const index = text.indexOf(' ');
if (index === -1) {
// No spaces — bold the whole thing
return `<b>${text}</b>`;
}
return `<b>${text.slice(0, index)}</b>${text.slice(index)}`;
}
render() {
VStack(() => {
h3("Results")
.marginTop(0.1, em)
.marginBottom(1, em)
.marginLeft(0.4, em)
.color("var(--accent2)")
if (this.jobs.length > 0) {
ZStack(() => {
for (let i = 0; i < this.jobs.length; i++) {
VStack(() => {
p(this.jobs[i].title)
.fontSize(1.2, em)
.fontWeight("bold")
.marginBottom(0.5, em)
p(this.jobs[i].company)
p(this.jobs[i].city + ", " + this.jobs[i].state)
.marginBottom(0.5, em)
p(this.boldUntilFirstSpace(this.jobs[i].salary))
})
.padding(1, em)
.borderRadius(5, "px")
.background("var(--darkbrown)")
}
})
.display("grid")
.gridTemplateColumns("repeat(auto-fill, minmax(250px, 1fr))")
.gap(1, em)
} else {
p("No Jobs!")
}
})
.height(100, vh)
.paddingLeft(2, em)
.paddingRight(2, em)
.paddingTop(2, em)
.gap(0, em)
.width(100, "%")
}
}
register(JobsGrid)

View File

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

View File

@@ -1,112 +0,0 @@
css(`
people- {
font-family: 'Arial';
}
people- h1 {
font-family: 'Bona';
}
people- p {
color: var(--accent);
}
people- p b {
color: var(--darkbrown);
}
`)
class People extends Shadow {
people = "";
constructor() {
super()
this.people = global.currentNetwork.data.members;
}
render() {
VStack(() => {
h1("People")
.color("rgb(158 136 105)")
.textAlign("center")
.marginBottom(0.25, em)
h3(global.currentNetwork.name)
.color("var(--quillred)")
.textAlign("center")
.margin(0)
.marginBottom(0.5, em)
.fontFamily("Bona")
if (this.people == "") {
LoadingCircle()
} else if (this.people.length > 0) {
for (let i = 0; i < this.people.length; i++) {
HStack(() => {
HStack(() => { })
.boxSizing("border-box")
.height(3.5, em)
.width(3.5, em)
.padding(0.5, em)
.border("1px solid var(--accent)")
.borderRadius(100, pct)
.background("black")
VStack(() => {
h3(this.people[i].first_name + " " + this.people[i].last_name)
.color("var(--brown)")
.fontSize(1.2, em)
.fontWeight("bold")
.marginVertical(0, em)
p("<b>Member since: </b>" + " " + this.convertDate(this.people[i].created))
})
.verticalAlign("center")
.gap(0.5, em)
})
.height(3.5, em)
.padding(0.75, em)
.gap(1, em)
}
} else {
h2("No Members")
.color("var(--brown)")
.fontWeight("bold")
.marginTop(7.5, em)
.marginBottom(0.5, em)
.textAlign("center")
p("Invite people to this network!")
.textAlign("center")
.color("var(--darkbrown)")
}
})
.position("relative")
.boxSizing("border-box")
.paddingVertical(1, em)
.height(100, pct)
.width(100, pct)
}
convertDate(rawDate) {
const parsed = new Date(rawDate);
if (isNaN(parsed.getTime())) return rawDate;
const month = parsed.toLocaleString("en-US", { month: "long", timeZone: "UTC" });
const day = parsed.getUTCDate();
const year = parsed.getUTCFullYear();
const ordinal = (n) => {
const mod100 = n % 100;
if (mod100 >= 11 && mod100 <= 13) return `${n}th`;
switch (n % 10) {
case 1: return `${n}st`;
case 2: return `${n}nd`;
case 3: return `${n}rd`;
default: return `${n}th`;
}
};
return `${month} ${ordinal(day)}, ${year}`;
}
}
register(People)

View File

@@ -3,34 +3,33 @@ import util from "../util.js"
class AppMenu extends Shadow {
selected = ""
apps = global.currentNetwork.apps.filter(app => (app !== "Settings" && app !== "Website"))
darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
images = {
"Dashboard": {src: ["home-src", "home-selected-src"]},
"People": {src: ["people-src", "people-selected-src"]},
"Settings": {src: ["settings-src", "settings-selected-src"]},
"Events": {src: ["events-src", "events-selected-src"]},
"Jobs": {src: ["jobs-src", "jobs-selected-src"]}
getImageURL(appName) {
let imgUrl = `${util.HOST}/db/apps/${appName}/icons/${appName}`
if(this.darkMode) {
imgUrl += "light"
}
imgUrl += ".svg"
imgUrl = imgUrl.toLowerCase()
return imgUrl
}
onNewSelection() {
this.$$("img").forEach((image) => {
image.style.borderBottom = "1px solid transparent"
const app = image.attributes.app.value
if (app === global.currentApp()) {
image.src = util.cssVariable(this.images[app].src[1])
} else {
image.src = util.cssVariable(this.images[app].src[0])
const appName = image.attributes.app.value
if (appName === global.currentApp()) {
image.style.borderBottom = "1px solid var(--text)"
}
image.src = this.getImageURL(appName)
})
}
render() {
// let apps = global.currentNetwork.apps
let apps = [
"Dashboard", "Jobs", "Events", "People"
]
let appCount = apps.filter(a => a !== "Settings").length
let apps = this.apps
let appCount = apps.length
let horizontalMargin = {
1: 50,
2: 10,
@@ -43,15 +42,13 @@ class AppMenu extends Shadow {
for(let i = 0; i < apps.length; i++) {
let app = apps[i]
if (app === "Settings") continue;
img(util.cssVariable(global.currentApp() === app ? this.images[app].src[1] : this.images[app].src[0]), "1.3em")
img(this.getImageURL(app), "1.3em")
.attr({app: app})
.padding(0.5, em)
.borderBottom(global.currentApp() === app ? "1px solid var(--text)" : "1px solid transparent")
.onTouch(async (done, e) => {
if(done) {
console.log(e)
e.target.style.borderBottom = "1px solid var(--text)"
global.openApp(app)
this.onNewSelection()
await Haptics.impact({ style: ImpactStyle.Light });
@@ -60,9 +57,9 @@ class AppMenu extends Shadow {
}
})
.display("grid")
.gridTemplateColumns(`repeat(${apps.filter(a => a !== "Settings").length}, 1fr)`)
.gridTemplateColumns(`repeat(${apps.filter(app => app !== "Settings").length}, 1fr)`)
.placeItems("center")
.borderTop("0.5px solid #783131")
.borderTop("0.5px solid var(--divider)")
.height("auto")
.zIndex(1)
.paddingTop(0.5, em)

View File

@@ -1,42 +1,30 @@
import "../apps/Forum/Forum.js"
import "../apps/Messages/Messages.js"
import "../apps/Jobs/Jobs.js"
import "../apps/People/People.js"
import "../apps/Events/Events.js"
import util from "../util.js"
class AppWindow extends Shadow {
render() {
ZStack(() => {
let app = global.currentApp()
switch(app) {
case "Dashboard":
Forum()
break;
case "Messages":
Messages()
break;
case "People":
People()
break;
case "Jobs":
Jobs()
break;
case "Events":
Events()
break;
if(window[app]) {
window[app]()
} else {
this.getCustomApp(app)
}
})
.height(100, pct)
.display("flex")
.position("relative")
.overflowY("scroll")
.onNavigate(() => {
this.rerender()
})
}
async getCustomApp(app) {
await import(`${util.HOST}/apps/${app.toLowerCase()}/${app.toLowerCase()}.js`);
if(window[app]) {
this.rerender()
} else {
console.error("Could not get app: ", app)
}
}
}
register(AppWindow)

View File

@@ -0,0 +1,37 @@
import "./AppWindow.js"
import "../Profile/Profile.js"
import "./TopBar.js"
class AppWindowContainer extends Shadow {
render() {
ZStack(() => {
VStack(() => {
TopBar()
AppWindow()
})
.width(100, pct)
.gap(0)
Profile()
.zIndex(3)
})
.height(100, pct)
.overflowY("hidden")
.display("flex")
.position("relative")
}
openProfile() {
this.$("profile-").top(20, px)
this.$("profile-").pointerEvents("auto")
}
closeProfile() {
this.$("profile-").top(100, vh)
this.$("profile-").pointerEvents("none")
}
}
register(AppWindowContainer)

View File

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

@@ -1,41 +0,0 @@
class SearchBar extends Shadow {
render() {
HStack(() => {
input("Search (coming soon!)", "80%")
.attr({
"type": "text",
"disabled": "true"
})
.paddingVertical(0.75, em)
.boxSizing("border-box")
.paddingHorizontal(1, em)
.color("var(--accent)")
.background("#fff1dd")
.marginTop(0.75, em)
.marginBottom(1, em)
.border("1px solid black")
.borderRadius(100, px)
.fontFamily("Arial")
.fontSize(1, em)
.outline("none")
.cursor("not-allowed")
p("+")
.fontWeight("bolder")
.paddingVertical(0.75, em)
.boxSizing("border-box")
.paddingHorizontal(1, em)
.background("#fff1dd")
.marginTop(0.75, em)
.marginBottom(1, em)
.border("1px solid black")
.borderRadius(15, px)
})
.width(100, pct)
.horizontalAlign("center")
.verticalAlign("center")
.gap(0.5, em)
}
}
register(SearchBar)

View File

@@ -1,44 +1,125 @@
import util from "../util"
import "./Toggle.js"
class Sidebar extends Shadow {
SIDEBAR_WIDTH
constructor(width) {
super()
this.SIDEBAR_WIDTH = width
}
SidebarItem(text) {
return p(text)
.fontSize(1.5, em)
.fontSize(1.2, em)
.fontWeight("bold")
.fontFamily("Sedan SC")
.marginLeft(2, em)
.fontStyle("italic")
.onClick(function () {
if(this.innerText === "Home") {
window.navigateTo("/")
.color("var(--headertext)")
.fontFamily("Arial")
.marginLeft(3, em)
.marginTop(2, em)
.onTap(function (done) {
if (done) {
if (this.innerText === "Logout") {
global.onLogout()
$("home-").closeSidebar();
return
}
window.navigateTo(this.innerText.toLowerCase().replace(/\s+/g, ""))
}
})
}
render() {
VStack(() => {
this.SidebarItem("Home")
this.SidebarItem("Map")
this.SidebarItem("Logout")
HStack(() => {
if (global.profile.image_path) {
img(`${util.HOST}${global.profile.image_path}`, "10em", "10em")
.borderRadius(100, pct)
}
})
.boxSizing("border-box")
.height(10, em)
.width(10, em)
.border("1px solid var(--accent)")
.borderRadius(100, pct)
.background("var(--darkaccent)")
.alignSelf("center")
.onClick((done) => {
if(done)
this.openProfile()
})
h2(global.profile.first_name + " " + global.profile.last_name)
.color("var(--headertext")
.textAlign("center")
.marginVertical(0.25, em)
.paddingBottom(0.5, em)
.textAlign("center")
.alignSelf("center")
.overflowWrap("break-word")
.wordBreak("break-word")
.width(100, pct)
.borderBottom("2px solid var(--divider)")
.onClick((done) => {
if(done)
this.openProfile()
})
.paddingBottom(1, em)
this.SidebarItem("Logout")
VStack(() => {
Toggle("Enable Push Notifications")
.marginLeft(1, em)
button("Delete Account")
.fontSize(0.9, em)
.marginBottom(2, em)
.background("var(--darkred)")
.paddingVertical(1, em)
.border("none")
.outline("1px solid var(--divider)")
.color("var(--text)")
.onTap((done) => this.deleteAccount())
})
.marginTop("auto")
.gap(2, em)
.paddingTop(30, vh)
.height(100, vh)
.width(70, vw)
.borderLeft("1px solid black")
})
.gap(1, em)
.paddingTop(15, vh)
.paddingHorizontal(1, em)
.height(105, vh)
.top(-5, vh)
.minWidth(0)
.boxSizing("border-box")
.width(this.SIDEBAR_WIDTH, px)
.borderLeft("1px solid var(--divider)")
.color("var(--text)")
.position("fixed")
.background("var(--main)")
.xRight(-70, vw)
.transition("right .3s")
.zIndex(1)
.background("var(--sidebar)")
}
toggle() {
if(this.style.right === "-70vw") {
this.style.right = "0vw"
} else {
this.style.right = "-70vw"
openProfile() {
$("appwindowcontainer-").openProfile()
$("home-").closeSidebar();
}
async deleteAccount() {
try {
const res = await util.authFetch(`${util.HOST}/auth/delete`, {
method: "DELETE",
credentials: "include",
headers: {
"X-Client": "mobile",
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify({ memberId: global.profile.id })
});
if (!res.ok) return;
global.onLogout()
} catch (err) {
console.error(err)
}
}
}

50
src/components/Toggle.js Normal file
View File

@@ -0,0 +1,50 @@
css(`
.toggle-input {
appearance: none;
width: 44px;
height: 24px;
background: #ccc;
border-radius: 12px;
position: relative;
cursor: pointer;
transition: background 0.2s;
}
.toggle-input::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: left 0.2s;
}
.toggle-input:checked {
background: var(--darkred);
}
.toggle-input:checked::after {
left: 22px;
}
`)
class Toggle extends Shadow {
render() {
HStack(() => {
input("", "44px", "24px")
.attr({ type: "checkbox", checked: global.profile.preferences.notifications ? "" : null, class: "toggle-input"})
.onChange(async (e) => {
await server.updateNotificationPreferences(global.profile.id, e.target.checked)
})
p("Enable Push Notifications")
.color("var(--text)")
})
.verticalAlign("center")
.gap(10, px)
}
}
register(Toggle)

53
src/components/TopBar.js Normal file
View File

@@ -0,0 +1,53 @@
import util from "../util.js"
class TopBar extends Shadow {
render() {
HStack(() => {
if (global.currentNetwork.logo) {
img(`${util.HOST}/db/images/${global.currentNetwork.logo}`, "2.5em", "2.5em")
.borderRadius("50", pct)
.objectFit("cover")
.padding(0.3, em)
.background("var(--accent)")
.onTouch(function (start) {
if(start) {
this.style.scale = "0.8"
} else if(start === false) {
this.style.scale = ""
if (!$("home-").sidebarOpen) {
$("home-").openSidebar()
}
}
})
} else {
HStack(() => { })
.height(2.5, em)
.width(2.5, em)
.padding(0.3, em)
.background("var(--accent)")
.borderRadius("50", pct)
}
p()
.state("app", function () {
this.innerText = global.currentApp()
})
.color("var(--headertext)")
.textAlign("center")
.fontFamily("Arial")
.fontSize("clamp(0.8rem, 40cqw, 7cqw)")
.fontWeight("bold")
})
.containerType("inline-size")
.paddingLeft(1, em)
.paddingBottom(1.5, em)
.verticalAlign("center")
.gap(0.5, em)
.marginTop(4, em)
.onNavigate(() => {
this.$("p").attr({app: global.currentApp()})
})
}
}
register(TopBar)

View File

@@ -1,3 +0,0 @@
class env {
BASE_URL = "https://parchment.page"
}

View File

@@ -1,7 +1,9 @@
import { PushNotifications } from '@capacitor/push-notifications';
import Socket from "/_/code/ws/Socket.js"
import "./Home/Home.js"
import "./Home/Login.js"
import "./Home/AuthPage/AuthPage.js"
import "./Home/ConnectionError.js"
import util from "./util.js"
const env = import.meta.env
let Global = class {
@@ -9,10 +11,11 @@ let Global = class {
profile = null
currentNetwork = ""
lastApp = ""
util = util
currentApp() {
const pathname = window.location.pathname;
const segments = pathname.split('/').filter(Boolean);
const segments = pathname.split('/').filter(Boolean)
const secondSegment = segments[1] || ""
const capitalized = secondSegment.charAt(0).toUpperCase() + secondSegment.slice(1);
return capitalized
@@ -32,13 +35,13 @@ let Global = class {
async fetchAppData() {
let personalSpace = this.currentNetwork === this.profile
if (personalSpace) { return {} }
let appData = await fetch(`${env.VITE_API_URL}/api/${personalSpace ? "my" : "org"}data/` + this.currentNetwork.id, {method: "GET"})
let appData = await fetch(`${util.HOST}/api/${personalSpace ? "my" : "org"}data/` + this.currentNetwork.id, {method: "GET"})
let json = await appData.json()
return json
}
onNavigate = async () => {
if(!global.profile) return
let selectedNetwork = this.networkFromPath()
if(!selectedNetwork) {
@@ -117,9 +120,8 @@ let Global = class {
}
async getProfile() {
console.log(env)
try {
const res = await fetch(`${env.VITE_API_URL}/profile`, {
const res = await util.authFetch(`${util.HOST}/auth/profile`, {
method: "GET",
credentials: "include",
headers: {
@@ -142,22 +144,80 @@ let Global = class {
}
}
constructor() {
async renderHome() {
location.reload()
}
async onLogout() {
await util.removeAuthToken()
await fetch(`${util.HOST}/auth/signout`, {
method: "GET",
credentials: "include"
});
this.profile = null
location.reload()
}
async setupPushNotifications() {
if (!Capacitor.isNativePlatform()) return;
PushNotifications.addListener('registration', async (token) => {
console.log('Device token:', token.value)
const stored = localStorage.getItem('deviceToken')
if (stored === token.value) return;
console.log("new push token")
await server.updatePushToken(token.value, global.profile.id, env.MODE)
localStorage.setItem('deviceToken', token.value)
});
PushNotifications.addListener('registrationError', (error) => {
console.error('Registration error:', JSON.stringify(error))
});
PushNotifications.addListener('pushNotificationReceived', (notification) => {
console.log('Notification received:', notification)
});
PushNotifications.addListener('pushNotificationActionPerformed', (action) => {
console.log('Notification tapped:', action.notification)
// navigate somewhere based on action.notification.data
});
const permission = await PushNotifications.requestPermissions();
if (permission.receive === 'granted') {
await PushNotifications.register();
console.log('after register:')
}
}
async init() {
try {
const module = await import(`${util.HOST}/@server/server.js`);
window.server = module.default;
} catch(E) {
console.error(E)
}
window.addEventListener("navigate", this.onNavigate)
this.getProfile().then(async (status) => {
if (status === 401) {
Login()
navigateTo("/")
AuthPage()
} else if(status === 500) {
ConnectionError()
} else {
console.log("else")
await this.Socket.init()
await this.setupPushNotifications()
await this.onNavigate()
Home()
}
})
}
constructor() {
this.init()
}
}
window.global = new Global()

View File

@@ -4,9 +4,9 @@
"start_url": "index.html",
"display": "standalone",
"icons": [{
"src": "_/imgs/logo.png",
"src": "_/icons/logo.svg",
"sizes": "512x512",
"type": "image/png"
"type": "image/svg"
}],
"background_color": "#31d53d",
"theme_color": "#31d53d"

View File

@@ -1,6 +1,10 @@
/*
Sam Russell
Captured Sun
2.24.26 - Allowing state() to watch other elements
2.21.26 - Making state() be called on initial definition, fixing fontSize so it works with clamp(), other strings
2.20.26 - Adding state()
2.19.26 - Adding dynamicText()
2.16.26 - Adding event objects to the onTouch callbacks
1.16.26 - Moving nav event dispatch out of pushState, adding null feature to attr()
1.5.26 - Switching verticalAlign and horizontalAlign names, adding borderVertical and Horizontal
@@ -469,7 +473,7 @@ HTMLElement.prototype.setUpState = function(styleFunc, cb) {
}
function StyleFunction(func) {
let styleFunction = function(value, unit = "px") {
let styleFunction = function(value, unit) {
if(typeof value === 'function') {
this.setUpState(styleFunction, value)
return this
@@ -525,7 +529,7 @@ HTMLElement.prototype.borderHorizontal = StyleFunction(function(value) {
return this
})
HTMLElement.prototype.fontSize = StyleFunction(function(value, unit = "px") {
HTMLElement.prototype.fontSize = StyleFunction(function(value, unit) {
switch(value) {
case "6xl":
@@ -567,7 +571,13 @@ HTMLElement.prototype.fontSize = StyleFunction(function(value, unit = "px") {
default:
break;
}
if(unit) {
this.style.fontSize = value + unit
} else {
this.style.fontSize = value
}
return this
})
@@ -691,6 +701,54 @@ HTMLElement.prototype.horizontalAlign = function (value) {
/* Elements */
HTMLElement.prototype.state = function(arg1, arg2, arg3) {
let el;
let attr;
let cb;
if(arg3) {
el = arg1
attr = arg2
cb = arg3
} else {
el = this
attr = arg1
cb = arg2
}
if (attr !== attr.toLowerCase()) {
throw new Error(`quill: dynamicText() attr "${attr}" must be lowercase`);
}
let handler = () => {
const value = el.getAttribute(attr);
cb.call(this, value)
}
new MutationObserver(handler)
.observe(el, { attributes: true, attributeFilter: [attr] });
handler()
return this
}
HTMLElement.prototype.dynamicText = function(attr, template) {
// Set initial text if attr already has a value
if (attr !== attr.toLowerCase()) {
throw new Error(`quill: dynamicText() attr "${attr}" must be lowercase`);
}
if (this.getAttribute(attr)) {
this.innerText = template.replace('{{}}', this.getAttribute(attr));
}
new MutationObserver(() => {
const value = this.getAttribute(attr);
this.innerText = template.replace('{{}}', value ?? '');
}).observe(this, { attributes: true, attributeFilter: [attr] });
return this
};
quill.setChildren = function(el, innerContent) {
if(typeof innerContent === "string") {
el.innerText = innerContent
@@ -1188,21 +1246,23 @@ HTMLElement.prototype.removeAllListeners = function() {
/* ATTRIBUTES */
HTMLElement.prototype.attr = function(attributes) {
if (
typeof attributes !== "object" ||
attributes === null ||
Array.isArray(attributes)
) {
throw new TypeError("attr() expects an object with key-value pairs");
}
for (const [key, value] of Object.entries(attributes)) {
HTMLElement.prototype.attr = function(arg1, arg2) {
if(typeof arg1 === "object") {
for (const [key, value] of Object.entries(arg1)) {
if(value === null) {
this.removeAttribute(key)
} else {
this.setAttribute(key, value);
}
}
return this;
} else if(typeof arg1 === "string" && arg2) {
this.setAttribute(arg1, arg2)
return this
} else if(typeof arg1 === "string") {
return this.getAttribute(arg1)
} else {
throw new TypeError("wrong parameter for attr(): ", arg1);
}
};

View File

@@ -1,7 +1,9 @@
<svg width="514" height="471" viewBox="0 0 514 471" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M351.026 262.387H269.536V470.629H351.026V262.387Z" fill="#FFE1B5"/>
<path d="M137.907 262.387H97.1572V470.629H137.907V262.387Z" fill="#FFE1B5"/>
<path d="M427.694 0.13364L427.787 0H86.2132L86.2749 0.13364C38.3958 1.56256 0 40.7294 0 88.9631C0 135.382 35.5534 173.444 80.9087 177.52C97.8707 177.474 135.963 172.863 137.464 133.804C138.4 109.41 119.32 88.8141 94.9307 87.8837L95.8867 62.8314C114.401 63.5355 131.522 71.4049 144.105 84.995C156.688 98.5801 163.231 116.257 162.522 134.761C161.108 171.527 136.128 196.245 97.1666 201.524V237.314H416.854V201.976C401.634 200.198 388.121 195.464 377.189 187.898C359.661 175.793 349.947 157.49 349.088 134.961C348.374 116.457 354.917 98.7857 367.515 85.2058C380.088 71.6105 397.214 63.7411 415.718 63.0421L416.679 88.0996C404.868 88.5416 393.94 93.5685 385.906 102.245C377.877 110.921 373.704 122.198 374.151 134.01C374.701 148.536 380.524 159.726 391.452 167.286C401.054 173.922 414.603 177.505 429.827 177.69C476.709 175.187 514 136.482 514 88.9631C514 40.7242 475.594 1.54714 427.694 0.13364Z" fill="#FFE1B5"/>
<path d="M416.85 262.387H376.1V470.629H416.85V262.387Z" fill="#FFE1B5"/>
<path d="M244.469 262.387H162.979V470.629H244.469V262.387Z" fill="#FFE1B5"/>
<svg width="306" height="315" viewBox="0 0 306 315" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M257.568 6.10352e-05H47.9351V28.2306H257.568V6.10352e-05Z" fill="#FFD5A4"/>
<path d="M207.362 174.274C205.545 172.318 203.134 171.234 200.549 171.234C195.203 171.234 190.836 176.021 190.836 181.891V314.449H210.262V181.891C210.262 178.991 209.249 176.301 207.362 174.274Z" fill="#FFD5A4"/>
<path d="M233.844 314.448V121.795C229.581 123.472 224.97 124.416 220.148 124.416H85.3542C80.5326 124.416 75.9207 123.472 71.6582 121.795V314.448H90.0011V181.89C90.0011 173.12 96.7093 165.993 104.955 165.993C109.008 165.993 112.781 167.67 115.611 170.71C118.371 173.679 119.909 177.662 119.909 181.89V314.448H137.797V156.524C137.797 147.755 144.506 140.627 152.751 140.627C156.769 140.627 160.542 142.304 163.407 145.309C166.203 148.314 167.705 152.297 167.705 156.524V314.448H185.594V181.89C185.594 173.12 192.302 165.993 200.547 165.993C204.6 165.993 208.374 167.67 211.204 170.71C213.964 173.679 215.501 177.662 215.501 181.89V314.448H233.844Z" fill="#FFD5A4"/>
<path d="M159.562 148.909C157.71 146.953 155.3 145.87 152.749 145.87C147.403 145.87 143.036 150.656 143.036 156.526V314.449H162.462V156.526C162.462 153.626 161.449 150.936 159.562 148.909Z" fill="#FFD5A4"/>
<path d="M111.769 174.274C109.952 172.318 107.542 171.234 104.956 171.234C99.6105 171.234 95.2432 176.021 95.2432 181.891V314.449H114.669V181.891C114.669 178.991 113.656 176.301 111.769 174.274Z" fill="#FFD5A4"/>
<path d="M245.831 106.423C233.916 98.9114 225.95 85.6696 225.95 70.611C225.95 70.4363 225.95 70.2965 225.95 70.1218H79.5569C79.5569 70.1218 79.5569 70.4363 79.5569 70.611C79.5569 85.7045 71.5909 98.9463 59.6768 106.423C65.5814 114.145 74.8751 119.176 85.3568 119.176H220.151C230.632 119.176 239.926 114.145 245.831 106.423Z" fill="#FFD5A4"/>
<path d="M37.1399 107.716C57.6141 107.716 74.2799 91.0507 74.2799 70.6115C74.2799 70.4368 74.2799 70.297 74.2799 70.1223H34.834C33.4015 70.1223 32.2136 68.9344 32.2136 67.5019C32.2136 66.0694 33.4015 64.8815 34.834 64.8815H270.671C272.103 64.8815 273.291 66.0694 273.291 67.5019C273.291 68.9344 272.103 70.1223 270.671 70.1223H231.225C231.225 70.1223 231.225 70.4368 231.225 70.6115C231.225 91.0856 247.891 107.716 268.365 107.716C288.839 107.716 305.505 91.0507 305.505 70.6115C305.505 50.1723 288.839 33.4716 268.365 33.4716H37.1399C16.6658 33.4716 0 50.1374 0 70.6115C0 91.0856 16.6658 107.716 37.1399 107.716Z" fill="#FFD5A4"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-5.0 -10.0 110.0 135.0">
<path d="m39.043 13.828c-0.37891 0-0.74219 0.15234-1.0117 0.42188-0.26562 0.26953-0.41797 0.63281-0.41406 1.0117v3.8164h-8.7109c-3.3477 0-6.0938 2.7461-6.0938 6.0977v9.8281c-2.9883 13.051-8.9258 25.238-17.371 35.625-0.007812 0.007813-0.015625 0.019532-0.019531 0.03125l-0.39844 0.51562c-1.8281 2.3828-0.03125 6.0117 2.9727 6.0117h14.816v1.875c0 3.9102 3.1953 7.1094 7.1055 7.1094h58.688c3.9102 0 7.1094-3.1992 7.1094-7.1094v-43.777-0.058594-2.0742-0.035156-7.9414c0-3.3516-2.7461-6.0938-6.0977-6.0938h-8.6992v-3.8203c0-0.37891-0.14844-0.74609-0.41797-1.0156-0.26953-0.26953-0.63672-0.41797-1.0156-0.41797s-0.74219 0.15234-1.0078 0.42188c-0.26953 0.26953-0.41797 0.63281-0.41797 1.0117v3.8203l-37.582-0.003906v-3.8164c0-0.37891-0.14844-0.74609-0.41797-1.0156-0.26953-0.26953-0.63672-0.41797-1.0156-0.41797zm-10.137 8.1094h8.7109v3.8281c0.003906 0.78906 0.64062 1.4219 1.4258 1.4258 0.37891 0 0.74219-0.14844 1.0117-0.41406 0.26953-0.26953 0.42187-0.63281 0.42187-1.0117v-3.8281h37.586v3.8281h-0.003906c0.003906 0.78516 0.64062 1.4219 1.4258 1.4258 0.37891 0 0.74219-0.14844 1.0117-0.41406 0.26953-0.26953 0.41797-0.63281 0.42188-1.0117v-3.8281h8.6992c1.8047 0 3.2383 1.4297 3.2383 3.2383v8.6172h-67.188v-0.60156-0.074218-7.9414c0-1.8047 1.4336-3.2344 3.2383-3.2383zm-3.5312 14.711h67.09c-3.0977 12.426-8.8789 24.031-16.969 33.98-0.011719 0.007813-0.019532 0.019532-0.03125 0.03125l-0.39062 0.51562c-1.5234 1.9883-3.8789 3.1562-6.3828 3.1562h-44.441c-0.003906-0.003906-0.011719-0.003906-0.015625-0.003906s-0.011719 0-0.015625 0.003906h-16.223c-0.82031 0-1.1953-0.76562-0.69922-1.4141l0.36719-0.48438 0.011719-0.019532c8.5-10.457 14.547-22.676 17.699-35.766zm67.477 8.707v33.707c0 2.3672-1.8828 4.2617-4.25 4.2617h-58.684c-2.3672 0-4.25-1.8945-4.25-4.2617v-1.875h43.027c3.3906 0 6.5859-1.582 8.6484-4.2734l0.36719-0.48437c6.5898-8.1016 11.703-17.266 15.141-27.074z" fill="#000000"/>
<svg width="92" height="73" viewBox="0 0 92 73" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M34.7556 0C34.3767 0 34.0134 0.15234 33.7439 0.42188C33.4782 0.69141 33.3259 1.05469 33.3298 1.43358V5.24998H24.6189C21.2712 5.24998 18.5251 7.99608 18.5251 11.3477V21.1758C15.5368 34.2268 9.5993 46.4138 1.1541 56.8008C1.14629 56.8086 1.13848 56.8203 1.13457 56.832L0.736131 57.3477C-1.09197 59.7305 0.704881 63.3593 3.70883 63.3593H18.5248V65.2343C18.5248 69.1445 21.7201 72.3438 25.6303 72.3438H84.3183C88.2285 72.3438 91.4277 69.1445 91.4277 65.2343V21.4574V21.3988V19.3246V19.2894V11.348C91.4277 7.9964 88.6816 5.2542 85.33 5.2542H76.6308V1.4339C76.6308 1.05499 76.4824 0.68781 76.2129 0.4183C75.9433 0.14877 75.5761 0.000329971 75.1973 0.000329971C74.8184 0.000329971 74.4551 0.15267 74.1895 0.42221C73.9199 0.69174 73.7715 1.05502 73.7715 1.43391V5.25421L36.1895 5.2503V1.4339C36.1895 1.05499 36.0411 0.687814 35.7715 0.418303C35.502 0.148773 35.1344 0 34.7556 0ZM24.6186 8.1094H33.3295V11.9375C33.3334 12.7266 33.9701 13.3594 34.7553 13.3633C35.1342 13.3633 35.4975 13.2149 35.767 12.9492C36.0365 12.6797 36.1888 12.3164 36.1888 11.9375V8.10944H73.7748V11.9375H73.7709C73.7748 12.7227 74.4115 13.3594 75.1967 13.3633C75.5756 13.3633 75.9389 13.2149 76.2084 12.9493C76.478 12.6798 76.6264 12.3165 76.6303 11.9376V8.10948H85.3295C87.1342 8.10948 88.5678 9.53918 88.5678 11.3478V19.965H21.3798V19.3634V19.2892V11.3478C21.3798 9.5431 22.8139 8.1133 24.6186 8.1094ZM21.0874 22.8204H88.1774C85.0797 35.2464 79.2985 46.8514 71.2084 56.8004C71.1966 56.8082 71.1888 56.8199 71.1771 56.8317L70.7865 57.3473C69.2631 59.3356 66.9076 60.5035 64.4037 60.5035H19.9627C19.9588 60.4996 19.951 60.4996 19.9471 60.4996C19.9432 60.4996 19.9353 60.4996 19.9314 60.5035H3.70844C2.88813 60.5035 2.51314 59.7379 3.00922 59.0894L3.37641 58.605L3.38813 58.5855C11.8881 48.1285 17.9354 35.9104 21.0874 22.8204ZM88.5644 31.5274V65.2344C88.5644 67.6016 86.6816 69.4961 84.3144 69.4961H25.6304C23.2632 69.4961 21.3804 67.6016 21.3804 65.2344V63.3594H64.4074C67.798 63.3594 70.9933 61.7774 73.0558 59.086L73.423 58.6016C80.0127 50.5 85.1264 41.3354 88.5644 31.5274Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,4 +1,27 @@
import { Preferences } from '@capacitor/preferences';
const env = import.meta.env
export default class util {
static HOST = env.VITE_API_URL
static async authFetch(url, options = {}) {
const { value: token } = await Preferences.get({ key: 'auth_token' });
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
'X-Client': 'mobile'
}
});
}
static async removeAuthToken() {
await Preferences.remove({ key: 'auth_token'})
}
static cssVariable(value) {
return getComputedStyle(document.documentElement)
.getPropertyValue("--" + value)

View File

@@ -6,7 +6,8 @@ export default defineConfig({
outDir: '../dist',
minify: false,
emptyOutDir: true,
sourcemap: true
sourcemap: true,
target: 'esnext' // modern version of browsers, allows top-level await
},
server: {
proxy: {
@@ -15,17 +16,25 @@ export default defineConfig({
changeOrigin: true,
ws: true
},
"/login": {
target: "http://localhost:10002",
changeOrigin: true
},
"/profile": {
"/profile/upload-image": {
target: "http://localhost:10002",
changeOrigin: true
},
"/api": {
target: "http://localhost:10002",
changeOrigin: true
},
"/db": {
target: "http://localhost:10002",
changeOrigin: true
},
"/apps": {
target: "http://localhost:10002",
changeOrigin: true
},
"/auth": {
target: "http://localhost:10002",
changeOrigin: true
}
},
host: true,