feat: add proper toasts to login and register
Some checks failed
Build and Publish Docker Image / Build and Publish (amd64) (push) Has been cancelled
Build and Publish Docker Image / Build and Publish (arm64) (push) Has been cancelled

This commit is contained in:
mrjvs
2026-08-15 22:52:03 +02:00
parent f796bc00ec
commit cc5f85485c
5 changed files with 147 additions and 28 deletions

View File

@@ -0,0 +1,106 @@
<script setup lang="ts">
import { Toast } from 'reka-ui/namespaced';
const toastStore = useToasts();
function handleOpenUpdate(id: number, open: boolean) {
if (!open) {
setTimeout(() => {
toastStore.remove(id);
}, 1000);
}
}
</script>
<template>
<Toast.Provider disable-swipe>
<div
v-for="toast of toastStore.getAll()"
:key="toast.id"
>
<Toast.Root
v-slot="{ open }"
as-child
force-mount
@update:open="val => handleOpenUpdate(toast.id, val)"
>
<div
class="toast"
:class="{
'type-success': toast.content.type === 'success',
'type-error': toast.content.type === 'error',
}"
>
<Transition
name="toast-slide"
appear
>
<div
v-if="open"
class="content"
>
<Toast.Title>
{{ toast.content.text }}
</Toast.Title>
</div>
</Transition>
</div>
</Toast.Root>
</div>
<Toast.Viewport class="toast-viewport" />
</Toast.Provider>
</template>
<style scoped>
.toast {
bottom: 0;
position: absolute;
left: 0;
right: 0;
display: flex;
justify-content: center;
pointer-events: none;
text-align: center;
}
.toast .content {
border-radius: 5px;
padding: 1em 2em;
pointer-events: initial;
}
.toast.type-error .content {
background: var(--red-shade-1);
}
.toast.type-success .content {
background: var(--green-shade-0);
}
</style>
<style>
.toast-viewport {
z-index: 55;
display: flex;
justify-content: center;
position: fixed;
left: 0;
right: 0;
bottom: 35px;
pointer-events: none;
}
.toast-slide-enter-active,
.toast-slide-leave-active {
transition: transform 500ms ease;
}
.toast-slide-enter-from,
.toast-slide-leave-to {
transform: translateY(150px);
}
.toast-slide-enter-to,
.toast-slide-leave-from {
transform: translateY(0px);
}
</style>

View File

@@ -11,6 +11,7 @@ useHead({
id="root"
class="main-body"
>
<ToastRenderer />
<Navbar />
<div class="wrapper">
<slot />

View File

@@ -2,6 +2,7 @@
const route = useRoute();
const auth = useAuthStore();
const authUtils = useAuthUtils();
const toasts = useToasts();
const redirect = computed(() => route.query.redirect?.toString() ?? null);
const registerURI = computed(() => {
if (redirect.value) {
@@ -14,7 +15,6 @@ const registerURI = computed(() => {
});
const loginForm = reactive({ username: '', password: '' });
const errorMessage = ref<string | null>();
async function loginSubmission() {
try {
@@ -32,11 +32,10 @@ async function loginSubmission() {
await authUtils.safelyRedirectAfterLogin(redirect.value);
} catch (error: unknown) {
const err = getApiError(error);
errorMessage.value = err.code;
setTimeout(() => { // TODO: replace this toast
errorMessage.value = null;
}, 5000);
toasts.publish({
type: 'error',
text: err.message
});
}
}
</script>
@@ -85,14 +84,6 @@ async function loginSubmission() {
</div>
</form>
</div>
<div
v-if="errorMessage"
class="banner-notice error"
>
<div>
<p>{{ errorMessage }}</p>
</div>
</div>
</div>
</template>

View File

@@ -4,6 +4,7 @@ import type { ApiAuthRegisterRequest } from '~~/shared/api-types';
const route = useRoute();
const auth = useAuthStore();
const authUtils = useAuthUtils();
const toasts = useToasts();
const redirect = computed(() => route.query.redirect?.toString() ?? null);
const loginURI = computed(() => {
if (redirect.value) {
@@ -16,8 +17,6 @@ const loginURI = computed(() => {
});
const registerForm = reactive({ email: '', username: '', mii_name: '', password: '', password_confirm: '' });
const errorMessage = ref<string | null>();
const captchaRef = useTemplateRef('captcha');
async function registerSubmission() {
@@ -47,10 +46,10 @@ async function registerSubmission() {
await authUtils.safelyRedirectAfterLogin(redirect.value);
} catch (error: unknown) {
const err = getApiError(error);
errorMessage.value = err.code;
setTimeout(() => { // TODO: replace this toast
errorMessage.value = null;
}, 5000);
toasts.publish({
type: 'error',
text: err.message
});
}
}
</script>
@@ -137,14 +136,6 @@ async function registerSubmission() {
</div>
</form>
</div>
<div
v-if="errorMessage"
class="banner-notice error"
>
<div>
<p>{{ errorMessage }}</p>
</div>
</div>
</div>
</template>

30
src/stores/toasts.ts Normal file
View File

@@ -0,0 +1,30 @@
export type ToastContent = {
type: 'success' | 'error';
text: string;
};
export type ToastItem = {
id: number;
content: ToastContent;
};
export const useToasts = defineStore('toasts', () => {
let idCounter = 0;
const toasts = ref<ToastItem[]>([]);
const readonlyToasts = computed(() => toasts.value);
return {
getAll() {
return readonlyToasts.value;
},
remove(id: number) {
toasts.value = toasts.value.filter(v => v.id !== id);
},
publish(item: ToastContent) {
toasts.value.push({
id: idCounter++,
content: item
});
}
};
});