Compare commits

...

83 commits

Author SHA1 Message Date
eeea9a9205
Merge branch 'main' into tauri
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-11 02:49:59 +02:00
c2b06f40b4
fix: align textbox to bottom of the screen
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-11 02:34:08 +02:00
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
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
36 changed files with 838 additions and 233 deletions

View file

@ -1,5 +0,0 @@
export default defineAppConfig({
title: "Gorb",
buildTimeString: new Date().toISOString(),
gitHash: process.env.GIT_SHORT_REV || "N/A"
})

View file

@ -33,6 +33,7 @@ body {
font-family: Arial, Helvetica, sans-serif;
box-sizing: border-box;
color: var(--text-color);
background: var(--optional-body-background);
background-color: var(--chat-background-color);
margin: 0;
}

View file

@ -21,7 +21,7 @@ const props = defineProps<{
background-color: var(--primary-color);
color: var(--text-color);
padding: 0.7dvh 1.2dvw;
padding: 0.4em 0.75em;
font-size: 1.1em;
transition: background-color 0.2s;

View file

@ -23,14 +23,14 @@ 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;
}

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

@ -2,8 +2,8 @@
<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" class="profile-popup" />
<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>
@ -32,9 +32,4 @@ const hidePopup = () => {
.member-item {
position: relative; /* Set the position to relative for absolute positioning of the popup */
}
.profile-popup {
position: absolute; /* Use absolute positioning */
left: -100px; /* Adjust this value to position the popup to the left */
}
</style>

View file

@ -60,7 +60,16 @@ const sanitized = ref<string>();
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"] });
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) => {
@ -99,6 +108,7 @@ function getDayDifference(date1: Date, date2: Date) {
align-items: center;
column-gap: 1dvw;
width: 100%;
overflow-wrap: anywhere;
}
.message:hover {
@ -178,12 +188,12 @@ function getDayDifference(date1: Date, date2: Date) {
<style module>
.message-text ul, h1, h2, h3, h4, h5, h6 {
padding-top: 1dvh;
padding-bottom: 1dvh;
padding-top: .5em;
padding-bottom: .5em;
margin: 0;
}
.message-text ul {
padding-left: 2dvw;
padding-left: 1em;
}
</style>

View file

@ -9,11 +9,28 @@
</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>
@ -106,11 +123,38 @@ function pushMessage(message: MessageResponse) {
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;
@ -158,14 +202,21 @@ if (accessToken && apiBase) {
function sendMessage(e: Event) {
e.preventDefault();
const message = {
message: messageInput.value
}
console.log("message:", message);
if (message.message) {
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));
messageInput.value = "";
console.log("MESSAGE SENT!!!");
// reset input field
messageInput.value = ""
if (messageTextboxInput.value) {
messageTextboxInput.value.innerText = ""
}
adjustTextboxHeight()
}
}
@ -241,38 +292,64 @@ router.beforeEach((to, from, next) => {
<style scoped>
#message-area {
display: grid;
grid-template-rows: 8fr 1fr;
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 var(--padding-color);
padding-bottom: 1dvh;
padding-top: 1dvh;
margin-bottom: 1dvh;
margin-top: auto; /* force it to the bottom of the screen */
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: var(--sidebar-background-color);
#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 {
@ -283,7 +360,7 @@ router.beforeEach((to, from, next) => {
padding-right: 1dvw;
}
#submit-button {
.message-box-button {
background-color: inherit;
border: none;
color: var(--primary-color);
@ -291,7 +368,7 @@ router.beforeEach((to, from, next) => {
font-size: 1.5em;
}
#submit-button:hover {
.message-box-button:hover {
color: var(--primary-highlighted-color);
cursor: pointer;
}

View file

@ -1,14 +1,100 @@
<template>
<div>
<h1>hi</h1>
<h5>TEST</h5>
<h5>TEST</h5>
<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;
}
</style>
.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

@ -43,6 +43,14 @@ async function changeEmail() {
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) {
@ -70,13 +78,6 @@ async function deleteAccount() {
</script>
<style scoped>
.subtitle {
display: block;
font-size: 0.8em;
font-weight: 800;
margin: 4dvh 0 0.5dvh 0.25dvw;
}
.user-data-fields {
min-width: 35dvw;
max-width: 35dvw;
@ -93,8 +94,4 @@ async function deleteAccount() {
color: var(--text-color);
background-color: var(--accent-color);
}
.profile-popup {
margin-left: 2dvw;
}
</style>

View file

@ -20,15 +20,25 @@
<Button style="margin-top: 2dvh" text="Save Changes" :callback="saveChanges"></Button>
</div>
<UserPopup v-if="user" :user="user" class="profile-popup"></UserPopup>
<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 Button from '~/components/Button.vue';
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()
@ -36,8 +46,6 @@ if (!user) {
alert("could not fetch user info, aborting :(")
}
let newPfpFile: File;
async function saveChanges() {
if (!user) return;
@ -64,7 +72,7 @@ async function saveChanges() {
alert('success!!')
} catch (error: any) {
if (error?.response?.status !== 200) {
alert(`error ${error?.response?.status} met whilst trying to update profile info`)
alert(`error ${error?.response?.status} met whilst trying to update profile info\n"${error?.response._data?.message}"`)
}
}
};
@ -87,12 +95,11 @@ async function changeAvatar() {
const file = input.files[0];
if (!file) return;
newPfpFile = file
const reader = new FileReader();
reader.addEventListener("onload", () => {
reader.addEventListener("load", () => {
if (reader.result && typeof reader.result === 'string') {
user.avatar = reader.result;
cropImageSrc.value = reader.result;
isCropPopupVisible.value = true;
}
});
reader.readAsDataURL(file);
@ -101,6 +108,19 @@ async function changeAvatar() {
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>
@ -108,13 +128,6 @@ async function changeAvatar() {
display: flex;
}
.subtitle {
display: block;
font-size: 0.8em;
font-weight: 800;
margin: 1.5dvh 0 0.5dvh 0.25dvw;
}
.user-data-fields {
min-width: 35dvw;
max-width: 35dvw;
@ -132,7 +145,7 @@ async function changeAvatar() {
background-color: var(--accent-color);
}
.profile-popup {
#profile-popup {
margin-left: 2dvw;
}
</style>

View file

@ -59,12 +59,14 @@ const props = defineProps<{
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;

View file

@ -1,4 +1,4 @@
import type { ChannelResponse, GuildMemberResponse, GuildResponse, MessageResponse } from "~/types/interfaces";
import type { ChannelResponse, GuildMemberResponse, GuildResponse, MessageResponse, StatsResponse } from "~/types/interfaces";
export const useApi = () => {
async function fetchGuilds(): Promise<GuildResponse[] | undefined> {
@ -41,6 +41,15 @@ export const useApi = () => {
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,
@ -51,6 +60,8 @@ export const useApi = () => {
fetchUsers,
fetchUser,
fetchMessages,
fetchMessage
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) {

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>
@ -51,7 +28,6 @@
<script lang="ts" setup>
import { FetchError } from 'ofetch';
import type { StatsResponse } from '~/types/interfaces';
const instanceUrl = ref<string | null | undefined>(null);
const instanceUrlInput = ref<string>();
@ -63,9 +39,7 @@ const registrationEnabled = useState("registrationEnabled", () => true);
const auth = useAuth();
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);
});
@ -73,8 +47,8 @@ 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" });
@ -87,10 +61,10 @@ async function selectInstance(e: Event) {
instanceUrl.value = origin;
useCookie("instance_url").value = origin;
console.log("set instance url to:", origin);
const { status, data, error } = await useFetch<StatsResponse>(`${apiBase.value}/stats`);
if (status.value == "success" && data.value) {
registrationEnabled.value = data.value.registration_enabled;
console.log("set registration enabled value to:", data.value.registration_enabled);
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;
}
@ -148,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,24 +3,28 @@
<div :class="{ hidden: loading, visible: !loading }" id="client-root">
<div id="homebar">
<div class="homebar-item">
main bar
<marquee>
gorb!!!!!
</marquee>
</div>
</div>
<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" />
<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>
<NuxtLink id="settings-menu" href="/settings">
<Icon name="lucide:settings" class="sidebar-icon white" alt="Settings menu" />
</NuxtLink>
<slot />
</div>
<slot />
</div>
</template>
@ -34,13 +38,11 @@ const guilds: GuildResponse[] | undefined = await fetchWithApi("/me/guilds");
<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 {
@ -49,79 +51,77 @@ const guilds: GuildResponse[] | undefined = await fetchWithApi("/me/guilds");
.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 var(--padding-color);
.homebar-item {
width: 100dvw;
}
#__nuxt {
#page-content {
display: flex;
flex-flow: column;
}
.grid-column {
padding-top: 1dvh;
}
#home {
padding-left: .5dvw;
padding-right: .5dvw;
}
.sidebar-icon {
width: 3rem;
height: 3rem;
}
#current-info {
grid-column: 2;
grid-row: 1;
flex-direction: row;
flex-grow: 1;
overflow: auto;
}
#left-column {
display: flex;
flex-direction: column;
gap: 2dvh;
padding-left: .5dvw;
padding-right: .5dvw;
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);
padding-top: 1.5dvh;
}
#middle-left-column {
padding-left: 1dvw;
padding-right: 1dvw;
border-right: 1px solid var(--padding-color);
}
#home-button {
border-bottom: 1px solid var(--padding-color);
padding-bottom: 1dvh;
}
#settings-menu {
position: absolute;
bottom: .25dvh
}
#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,21 @@ 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);
@ -19,6 +33,14 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
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);
}
}
}
}

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

@ -15,6 +15,7 @@
"@nuxt/image": "1.10.0",
"@pinia/nuxt": "0.11.0",
"@tauri-apps/plugin-http": "~2.5.0",
"cropperjs": "^2.0.0",
"dompurify": "^3.2.6",
"marked": "^15.0.12",
"nuxt": "^3.17.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

@ -44,10 +44,9 @@ const apiBase = useCookie("api_base");
if (apiBase.value) {
console.log("apiBase:", 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;
const stats = await useApi().fetchInstanceStats(apiBase.value);
if (stats) {
registrationEnabled.value = stats.registration_enabled;
}
}

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,32 +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 registrationEnabled = useState("registrationEnabled", () => true);
const emailVerificationRequired = useState("emailVerificationRequired", () => false);
const registrationSubmitted = ref(false);
const emailSent = ref(false);
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 { status, data, error } = await useFetch<StatsResponse>(`${apiBase.value}/stats`);
if (status.value == "success" && data.value) {
registrationEnabled.value = data.value.registration_enabled;
console.log("set registration enabled value to:", data.value.registration_enabled);
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);
}
}
@ -90,8 +145,6 @@ 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}`
@ -133,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

@ -83,34 +83,36 @@ function handleMemberClick(member: GuildMemberResponse) {
<style>
#middle-left-column {
padding-left: 1dvw;
padding-right: 1dvw;
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 {
padding-top: 1dvh;
padding-left: 1dvw;
padding-right: 1dvw;
width: 15rem;
border-left: 1px solid var(--padding-color);
background: var(--optional-member-list-background);
}
#members-list {
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: scroll;
padding-left: 1dvw;
padding-right: 1dvw;
margin-top: 1dvh;
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: grid;
grid-template-columns: 2dvw 1fr;
display: flex;
margin-top: .5em;
margin-bottom: .5em;
gap: 1em;
gap: .5em;
align-items: center;
text-align: left;
cursor: pointer;
@ -119,7 +121,7 @@ function handleMemberClick(member: GuildMemberResponse) {
#channels-list {
display: flex;
flex-direction: column;
gap: 1dvh;
gap: .5em;
}
.member-avatar {

View file

@ -3,6 +3,13 @@
<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>
@ -24,11 +31,10 @@
</p>
<p style="font-size: .8em; color: var(--secondary-text-color)">
Version Hash: {{ appConfig.gitHash }}
Version Hash: {{ appConfig.public.gitHash }}
<br>
Build Time: {{ appConfig.buildTimeString }}
Build Time: {{ appConfig.public.buildTimeString }}
</p>
</ul>
</div>
<div id="sub-page">
@ -40,11 +46,8 @@
<script lang="ts" setup>
import Button from '~/components/Button.vue';
const { logout } = useAuth()
const appConfig = useAppConfig()
const appConfig = useRuntimeConfig()
interface Page {
displayName: string;
@ -99,6 +102,16 @@ function selectCategory(page: 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>
@ -117,6 +130,7 @@ function selectCategory(page: Page) {
#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;
@ -168,6 +182,16 @@ function selectCategory(page: Page) {
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;
}
@ -180,7 +204,10 @@ function selectCategory(page: Page) {
}
/* applies to child pages too */
:deep(h5) {
color: red;
: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;

110
pnpm-lock.yaml generated
View file

@ -23,6 +23,9 @@ importers:
'@tauri-apps/plugin-http':
specifier: ~2.5.0
version: 2.5.0
cropperjs:
specifier: ^2.0.0
version: 2.0.0
dompurify:
specifier: ^3.2.6
version: 3.2.6
@ -211,6 +214,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==}
@ -1997,6 +2033,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'}
@ -5083,6 +5122,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
@ -7079,6 +7184,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

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"
}

View file

@ -7,6 +7,7 @@
--sidebar-background-color: #2e2a27;
--sidebar-highlighted-background-color: #36322b;
--topbar-background-color: #2a2723;
--chatbox-background-color: #1a1713;
--padding-color: #484848;

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

@ -1,4 +0,0 @@
[
"dark.css",
"light.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 */
}

View file

@ -7,6 +7,7 @@
--sidebar-background-color: #dbd8d4;
--sidebar-highlighted-background-color: #d4d0ca;
--topbar-background-color: #dfdbd6;
--chatbox-background-color: #dfdbd6;
--padding-color: #484848;

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"
}