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

This commit is contained in:
SauceyRed 2025-07-09 17:53:35 +00:00
commit 822b16ae07
23 changed files with 477 additions and 66 deletions

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

@ -3,7 +3,7 @@
<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" />
<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 {

View file

@ -158,10 +158,12 @@ if (accessToken && apiBase) {
function sendMessage(e: Event) {
e.preventDefault();
const text = messageInput.value;
console.log("text:", text);
if (text) {
ws.send(text);
const message = {
message: messageInput.value
}
console.log("message:", message);
if (message.message) {
ws.send(JSON.stringify(message));
messageInput.value = "";
console.log("MESSAGE SENT!!!");
}

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

@ -78,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;
@ -101,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;