Compare commits

...
Sign in to create a new pull request.

7 commits

Author SHA1 Message Date
8be948623b ci: only run on push
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
prevents duplicate CIs from running at the same time
2025-07-22 18:55:57 +02:00
82e0e59617
fix: logout not working due to use of outdated HTTP method
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-21 12:33:21 +02:00
b07a0aa5a0 Merge pull request 'Implement invite redemption page' (#54) from invite-page into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Reviewed-on: #54
Reviewed-by: Twig <git@beaver.mom>
2025-07-20 09:15:52 +00:00
eb2af0f7ec Merge branch 'main' into invite-page
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-20 09:15:22 +00:00
ddf173ee8b
fix: broken cropping tool due to missing imports
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-20 04:57:56 +02:00
6a65b257e0
feat: add basic page for viewing and accepting an invite
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-20 02:34:30 +02:00
c87fffe6c9
feat: add fetchInvite function 2025-07-20 02:33:57 +02:00
6 changed files with 190 additions and 4 deletions

View file

@ -8,7 +8,6 @@ steps:
- pnpm build
when:
- event: push
- event: pull_request
- name: container-build-and-publish
image: docker

View file

@ -10,6 +10,7 @@
<script lang="ts" setup>
import Cropper from 'cropperjs';
import Button from '../UserInterface/Button.vue';
const props = defineProps({
imageSrc: String,

View file

@ -33,6 +33,7 @@
</template>
<script lang="ts" setup>
import CropPopup from '~/components/Popups/CropPopup.vue';
import UserPopup from '~/components/User/UserPopup.vue';
import Button from '~/components/UserInterface/Button.vue';
@ -41,7 +42,7 @@ 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()

View file

@ -94,6 +94,10 @@ export const useApi = () => {
await fetchWithApi("/auth/reset-password", { method: "POST", body: { password, token } });
}
async function fetchInvite(id: string): Promise<GuildResponse | undefined> {
return await fetchWithApi(`/invites/${id}`);
}
return {
fetchGuilds,
fetchGuild,
@ -115,6 +119,7 @@ export const useApi = () => {
fetchInstanceStats,
sendVerificationEmail,
sendPasswordResetEmail,
resetPassword
resetPassword,
fetchInvite
}
}

View file

@ -41,7 +41,7 @@ export const useAuth = () => {
async function logout() {
console.log("access:", accessToken.value);
await fetchWithApi("/auth/logout", { method: "GET", credentials: "include" });
await fetchWithApi("/auth/logout", { method: "DELETE", credentials: "include" });
clearAuth();
return await navigateTo("/login");

180
pages/invite/[inviteId].vue Normal file
View file

@ -0,0 +1,180 @@
<template>
<div id="invite-root">
<div id="invite-container">
<div id="guild-container" v-if="guild">
<h1>You have been invited to {{ guild.name }}!</h1>
<div id="guild-card">
<div id="card-grid">
<div id="guild-details">
<div id="guild-name" title="Server name">
<span>{{ guild.name }}</span>
</div>
<div id="guild-member-count" :title="`${guild.member_count} members`">
<Icon name="lucide:users" />
<span>{{ guild.member_count }}</span>
</div>
</div>
<VerticalSpacer id="space" />
<div id="guild-description">
<span>{{ guild.description }}</span>
</div>
<div id="guild-icon">
<NuxtImg v-if="guild.icon" id="guild-icon-img" :src="guild.icon" :alt="`${guild.name} server icon`" />
</div>
</div>
<Button :text="isMember ? 'Joined' : 'Join'" variant="normal" :callback="acceptInvite" />
</div>
</div>
<div v-else-if="errorMessage">
<h1>{{ errorMessage }}</h1>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import Button from '~/components/UserInterface/Button.vue';
import VerticalSpacer from '~/components/UserInterface/VerticalSpacer.vue';
import type { GuildResponse } from '~/types/interfaces';
const route = useRoute();
const { fetchInvite, joinGuild, fetchMembers } = useApi();
const { getUser } = useAuth();
const inviteId = route.params.inviteId as string;
const guild = ref<GuildResponse>();
const errorMessage = ref<string>();
const isMember = ref(false);
const accessToken = useCookie("access_token");
if (inviteId) {
try {
guild.value = await fetchInvite(inviteId);
console.log("invite guild:", guild.value);
if (accessToken.value && guild.value) {
const members = await fetchMembers(guild.value.uuid);
const me = await getUser();
if (me && members.find(member => member.user.uuid == me.uuid)) {
isMember.value = true;
}
}
} catch (error: any) {
if (error.response) {
if (error.status == 404) {
errorMessage.value = "That invite doesn't exist or has expired.";
}
}
console.error(error);
}
}
async function acceptInvite() {
if (accessToken.value && guild.value) {
await joinGuild(inviteId);
return await navigateTo(`/servers/${guild.value.uuid}`);
}
return await navigateTo(`/login?redirect_to=${route.fullPath}`);
}
</script>
<style>
#invite-root {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100dvh;
}
#invite-container {
border: .5rem solid var(--chat-highlighted-background-color);
border-radius: var(--standard-radius);
height: 50%;
width: 50%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 50%;
height: 60%;
}
#guild-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 50%;
height: 60%;
}
#guild-card {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
background-color: var(--sidebar-highlighted-background-color);
border: .5rem solid black;
border-radius: var(--standard-radius);
padding: .5rem;
}
#card-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 5rem auto 1fr;
height: 100%;
width: 100%;
}
#guild-details {
grid-row: 1;
grid-column: span 2;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#guild-name {
font-size: 2rem;
flex-direction: column;
}
#guild-member-count {
gap: .3rem;
}
#space {
grid-row: 2;
grid-column: span 3;
}
#guild-description {
grid-row: 3;
grid-column: span 3;
word-break: break-all;
padding: .3rem;
}
#guild-name, #guild-member-count {
display: flex;
justify-content: center;
align-items: center;
}
#guild-icon-img {
height: 100%;
width: 100%;
object-fit: scale-down;
}
</style>