Compare commits

...

184 commits

Author SHA1 Message Date
71242d0543 revert 04358e9c91
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
revert feat: make settings menu remember your page on reload

it breaks the back button, my bad
2025-07-10 19:37:04 +00:00
04358e9c91
feat: make settings menu remember your page on reload
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-10 21:04:27 +02:00
f59162bad5
fix: width of icon and channel lists
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-10 20:13:25 +02:00
2790772cb7
fix: some of the worst merging i've done in my life
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-10 16:32:41 +02:00
97ad3155c3
Merge remote-tracking branch 'origin/multiline-message-box'
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-10 16:25:36 +02:00
7ffe543f44
fix: invalid css 2025-07-10 16:24:10 +02:00
a9b0eda9e2
Merge remote-tracking branch 'origin/ui-refactor'
Some checks failed
ci/woodpecker/push/build-and-publish Pipeline failed
2025-07-10 16:19:21 +02:00
92c0024411
Merge branch 'ui-refactor' 2025-07-10 16:18:35 +02:00
c3f3c95af6
Merge branch 'comedical-main-bar' 2025-07-10 16:17:29 +02:00
53dfdd8f9d
Merge branch 'home-page' 2025-07-10 16:16:48 +02:00
91d2e45559
Merge remote-tracking branch 'origin/improve-theming' 2025-07-10 16:16:34 +02:00
323178af6b
undo the last 6 merges i fucked up
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-10 16:10:55 +02:00
0afb269788
feat: start adding back button for settings 2025-07-10 15:48:14 +02:00
e71db5f571
chore: remove unused class attribute in message text box element
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-10 11:56:54 +02:00
2665f29341
feat: align message box buttons to bottom 2025-07-10 11:55:45 +02:00
a1cd9c482c
feat: increase width of channels and members lists
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-10 11:46:25 +02:00
b67d568c5e fix: display names with empty string not being replaced by usernames in member list
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-09 20:00:45 +02:00
7ff2b35ae2 Merge pull request 'Implement email verification' (#26) from email-verification into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Reviewed-on: #26
2025-07-09 17:54:33 +00:00
822b16ae07 Merge branch 'main' into email-verification
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-09 17:53:35 +00:00
e58809b424
feat: implement multiline message box
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-09 16:34:39 +02:00
4b6dc03b13
feat: refresh page when email is changed and instance requires verification
Some checks failed
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline failed
2025-07-09 07:30:46 +02:00
84aa1f95fe
feat: add auto redirection in verify-email page 2025-07-09 07:30:06 +02:00
2c670ebc6a
feat: add email verification handling in register page 2025-07-09 07:29:03 +02:00
2863651f70
feat: switch to use fetcvhInstanceStats from api in login page to check for whether registration is enabled 2025-07-09 07:28:18 +02:00
9f66643b99
feat: add attempting to set instance URL from auth middleware 2025-07-09 07:27:44 +02:00
dd63095526
feat: add check for email verification requirement and redirect if needed in auth middleware 2025-07-09 07:27:16 +02:00
e95e81112b
feat: improve looks of auth page 2025-07-09 07:25:57 +02:00
9aec279d05
feat: refresh page when auth tokens are cleared 2025-07-09 07:25:39 +02:00
9e5d48931d
feat: switch to use fetcvhInstanceStats from api composable in auth.vue 2025-07-09 07:25:10 +02:00
6154bb89d0
feat: change some console.log calls 2025-07-09 07:23:51 +02:00
397e94798f
feat: remove unused type import 2025-07-09 07:21:49 +02:00
04689e57ea
feat: remove old logout and other auth functions from login/register page 2025-07-09 07:21:16 +02:00
5c178f99ae
feat: add fetchInstanceStats and sendVerificationEmail to api composable 2025-07-09 07:19:40 +02:00
f1bec945fe
feat: multiline message box
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
this implementation SUCKS and i probably have to write an entirely custom solution where every line is a div or something
2025-07-09 03:36:30 +02:00
a6572c20f9
fix: remove accidental duplication of member list
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
this was for local testing, forgot to remove it
2025-07-09 01:04:37 +02:00
5cdeb36200
refactor: more dv to em
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-09 00:46:59 +02:00
9a2fe8fb37
refactor: change a lot of dynamic sizes to em and rem
this makes the client feel a LOT snappier
2025-07-09 00:43:23 +02:00
730b0cb1dc
refactor: change the client from table to flexbox
this makes the server, channel, and member list a constant size, making the text messages take up the entire remaining width
this also fixes the text wrapping you have already fixed on one of your branches

this change is required if we want to make the member list toggelable, or channel list resizable
2025-07-09 00:25:52 +02:00
8932070fcc
feat: spice up the home bar
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-08 21:11:14 +02:00
bd6307f16e
fix: rephrase sentace from "server" to "guild"
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-08 21:00:05 +02:00
bb5ff37509
fix: put the home page into index.vue instead
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-08 20:58:40 +02:00
19113f1303
feat: implement basic home page
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-08 20:57:40 +02:00
51c2578e60
fix: remove unused code, set chatbox input to a transparent colour
All checks were successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-08 15:45:18 +02:00
fa93e00759
feat: improve message box :3
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-07 22:22:20 +02:00
c0ad1a7c0d
feat: pretty button :3
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-07 21:27:45 +02:00
f91c299575
fix: add missing commas from themes
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-07 21:04:41 +02:00
d0fe0db374
feat: more theme styling options, and description.css
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-07 21:00:42 +02:00
8f06f25efe
feat: start adding back button for settings
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-07 20:40:49 +02:00
a111180b52
feat: add word wrapping in messages
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-07 20:23:47 +02:00
0c6cae110f
feat: add more restrictions to markdown sanitization
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-07 19:53:50 +02:00
f8ede8385e Merge pull request 'feat: woke-theme' (#19) from woke-theme into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Reviewed-on: #19
Reviewed-by: looga <gitbub@iamallof.me>
Reviewed-by: SauceyRed <saucey@saucey.red>
2025-07-07 14:55:29 +00:00
5c852a2df7
refactor: change internal woke ID to rainbow-capitalism
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-06 02:35:21 +02:00
b1ae35c43b
fix: make woke theme server list less jaring
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-06 02:27:47 +02:00
936d036253
fix: remove weird gradient
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-06 02:22:24 +02:00
11209e0618
fix: highlight colours
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-06 02:21:23 +02:00
24d8905ef2
feat: woke theme
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-06 02:20:11 +02:00
519a5555a9 Merge pull request 'pfp cropping' (#12) from pfp-cropping into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Reviewed-on: #12
Reviewed-by: SauceyRed <saucey@saucey.red>
2025-07-05 17:34:27 +00:00
825cf2ba52
fix: minor changes to fix review
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-05 19:32:14 +02:00
def96c4df3
Merge branch 'main' into pfp-cropping 2025-07-05 19:30:08 +02:00
fb35be390e Merge pull request 'Add theme switching!!!!' (#18) from settings-appearance into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Reviewed-on: #18
Reviewed-by: SauceyRed <saucey@saucey.red>
2025-07-05 17:29:45 +00:00
2d8475b20a
fix: convert arrow function for consistency
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-05 19:22:06 +02:00
2c4489917a
fix: use $fetch over fetch
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-05 19:05:35 +02:00
7098dda6b4
fix: remove un-necessary imports
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-05 19:03:24 +02:00
6abfd8e44b
chore: pascalCase 2025-07-05 19:02:57 +02:00
441dc0c15c
feat: actually add theme switching :mind_blown:
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-05 17:36:08 +02:00
1066822dd5
feat: use flexboxes instead! 2025-07-05 17:13:17 +02:00
c03f72cecc
feat: implement "hash navigation" for settings 2025-07-05 17:05:40 +02:00
768b011961
feat: add theme previews 2025-07-05 17:02:22 +02:00
94fee82893
refactor: minor refactor 2025-07-05 15:51:09 +02:00
9f1232a668
fix: ensure memberlist isn't taller than the screen
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
i'm not sure why you need to set a max height, this isn't really a great soulution to the problem either, but we need *something* right now
2025-07-05 15:38:00 +02:00
59a2855a1d Merge pull request 'fix build time and version hash being evaluated at runtime' (#17) from settings-metadata into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Reviewed-on: #17
2025-07-05 13:26:20 +00:00
c1021b1192
fix: fix build time and version hash being evaluated at runtime
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-05 15:24:09 +02:00
aa335b086a
feat: use logout endpoint for logout and move old logic to revoke in auth
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-05 02:38:23 +02:00
87f4ecc9dc Merge pull request 'feat: add links, build time, and git hash to settings menu' (#13) from settings-metadata into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Reviewed-on: #13
Reviewed-by: SauceyRed <saucey@saucey.red>
2025-07-04 23:40:23 +00:00
c0df384d7b Merge branch 'main' into settings-metadata
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-04 23:39:54 +00:00
059282706b
fix: remove random accidental import, fix build system
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-05 01:27:04 +02:00
0f11e4e545
feat: update message sending to use JSON to match backend change
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-05 01:10:36 +02:00
c8b7c1d909
feat: add links, build time, and git hash to settings menu
Some checks failed
ci/woodpecker/push/build-and-publish Pipeline failed
ci/woodpecker/pr/build-and-publish Pipeline failed
2025-07-04 13:05:09 +02:00
e9717b137e
fix: minor polish
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-04 10:17:12 +02:00
33dbcb5861
refactor: move yet more stuff into the popup component
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-04 10:00:38 +02:00
d9c6faa6ab
refactor: move stuff from profile to within the crop popup itself
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-04 09:43:05 +02:00
181fcd04db
feat: ensure there's a background colour for the user popup's avatar
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-04 09:27:51 +02:00
168f0f7b12
feat: make pfp cropper into a popup
not sure why i can't resize it though, will probably just "click" later
2025-07-04 09:24:28 +02:00
3c4965c06f
feat: start implementing image cropping when uploading pfp
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
still need to fix the selection to within the canvas boundries, and fix theming
2025-07-04 08:04:50 +02:00
873f1c81a9
fix: remove weird spacing due to weird profile popup inheritance 2025-07-04 06:34:15 +02:00
3c5525d294
feat: display proper error messages when the client fails to update profile info
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-03 20:33:33 +02:00
1dfc9c266c
feat: dynamically load pfp when uploading a new one 2025-07-03 20:29:15 +02:00
0565964b1b
fix: baseURL in theme fetching resulting in double slash if baseURL is set to /
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-03 20:11:20 +02:00
ea44621adc
fix: theme link not taking baseURL into account
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-03 19:54:10 +02:00
d88f5d9aea Merge pull request 'Polish GUI' (#7) from GUI-polish into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Reviewed-on: #7
Reviewed-by: SauceyRed <saucey@saucey.red>
2025-07-03 17:18:25 +00:00
edaf1aa726
Merge branch 'main' into GUI-polish
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-03 19:17:45 +02:00
fea11660c3 Merge pull request 'feat: Add theming' (#8) from theming into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Reviewed-on: #8
Reviewed-by: SauceyRed <saucey@saucey.red>
2025-07-03 17:14:06 +00:00
126ae5e18d
fix: properly insert stylesheet with useHead()
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-03 19:13:29 +02:00
41a0f3f14b Merge branch 'main' into theming
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-03 17:09:10 +00:00
2d424847f7
fix: lazely remove profile popups
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-03 19:04:16 +02:00
4ebb436fb2
fix: remove LLM pilled comments 2025-07-03 19:03:05 +02:00
0befc42ec8
fix: use pascalCase 2025-07-03 19:02:44 +02:00
d90252542d
feat: split account into two pages, to add email updates 2025-07-03 18:59:04 +02:00
6fc8de170b Merge pull request 'feat: Use actual server icons instead of placeholder' (#6) from server-icons into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Reviewed-on: #6
Reviewed-by: SauceyRed <saucey@saucey.red>
2025-07-03 16:44:05 +00:00
c203d26b26
feat: settings button
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
for some reason the settings menu is a bit wonky with this, but that's likely an issue with the settings menu itself interacting with nuxt being nuxt
2025-07-03 18:14:32 +02:00
a9ad6ba535
chore: unwanted leading tab
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-03 18:09:52 +02:00
5a09b39fcf
feat: Add date to messages if they weren't sent today 2025-07-03 18:09:25 +02:00
49ad1d195d
Merge branch 'theming' into GUI-polish 2025-07-03 17:52:21 +02:00
8033fd27e1
feat: Add theming, no settings menu yet
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-02 23:50:09 +02:00
692dd6d1c7
feat: icons, user popus, and polish
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
user popus are WEIRD, and i need help with layering
2025-07-02 21:42:24 +02:00
8c92a7ad0c
feat: Use actual server icons instead of placeholder
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-02 20:17:30 +02:00
cca2c5ffd9 chore: CI thingy
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-23 21:50:40 +02:00
8102412ef2 chore: commit to make the CI work
the CI server was down during the last commit
2025-06-23 21:46:55 +02:00
6141cac41a Merge branch 'settings-page'
All checks were successful
ci/woodpecker/manual/build-and-publish Pipeline was successful
co-authored-by: JustTemmie <git@beaver.mom>
2025-06-23 21:11:08 +02:00
9fd9fb6744 feat: code polishing
co-authored-by: JustTemmie <git@beaver.mom>
co-authored-by: SauceyRed <saucey@saucey.red>
2025-06-23 20:46:05 +02:00
714f75ce12 feat: update button component
co-authored-by: JustTemmie <git@beaver.mom>
2025-06-23 20:02:09 +02:00
5560680635
feat: use dynamic units, minor refactoring 2025-06-11 17:42:35 +02:00
010472c83d
feat: implement scroll position retention 2025-06-07 06:12:47 +02:00
acca8468f0
feat: make <NuxtPage> keepalive 2025-06-07 06:12:46 +02:00
61df171c59
feat: create function to push messages to chat 2025-06-07 06:12:46 +02:00
2c76edaa32
feat: update scrollToBottom() 2025-06-07 06:12:46 +02:00
ccefc8ca19
feat: remove useless css properties 2025-06-07 06:12:46 +02:00
cca348b476
feat: add spacing between grouped messages 2025-06-07 06:12:45 +02:00
c0f4697d00
feat: move some colors to root variables 2025-06-07 06:12:45 +02:00
774e10d68c
fix: automatic auth refresh
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-06 01:57:19 +02:00
d49d533724
feat: change members list back to using flex display
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-06 01:56:15 +02:00
3899843a7c
feat: change message area back to using flex display, improvement for UI 2025-06-06 01:55:18 +02:00
22b43cde79
feat: make profile settings headings into block labels for accessibility
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-06-03 22:04:05 +02:00
d22e77ed14
feat: improve appearance of members list, same method as messages 2025-06-03 20:53:30 +02:00
67e10a4387
feat: refactor to allow more markdown tags, syling changes to make lists and headings not take up as much space 2025-06-03 20:52:42 +02:00
263c823ceb
fix: tabIndex not working after changing messages to use "display: contents;" 2025-06-03 20:50:56 +02:00
82fde5671d
fix: last message from user not having bottom margin 2025-06-03 20:49:41 +02:00
d986f601de
feat: improve 24-hour to 12-hour format conversion by using Date methods
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-03 20:42:34 +02:00
d85eb03ad0
feat: change message history grids to fix scaling of message timestamps
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-03 16:18:13 +02:00
a56e12149b
fix: modify message css to avoid weird line spacing
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-03 06:15:51 +02:00
a38589615b
chore: finnish merge
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-06-02 13:14:08 +02:00
cb1979a941
Merge branch 'settings-page' of ssh://git.gorb.app:2022/gorb/frontend into settings-page 2025-06-02 13:13:44 +02:00
acc4fa14b7
feat: finish up the my account section for now 2025-06-02 13:10:42 +02:00
8a3bb89f8a
Merge branch 'main' into settings-page 2025-06-02 12:36:20 +02:00
4b1f1266b0
fix: type hinting 2025-06-02 12:36:04 +02:00
76952922bf
feat: add tabbing for channels list, members list, and messages
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-02 11:40:57 +02:00
c7e7f33240
feat: add sanitized ref variable
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-02 11:06:08 +02:00
2008033216
feat: add DOMPurify and marked packages
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-02 11:03:54 +02:00
63b780e5ab
feat: implement basic markdown for messages
Some checks failed
ci/woodpecker/push/build-and-publish Pipeline failed
2025-06-02 00:57:13 +02:00
35852d8cad
feat: make URLs in messages become links
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 23:10:08 +02:00
a8e8c6b2ef
feat: define expected types for pfp input element
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 20:34:40 +02:00
0ddddd210e
feat: fucking explode i hate this
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 20:19:04 +02:00
622abc9155
feat: profile page, EXCEPT FUCKING AVATARS AAAAAHHH
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 19:24:50 +02:00
256889a573
feat: favicon!! 2025-06-01 17:46:09 +02:00
6182e00dd9
feat: decrease top padding of members container
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 17:38:03 +02:00
e8d37af75e
feat: update appearance of members list
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 17:35:36 +02:00
532aba5c21
feat: merge two #member-list blocks 2025-06-01 17:03:19 +02:00
f6523ae97f
feat: remove unnecessary styles from client layout 2025-06-01 17:02:58 +02:00
626c1c8453
fix: older messages loading in reverse order
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 16:58:53 +02:00
82796377ee
feat: change non-grouped messages to have margin-top instead of margin-bottom
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 16:57:56 +02:00
cb5360c687
Merge branch 'main' into settings-page 2025-06-01 16:38:02 +02:00
a2c04af8ce
feat: basic user popup implemented
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 16:36:59 +02:00
9fee630a68
fix: some messages not getting type correctly due to nextTick() use
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 16:23:42 +02:00
2c2013fa81
feat: update offset for fetching older messages 2025-06-01 16:22:51 +02:00
5b1d25807e
feat: fix messages not grouping correctly and implement scrolling for loading previous messages
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 16:13:46 +02:00
fc266ffcc3
feat: log out button should work?
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 12:20:21 +02:00
162ca6833f feat: change colour of focused settings category
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 07:28:09 +02:00
39fb0a9eab feat: start of "My Account" page, need API help
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 07:15:20 +02:00
705b37fa06 feat: button components
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 06:13:00 +02:00
5012517e9b feat: Fundementals for settings pages
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 05:04:38 +02:00
55f4f0401b
fix: v not included before API version number
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 01:44:18 +02:00
80f05bb514
feat: make redirects replace current url so you can go back to previous page without being immediately redirected
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 01:42:18 +02:00
038b1af44e
fix: registration and registration detection handling 2025-06-01 01:41:36 +02:00
febdbb9421
feat: add query parameters options to fetchMessages function
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-06-01 01:00:57 +02:00
417a558109
feat: update /me/guilds object response type 2025-06-01 00:59:50 +02:00
4b2c3af5e5
feat: change styling of message area a bit 2025-05-31 17:13:59 +02:00
1c5b136323
feat: change to fetch guild members list and improve appearance of members list 2025-05-31 17:12:07 +02:00
00d6eb0a00
fix: missing return type in some fetch functions 2025-05-31 17:11:08 +02:00
abf3b248c4
feat: redirect to first channel in list when going to server url 2025-05-31 16:35:11 +02:00
5011affd49
feat: remove margin-top from message box and have it be set dynamically on last message 2025-05-31 16:34:25 +02:00
3fd28ed3fc
fix: /guilds/me variable expecting wrong response object type 2025-05-31 16:33:22 +02:00
9a84315b64
fix: apiBase not including api version when automatically fetched 2025-05-31 16:32:04 +02:00
115b7d8341
feat: remove old utils in favor of api composable 2025-05-31 16:31:40 +02:00
310b1cc2df
feat: remove unnecessary log statement
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-05-31 15:28:50 +02:00
04af2be87f
fix: message grouping when messages are received from websocket 2025-05-31 15:28:33 +02:00
4da2ede58a
feat: change grouped messages being called compact to grouped
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-05-31 14:50:44 +02:00
3c65a700ff
fix: fetching from malformed endpoints, again 2025-05-31 14:46:01 +02:00
aa710e0a4d
feat: rename some occurrences of guild back to server 2025-05-31 14:38:49 +02:00
4eeb3a8c2a
fix: fetching from malformed endpoints 2025-05-31 14:31:32 +02:00
fe1474416f
feat: change all occurrences of server with guild 2025-05-31 14:27:37 +02:00
57f31d487e
fix: apiBase not including api version
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-05-31 02:55:44 +02:00
76cede3d67 Merge pull request 'Fix fetched API endpoint not being used correctly' (#4) from dev into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Reviewed-on: #4
2025-05-31 00:40:21 +00:00
a2e64b432d Merge pull request 'Appearance improvements, update of endpoints, and implementation of .well-known for checking API URL' (#3) from dev into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Reviewed-on: #3
2025-05-30 23:21:01 +00:00
381124f778 Merge pull request 'dev' (#2) from dev into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Reviewed-on: #2
2025-05-29 14:48:11 +00:00
54 changed files with 2192 additions and 481 deletions

View file

@ -1,5 +1,7 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup

31
app.vue
View file

@ -1,7 +1,7 @@
<template>
<div>
<Banner v-if="banner" />
<NuxtPage />
<NuxtPage :keepalive="true" />
</div>
</template>
@ -9,6 +9,22 @@
const banner = useState("banner", () => false);
let currentTheme = "dark" // default theme
const savedTheme = localStorage.getItem("selectedTheme");
if (savedTheme) {
currentTheme = savedTheme;
}
const baseURL = useRuntimeConfig().app.baseURL;
useHead({
link: [
{
rel: "stylesheet",
href: `${baseURL}themes/${currentTheme}.css`
}
]
})
</script>
<style>
@ -16,13 +32,14 @@ html,
body {
font-family: Arial, Helvetica, sans-serif;
box-sizing: border-box;
color: rgb(190, 190, 190);
background-color: rgb(30, 30, 30);
color: var(--text-color);
background: var(--optional-body-background);
background-color: var(--chat-background-color);
margin: 0;
}
*:focus-visible {
outline: 1px solid rgb(150, 150, 150);
outline: var(--outline-border);
}
a {
@ -34,15 +51,15 @@ a {
}
.bottom-border {
border-bottom: 1px solid rgb(70, 70, 70);
border-bottom: 1px solid var(--padding-color);
}
.left-border {
border-left: 1px solid rgb(70, 70, 70);
border-left: 1px solid var(--padding-color);
}
.right-border {
border-right: 1px solid rgb(70, 70, 70);
border-right: 1px solid var(--padding-color);
}
.rounded-corners {

51
components/Button.vue Normal file
View file

@ -0,0 +1,51 @@
<template>
<div @click="props.callback()" class="button" :class="props.variant + '-button'">
{{ props.text }}
</div>
</template>
<script lang="ts" setup>
const props = defineProps<{
text: string,
callback: CallableFunction,
variant?: "normal" | "scary" | "neutral",
}>();
</script>
<style scoped>
.button {
cursor: pointer;
background-color: var(--primary-color);
color: var(--text-color);
padding: 0.4em 0.75em;
font-size: 1.1em;
transition: background-color 0.2s;
border-radius: 0.7rem;
text-decoration: none;
display: inline-block;
}
.button:hover {
background-color: var(--primary-highlighted-color);
}
.scary-button {
background-color: red;
}
.scary-button:hover {
background-color: red;
}
.neutral-button {
background-color: var(--accent-color);
}
.neutral-button:hover {
background-color: var(--accent-highlighted-color);
}
</style>

View file

@ -1,11 +1,11 @@
<template>
<div v-if="isCurrentChannel" class="channel-list-link-container rounded-corners current-channel">
<NuxtLink class="channel-list-link" :href="props.href">
<div v-if="isCurrentChannel" class="channel-list-link-container rounded-corners current-channel" tabindex="0">
<NuxtLink class="channel-list-link" :href="props.href" tabindex="-1">
# {{ props.name }}
</NuxtLink>
</div>
<div v-else class="channel-list-link-container rounded-corners">
<NuxtLink class="channel-list-link" :href="props.href">
<div v-else class="channel-list-link-container rounded-corners" tabindex="0">
<NuxtLink class="channel-list-link" :href="props.href" tabindex="-1">
# {{ props.name }}
</NuxtLink>
</div>
@ -23,19 +23,19 @@ const isCurrentChannel = props.uuid == props.currentUuid;
.channel-list-link {
text-decoration: none;
color: inherit;
padding-left: .5dvw;
padding-right: .5dvw;
padding-left: .25em;
padding-right: .25em;
}
.channel-list-link-container {
text-align: left;
display: flex;
height: 4dvh;
height: 1.5em;
white-space: nowrap;
align-items: center;
}
.current-channel {
background-color: rgb(70, 70, 70);
background-color: var(--sidebar-highlighted-background-color);
}
</style>

93
components/CropPopup.vue Normal file
View file

@ -0,0 +1,93 @@
<template>
<div id="fullscreen-container">
<div id="crop-preview">
<img ref="image" :src="imageSrc" style="min-height: 35dvh;">
<Button text="Crop" :callback="cropImage"></Button>
<Button text="Cancel" :callback="closePopup"></Button>
</div>
</div>
</template>
<script lang="ts" setup>
import Cropper from 'cropperjs';
const props = defineProps({
imageSrc: String,
onCrop: Function,
onClose: Function,
});
const image = ref<HTMLImageElement | null>(null);
const cropper = ref<Cropper | null>(null);
watch(image, (newValue) => {
if (newValue) {
cropper.value = new Cropper(newValue)
const cropperCanvas = cropper.value.getCropperCanvas()
const cropperSelection = cropper.value.getCropperSelection()
if (cropperCanvas) {
cropperCanvas.background = false
}
if (cropperSelection) {
cropperSelection.precise = true
cropperSelection.aspectRatio = 1
cropperSelection.initialCoverage = 1
}
}
});
async function cropImage() {
if (cropper) {
const selection = cropper.value?.getCropperSelection();
if (selection) {
const canvas = await selection.$toCanvas({width: 256, height: 256})
canvas.toBlob((blob) => {
if (blob && props.onCrop) {
const reader = new FileReader();
reader.addEventListener("load", () => {
if (reader.result && typeof reader.result === 'string') {
if (props.onCrop) {
props.onCrop(blob, reader.result)
}
}
});
const file = new File([blob], 'preview.png', { type: 'image/png' })
reader.readAsDataURL(file)
}
});
}
}
}
function closePopup() {
if (props.onClose) {
props.onClose();
}
}
</script>
<style scoped>
.button {
margin: 0.2em
}
#fullscreen-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
background: rgba(0, 0, 0, 0.5);
}
#crop-preview {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>

View file

@ -20,7 +20,7 @@ const route = useRoute();
async function generateInvite(): Promise<void> {
const createdInvite: InviteResponse | undefined = await fetchWithApi(
`/servers/${route.params.serverId}/invites`,
`/guilds/${route.params.serverId}/invites`,
{ method: "POST", body: { custom_id: "oijewfoiewf" } }
);

View file

@ -0,0 +1,35 @@
<template>
<div class="member-item" @click="togglePopup" @blur="hidePopup" tabindex="0">
<img v-if="props.member.user.avatar" class="member-avatar" :src="props.member.user.avatar" :alt="props.member.user.display_name ?? props.member.user.username" />
<Icon v-else class="member-avatar" name="lucide:user" />
<span class="member-display-name">{{ props.member.user.display_name || props.member.user.username }}</span>
<UserPopup v-if="isPopupVisible" :user="props.member.user" id="profile-popup" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import type { GuildMemberResponse } from '~/types/interfaces';
import UserPopup from './UserPopup.vue';
const props = defineProps<{
member: GuildMemberResponse
}>();
const isPopupVisible = ref(false);
const togglePopup = () => {
isPopupVisible.value = false;
// isPopupVisible.value = !isPopupVisible.value;
};
const hidePopup = () => {
isPopupVisible.value = false;
};
</script>
<style>
.member-item {
position: relative; /* Set the position to relative for absolute positioning of the popup */
}
</style>

View file

@ -1,40 +1,39 @@
<template>
<div v-if="props.type == 'normal'" class="message normal-message" :class="{ 'message-margin-bottom': props.marginBottom }">
<div v-if="props.type == 'normal'" :id="props.last ? 'last-message' : undefined" class="message normal-message">
<div class="left-column">
<img v-if="props.img" class="message-author-avatar" :src="props.img" :alt="username">
<img v-if="props.img" class="message-author-avatar" :src="props.img" :alt="username" />
<Icon v-else name="lucide:user" class="message-author-avatar" />
</div>
<div class="message-data">
<div class="message-metadata">
<span class="message-author-username">
<span class="message-author-username" tabindex="0">
{{ username }}
</span>
<span class="message-date" :title="date.toString()">
{{ messageDate }}
<span v-if="getDayDifference(date, currentDate) === 1">Yesterday at</span>
<span v-else-if="getDayDifference(date, currentDate) > 1 ">{{ date.toLocaleDateString(undefined) }},</span>
{{ date.toLocaleTimeString(undefined, { timeStyle: "short" }) }}
</span>
</div>
<div class="message-text">
{{ text }}
</div>
<div class="message-text" v-html="sanitized" tabindex="0"></div>
</div>
</div>
<div v-else ref="messageElement" class="message compact-message">
<div v-else ref="messageElement" :id="props.last ? 'last-message' : undefined" class="message grouped-message" :class="{ 'message-margin-bottom': props.marginBottom }">
<div class="left-column">
<div>
<span :class="{ 'invisible': dateHidden }" class="message-date" :title="date.toString()">
{{ messageDate }}
</span>
</div>
<span :class="{ 'invisible': dateHidden }" class="message-date side-message-date" :title="date.toString()">
{{ date.toLocaleTimeString(undefined, { timeStyle: "short" }) }}
</span>
</div>
<div class="message-data">
<div class="message-text">
{{ text }}
</div>
<div class="message-text" :class="$style['message-text']" v-html="sanitized" tabindex="0"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import DOMPurify from 'dompurify';
import { parse } from 'marked';
const props = defineProps<{
class?: string,
img?: string | null,
@ -42,53 +41,62 @@ const props = defineProps<{
text: string,
timestamp: number,
format: "12" | "24",
type: "normal" | "compact",
marginBottom: boolean
type: "normal" | "grouped",
marginBottom: boolean,
last: boolean
}>();
const messageDate = ref<string>();
const messageElement = ref<HTMLDivElement>();
const dateHidden = ref<boolean>(true);
const date = new Date(props.timestamp);
const currentDate: Date = new Date()
console.log("Message.vue: message:", props.text);
console.log("Message.vue: message type:", props.type);
console.log("message:", props.text);
console.log("author:", props.username);
let dateHour = date.getHours();
let dateMinute = date.getMinutes();
if (props.format == "12") {
if (dateHour > 12) {
dateHour = dateHour - 12;
messageDate.value = `${dateHour}:${dateMinute < 10 ? "0" + dateMinute : dateMinute} PM`
} else {
if (dateHour == 0) {
dateHour = 12;
}
messageDate.value = `${dateHour}:${dateMinute < 10 ? "0" + dateMinute : dateMinute} ${dateHour >= 0 && dateHour < 13 ? "AM" : "PM"}`
}
} else {
messageDate.value = `${dateHour}:${dateMinute < 10 ? "0" + dateMinute : dateMinute}`
}
const sanitized = ref<string>();
onMounted(() => {
onMounted(async () => {
const parsed = await parse(props.text, { gfm: true });
sanitized.value = DOMPurify.sanitize(parsed, {
ALLOWED_TAGS: [
"strong", "em", "br", "blockquote",
"code", "ul", "ol", "li", "a", "h1",
"h2", "h3", "h4", "h5", "h6"
],
ALLOW_DATA_ATTR: false,
ALLOW_SELF_CLOSE_IN_ATTR: false,
ALLOWED_ATTR: []
});
console.log("adding listeners")
await nextTick();
messageElement.value?.addEventListener("mouseenter", (e: Event) => {
console.log("mouse enter");
dateHidden.value = false;
});
messageElement.value?.addEventListener("mouseleave", (e: Event) => {
console.log("mouse leave");
dateHidden.value = true;
});
console.log("added listeners");
});
//function toggleTooltip(e: Event) {
// showHover.value = !showHover.value;
//}
function getDayDifference(date1: Date, date2: Date) {
const midnight1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate());
const midnight2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
const timeDifference = midnight2.getTime() - midnight1.getTime();
const dayDifference = timeDifference / (1000 * 60 * 60 * 24);
return Math.round(dayDifference);
}
</script>
<style scoped>
@ -96,12 +104,27 @@ onMounted(() => {
text-align: left;
/* border: 1px solid lightcoral; */
display: grid;
grid-template-columns: 1fr 19fr;
grid-template-columns: 2rem 1fr;
align-items: center;
column-gap: 1dvw;
width: 100%;
overflow-wrap: anywhere;
}
.message-margin-bottom {
margin-bottom: 1dvh;
.message:hover {
background-color: var(--chat-highlighted-background-color);
}
.normal-message {
margin-top: 1dvh;
}
.grouped-message {
margin-top: .3em;
}
#last-message {
margin-bottom: 2dvh;
}
.message-metadata {
@ -115,8 +138,8 @@ onMounted(() => {
margin-left: .5dvw;
display: flex;
flex-direction: column;
gap: 1dvh;
height: 100%;
height: fit-content;
width: 100%;
}
.message-author {
@ -125,15 +148,16 @@ onMounted(() => {
}
.message-author-avatar {
height: 2.3em;
width: 2.3em;
width: 100%;
border-radius: 50%;
}
.left-column {
margin-right: .5dvw;
min-width: 2rem;
display: flex;
justify-content: center;
text-align: center;
align-content: center;
white-space: nowrap;
}
.author-username {
@ -143,14 +167,33 @@ onMounted(() => {
.message-date {
font-size: .7em;
color: rgb(150, 150, 150);
color: var(--secondary-text-color);
cursor: default;
}
.side-message-date {
font-size: .625em;
display: flex;
align-items: center;
align-content: center;
}
/*
.message-date-tooltip {
height: 20px;;
width: 20px;
}
*/
</style>
</style>
<style module>
.message-text ul, h1, h2, h3, h4, h5, h6 {
padding-top: .5em;
padding-bottom: .5em;
margin: 0;
}
.message-text ul {
padding-left: 1em;
}
</style>

View file

@ -1,97 +1,167 @@
<template>
<div id="message-area">
<div id="messages" ref="messagesElement">
<div v-for="(message, i) of messages">
<Message :username="message.user.display_name ?? message.user.username"
:text="message.message" :timestamp="messageTimestamps[message.uuid]" :img="message.user.avatar"
format="12" :type="messagesType[message.uuid]"
:margin-bottom="messages[i + 1] && messagesType[messages[i + 1].uuid] == 'normal'" />
</div>
<Message v-for="(message, i) of messages" :username="message.user.display_name ?? message.user.username"
:text="message.message" :timestamp="messageTimestamps[message.uuid]" :img="message.user.avatar"
format="12" :type="messagesType[message.uuid]"
:margin-bottom="(messages[i + 1] && messagesType[messages[i + 1].uuid] == 'normal') ?? false"
:last="i == messages.length - 1" />
</div>
<div id="message-box" class="rounded-corners">
<form id="message-form" @submit="sendMessage">
<input v-model="messageInput" id="message-box-input" class="rounded-corners" type="text"
name="message-input" autocomplete="off">
<button id="submit-button" type="submit">
<Icon name="lucide:send" />
</button>
<div id="message-box-left-elements">
<span class="message-box-button">
<Icon name="lucide:file-plus-2" />
</span>
</div>
<div id="message-textarea">
<div id="message-textbox-input"
role="textbox" ref="messageTextboxInput"
autocorrect="off" spellcheck="true" contenteditable="true"
@keydown="handleTextboxKeyDown" @input="handleTextboxInput">
</div>
</div>
<div id="message-box-right-elements">
<button class="message-box-button" type="submit">
<Icon name="lucide:send" />
</button>
<span class="message-box-button">
<Icon name="lucide:image-play" />
</span>
</div>
</form>
</div>
</div>
</template>
<script lang="ts" setup>
import type { MessageResponse } from '~/types/interfaces';
import type { MessageResponse, ScrollPosition } from '~/types/interfaces';
import scrollToBottom from '~/utils/scrollToBottom';
const props = defineProps<{ channelUrl: string, amount?: number, offset?: number }>();
const messageTimestamps = ref<Record<string, number>>({});
const messagesType = ref<Record<string, "normal" | "compact">>({});
const messagesType = ref<Record<string, "normal" | "grouped">>({});
const messageGroupingMaxDifference = useRuntimeConfig().public.messageGroupingMaxDifference
const messagesRes: MessageResponse[] | undefined = await fetchWithApi(
`${props.channelUrl}/messages`,
{ query: { "amount": props.amount ?? 100, "offset": props.offset ?? 0 } }
);
if (messagesRes) {
messagesRes.reverse();
console.log("messages res:", messagesRes.map(msg => msg.message));
const firstMessageByUsers = ref<Record<string, MessageResponse | undefined>>({});
for (const message of messagesRes) {
messageTimestamps.value[message.uuid] = uuidToTimestamp(message.uuid);
console.log("message:", message.message);
const firstByUser = firstMessageByUsers.value[message.user.uuid];
if (firstByUser) {
console.log("first by user exists");
if (message.user.uuid != firstByUser.user.uuid) {
console.log("message is by new user, setting their first message")
firstMessageByUsers.value[message.user.uuid] = message;
console.log("RETURNING FALSE");
messagesType.value[message.uuid] = "normal";
continue;
}
} else {
console.log("first by user doesn't exist");
console.log(`setting first post by user ${message.user.username} to "${message.message}" with timestamp ${messageTimestamps.value[message.uuid]}`);
const firstMessageByUsers = ref<Record<string, MessageResponse | undefined>>({});
const previousMessage = ref<MessageResponse>();
function groupMessage(message: MessageResponse, options?: { prevMessage?: MessageResponse, reverse?: boolean }) {
messageTimestamps.value[message.uuid] = uuidToTimestamp(message.uuid);
console.log("message:", message.message);
console.log("author:", message.user.username, `(${message.user.uuid})`);
if (!previousMessage.value || previousMessage.value && message.user.uuid != previousMessage.value.user.uuid) {
console.log("no previous message or author is different than last messsage's");
messagesType.value[message.uuid] = "normal";
previousMessage.value = message;
console.log("set previous message to:", previousMessage.value.message);
console.log(`setting first post by user ${message.user.username} to "${message.message}" with timestamp ${messageTimestamps.value[message.uuid]}`);
firstMessageByUsers.value[message.user.uuid] = message;
return;
}
const firstByUser = firstMessageByUsers.value[message.user.uuid];
if (firstByUser) {
console.log("first by user exists:", firstByUser);
if (message.user.uuid != firstByUser.user.uuid) {
console.log("message is by new user, setting their first message")
firstMessageByUsers.value[message.user.uuid] = message;
console.log("RETURNING FALSE");
messagesType.value[message.uuid] = "normal";
continue;
return;
}
const messageGroupingMaxDifference = useRuntimeConfig().public.messageGroupingMaxDifference;
const prevTimestamp = messageTimestamps.value[firstByUser.uuid];
const timestamp = messageTimestamps.value[message.uuid];
console.log("first message timestamp:", prevTimestamp);
console.log("timestamp:", timestamp);
const diff = (timestamp - prevTimestamp);
console.log("min diff:", messageGroupingMaxDifference);
console.log("diff:", diff);
const lessThanMax = diff <= messageGroupingMaxDifference;
console.log("group?", lessThanMax);
if (!lessThanMax) {
console.log("diff exceeds max");
console.log(`setting first post by user ${message.user.username} to "${message.message}" with timestamp ${messageTimestamps.value[message.uuid]}`)
firstMessageByUsers.value[message.user.uuid] = message;
messagesType.value[message.uuid] = "normal";
continue;
}
console.log("RETURNING " + lessThanMax.toString().toUpperCase());
messagesType.value[message.uuid] = "compact";
} else {
console.log("first by user doesn't exist");
console.log(`setting first post by user ${message.user.username} to "${message.message}" with timestamp ${messageTimestamps.value[message.uuid]}`);
firstMessageByUsers.value[message.user.uuid] = message;
console.log("RETURNING FALSE");
messagesType.value[message.uuid] = "normal";
return;
}
const prevTimestamp = messageTimestamps.value[firstByUser.uuid];
const timestamp = messageTimestamps.value[message.uuid];
console.log("first message timestamp:", prevTimestamp);
console.log("timestamp:", timestamp);
const diff = Math.abs(timestamp - prevTimestamp);
console.log("min diff:", messageGroupingMaxDifference);
console.log("diff:", diff);
const lessThanMax = diff <= messageGroupingMaxDifference;
console.log("group?", lessThanMax);
if (!lessThanMax) {
console.log("diff exceeds max");
console.log(`setting first post by user ${message.user.username} to "${message.message}" with timestamp ${messageTimestamps.value[message.uuid]}`)
firstMessageByUsers.value[message.user.uuid] = message;
messagesType.value[message.uuid] = "normal";
return;
}
console.log("RETURNING " + lessThanMax.toString().toUpperCase());
messagesType.value[message.uuid] = "grouped";
}
if (messagesRes) {
messagesRes.reverse();
console.log("messages res:", messagesRes.map(msg => msg.message));
for (const message of messagesRes) {
groupMessage(message);
}
}
function pushMessage(message: MessageResponse) {
groupMessage(message);
messages.value.push(message);
}
function handleTextboxKeyDown(event: KeyboardEvent) {
if (event.key === "Enter" && event.shiftKey && messageTextboxInput.value) {
// this enters a newline, due to not preventing default
} else if (event.key === "Enter") {
event.preventDefault()
sendMessage(event)
}
adjustTextboxHeight()
}
function handleTextboxInput() {
if (messageTextboxInput.value) {
messageInput.value = messageTextboxInput.value.innerText;
}
adjustTextboxHeight()
}
// this technically uses pixel units, but it's still set using dynamic units
function adjustTextboxHeight() {
if (messageTextboxInput.value && messageTextboxDisplay.value) {
messageTextboxInput.value.style.height = "auto"
messageTextboxInput.value.style.height = `${messageTextboxInput.value.scrollHeight}px`
}
}
const messages = ref<MessageResponse[]>([]);
const messageInput = ref<string>();
const messageInput = ref<string>("");
const messagesElement = ref<HTMLDivElement>();
const messageTextboxInput = ref<HTMLDivElement>();
const messageTextboxDisplay = ref<HTMLDivElement>();
if (messagesRes) messages.value = messagesRes;
const accessToken = useCookie("access_token").value;
const apiBase = useCookie("api_base").value;
const { refresh } = useAuth();
const { fetchMessages } = useApi();
let ws: WebSocket;
@ -115,12 +185,14 @@ if (accessToken && apiBase) {
console.log("event data:", event.data);
console.log("message uuid:", event.data.uuid);
const parsedData = JSON.parse(event.data);
messageTimestamps.value[parsedData.uuid] = uuidToTimestamp(parsedData.uuid);
messages.value.push(parsedData);
console.log("parsed message type:", messagesType.value[parsedData.uuid]);
console.log("parsed message timestamp:", messageTimestamps.value[parsedData.uuid]);
pushMessage(parsedData);
await nextTick();
if (messagesElement.value) {
console.log("scrolling to bottom");
scrollToBottom(messagesElement);
scrollToBottom(messagesElement.value);
}
});
@ -130,75 +202,173 @@ if (accessToken && apiBase) {
function sendMessage(e: Event) {
e.preventDefault();
const text = messageInput.value;
console.log("text:", text);
if (text) {
ws.send(text);
messageInput.value = "";
console.log("MESSAGE SENT!!!");
if (messageInput.value && messageInput.value.trim() !== "") {
const message = {
message: messageInput.value.trim().replace(/\n/g, "<br>") // trim, and replace \n with <br>
}
console.log("message:", message);
ws.send(JSON.stringify(message));
// reset input field
messageInput.value = ""
if (messageTextboxInput.value) {
messageTextboxInput.value.innerText = ""
}
adjustTextboxHeight()
}
}
const route = useRoute();
onMounted(async () => {
if (import.meta.server) return;
if (messagesElement.value) {
scrollToBottom(messagesElement);
scrollToBottom(messagesElement.value);
let fetched = false;
const amount = messages.value.length;
let offset = messages.value.length;
messagesElement.value.addEventListener("scroll", async (e) => {
if (e.target) {
const target = e.target as HTMLDivElement;
if (target.scrollTop <= target.scrollHeight * 0.1) {
if (fetched) return;
fetched = true;
console.log("scroll height is at 10% or less");
//console.log("current oldest:", currentOldestMessage);
const olderMessages = await fetchMessages(route.params.channelId as string, { amount, offset });
if (olderMessages) {
olderMessages.reverse();
console.log("older messages:", olderMessages);
if (olderMessages.length == 0) return;
olderMessages.reverse();
for (const [i, oldMessage] of olderMessages.entries()) {
console.log("old message:", oldMessage);
messages.value.unshift(oldMessage);
for (const message of messages.value) {
groupMessage(message);
}
}
offset += offset;
}
} else {
fetched = false;
}
}
});
}
});
let scrollPosition = ref<Record<string, ScrollPosition>>({});
onActivated(async () => {
await nextTick();
console.log("scroll activated");
if (messagesElement.value) {
if (scrollPosition.value[route.params.channelId as string]) {
console.log("saved scroll position:", scrollPosition.value);
setScrollPosition(messagesElement.value, scrollPosition.value[route.params.channelId as string]);
console.log("scrolled to saved scroll position");
} else {
scrollToBottom(messagesElement.value);
console.log("scrolled to bottom");
}
}
});
const router = useRouter();
router.beforeEach((to, from, next) => {
console.log("scroll hi");
if (messagesElement.value && from.params.channelId) {
scrollPosition.value[from.params.channelId as string] = getScrollPosition(messagesElement.value)
console.log("set saved scroll position to:", scrollPosition.value);
}
next()
})
</script>
<style scoped>
#message-area {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 8fr 1fr;
justify-content: space-between;
display: flex;
flex-direction: column;
padding-left: 1dvw;
padding-right: 1dvw;
overflow: hidden;
flex-grow: 1;
}
#message-box {
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
border: 1px solid rgb(70, 70, 70);
padding-bottom: 1dvh;
padding-top: 1dvh;
margin-bottom: 1dvh;
margin-top: 2dvh;
margin-bottom: 2dvh;
margin-left: 1dvw;
margin-right: 1dvw;
padding-left: 2%;
padding-right: 2%;
align-items: center;
color: var(--text-color);
border: 1px solid var(--padding-color);
background-color: var(--chatbox-background-color);
}
#message-form {
display: flex;
justify-content: center;
flex-direction: row;
gap: .55em;
}
#message-box-input {
width: 80%;
background-color: rgb(50, 50, 50);
#message-textarea {
flex-grow: 1;
min-height: 2.35em;
}
#message-textbox-input {
width: 100%;
max-height: 50dvh;
padding: 0.5em 0;
user-select: text;
font-family: inherit;
font-size: inherit;
line-height: normal;
border: none;
color: inherit;
padding-left: 1dvw;
padding-right: 1dvw;
background-color: #40404000; /* completely transparent colour */
text-align: left;
word-break: break-word;
overflow-wrap: break-word;
overflow-y: auto;
}
#message-box-left-elements, #message-box-right-elements {
display: flex;
align-items: end;
}
#messages {
overflow-y: scroll;
display: flex;
flex-direction: column;
gap: 1dvh;
padding-left: 1dvw;
padding-right: 1dvw;
}
#submit-button {
.message-box-button {
background-color: inherit;
border: none;
color: rgb(200, 200, 200);
color: var(--primary-color);
transition: color 100ms;
font-size: 1.5em;
}
#submit-button:hover {
color: rgb(255, 255, 255);
.message-box-button:hover {
color: var(--primary-highlighted-color);
cursor: pointer;
}
</style>

View file

@ -0,0 +1,100 @@
<template>
<div>
<h1>Appearance</h1>
<p class="subtitle">THEMES</p>
<div class="themes">
<div v-for="theme of themes" class="theme-preview-container">
<span class="theme-preview"
:title="theme.displayName"
:style="{background:`linear-gradient(${theme.previewGradient})`}"
@click="changeTheme(theme.id, theme.themeUrl)"
>
<span class="theme-title" :style="{color:`${theme.complementaryColor}`}">
{{ theme.displayName }}
</span>
</span>
</div>
</div>
<p class="subtitle">ICONS</p>
<div class="themes">
</div>
</div>
</template>
<script lang="ts" setup>
const runtimeConfig = useRuntimeConfig()
const defaultThemes = runtimeConfig.public.defaultThemes
const baseURL = runtimeConfig.app.baseURL;
let themeLinkElement: HTMLLinkElement | null = null;
const themes: Array<Theme> = []
interface Theme {
id: string
displayName: string
previewGradient: string
complementaryColor: string
themeUrl: string
}
function changeTheme(id: string, url: string) {
if (themeLinkElement && themeLinkElement.getAttribute('href') === `${baseURL}themes/${url}`) {
return;
}
localStorage.setItem("selectedTheme", id);
// if the theme didn't originally load for some reason, create it
if (!themeLinkElement) {
themeLinkElement = document.createElement('link');
themeLinkElement.rel = 'stylesheet';
document.head.appendChild(themeLinkElement);
}
themeLinkElement.href = `${baseURL}themes/${url}`;
}
async function fetchThemes() {
for (const theme of defaultThemes) {
const themeConfig = await $fetch(`${baseURL}themes/${theme}.json`) as Theme
themeConfig.id = theme
themes.push(themeConfig)
}
}
await fetchThemes()
</script>
<style scoped>
.themes {
display: flex;
}
.theme-preview-container {
margin: .5em;
width: 5em;
height: 5em;
}
.theme-preview {
width: 5em;
height: 5em;
border-radius: 100%;
border: .1em solid var(--primary-color);
display: inline-block;
text-align: center;
align-content: center;
cursor: pointer;
}
.theme-title {
font-size: .8em;
line-height: 5em; /* same height as the parent to centre it vertically */
}
</style>

View file

@ -0,0 +1,12 @@
<template>
<div>
<h1>Keybinds (TBA)</h1>
</div>
</template>
<script lang="ts" setup>
</script>
<style scoped>
</style>

View file

@ -0,0 +1,12 @@
<template>
<div>
<h1>Language (TBA)</h1>
</div>
</template>
<script lang="ts" setup>
</script>
<style scoped>
</style>

View file

@ -0,0 +1,12 @@
<template>
<div>
<h1>Notifications (TBA)</h1>
</div>
</template>
<script lang="ts" setup>
</script>
<style scoped>
</style>

View file

@ -0,0 +1,97 @@
<template>
<div v-if="user">
<h1>Account</h1>
<p class="subtitle">E-MAIL</p>
<input id="profile-about-me-input" class="profile-data-input" type="text" v-model="user.email" placeholder="john@example.org" />
<br>
<Button text="Submit" :callback=changeEmail style="margin-top: .4em"></Button>
<p class="subtitle">PASSWORD</p>
<Button text="Reset Password" :callback=resetPassword></Button>
<p class="subtitle">ACCOUNT DELETION</p>
<Button text="Delete Account (tbd)" :callback=deleteAccount variant="scary"></Button>
</div>
</template>
<script lang="ts" setup>
import Button from '~/components/Button.vue';
import type { UserResponse } from '~/types/interfaces';
const { fetchUser } = useAuth();
const user: UserResponse | undefined = await fetchUser()
if (!user) {
alert("could not fetch user info, aborting :(")
}
async function changeEmail() {
if (!user) return;
const formData = new FormData()
const bytes = new TextEncoder().encode(JSON.stringify({
email: user.email,
}));
formData.append('json', new Blob([bytes], { type: 'application/json' }));
try {
await fetchWithApi('/me', {
method: 'PATCH',
body: formData
})
const apiBase = useCookie("api_base").value;
if (apiBase) {
const stats = await useApi().fetchInstanceStats(apiBase);
if (stats.email_verification_required) {
return window.location.reload();
}
}
alert('success!!')
} catch (error: any) {
if (error?.response?.status !== 200) {
alert(`error ${error?.response?.status} met whilst trying to update profile info`)
}
}
};
async function resetPassword () {
await fetchWithApi("/auth/reset-password", {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
query: {
identifier: user?.username
}
});
}
async function deleteAccount() {
alert("TBD")
}
</script>
<style scoped>
.user-data-fields {
min-width: 35dvw;
max-width: 35dvw;
}
.profile-data-input {
min-width: 30dvw;
margin: 0.07dvh;
padding: 0.1dvh 0.7dvw;
height: 2.5em;
font-size: 1em;
border-radius: 8px;
border: none;
color: var(--text-color);
background-color: var(--accent-color);
}
</style>

View file

@ -0,0 +1,12 @@
<template>
<div>
<h1>Connections (TBA)</h1>
</div>
</template>
<script lang="ts" setup>
</script>
<style scoped>
</style>

View file

@ -0,0 +1,12 @@
<template>
<div>
<h1>Devices (TBA)</h1>
</div>
</template>
<script lang="ts" setup>
</script>
<style scoped>
</style>

View file

@ -0,0 +1,13 @@
<template>
<div>
<h1>Privacy (TBA)</h1>
</div>
</template>
<script lang="ts" setup>
import Button from '~/components/Button.vue';
</script>
<style scoped>
</style>

View file

@ -0,0 +1,151 @@
<template>
<div>
<h1>Profile</h1>
<div class="profile-container">
<div class="user-data-fields" v-if="user">
<p class="subtitle">AVATAR</p>
<Button text="Change Avatar" :callback="changeAvatar" style="margin-right: 0.8dvw;"></Button>
<Button text="Remove Avatar" :callback="removeAvatar" variant="neutral"></Button>
<label for="profile-display-name-input" class="subtitle">DISPLAY NAME</label>
<input id="profile-display-name-input" class="profile-data-input" type="text" v-model="user.display_name" placeholder="Enter display name" />
<label for="profile-username-input" class="subtitle">USERNAME</label>
<input id="profile-username-input" class="profile-data-input" type="text" v-model="user.username" placeholder="Enter username" />
<label for="profile-pronouns-input" class="subtitle">PRONOUNS</label>
<input id="profile-pronouns-input" class="profile-data-input" type="text" v-model="user.pronouns" placeholder="Enter pronouns" />
<label for="profile-about-me-input" class="subtitle">ABOUT ME</label>
<input id="profile-about-me-input" class="profile-data-input" type="text" v-model="user.about" placeholder="About me" />
<Button style="margin-top: 2dvh" text="Save Changes" :callback="saveChanges"></Button>
</div>
<UserPopup v-if="user" :user="user" id="profile-popup"></UserPopup>
</div>
</div>
<CropPopup
v-if="isCropPopupVisible"
:imageSrc="cropImageSrc"
:onCrop="handleCrop"
:onClose="closeCropPopup"
/>
</template>
<script lang="ts" setup>
import type { UserResponse } from '~/types/interfaces';
let newPfpFile: File;
const isCropPopupVisible = ref(false);
const cropImageSrc = ref("")
;
const { fetchUser } = useAuth();
const user: UserResponse | undefined = await fetchUser()
if (!user) {
alert("could not fetch user info, aborting :(")
}
async function saveChanges() {
if (!user) return;
const formData = new FormData()
if (newPfpFile) {
formData.append("avatar", newPfpFile)
}
const bytes = new TextEncoder().encode(JSON.stringify({
display_name: user.display_name,
username: user.username,
pronouns: user.pronouns,
about: user.about,
}));
formData.append('json', new Blob([bytes], { type: 'application/json' }));
try {
await fetchWithApi('/me', {
method: 'PATCH',
body: formData
})
alert('success!!')
} catch (error: any) {
if (error?.response?.status !== 200) {
alert(`error ${error?.response?.status} met whilst trying to update profile info\n"${error?.response._data?.message}"`)
}
}
};
async function removeAvatar() {
alert("TBD")
// await fetchWithApi(`/auth/reset-password`);
}
async function changeAvatar() {
if (!user) return;
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.addEventListener("change", (e: Event) => {
if (input.files?.length && input.files.length > 0) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.addEventListener("load", () => {
if (reader.result && typeof reader.result === 'string') {
cropImageSrc.value = reader.result;
isCropPopupVisible.value = true;
}
});
reader.readAsDataURL(file);
}
})
input.click()
}
function handleCrop(blob: Blob, url: string) {
if (!user) return;
user.avatar = url;
newPfpFile = new File([blob], 'avatar.png', { type: 'image/png' })
closeCropPopup()
}
function closeCropPopup() {
isCropPopupVisible.value = false
}
</script>
<style scoped>
.profile-container {
display: flex;
}
.user-data-fields {
min-width: 35dvw;
max-width: 35dvw;
}
.profile-data-input {
min-width: 30dvw;
margin: 0.07dvh;
padding: 0.1dvh 0.7dvw;
height: 2.5em;
font-size: 1em;
border-radius: 8px;
border: none;
color: var(--text-color);
background-color: var(--accent-color);
}
#profile-popup {
margin-left: 2dvw;
}
</style>

20
components/UserArea.vue Normal file
View file

@ -0,0 +1,20 @@
<template>
<div id="user-panel">
HELLO!!
</div>
</template>
<script lang="ts" setup>
import type { UserResponse } from '~/types/interfaces';
const props = defineProps<{
user: UserResponse,
}>();
</script>
<style scoped>
#user-panel {
width: 100%;
}
</style>

89
components/UserPopup.vue Normal file
View file

@ -0,0 +1,89 @@
<template>
<div id="profile-popup">
<img v-if="props.user.avatar" id="avatar" :src="props.user.avatar" alt="profile avatar">
<Icon v-else id="avatar" name="lucide:user" />
<div id="cover-color"></div>
<div id="main-body">
<p id="display-name">
<strong>{{ props.user.display_name }}</strong>
</p>
<p id="username-and-pronouns">
{{ props.user.username }}
<span v-if="props.user.pronouns"> - {{ props.user.pronouns }}</span>
</p>
<div id="about-me" v-if="props.user.about">
{{ props.user.about }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { UserResponse } from '~/types/interfaces';
const { fetchMembers } = useApi();
const props = defineProps<{
user: UserResponse
}>();
</script>
<style scoped>
#profile-popup {
min-width: 300px;
max-width: 300px;
border-radius: 8px;
position: relative;
display: flex;
flex-direction: column;
}
#cover-color {
border-radius: 12px 12px 0 0;
min-height: 60px;
background-color: var(--primary-color);
}
#main-body {
border-radius: 0 0 12px 12px;
padding: 12px;
min-height: 280px;
background-color: var(--accent-color);
overflow-wrap: break-word;
hyphens: manual;
}
#avatar {
width: 96px;
height: 96px;
border: 5px solid #4b3018;
background-color: var(--secondary-color);
border-radius: 100%;
position: absolute;
left: 16px;
top: 16px;
}
#display-name {
margin-top: 60px;
margin-bottom: 0;
font-size: 28px;
}
#username-and-pronouns {
margin: 2px;
font-size: 16px;
}
#about-me {
background-color: var(--secondary-color);
border-radius: 12px;
margin-top: 32px;
padding: 16px;
font-size: 16px;
}
</style>

67
composables/api.ts Normal file
View file

@ -0,0 +1,67 @@
import type { ChannelResponse, GuildMemberResponse, GuildResponse, MessageResponse, StatsResponse } from "~/types/interfaces";
export const useApi = () => {
async function fetchGuilds(): Promise<GuildResponse[] | undefined> {
return await fetchWithApi(`/guilds`);
}
async function fetchGuild(guildId: string): Promise<GuildResponse | undefined> {
return await fetchWithApi(`/guilds/${guildId}`);
}
async function fetchChannels(guildId: string): Promise<ChannelResponse[] | undefined> {
return await fetchWithApi(`/guilds/${guildId}/channels`);
}
async function fetchChannel(channelId: string): Promise<ChannelResponse | undefined> {
return await fetchWithApi(`/channels/${channelId}`)
}
async function fetchMembers(guildId: string): Promise<GuildMemberResponse[] | undefined> {
return await fetchWithApi(`/guilds/${guildId}/members`);
}
async function fetchMember(guildId: string, memberId: string): Promise<GuildMemberResponse | undefined> {
return await fetchWithApi(`/guilds/${guildId}/members/${memberId}`);
}
async function fetchUsers() {
return await fetchWithApi(`/users`);
}
async function fetchUser(userId: string) {
return await fetchWithApi(`/users/${userId}`);
}
async function fetchMessages(channelId: string, options?: { amount?: number, offset?: number }): Promise<MessageResponse[] | undefined> {
return await fetchWithApi(`/channels/${channelId}/messages`, { query: { amount: options?.amount ?? 100, offset: options?.offset ?? 0 } });
}
async function fetchMessage(channelId: string, messageId: string): Promise<MessageResponse | undefined> {
return await fetchWithApi(`/channels/${channelId}/messages/${messageId}`);
}
async function fetchInstanceStats(apiBase: string): Promise<StatsResponse> {
return await $fetch(`${apiBase}/stats`, { method: "GET" });
}
async function sendVerificationEmail(): Promise<void> {
const email = useAuth().user.value?.email;
await fetchWithApi("/auth/verify-email", { method: "POST", body: { email } });
}
return {
fetchGuilds,
fetchGuild,
fetchChannels,
fetchChannel,
fetchMembers,
fetchMember,
fetchUsers,
fetchUser,
fetchMessages,
fetchMessage,
fetchInstanceStats,
sendVerificationEmail
}
}

View file

@ -7,6 +7,7 @@ export const useAuth = () => {
async function clearAuth() {
accessToken.value = null;
user.value = null;
await navigateTo("/login");
}
async function register(username: string, email: string, password: string) {
@ -37,13 +38,19 @@ export const useAuth = () => {
//await fetchUser();
}
async function logout(password: string) {
console.log("password:", password);
async function logout() {
console.log("access:", accessToken.value);
const hashedPass = await hashPassword(password);
console.log("hashed");
const res = await fetchWithApi("/auth/revoke", {
await fetchWithApi("/auth/logout", { method: "GET", credentials: "include" });
clearAuth();
return await navigateTo("/login");
}
async function revoke(password: string) {
const hashedPass = await hashPassword(password);
await fetchWithApi("/auth/revoke", {
method: "POST",
body:
{
@ -54,10 +61,6 @@ export const useAuth = () => {
clearAuth();
}
async function revoke() {
clearAuth();
}
async function refresh() {
console.log("refreshing");
const res = await fetchWithApi("/auth/refresh", {
@ -75,7 +78,7 @@ export const useAuth = () => {
async function fetchUser() {
if (!accessToken.value) return;
console.log("fetchuser access token:", accessToken.value);
const res = await fetchWithApi("/users/me") as UserResponse;
const res = await fetchWithApi("/me") as UserResponse;
user.value = res;
return user.value;
}
@ -88,6 +91,20 @@ export const useAuth = () => {
return user.value;
}
// as in email the password link
async function resetPassword() {
// ...
}
async function disableAccount() {
// ...
}
async function deleteAccount() {
// ...
}
return {
accessToken,
register,

View file

@ -20,30 +20,7 @@
<slot />
</div>
<div v-if="instanceUrl">
Instance URL is set to {{ instanceUrl }}
</div>
<div v-if="auth.accessToken.value">
You're logged in!
<form @submit="logout">
<div>
<label for="logout-password">Password</label>
<br>
<input type="password" name="logout-password" id="logout-password" v-model="form.password"
required>
</div>
<div>
<button type="submit">Log out</button>
</div>
</form>
<div>
<button @click="refresh">Refresh</button>
</div>
<div>
<button @click="showUser">Show user</button>
</div>
<div>
<button @click="getUser">Get me</button>
</div>
Instance URL is set to <span style="color: var(--primary-color);">{{ instanceUrl }}</span>
</div>
</div>
</div>
@ -55,28 +32,14 @@ import { FetchError } from 'ofetch';
const instanceUrl = ref<string | null | undefined>(null);
const instanceUrlInput = ref<string>();
const instanceError = ref<string>();
const requestUrl = useRequestURL();
const apiVersion = useRuntimeConfig().public.apiVersion;
const apiBase = useCookie("api_base");
const gorbTxtError = ref<string>("");
const registrationEnabled = useState("registrationEnabled", () => true);
const auth = useAuth();
const { status, data: gorbTxt } = await useFetch(`${requestUrl.protocol}//${requestUrl.host}/.well-known/gorb.txt`, { responseType: "text" });
if (status.value == "success" && gorbTxt.value) {
console.log("got gorb.txt:", gorbTxt.value);
const parsed = parseWellKnown(gorbTxt.value as string);
if (parsed.ApiBaseUrl) {
apiBase.value = parsed.ApiBaseUrl;
console.log("set apiBase to:", parsed.ApiBaseUrl);
}
} else {
gorbTxtError.value = "Failed to find that instance.";
}
onMounted(async () => {
const cookie = useCookie("instance_url").value;
instanceUrl.value = cookie;
console.log(cookie);
instanceUrl.value = useCookie("instance_url").value;
console.log("set instance url to:", instanceUrl.value);
});
@ -84,20 +47,25 @@ async function selectInstance(e: Event) {
e.preventDefault();
console.log("trying input instance");
if (instanceUrlInput.value) {
console.log("input has value");
const gorbTxtUrl = new URL(`/.well-known/gorb.txt`, instanceUrlInput.value);
console.log("input has value");
try {
console.log("trying to get gorb.txt:", gorbTxtUrl);
const res = await $fetch.raw(gorbTxtUrl.href, { responseType: "text" });
const parsed = parseWellKnown(res._data as string);
console.log("parsed:", parsed);
if (parsed.ApiBaseUrl) {
apiBase.value = parsed.ApiBaseUrl;
apiBase.value = `${parsed.ApiBaseUrl}/v${apiVersion}`;
console.log("set apiBase to:", parsed.ApiBaseUrl);
const origin = new URL(res.url).origin;
instanceUrl.value = origin;
useCookie("instance_url").value = origin;
console.log("set instance url to:", origin);
const stats = await useApi().fetchInstanceStats(apiBase.value);
if (stats) {
registrationEnabled.value = stats.registration_enabled;
console.log("set registration enabled value to:", stats.registration_enabled);
}
return;
}
instanceError.value = "That URL is not a valid instance.";
@ -154,18 +122,22 @@ async function showUser(e: Event) {
align-items: center;
}
#auth-form-container,
#auth-form-container form {
#auth-form-container {
display: flex;
width: 50dvw;
width: 20dvw;
flex-direction: column;
align-items: center;
text-align: center;
gap: 1em;
margin-bottom: 2dvh;
}
#auth-form-container form {
display: flex;
flex-direction: column;
text-align: left;
margin-top: 10dvh;
gap: 1em;
}
#instance-error-container {

View file

@ -3,83 +3,46 @@
<div :class="{ hidden: loading, visible: !loading }" id="client-root">
<div id="homebar">
<div class="homebar-item">
main bar
</div>
</div>
<div id="left-column">
<NuxtLink id="home-button" href="/">
<Icon name="lucide:house" class="white" size="2rem" />
</NuxtLink>
<div id="servers-list">
<NuxtLink v-for="server of servers" :href="`/servers/${server.uuid}`">
<Icon name="lucide:server" class="white" size="2rem" />
</NuxtLink>
<marquee>
gorb!!!!!
</marquee>
</div>
</div>
<slot />
<div id = "page-content">
<div id="left-column">
<NuxtLink id="home-button" href="/">
<img class="sidebar-icon" src="/public/icon.svg"/>
</NuxtLink>
<div id="servers-list">
<NuxtLink v-for="guild of guilds" :href="`/servers/${guild.uuid}`">
<img v-if="guild.icon" class="sidebar-icon" :src="guild.icon" :alt="guild.name"/>
<Icon v-else name="lucide:server" class="sidebar-icon white" :alt="guild.name" />
</NuxtLink>
</div>
<NuxtLink id="settings-menu" href="/settings">
<Icon name="lucide:settings" class="sidebar-icon white" alt="Settings menu" />
</NuxtLink>
</div>
<slot />
</div>
</div>
</template>
<script lang="ts" setup>
import type { GuildResponse } from '~/types/interfaces';
const loading = useState("loading", () => false);
const servers: GuildResponse[] | undefined = await fetchWithApi("/me/guilds");
//const servers = await fetchWithApi("/servers") as { uuid: string, name: string, description: string }[];
//console.log("servers:", servers);
const members = [
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
},
{
id: "3287484395",
displayName: "SauceyRed"
}
];
const guilds: GuildResponse[] | undefined = await fetchWithApi("/me/guilds");
</script>
<style>
#client-root {
/* border: 1px solid white; */
height: 100dvh;
display: grid;
grid-template-columns: 1fr 4fr 18fr 4fr;
grid-template-rows: 4dvh auto;
width: 100dvw;
display: flex;
flex-direction: column;
text-align: center;
}
.hidden {
@ -88,91 +51,77 @@ const members = [
.visible {
opacity: 100%;
transition-duration: 500ms;
transition: opacity 500ms;
}
#homebar {
grid-row: 1;
grid-column: 1 / -1;
min-height: 4dvh;
display: flex;
justify-content: space-evenly;
align-items: center;
background: var(--optional-topbar-background);
background-color: var(--topbar-background-color);
border-bottom: 1px solid var(--padding-color);
padding-left: 5dvw;
padding-right: 5dvw;
}
#client-root>div:nth-child(-n+4) {
border-bottom: 1px solid rgb(70, 70, 70);
.homebar-item {
width: 100dvw;
}
#__nuxt {
#page-content {
display: flex;
flex-flow: column;
}
.grid-column {
padding-top: 1dvh;
}
#home {
padding-left: .5dvw;
padding-right: .5dvw;
}
#current-info {
grid-column: 2;
grid-row: 1;
}
#test {
grid-column: 3;
grid-row: 1;
}
.member-item {
display: flex;
justify-content: center;
align-items: center;
}
#message-history,
#members-list {
padding-top: 3dvh;
}
#message-history {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-left: 3dvw;
padding-right: 3dvw;
flex-direction: row;
flex-grow: 1;
overflow: auto;
}
#left-column {
display: flex;
flex-direction: column;
gap: 2dvh;
padding-left: .5dvw;
padding-right: .5dvw;
border-right: 1px solid rgb(70, 70, 70);
padding-top: 1.5dvh;
}
#middle-left-column {
padding-left: 1dvw;
padding-right: 1dvw;
border-right: 1px solid rgb(70, 70, 70);
}
#home-button {
border-bottom: 1px solid rgb(70, 70, 70);
padding-bottom: 1dvh;
gap: .75em;
padding-left: .25em;
padding-right: .25em;
padding-top: .5em;
border-right: 1px solid var(--padding-color);
background: var(--optional-sidebar-background);
background-color: var(--sidebar-background-color);
}
#servers-list {
display: flex;
flex-direction: column;
gap: 1dvh;
gap: 1em;
width: 3.2rem;
padding-top: .5em;
}
#middle-left-column {
padding-left: .25em;
padding-right: .25em;
border-right: 1px solid var(--padding-color);
min-width: 13em;
max-width: 13em;
overflow-y: scroll;
overflow-x: hidden;
}
.sidebar-icon {
width: 3rem;
height: 3rem;
overflow-y: scroll;
overflow-x: hidden;
}
#home-button {
border-bottom: 1px solid var(--padding-color);
padding-bottom: .375em;
}
#settings-menu {
position: absolute;
bottom: .25em;
}
</style>

View file

@ -2,7 +2,48 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
console.log("to.fullPath:", to.fullPath);
const loading = useState("loading");
const accessToken = useCookie("access_token").value;
if (["/login", "/register"].includes(to.path)) {
const apiBase = useCookie("api_base").value;
const { fetchInstanceStats } = useApi();
console.log("[AUTH] instance url:", apiBase);
if (apiBase && !Object.keys(to.query).includes("special") && to.path != "/verify-email") {
const user = await useAuth().getUser();
const stats = await fetchInstanceStats(apiBase);
console.log("[AUTH] stats:", stats);
console.log("[AUTH] email verification check:", user?.email && !user.email_verified && stats.email_verification_required);
if (user?.email && !user.email_verified && stats.email_verification_required) {
return await navigateTo("/register?special=verify_email");
}
}
if (["/login", "/register"].includes(to.path) && !Object.keys(to.query).includes("special")) {
console.log("path is login or register");
const apiBase = useCookie("api_base");
console.log("apiBase gotten:", apiBase.value);
if (!apiBase.value) {
const requestUrl = useRequestURL();
console.log("request url:", requestUrl.href);
const apiVersion = useRuntimeConfig().public.apiVersion;
console.log("api version:", apiVersion);
console.log("apiBase not set");
const { status, data: gorbTxt } = await useFetch(`${requestUrl.protocol}//${requestUrl.host}/.well-known/gorb.txt`, { responseType: "text" });
if (status.value == "success" && gorbTxt.value) {
console.log("got gorb.txt:", gorbTxt.value);
const parsed = parseWellKnown(gorbTxt.value as string);
if (parsed.ApiBaseUrl) {
apiBase.value = `${parsed.ApiBaseUrl}/v${apiVersion}`;
console.log("set apiBase to:", parsed.ApiBaseUrl);
console.log("hHEYOO");
const instanceUrl = useCookie("instance_url");
console.log("hHEYOO 2");
console.log("instance url:", instanceUrl.value);
if (!instanceUrl.value) {
instanceUrl.value = `${requestUrl.protocol}//${requestUrl.host}`;
console.log("set instance url to:", instanceUrl.value);
}
}
}
}
if (accessToken) {
return await navigateTo("/");
}

15
middleware/server.ts Normal file
View file

@ -0,0 +1,15 @@
import type { ChannelResponse } from "~/types/interfaces";
export default defineNuxtRouteMiddleware(async (to, from) => {
const { fetchChannels } = useApi();
const guildId = to.params.serverId as string;
const channels: ChannelResponse[] | undefined = await fetchChannels(guildId);
console.log("channels:", channels);
if (channels && channels.length > 0) {
console.log("wah");
return await navigateTo(`/servers/${guildId}/channels/${channels[0].uuid}`, { replace: true });
}
})

View file

@ -27,7 +27,12 @@ export default defineNuxtConfig({
runtimeConfig: {
public: {
apiVersion: 1,
messageGroupingMaxDifference: 300000
messageGroupingMaxDifference: 300000,
buildTimeString: new Date().toISOString(),
gitHash: process.env.GIT_SHORT_REV || "N/A",
defaultThemes: [
"light", "ash", "dark", "rainbow-capitalism"
]
}
},
/* nitro: {

View file

@ -14,6 +14,9 @@
"@nuxt/icon": "1.13.0",
"@nuxt/image": "1.10.0",
"@pinia/nuxt": "0.11.0",
"cropperjs": "^2.0.0",
"dompurify": "^3.2.6",
"marked": "^15.0.12",
"nuxt": "^3.17.0",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0",

View file

@ -1,6 +1,19 @@
<template>
<NuxtLayout>
<div id="left-bar">
</div>
<div id="middle-bar">
<h1>
Welcome to gorb :3
</h1>
<p>
Click on a guild to the left to view a guild.
<br>
Or click the button in the bottom left to join a guild.
</p>
</div>
<div id="right-bar">
</div>
</NuxtLayout>
</template>

View file

@ -43,10 +43,10 @@ const registrationEnabled = ref<boolean>(true);
const apiBase = useCookie("api_base");
if (apiBase.value) {
const statsUrl = new URL("/stats", apiBase.value).href;
const { status, data } = await useFetch<StatsResponse>(statsUrl);
if (status.value == "success" && data.value) {
registrationEnabled.value = data.value.registration_enabled;
console.log("apiBase:", apiBase.value);
const stats = await useApi().fetchInstanceStats(apiBase.value);
if (stats) {
registrationEnabled.value = stats.registration_enabled;
}
}
@ -62,9 +62,9 @@ async function formLogin(e: Event) {
console.log("logged in");
if (query.redirect_to) {
console.log("redirecting to:", query.redirect_to);
return await navigateTo(query.redirect_to);
return await navigateTo(query.redirect_to, { replace: true });
}
return await navigateTo("/");
return await navigateTo("/", { replace: true });
} catch (error) {
console.error("Error logging in:", error);
}

View file

@ -1,6 +1,6 @@
<template>
<NuxtLayout>
<form v-if="registrationEnabled" @submit="register">
<form v-if="registrationEnabled && !registrationSubmitted && !showEmailVerificationScreen" @submit="register">
<div>
<!--
<span class="form-error" v-if="errors.username.length > 0">
@ -32,29 +32,87 @@
<button type="submit">Register</button>
</div>
</form>
<div v-else-if="registrationEnabled && (registrationSubmitted || showEmailVerificationScreen) && !emailSent">
<p v-if="emailVerificationRequired">
This instance requires email verification to use it.
<br><br>
<span v-if="registrationSubmitted">
Please open the link sent to your email.
</span>
<span v-else-if="showEmailVerificationScreen">
Please click on the link you've already received, or click on the button below to receive a new email.
</span>
</p>
<p v-else>
Would you like to verify your email?
<!--
<br>
This is required for resetting your password and making other important changes.
-->
</p>
<Button v-if="(!emailVerificationRequired || showEmailVerificationScreen) && !emailSent" text="Send email" variant="neutral" :callback="sendEmail"></Button>
</div>
<div v-else-if="emailSent">
<p>
An email has been sent and should arrive soon.
<br>
If you don't see it in your inbox, try checking the spam folder.
</p>
</div>
<div v-else>
<h3>This instance has disabled registration.</h3>
</div>
<div>
Already have an account? <NuxtLink :href="loginUrl">Log in</NuxtLink>!
<div v-if="loggedIn">
<Button text="Log out" variant="scary" :callback="() => {}"></Button>
</div>
<div v-else>
Already have an account? <NuxtLink :href="loginUrl">Log in</NuxtLink>!
</div>
</NuxtLayout>
</template>
<script lang="ts" setup>
import type { StatsResponse } from '~/types/interfaces';
definePageMeta({
layout: "auth"
})
});
const instanceUrl = useCookie("instance_url").value;
const registrationEnabled = ref<boolean>(false);
const registrationEnabled = useState("registrationEnabled", () => true);
const emailVerificationRequired = useState("emailVerificationRequired", () => false);
const registrationSubmitted = ref(false);
const emailSent = ref(false);
if (instanceUrl) {
const statsUrl = new URL("/stats", instanceUrl).href;
const { status, data, error } = await useFetch<StatsResponse>(statsUrl);
if (status.value == "success" && data.value) {
registrationEnabled.value = data.value.registration_enabled;
const auth = useAuth();
const loggedIn = ref(await auth.getUser());
const query = new URLSearchParams(useRoute().query as Record<string, string>);
const user = await useAuth().getUser();
if (user?.email_verified) {
if (query.get("redirect_to")) {
await navigateTo(query.get("redirect_to"));
} else {
await navigateTo("/");
}
}
const showEmailVerificationScreen = query.get("special") == "verify_email";
console.log("show email verification screen?", showEmailVerificationScreen);
const { fetchInstanceStats, sendVerificationEmail } = useApi();
console.log("wah");
console.log("weoooo")
const apiBase = useCookie("api_base");
console.log("apiBase:", apiBase.value);
if (apiBase.value) {
const stats = await fetchInstanceStats(apiBase.value);
if (stats) {
registrationEnabled.value = stats.registration_enabled;
console.log("set registration enabled value to:", stats.registration_enabled);
emailVerificationRequired.value = stats.email_verification_required;
console.log("set email verification required value to:", stats.email_verification_required);
}
}
@ -87,17 +145,9 @@ const errorMessages = reactive({
*/
//const authStore = useAuthStore();
const auth = useAuth();
const query = useRoute().query as Record<string, string>;
const searchParams = new URLSearchParams(query);
const loginUrl = `/login?${searchParams}`
onMounted(() => {
if (auth.accessToken.value) {
//return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string);
}
});
/*
watch(() => form.username, (newValue) => {
console.log("username change:", newValue);
@ -136,13 +186,22 @@ async function register(e: Event) {
console.log("Sending registration data");
try {
await auth.register(form.username, form.email, form.password);
return await navigateTo(query.redirect_to);
if (!emailVerificationRequired.value) {
return await navigateTo(query.get("redirect_to"));
}
await sendVerificationEmail();
registrationSubmitted.value = true;
} catch (error) {
console.error("Error registering:", error);
}
//return navigateTo(redirectTo ? redirectTo as string : useAppConfig().baseURL as string);
}
async function sendEmail() {
await sendVerificationEmail();
emailSent.value = true;
}
</script>
<style></style>

View file

@ -5,7 +5,7 @@
<h3>
{{ server?.name }}
<span>
<button @click="showServerSettings">
<button @click="showGuildSettings">
<Icon name="lucide:settings" />
</button>
</span>
@ -20,15 +20,13 @@
<div id="channels-list">
<Channel v-for="channel of channels" :name="channel.name"
:uuid="channel.uuid" :current-uuid="(route.params.channelId as string)"
:href="`/guilds/${route.params.serverId}/channels/${channel.uuid}`" />
:href="`/servers/${route.params.serverId}/channels/${channel.uuid}`" />
</div>
</div>
<MessageArea :channel-url="channelUrlPath" />
<div id="members-list">
<div class="member-item" v-for="member of members">
<img v-if="member.avatar" :src="member.avatar" :alt="member.displayName" height="30" />
<Icon v-else name="lucide:user" size="30" />
<span class="member-display-name">{{ member.displayName }}</span>
<div id="members-container">
<div id="members-list">
<MemberEntry v-for="member of members" :member="member" tabindex="0"/>
</div>
</div>
</NuxtLayout>
@ -40,7 +38,7 @@ const route = useRoute();
const loading = useState("loading");
const channelUrlPath = `/channels/${route.params.channelId}`;
const channelUrlPath = `channels/${route.params.channelId}`;
const server = ref<GuildResponse | undefined>();
const channels = ref<ChannelResponse[] | undefined>();
@ -48,108 +46,93 @@ const channel = ref<ChannelResponse | undefined>();
const showInvitePopup = ref(false);
import type { ChannelResponse, GuildResponse, MessageResponse } from "~/types/interfaces";
import UserPopup from "~/components/UserPopup.vue";
import type { ChannelResponse, GuildMemberResponse, GuildResponse, MessageResponse } from "~/types/interfaces";
//const servers = await fetchWithApi("/servers") as { uuid: string, name: string, description: string }[];
//console.log("channelid: servers:", servers);
const members = [
{
id: "3287484395",
displayName: "SauceyRed",
avatar: ""
},
{
id: "3287484395",
displayName: "JustTemmie",
avatar: ""
},
{
id: "3287484395",
displayName: "GOIN!!!!!!",
avatar: ""
},
{
id: "3287484395",
displayName: "SauceyRed",
avatar: ""
},
{
id: "3287484395",
displayName: "Hatsune Miku Official",
avatar: ""
},
{
id: "3287484395",
displayName: "Hatsune Miku Official",
avatar: ""
},
{
id: "3287484395",
displayName: "Hatsune Miku Official",
avatar: ""
},
{
id: "3287484395",
displayName: "SauceyRed",
avatar: ""
},
{
id: "3287484395",
displayName: "SauceyRed",
avatar: ""
}
];
const { fetchMembers } = useApi();
const members = await fetchMembers(route.params.serverId as string);
onMounted(async () => {
console.log("channelid: set loading to true");
server.value = await fetchWithApi(`servers/${route.params.serverId}`);
const guildUrl = `guilds/${route.params.serverId}`;
server.value = await fetchWithApi(guildUrl);
channels.value = await fetchWithApi(`/channels`);
channels.value = await fetchWithApi(`${guildUrl}/channels`);
console.log("channels:", channels.value);
channel.value = await fetchWithApi(route.path);
channel.value = await fetchWithApi(`/channels/${route.params.channelId}`);
console.log("channel:", channel.value);
console.log("channelid: channel:", channel);
console.log("channelid: set loading to false");
});
function showServerSettings() { }
function showGuildSettings() { }
function toggleInvitePopup(e: Event) {
e.preventDefault();
showInvitePopup.value = !showInvitePopup.value;
}
function handleMemberClick(member: GuildMemberResponse) {
}
</script>
<style>
.member-item {
display: flex;
justify-content: center;
align-items: center;
margin-top: .5em;
margin-bottom: .5em;
}
#members-list {
padding-top: 3dvh;
}
#middle-left-column {
padding-left: 1dvw;
padding-right: 1dvw;
border-right: 1px solid rgb(70, 70, 70);
padding-left: .5em;
padding-right: .5em;
border-right: 1px solid var(--padding-color);
background: var(--optional-channel-list-background);
background-color: var(--sidebar-background-color);
}
#members-container {
width: 15rem;
border-left: 1px solid var(--padding-color);
background: var(--optional-member-list-background);
}
#members-list {
padding-left: 1dvw;
padding-right: 1dvw;
border-left: 1px solid rgb(70, 70, 70);
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: scroll;
padding-left: 1.25em;
padding-right: 1.25em;
padding-top: 0.75em;
padding-bottom: 0.75em;
max-height: calc(100% - 0.75em * 2); /* 100% - top and bottom */
}
.member-item {
display: flex;
margin-top: .5em;
margin-bottom: .5em;
gap: .5em;
align-items: center;
text-align: left;
cursor: pointer;
}
#channels-list {
display: flex;
flex-direction: column;
gap: 1dvh;
gap: .5em;
}
.member-avatar {
height: 2.3em;
width: 2.3em;
border-radius: 50%;
}
.member-display-name {
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View file

@ -5,8 +5,9 @@
</template>
<script lang="ts" setup>
const server = await fetchWithApi(`/guilds/${useRoute().params.serverId}`);
console.log("server:", server);
definePageMeta({
middleware: "server"
});
</script>

213
pages/settings.vue Normal file
View file

@ -0,0 +1,213 @@
<template>
<div id="settings-page-container">
<div id="settings-page">
<div id="sidebar">
<ul>
<p>
<span @click="$router.go(-1)">
<Icon class="back-button" size="2em" name="lucide:circle-arrow-left" alt="Back"></Icon>
</span>
</p>
<span class="spacer"></span>
<!-- categories and dynamic settings pages -->
<div v-for="category in categories" :key="category.displayName">
<h2>{{ category.displayName }}</h2>
<li v-for="page in category.pages" :key="page.displayName" @click="selectCategory(page)"
:class="{ 'sidebar-focus': selectedPage === page.displayName }">
{{ page.displayName }}
</li>
<span class="spacer"></span>
</div>
<p>
<Button text="Log Out" :callback=logout variant="scary"></Button>
</p>
<span class="spacer"></span>
<p id="links-and-socials">
<NuxtLink href="https://git.gorb.app/gorb/frontend" title="Source"><Icon name="lucide:git-branch-plus" /></NuxtLink>
<NuxtLink href="https://docs.gorb.app" title="Backend Documentation"><Icon name="lucide:book-open-text" /></NuxtLink>
</p>
<p style="font-size: .8em; color: var(--secondary-text-color)">
Version Hash: {{ appConfig.public.gitHash }}
<br>
Build Time: {{ appConfig.public.buildTimeString }}
</p>
</ul>
</div>
<div id="sub-page">
<component :is="currentPage.pageData" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
const { logout } = useAuth()
const appConfig = useRuntimeConfig()
interface Page {
displayName: string;
pageData: any; // is actually Component but TS is yelling at me :(
}
interface Category {
displayName: string;
pages: Page[];
}
import Profile from '~/components/Settings/UserSettings/Profile.vue';
import Account from '~/components/Settings/UserSettings/Account.vue';
import Privacy from '~/components/Settings/UserSettings/Privacy.vue';
import Devices from '~/components/Settings/UserSettings/Devices.vue';
import Connections from '~/components/Settings/UserSettings/Connections.vue';
import Appearance from '~/components/Settings/AppSettings/Appearance.vue';
import Notifications from '~/components/Settings/AppSettings/Notifications.vue';
import Keybinds from '~/components/Settings/AppSettings/Keybinds.vue';
import Language from '~/components/Settings/AppSettings/Language.vue';
const settingsCategories = {
userSettings: {
displayName: "User Settings",
pages: [
{ displayName: "Profile", pageData: Profile },
{ displayName: "Account", pageData: Account },
{ displayName: "Privacy", pageData: Privacy },
{ displayName: "Devices", pageData: Devices },
{ displayName: "Connections", pageData: Connections },
]
},
appSettings: {
displayName: "App Settings",
pages: [
{ displayName: "Appearance", pageData: Appearance },
{ displayName: "Notifications", pageData: Notifications },
{ displayName: "Keybinds", pageData: Keybinds },
{ displayName: "Language", pageData: Language },
]
},
};
const categories = Object.values(settingsCategories);
let currentPage = ref(categories[0].pages[0]);
let selectedPage = ref(currentPage.value.displayName); // used to highlight the current channel
function selectCategory(page: Page) {
currentPage.value = page;
selectedPage.value = page.displayName;
};
// redirects to you privacy if you go to settings#privacy
onMounted(() => {
const hash = window.location.hash.substring(1).toLowerCase();
const foundPage = categories.flatMap(category => category.pages).find(page => page.displayName.toLowerCase() === hash);
if (foundPage) {
currentPage.value = foundPage;
selectedPage.value = foundPage.displayName;
}
});
</script>
<style scoped>
#settings-page-container {
height: 100%;
align-content: center;
overflow-y: hidden;
margin: 0;
}
#settings-page {
height: 100vh;
display: flex;
}
#sidebar {
min-width: 25dvw;
max-width: 25dvw;
background: var(--optional-channel-list-background);
background-color: var(--sidebar-background-color);
color: var(--text-color);
padding: 1dvh 1dvw;
margin-left: 0;
overflow-y: auto;
height: 100vh;
}
#sidebar h2 {
font-size: 0.95em;
padding: 0 0.8dvw;
}
#sidebar ul {
list-style-type: none;
padding: 0;
margin: 0;
}
#sidebar li {
border-radius: 8px;
padding: 0.8dvh 0.8dvw;
font-size: 1.4em;
cursor: pointer;
transition: background-color 0.3s;
}
#sidebar p {
margin: 2dvh 0.8dvw;
}
.sidebar-focus {
background-color: var(--sidebar-highlighted-background-color);
}
#sidebar li:hover {
background-color: var(--sidebar-highlighted-background-color);
}
#sub-page {
flex-grow: 1;
min-width: 70dvw;
max-width: 70dvw;
padding-left: 1.5rem;
margin-right: auto;
overflow-y: auto;
height: 100vh;
}
.back-button {
cursor: pointer;
color: var(--primary-color);
transition: color 100ms;
}
.back-button:hover{
color: var(--primary-highlighted-color);
}
#links-and-socials * {
margin-right: 0.2em;
}
.spacer {
height: 0.2dvh;
display: block;
margin: 0.8dvh 1dvw;
background-color: var(--padding-color);
}
/* applies to child pages too */
:deep(.subtitle) {
display: block;
font-size: 0.8em;
font-weight: 800;
margin: 4dvh 0 0.5dvh 0.25dvw;
}
</style>

View file

@ -15,6 +15,12 @@ const token = useRoute().query.token;
try {
const res = await fetchWithApi("/auth/verify-email", { query: { token } });
console.log("hi");
const query = useRoute().query;
if (query.redirect_to) {
await navigateTo(`/?redirect_to=${query.redirect_to}`);
} else {
await navigateTo("/");
}
} catch (error) {
console.error("Error verifying email:", error);
errorMessage.value = error;

136
pnpm-lock.yaml generated
View file

@ -20,6 +20,15 @@ importers:
'@pinia/nuxt':
specifier: 0.11.0
version: 0.11.0(magicast@0.3.5)(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)))
cropperjs:
specifier: ^2.0.0
version: 2.0.0
dompurify:
specifier: ^3.2.6
version: 3.2.6
marked:
specifier: ^15.0.12
version: 15.0.12
nuxt:
specifier: ^3.17.0
version: 3.17.0(@netlify/blobs@8.2.0)(@parcel/watcher@2.5.1)(@types/node@22.15.3)(db0@0.3.2)(eslint@9.25.1(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.29.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.40.1)(terser@5.39.0)(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.3)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.1))(yaml@2.7.1)
@ -199,6 +208,39 @@ packages:
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
engines: {node: '>=0.1.90'}
'@cropper/element-canvas@2.0.0':
resolution: {integrity: sha512-GPtGJgSm92crJhhhwUsaMw3rz2KfJWWSz7kRAlufFEV/EHTP5+6r6/Z1BCGRna830i+Avqbm435XLOtA7PVJwA==}
'@cropper/element-crosshair@2.0.0':
resolution: {integrity: sha512-KfPfyrdeFvUC31Ws7ATtcalWWSaMtrC6bMoCipZhqbUOE7wZoL4ecDSL6BUOZxPa74awZUqfzirCDjHvheBfyw==}
'@cropper/element-grid@2.0.0':
resolution: {integrity: sha512-i78SQ0IJTLFveKX6P7svkfMYVdgHrQ8ZmmEw8keFy9n1ZVbK+SK0UHK5FNMRNI/gtVhKJOGEnK/zeyjUdj4Iyw==}
'@cropper/element-handle@2.0.0':
resolution: {integrity: sha512-ZJvW+0MkK9E8xYymGdoruaQn2kwjSHFpNSWinjyq6csuVQiCPxlX5ovAEDldmZ9MWePPtWEi3vLKQOo2Yb0T8g==}
'@cropper/element-image@2.0.0':
resolution: {integrity: sha512-9BxiTS/aHRmrjopaFQb9mQQXmx4ruhYHGkDZMVz24AXpMFjUY6OpqrWse/WjzD9tfhMFvEdu17b3VAekcAgpeg==}
'@cropper/element-selection@2.0.0':
resolution: {integrity: sha512-ensNnbIfJsJ8bhbJTH/RXtk2URFvTOO4TvfRk461n2FPEC588D7rwBmUJxQg74IiTi4y1JbCI+6j+4LyzYBLCQ==}
'@cropper/element-shade@2.0.0':
resolution: {integrity: sha512-jv/2bbNZnhU4W+T4G0c8ADocLIZvQFTXgCf2RFDNhI5UVxurzWBnDdb8Mx8LnVplnkTqO+xUmHZYve0CwgWo+Q==}
'@cropper/element-viewer@2.0.0':
resolution: {integrity: sha512-zY+3VRN5TvpM8twlphYtXw0tzJL2VgzeK7ufhL1BixVqOdRxwP13TprYIhqwGt9EW/SyJZUiaIu396T89kRX8A==}
'@cropper/element@2.0.0':
resolution: {integrity: sha512-lsthn0nQq73GExUE7Mg/ss6Q3RXADGDv055hxoLFwvl/wGHgy6ZkYlfLZ/VmgBHC6jDK5IgPBFnqrPqlXWSGBA==}
'@cropper/elements@2.0.0':
resolution: {integrity: sha512-PQkPo1nUjxLFUQuHYu+6atfHxpX9B41Xribao6wpvmvmNIFML6LQdNqqWYb6LyM7ujsu71CZdBiMT5oetjJVoQ==}
'@cropper/utils@2.0.0':
resolution: {integrity: sha512-cprLYr+7kK3faGgoOsTW9gIn5sefDr2KwOmgyjzIXk+8PLpW8FgFKEg5FoWfRD5zMAmkCBuX6rGKDK3VdUEGrg==}
'@dabh/diagnostics@2.0.3':
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
@ -1199,6 +1241,9 @@ packages:
'@types/triple-beam@1.3.5':
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
@ -1891,6 +1936,9 @@ packages:
resolution: {integrity: sha512-onMB0OkDjkXunhdW9htFjEhqrD54+M94i6ackoUkjHKbRnXdyEyKRelp4nJ1kAz32+s27jP1FsebpJCVl0BsvA==}
engines: {node: '>=18.0'}
cropperjs@2.0.0:
resolution: {integrity: sha512-TO2j0Qre01kPHbow4FuTrbdEB4jTmGRySxW49jyEIqlJZuEBfrvCTT0vC3eRB2WBXudDfKi1Onako6DKWKxeAQ==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@ -2113,6 +2161,9 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dompurify@3.2.6:
resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==}
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
@ -3092,6 +3143,11 @@ packages:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'}
marked@15.0.12:
resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==}
engines: {node: '>= 18'}
hasBin: true
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@ -4948,6 +5004,72 @@ snapshots:
'@colors/colors@1.6.0': {}
'@cropper/element-canvas@2.0.0':
dependencies:
'@cropper/element': 2.0.0
'@cropper/utils': 2.0.0
'@cropper/element-crosshair@2.0.0':
dependencies:
'@cropper/element': 2.0.0
'@cropper/utils': 2.0.0
'@cropper/element-grid@2.0.0':
dependencies:
'@cropper/element': 2.0.0
'@cropper/utils': 2.0.0
'@cropper/element-handle@2.0.0':
dependencies:
'@cropper/element': 2.0.0
'@cropper/utils': 2.0.0
'@cropper/element-image@2.0.0':
dependencies:
'@cropper/element': 2.0.0
'@cropper/element-canvas': 2.0.0
'@cropper/utils': 2.0.0
'@cropper/element-selection@2.0.0':
dependencies:
'@cropper/element': 2.0.0
'@cropper/element-canvas': 2.0.0
'@cropper/element-image': 2.0.0
'@cropper/utils': 2.0.0
'@cropper/element-shade@2.0.0':
dependencies:
'@cropper/element': 2.0.0
'@cropper/element-canvas': 2.0.0
'@cropper/element-selection': 2.0.0
'@cropper/utils': 2.0.0
'@cropper/element-viewer@2.0.0':
dependencies:
'@cropper/element': 2.0.0
'@cropper/element-canvas': 2.0.0
'@cropper/element-image': 2.0.0
'@cropper/element-selection': 2.0.0
'@cropper/utils': 2.0.0
'@cropper/element@2.0.0':
dependencies:
'@cropper/utils': 2.0.0
'@cropper/elements@2.0.0':
dependencies:
'@cropper/element': 2.0.0
'@cropper/element-canvas': 2.0.0
'@cropper/element-crosshair': 2.0.0
'@cropper/element-grid': 2.0.0
'@cropper/element-handle': 2.0.0
'@cropper/element-image': 2.0.0
'@cropper/element-selection': 2.0.0
'@cropper/element-shade': 2.0.0
'@cropper/element-viewer': 2.0.0
'@cropper/utils@2.0.0': {}
'@dabh/diagnostics@2.0.3':
dependencies:
colorspace: 1.1.4
@ -6094,6 +6216,9 @@ snapshots:
'@types/triple-beam@1.3.5': {}
'@types/trusted-types@2.0.7':
optional: true
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 22.15.3
@ -6875,6 +7000,11 @@ snapshots:
croner@9.0.0: {}
cropperjs@2.0.0:
dependencies:
'@cropper/elements': 2.0.0
'@cropper/utils': 2.0.0
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@ -7082,6 +7212,10 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.2.6:
optionalDependencies:
'@types/trusted-types': 2.0.7
domutils@3.2.2:
dependencies:
dom-serializer: 2.0.0
@ -8170,6 +8304,8 @@ snapshots:
dependencies:
semver: 6.3.1
marked@15.0.12: {}
math-intrinsics@1.1.0: {}
mdn-data@2.0.28: {}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

121
public/icon.svg Normal file
View file

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="drawing.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="0.35399828"
inkscape:cx="2.8248725"
inkscape:cy="731.64198"
inkscape:window-width="1440"
inkscape:window-height="863"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="matrix(0.93746412,0,0,0.93746412,18.749279,18.749282)">
<g
id="g1543"
style="display:inline"
transform="matrix(3.7794744,0,0,3.7794744,-150.52528,-298.33565)">
<circle
style="fill:#000000;stroke-width:0.264583"
id="path60"
cx="106.78797"
cy="145.89536"
r="72.25267" />
<circle
style="fill:#f4741f;fill-opacity:1;stroke-width:0.257596"
id="path899"
cx="106.78797"
cy="145.89536"
r="65.485863" />
</g>
<g
id="g1460-1"
transform="matrix(4.1481001,0,0,4.1481002,45.149918,-354.52402)"
style="display:inline">
<circle
style="fill:#000000;fill-opacity:1;stroke-width:0.29496"
id="path1129-3"
cx="78.140816"
cy="136.65092"
r="17.372646" />
<circle
style="fill:#f4741f;fill-opacity:1;stroke-width:0.294225"
id="path1354-0"
cx="86.078323"
cy="136.65092"
r="11.576728" />
</g>
<g
id="g1460-1-3"
transform="matrix(4.1481001,0,0,4.1481002,-187.26754,-354.52402)"
style="display:inline">
<circle
style="fill:#000000;fill-opacity:1;stroke-width:0.29496"
id="path1129-3-4"
cx="78.140816"
cy="136.65092"
r="17.372646" />
<circle
style="fill:#f4741f;fill-opacity:1;stroke-width:0.294225"
id="path1354-0-6"
cx="86.078323"
cy="136.65092"
r="11.576728" />
</g>
<g
id="g3530"
transform="matrix(3.7794744,0,0,3.7794744,-150.52528,-294.3357)">
<path
style="fill:none;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 106.78797,168.1205 c 0,0 -17.461156,15.02392 -28.153795,0.49121"
id="path1817" />
<path
style="fill:none;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 106.78797,168.1205 c 0,0 17.46116,15.02392 28.15379,0.49121"
id="path1817-6" />
<path
style="fill:none;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 106.78797,168.1205 -5.74494,-9.7603"
id="path2191"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 106.78797,168.1205 5.74494,-9.7603"
id="path2191-6"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:7;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 101.04303,158.3602 h 11.48988"
id="path2675"
sodipodi:nodetypes="cc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

20
public/themes/ash.css Normal file
View file

@ -0,0 +1,20 @@
:root {
--text-color: #f0e5e0;
--secondary-text-color: #e8e0db;
--chat-background-color: #2f2e2d;
--chat-highlighted-background-color: #3f3b38;
--sidebar-background-color: #3e3a37;
--sidebar-highlighted-background-color: #46423b;
--topbar-background-color: #3a3733;
--chatbox-background-color: #3a3733;
--padding-color: #e0e0e0;
--primary-color: #f07028;
--primary-highlighted-color: #f28f4b;
--secondary-color: #683820;
--secondary-highlighted-color: #885830;
--accent-color: #a04b24;
--accent-highlighted-color: #b86038;
}

6
public/themes/ash.json Normal file
View file

@ -0,0 +1,6 @@
{
"displayName": "Ash",
"previewGradient": "45deg, #2f2e2d, #46423b",
"complementaryColor": "white",
"themeUrl": "ash.css"
}

20
public/themes/dark.css Normal file
View file

@ -0,0 +1,20 @@
:root {
--text-color: #f7eee8;
--secondary-text-color: #f0e8e4;
--chat-background-color: #1f1e1d;
--chat-highlighted-background-color: #2f2b28;
--sidebar-background-color: #2e2a27;
--sidebar-highlighted-background-color: #36322b;
--topbar-background-color: #2a2723;
--chatbox-background-color: #1a1713;
--padding-color: #484848;
--primary-color: #f4741f;
--primary-highlighted-color: #f68a3f;
--secondary-color: #7c4018;
--secondary-highlighted-color: #8f5b2c;
--accent-color: #b35719;
--accent-highlighted-color: #c76a2e;
}

6
public/themes/dark.json Normal file
View file

@ -0,0 +1,6 @@
{
"displayName": "Dark",
"previewGradient": "45deg, #1f1e1d, #36322b",
"complementaryColor": "white",
"themeUrl": "dark.css"
}

View file

@ -0,0 +1,29 @@
/* this is not a real theme, but rather a template for themes */
:root {
--text-color: #161518;
--secondary-text-color: #2b2930;
--chat-background-color: #80808000;
--chat-highlighted-background-color: #ffffff20;
--sidebar-background-color: #80808000;
--sidebar-highlighted-background-color: #ffffff20;
--topbar-background-color: #80808000;
--chatbox-background-color: #80808000;
--padding-color: #80808000;
--primary-color: #21b1ff80;
--primary-highlighted-color: #18a0df80;
--secondary-color: #ffd80080;
--secondary-highlighted-color: #dfb80080;
--accent-color: #ff218c80;
--accent-highlighted-color: #df1b6f80;
--optional-body-background: ; /* background element for the body */
--optional-chat-background: ; /* background element for the chat box */
--optional-topbar-background: ; /* background element for the topbar */
--optional-sidebar-background: ; /* background element for left server sidebar */
--optional-channel-list-background: ; /* background element for channel list and settings list */
--optional-member-list-background: ; /* background element for member list */
--optional-message-box-background: ; /* background element for message box */
}

20
public/themes/light.css Normal file
View file

@ -0,0 +1,20 @@
:root {
--text-color: #170f08;
--secondary-text-color: #2f2b28;
--chat-background-color: #f0ebe8;
--chat-highlighted-background-color: #e8e4e0;
--sidebar-background-color: #dbd8d4;
--sidebar-highlighted-background-color: #d4d0ca;
--topbar-background-color: #dfdbd6;
--chatbox-background-color: #dfdbd6;
--padding-color: #484848;
--primary-color: #df5f0b;
--primary-highlighted-color: #ef6812;
--secondary-color: #e8ac84;
--secondary-highlighted-color: #f8b68a;
--accent-color: #e68b4e;
--accent-highlighted-color: #f69254;
}

6
public/themes/light.json Normal file
View file

@ -0,0 +1,6 @@
{
"displayName": "Light",
"previewGradient": "45deg, #f0ebe8, #d4d0ca",
"complementaryColor": "black",
"themeUrl": "light.css"
}

View file

@ -0,0 +1,28 @@
:root {
--text-color: #161518;
--secondary-text-color: #2b2930;
--chat-background-color: #80808000;
--chat-highlighted-background-color: #ffffff20;
--sidebar-background-color: #80808000;
--sidebar-highlighted-background-color: #ffffff20;
--topbar-background-color: #80808000;
--chatbox-background-color: #80808040;
--padding-color: #80808000;
--primary-color: #21b1ff80;
--primary-highlighted-color: #18a0df80;
--secondary-color: #ffd80080;
--secondary-highlighted-color: #dfb80080;
--accent-color: #ff218c80;
--accent-highlighted-color: #df1b6f80;
/* --optional-body-background: background */
--optional-body-background: linear-gradient(45deg, #ed222480, #ed222480, #ed222480, #ed222480, #ed222480, #ed222480, #f35b2280, #f9962180, #f5c11e80, #f1eb1b80, #f1eb1b80, #f1eb1b80, #63c72080, #0c9b4980, #21878d80, #3954a580, #61379b80, #93288e80);
--optional-topbar-background: linear-gradient(-12.5deg, cyan, pink, white, pink, cyan);
--optional-sidebar-background: linear-gradient(90deg, #55cdfcd0, #f7a8b8d0, #ffffffd0, #f7a8b8d0, #55cdfcd0);
--optional-channel-list-background: linear-gradient(82deg, #d52c00b0, #e29688b0, #ffffffb0, #d27fa4b0, #a20062b0);
--optional-member-list-background: linear-gradient(3deg, #ff0080, #c8259d, #8c4799, #442e9f, #0032a0);
--optional-message-box-background: linear-gradient(3deg, #ff0080, #c8259d, #8c4799, #442e9f, #0032a0);
}

View file

@ -0,0 +1,6 @@
{
"displayName": "Woke",
"previewGradient": "45deg, #ed2224, #ed2224, #f35b22, #f99621, #f5c11e, #f1eb1b 27%, #f1eb1b, #f1eb1b 33%, #63c720, #0c9b49, #21878d, #3954a5, #61379b, #93288e, #93288e",
"complementaryColor": "white",
"themeUrl": "rainbow-capitalism.css"
}

View file

@ -23,6 +23,14 @@ export interface GuildResponse {
member_count: number
}
export interface GuildMemberResponse {
uuid: string,
nickname: string,
user_uuid: string,
guild_uuid: string,
user: UserResponse
}
export interface ChannelResponse {
uuid: string,
guild_uuid: string,
@ -50,9 +58,11 @@ export interface UserResponse {
username: string,
display_name: string | null,
avatar: string | null,
pronouns: string | null,
about: string | null,
email?: string,
email_verified?: boolean
}
}
export interface StatsResponse {
accounts: number,
@ -62,3 +72,14 @@ export interface StatsResponse {
email_verification_required: boolean,
build_number: string
}
export interface ScrollPosition {
scrollHeight: number,
scrollWidth: number,
scrollTop: number,
scrollLeft: number
offsetHeight: number,
offsetWidth: number,
offsetTop: number,
offsetLeft: number
}

View file

@ -1,6 +0,0 @@
import type { UserResponse } from "~/types/interfaces"
export default async (serverId: string, memberId: string): Promise<UserResponse> => {
const user = await fetchWithApi(`/guilds/${serverId}/members/${memberId}`) as UserResponse;
return user;
}

View file

@ -1,6 +0,0 @@
import type { UserResponse } from "~/types/interfaces"
export default async (serverId: string, userId: string): Promise<UserResponse> => {
const user = await fetchWithApi(`/users/${userId}`) as UserResponse;
return user;
}

View file

@ -9,8 +9,6 @@ export default async <T>(path: string, options: NitroFetchOptions<string> = {})
path = path.slice(0, path.lastIndexOf("/"));
}
console.log("formatted path:", path);
const accessToken = useCookie("access_token");
console.log("access token:", accessToken.value);
const apiBase = useCookie("api_base").value;
const apiVersion = useRuntimeConfig().public.apiVersion;
console.log("heyoooo")
@ -21,23 +19,24 @@ export default async <T>(path: string, options: NitroFetchOptions<string> = {})
}
console.log("path:", path)
const { revoke, refresh } = useAuth();
console.log("access token 2:", accessToken.value);
let headers: HeadersInit = {};
if (accessToken.value) {
headers = {
...options.headers,
"Authorization": `Bearer ${accessToken.value}`
};
} else {
headers = {
...options.headers
};
}
let reauthFailed = false;
while (!reauthFailed) {
const accessToken = useCookie("access_token");
console.log("access token:", accessToken.value);
if (accessToken.value) {
headers = {
...options.headers,
"Authorization": `Bearer ${accessToken.value}`
};
} else {
headers = {
...options.headers
};
}
try {
console.log("fetching:", URL.parse(apiBase + path));
const res = await $fetch<T>(URL.parse(apiBase + path)!.href, {
@ -74,9 +73,10 @@ export default async <T>(path: string, options: NitroFetchOptions<string> = {})
console.log("Path is refresh endpoint, throwing error");
throw error;
}
} else {
console.log("throwing error:", error);
throw error;
}
console.log("throwing error");
throw error;
}
}
}

View file

@ -0,0 +1,14 @@
import type { ScrollPosition } from "~/types/interfaces";
export default (element: HTMLElement): ScrollPosition => {
return {
scrollHeight: element.scrollHeight,
scrollWidth: element.scrollWidth,
scrollTop: element.scrollTop,
scrollLeft: element.scrollLeft,
offsetHeight: element.offsetHeight,
offsetWidth: element.offsetWidth,
offsetTop: element.offsetTop,
offsetLeft: element.offsetLeft
};
}

View file

@ -1,6 +1,6 @@
export default (element: Ref<HTMLElement | undefined, HTMLElement | undefined>) => {
if (element.value) {
element.value.scrollTo({ top: element.value.scrollHeight });
export default (element: HTMLElement) => {
if (element) {
element.scrollTo({ top: element.scrollHeight });
return;
}
}

View file

@ -0,0 +1,5 @@
import type { ScrollPosition } from "~/types/interfaces";
export default (element: HTMLElement, scrollPosition: ScrollPosition) => {
return element.scrollTo({ top: scrollPosition.scrollTop, left: scrollPosition.scrollLeft });
}