mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-08-26 18:47:47 -05:00
feat: add proper toasts to login and register
This commit is contained in:
106
src/components/ToastRenderer.vue
Normal file
106
src/components/ToastRenderer.vue
Normal 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>
|
||||
@@ -11,6 +11,7 @@ useHead({
|
||||
id="root"
|
||||
class="main-body"
|
||||
>
|
||||
<ToastRenderer />
|
||||
<Navbar />
|
||||
<div class="wrapper">
|
||||
<slot />
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
30
src/stores/toasts.ts
Normal 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
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user