mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-08-27 02:56:47 -05:00
feat: add useAsync and implement it on every async mutation method
This commit is contained in:
@@ -2,6 +2,15 @@
|
||||
import { Toast } from 'reka-ui/namespaced';
|
||||
|
||||
const toastStore = useToasts();
|
||||
const route = useRoute();
|
||||
const routeKey = computed(() => route.path);
|
||||
|
||||
watch(routeKey, () => {
|
||||
toastStore.clear(); // Clear toasts when route changes
|
||||
});
|
||||
onUnmounted(() => {
|
||||
toastStore.clear(); // Clear toasts toast rendered gets removed (route change)
|
||||
});
|
||||
|
||||
function handleOpenUpdate(id: number, open: boolean) {
|
||||
if (!open) {
|
||||
|
||||
43
src/composables/useAsync.ts
Normal file
43
src/composables/useAsync.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useAsyncState } from '@vueuse/core';
|
||||
|
||||
export type UseAsyncOptions<TResult, TArgs extends any[] = []> = {
|
||||
handler(...args: TArgs): Promise<TResult>;
|
||||
onSuccess?: (data: TResult) => void;
|
||||
onError?: (error: any) => void;
|
||||
allowParallel?: boolean;
|
||||
};
|
||||
|
||||
export type UseAsyncResult<TResult, TArgs extends any[] = []> = {
|
||||
state: Ref<TResult | null>;
|
||||
isLoading: Ref<boolean>;
|
||||
error: Ref<unknown>;
|
||||
execute: (...args: TArgs) => Promise<void>;
|
||||
};
|
||||
|
||||
export function useAsync<TResult, TArgs extends any[] = []>(ops: UseAsyncOptions<TResult, TArgs>): UseAsyncResult<TResult, TArgs> {
|
||||
const preventDuringLoading = !ops.allowParallel;
|
||||
const output = useAsyncState<TResult | null, TArgs, true>(ops.handler, null, {
|
||||
immediate: false,
|
||||
onSuccess(data) {
|
||||
ops.onSuccess?.(data as any);
|
||||
},
|
||||
onError(err) {
|
||||
ops.onError?.(err);
|
||||
}
|
||||
});
|
||||
|
||||
async function execute(...args: TArgs) {
|
||||
if (preventDuringLoading && output.isLoading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await output.executeImmediate(...args);
|
||||
}
|
||||
|
||||
return {
|
||||
state: output.state,
|
||||
isLoading: output.isLoading,
|
||||
error: output.error,
|
||||
execute
|
||||
};
|
||||
}
|
||||
@@ -18,6 +18,7 @@ useHead({
|
||||
id="root"
|
||||
class="main-body"
|
||||
>
|
||||
<ToastRenderer />
|
||||
<div class="docs-wrapper">
|
||||
<a
|
||||
href="/"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div id="root">
|
||||
<ToastRenderer />
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,17 +2,13 @@
|
||||
import type { ApiAuthForgotPasswordRequest } from '~~/shared/api-types';
|
||||
|
||||
const { t } = useI18n();
|
||||
const toasts = useToasts();
|
||||
const captchaRef = useTemplateRef('captcha');
|
||||
|
||||
const form = reactive({ emailOrUsername: '' });
|
||||
|
||||
const errorMessage = ref<string | null>();
|
||||
const successMessage = ref<string | null>();
|
||||
const captchaRef = useTemplateRef('captcha');
|
||||
|
||||
async function submit() {
|
||||
errorMessage.value = null;
|
||||
successMessage.value = null;
|
||||
try {
|
||||
const { execute } = useAsync({
|
||||
async handler() {
|
||||
let captchaResponse: string | null = null;
|
||||
if (captchaRef.value) {
|
||||
captchaResponse = await captchaRef.value.getToken();
|
||||
@@ -28,21 +24,21 @@ async function submit() {
|
||||
captchaResponse: captchaResponse ?? undefined
|
||||
} satisfies ApiAuthForgotPasswordRequest
|
||||
});
|
||||
|
||||
// Success
|
||||
form.emailOrUsername = '';
|
||||
successMessage.value = 'An email has been sent.';
|
||||
setTimeout(() => {
|
||||
successMessage.value = null;
|
||||
}, 5000);
|
||||
} catch (error: unknown) {
|
||||
},
|
||||
onSuccess() {
|
||||
toasts.publish({
|
||||
type: 'success',
|
||||
text: 'An email has been sent.'
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
const err = getApiError(error);
|
||||
errorMessage.value = err.code;
|
||||
setTimeout(() => {
|
||||
errorMessage.value = null;
|
||||
}, 5000);
|
||||
toasts.publish({
|
||||
type: 'error',
|
||||
text: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -50,7 +46,7 @@ async function submit() {
|
||||
<div class="account-form-wrapper">
|
||||
<form
|
||||
class="account forgot-password"
|
||||
@submit.prevent="submit"
|
||||
@submit.prevent="execute"
|
||||
>
|
||||
<h2>{{ t('account.forgotPassword.header') }}</h2>
|
||||
<p>{{ t('account.forgotPassword.sub') }}</p>
|
||||
@@ -77,22 +73,6 @@ async function submit() {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
v-if="successMessage"
|
||||
class="banner-notice success"
|
||||
>
|
||||
<div>
|
||||
<p>{{ successMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
class="banner-notice error"
|
||||
>
|
||||
<div>
|
||||
<p>{{ errorMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,30 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable vue/no-v-html -- locale files still have raw html */
|
||||
import {
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogRoot,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger, ToastDescription, ToastProvider, ToastRoot, ToastViewport
|
||||
} from 'reka-ui';
|
||||
import { watchImmediate } from '@vueuse/core';
|
||||
import { AlertDialog } from 'reka-ui/namespaced';
|
||||
import type { ApiAccountUpdateRequest } from '~~/shared/api-types';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const toasts = useToasts();
|
||||
const route = useRoute();
|
||||
definePageMeta({
|
||||
needsAuth: true
|
||||
});
|
||||
|
||||
const upgradeSuccess = ref(useRoute().query.upgrade_success === 'true');
|
||||
const upgradeError = ref(useRoute().query.upgrade_success === 'false');
|
||||
const showToast = computed(() => upgradeSuccess.value || upgradeError.value);
|
||||
if (showToast.value) {
|
||||
const upgradeSuccessQuery = computed(() => route.query.upgrade_success);
|
||||
watchImmediate(upgradeSuccessQuery, (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
if (val === 'true') {
|
||||
toasts.publish({
|
||||
type: 'success',
|
||||
text: 'Account upgraded successfully'
|
||||
});
|
||||
}
|
||||
if (val === 'false') {
|
||||
toasts.publish({
|
||||
type: 'error',
|
||||
text: 'Account upgrade failed'
|
||||
});
|
||||
}
|
||||
useRouter().replace({ query: {} });
|
||||
}
|
||||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const { data: profile, refresh } = await useApiFetch('/api/auth/me');
|
||||
const { data: connections, refresh: refreshConnections } = await useApiFetch('/api/auth/me-connections');
|
||||
|
||||
@@ -33,10 +39,8 @@ const dialogContainer = ref(null);
|
||||
const deleteModalOpen = ref(false);
|
||||
const editModalOpen = ref(false);
|
||||
|
||||
async function updateServerEnvironment(
|
||||
env: ApiAccountUpdateRequest['environment']
|
||||
) {
|
||||
try {
|
||||
const { execute: executeUpdateServerEnvironment } = useAsync({
|
||||
async handler(env: ApiAccountUpdateRequest['environment']) {
|
||||
await apiFetch('/api/account/update', {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
@@ -44,49 +48,65 @@ async function updateServerEnvironment(
|
||||
} satisfies ApiAccountUpdateRequest
|
||||
});
|
||||
await refresh();
|
||||
} catch (error: unknown) {
|
||||
},
|
||||
onError(error) {
|
||||
const err = getApiError(error);
|
||||
alert(err.code);
|
||||
toasts.publish({
|
||||
type: 'error',
|
||||
text: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function deleteAccount() {
|
||||
try {
|
||||
const { execute: executeDeleteAccount } = useAsync({
|
||||
async handler() {
|
||||
await apiFetch('/api/account/delete', {
|
||||
method: 'POST'
|
||||
});
|
||||
authStore.logout();
|
||||
await navigateTo('/');
|
||||
} catch (error: unknown) {
|
||||
},
|
||||
onError(error) {
|
||||
const err = getApiError(error);
|
||||
alert(err.code);
|
||||
toasts.publish({
|
||||
type: 'error',
|
||||
text: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function linkDiscord() {
|
||||
try {
|
||||
const { execute: executeLinkDiscord } = useAsync({
|
||||
async handler() {
|
||||
const result = await apiFetch('/api/account/discord-link', {
|
||||
method: 'GET'
|
||||
});
|
||||
await navigateTo(result.url, { external: true });
|
||||
} catch (error: unknown) {
|
||||
},
|
||||
onError(error) {
|
||||
const err = getApiError(error);
|
||||
alert(err.code);
|
||||
toasts.publish({
|
||||
type: 'error',
|
||||
text: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function unlinkDiscord() {
|
||||
try {
|
||||
const { execute: executeUnlinkDiscord } = useAsync({
|
||||
async handler() {
|
||||
await apiFetch('/api/account/discord-unlink', {
|
||||
method: 'POST'
|
||||
});
|
||||
await refresh();
|
||||
await refreshConnections();
|
||||
} catch (error: unknown) {
|
||||
},
|
||||
onError(error) {
|
||||
const err = getApiError(error);
|
||||
alert(err.code);
|
||||
toasts.publish({
|
||||
type: 'error',
|
||||
text: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: `Account`
|
||||
@@ -152,18 +172,18 @@ useHead({
|
||||
{{ $t("account.settings.upgrade") }}
|
||||
</p>
|
||||
</NuxtLink>
|
||||
<AlertDialogRoot v-model:open="deleteModalOpen">
|
||||
<AlertDialogTrigger
|
||||
<AlertDialog.Root v-model:open="deleteModalOpen">
|
||||
<AlertDialog.Trigger
|
||||
id="account-delete"
|
||||
class="secondary"
|
||||
>
|
||||
{{ $t("account.settings.delete.button") }}
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogPortal :to="dialogContainer ?? undefined">
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogContent class="modal">
|
||||
<AlertDialogTitle>{{ $t("account.settings.delete.modalTitle") }}?</AlertDialogTitle>
|
||||
<AlertDialogDescription class="modal-caption">
|
||||
</AlertDialog.Trigger>
|
||||
<AlertDialog.Portal :to="dialogContainer ?? undefined">
|
||||
<AlertDialog.Overlay />
|
||||
<AlertDialog.Content class="modal">
|
||||
<AlertDialog.Title>{{ $t("account.settings.delete.modalTitle") }}?</AlertDialog.Title>
|
||||
<AlertDialog.Description class="modal-caption">
|
||||
<p
|
||||
style="white-space: pre-line;"
|
||||
>
|
||||
@@ -172,24 +192,24 @@ useHead({
|
||||
<p class="noundo">
|
||||
{{ $t('account.settings.delete.modalCaution') }}
|
||||
</p>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialog.Description>
|
||||
<div class="modal-button-wrapper">
|
||||
<AlertDialogCancel class="cancel">
|
||||
<AlertDialog.Cancel class="cancel">
|
||||
{{ $t("modals.cancel") }}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
class="alert"
|
||||
@click="deleteAccount"
|
||||
</AlertDialog.Cancel>
|
||||
<AlertDialog.Action
|
||||
class="alert."
|
||||
@click="executeDeleteAccount"
|
||||
>
|
||||
{{ $t("account.settings.delete.modalConfirm") }}
|
||||
</AlertDialogAction>
|
||||
</AlertDialog.Action>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
</div>
|
||||
</div>
|
||||
<AlertDialogRoot v-model:open="editModalOpen">
|
||||
<AlertDialog.Root v-model:open="editModalOpen">
|
||||
<div class="settings-wrapper">
|
||||
<h2
|
||||
id="user-settings"
|
||||
@@ -202,30 +222,30 @@ useHead({
|
||||
{{ $t("account.settings.settingCards.profile") }}
|
||||
</h2>
|
||||
|
||||
<AlertDialogTrigger
|
||||
<AlertDialog.Trigger
|
||||
class="edit"
|
||||
>
|
||||
<Icon
|
||||
name="ph:pencil"
|
||||
size="26"
|
||||
/>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogPortal :to="dialogContainer ?? undefined">
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogContent class="modal">
|
||||
<AlertDialogTitle>{{ $t("account.settings.unavailable") }}.</AlertDialogTitle>
|
||||
<AlertDialogDescription class="modal-caption">
|
||||
</AlertDialog.Trigger>
|
||||
<AlertDialog.Portal :to="dialogContainer ?? undefined">
|
||||
<AlertDialog.Overlay />
|
||||
<AlertDialog.Content class="modal">
|
||||
<AlertDialog.Title>{{ $t("account.settings.unavailable") }}.</AlertDialog.Title>
|
||||
<AlertDialog.Description class="modal-caption">
|
||||
<p>
|
||||
{{ $t('account.settings.settingCards.no_edit_from_dashboard') }}
|
||||
</p>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialog.Description>
|
||||
<div class="modal-button-wrapper">
|
||||
<AlertDialogCancel class="cancel">
|
||||
<AlertDialog.Cancel class="cancel">
|
||||
{{ $t("modals.close") }}
|
||||
</AlertDialogCancel>
|
||||
</AlertDialog.Cancel>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Portal>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<p class="label">
|
||||
@@ -316,7 +336,7 @@ useHead({
|
||||
"
|
||||
id="save-server-selection"
|
||||
class="button secondary"
|
||||
@click.prevent="() => updateServerEnvironment(selectedServerEnv)"
|
||||
@click.prevent="() => executeUpdateServerEnvironment(selectedServerEnv)"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
@@ -339,14 +359,14 @@ useHead({
|
||||
<h2 class="header">
|
||||
{{ $t("account.account") }}
|
||||
</h2>
|
||||
<AlertDialogTrigger
|
||||
<AlertDialog.Trigger
|
||||
class="edit"
|
||||
>
|
||||
<Icon
|
||||
name="ph:pencil"
|
||||
size="26"
|
||||
/>
|
||||
</AlertDialogTrigger>
|
||||
</AlertDialog.Trigger>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<p class="label">
|
||||
@@ -403,7 +423,7 @@ useHead({
|
||||
v-if="profile.discordId"
|
||||
id="remove-discord-connection"
|
||||
class="button secondary"
|
||||
@click="unlinkDiscord"
|
||||
@click="executeUnlinkDiscord"
|
||||
>
|
||||
{{ $t("account.settings.settingCards.removeDiscord") }}
|
||||
</button>
|
||||
@@ -411,7 +431,7 @@ useHead({
|
||||
{{ $t("account.settings.settingCards.noDiscordLinked") }}
|
||||
<NuxtLink
|
||||
:style="{cursor: 'pointer'}"
|
||||
@click="linkDiscord"
|
||||
@click="executeLinkDiscord"
|
||||
>
|
||||
{{ $t("account.settings.settingCards.linkDiscord") }}
|
||||
</NuxtLink>
|
||||
@@ -431,36 +451,13 @@ useHead({
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogRoot>
|
||||
</AlertDialog.Root>
|
||||
<div
|
||||
id="delete-account"
|
||||
:class="{ 'modal-wrapper': true, hidden: !(deleteModalOpen || editModalOpen) }"
|
||||
>
|
||||
<div ref="dialogContainer" />
|
||||
</div>
|
||||
<div
|
||||
v-if="showToast"
|
||||
:class="{'banner-notice': true, success: upgradeSuccess, error: upgradeError }"
|
||||
>
|
||||
<ToastProvider>
|
||||
<ToastRoot as="div">
|
||||
<ToastDescription
|
||||
v-if="upgradeSuccess"
|
||||
as="p"
|
||||
>
|
||||
Account upgraded successfully
|
||||
</ToastDescription>
|
||||
<ToastDescription
|
||||
v-if="upgradeError"
|
||||
as="p"
|
||||
>
|
||||
Account upgrade failed
|
||||
</ToastDescription>
|
||||
</ToastRoot>
|
||||
|
||||
<ToastViewport as="p" />
|
||||
</ToastProvider>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ const registerURI = computed(() => {
|
||||
|
||||
const loginForm = reactive({ username: '', password: '' });
|
||||
|
||||
async function loginSubmission() {
|
||||
try {
|
||||
const { execute } = useAsync({
|
||||
async handler() {
|
||||
const res = await $fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
@@ -30,14 +30,15 @@ async function loginSubmission() {
|
||||
refreshToken: res.refreshToken
|
||||
});
|
||||
await authUtils.safelyRedirectAfterLogin(redirect.value);
|
||||
} catch (error: unknown) {
|
||||
},
|
||||
onError(error) {
|
||||
const err = getApiError(error);
|
||||
toasts.publish({
|
||||
type: 'error',
|
||||
text: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -45,7 +46,7 @@ async function loginSubmission() {
|
||||
<div class="account-form-wrapper">
|
||||
<form
|
||||
class="account"
|
||||
@submit.prevent="loginSubmission"
|
||||
@submit.prevent="execute"
|
||||
>
|
||||
<h2>{{ $t("account.loginForm.login") }}</h2>
|
||||
<p>{{ $t("account.loginForm.detailsPrompt") }}</p>
|
||||
|
||||
@@ -37,7 +37,6 @@ const miiFaceUrl = ref<string>('');
|
||||
const miiBodyUrl = ref<string>('');
|
||||
const loadingCanvas = ref<boolean>(false);
|
||||
// these are used in the save modal
|
||||
const saving = ref<boolean>(false);
|
||||
let oldMii: Mii | null = null;
|
||||
let oldMiiNeutralUrl = '';
|
||||
let oldMiiSorrowUrl = '';
|
||||
@@ -277,34 +276,37 @@ function getSubPageFromValue(value: string, newSubTab: string, newTab: string) {
|
||||
return subpageIndex;
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
await apiFetch('/api/account/update', {
|
||||
const toasts = useToasts();
|
||||
const { isLoading: isSaving, execute } = useAsync({
|
||||
handler() {
|
||||
return apiFetch('/api/account/update', {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
mii: { name: mii.value.miiName, primary: 'Y', data: mii.value.encode().toString('base64') }
|
||||
} satisfies ApiAccountUpdateRequest
|
||||
});
|
||||
|
||||
},
|
||||
onSuccess() {
|
||||
toasts.publish({
|
||||
type: 'success',
|
||||
text: t('miiEditor.miiSaved')
|
||||
});
|
||||
setTimeout(() => {
|
||||
// TODO - Make this prettier
|
||||
alert(t('miiEditor.miiSaved'));
|
||||
|
||||
navigateTo('/account');
|
||||
}, 3000);
|
||||
|
||||
// await refresh();
|
||||
} catch (error: unknown) {
|
||||
},
|
||||
onError(error) {
|
||||
const err = getApiError(error);
|
||||
alert(`${err.code}: ${err.message}`);
|
||||
toasts.publish({
|
||||
type: 'error',
|
||||
text: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ 'miieditor-wrapper': true, saving: saving }">
|
||||
<div :class="{ 'miieditor-wrapper': true, saving: isSaving }">
|
||||
<svg
|
||||
class="logotype"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -654,7 +656,7 @@ async function handleSave() {
|
||||
<button
|
||||
id="saveButton"
|
||||
:class="{ button: true, primary: true }"
|
||||
@click.prevent="handleSave()"
|
||||
@click.prevent="execute()"
|
||||
>
|
||||
{{ $t("miiEditor.save") }}!
|
||||
</button>
|
||||
|
||||
@@ -19,8 +19,8 @@ const loginURI = computed(() => {
|
||||
const registerForm = reactive({ email: '', username: '', mii_name: '', password: '', password_confirm: '' });
|
||||
const captchaRef = useTemplateRef('captcha');
|
||||
|
||||
async function registerSubmission() {
|
||||
try {
|
||||
const { execute } = useAsync({
|
||||
async handler() {
|
||||
let captchaResponse: string | null = null;
|
||||
if (captchaRef.value) {
|
||||
captchaResponse = await captchaRef.value.getToken();
|
||||
@@ -44,14 +44,15 @@ async function registerSubmission() {
|
||||
refreshToken: res.refreshToken
|
||||
});
|
||||
await authUtils.safelyRedirectAfterLogin(redirect.value);
|
||||
} catch (error: unknown) {
|
||||
},
|
||||
onError(error) {
|
||||
const err = getApiError(error);
|
||||
toasts.publish({
|
||||
type: 'error',
|
||||
text: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -59,7 +60,7 @@ async function registerSubmission() {
|
||||
<div class="account-form-wrapper">
|
||||
<form
|
||||
class="account register"
|
||||
@submit.prevent="registerSubmission"
|
||||
@submit.prevent="execute"
|
||||
>
|
||||
<h2>{{ $t("account.loginForm.register") }}</h2>
|
||||
<p>{{ $t("account.loginForm.detailsPrompt") }}</p>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import type { ApiAuthResetPasswordRequest } from '~~/shared/api-types';
|
||||
|
||||
const route = useRoute();
|
||||
const toasts = useToasts();
|
||||
const { t } = useI18n();
|
||||
const form = reactive({ password: '', passwordConfirm: '' });
|
||||
const errorMessage = ref<string | null>();
|
||||
|
||||
async function submit() {
|
||||
const resetToken = route.query.token?.toString();
|
||||
try {
|
||||
const { execute } = useAsync({
|
||||
async handler() {
|
||||
const resetToken = route.query.token?.toString();
|
||||
if (!resetToken) {
|
||||
throw new Error('No reset token provided');
|
||||
}
|
||||
@@ -21,14 +21,15 @@ async function submit() {
|
||||
} satisfies ApiAuthResetPasswordRequest
|
||||
});
|
||||
await navigateTo('/account');
|
||||
} catch (error: unknown) {
|
||||
},
|
||||
onError(error) {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -36,7 +37,7 @@ async function submit() {
|
||||
<div class="account-form-wrapper">
|
||||
<form
|
||||
class="account"
|
||||
@submit.prevent="submit"
|
||||
@submit.prevent="execute"
|
||||
>
|
||||
<h2>{{ t('account.resetPassword.header') }}</h2>
|
||||
<p>{{ t('account.resetPassword.sub') }}</p>
|
||||
@@ -69,14 +70,6 @@ async function submit() {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
class="banner-notice error"
|
||||
>
|
||||
<div>
|
||||
<p>{{ errorMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ definePageMeta({
|
||||
needsAuth: true
|
||||
});
|
||||
|
||||
const toasts = useToasts();
|
||||
const { data: tierData } = await useApiFetch('/api/account/tiers');
|
||||
const { data: profile } = await useApiFetch('/api/auth/me');
|
||||
const sortedTiers = computed(() =>
|
||||
@@ -41,20 +42,24 @@ const goalTextVars = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
async function unsubscribe() {
|
||||
try {
|
||||
const { execute: executeUnsubscribe } = useAsync({
|
||||
async handler() {
|
||||
await apiFetch('/api/account/unsubscribe', {
|
||||
method: 'POST'
|
||||
});
|
||||
await navigateTo('/account');
|
||||
} catch (error: unknown) {
|
||||
},
|
||||
onError(error) {
|
||||
const err = getApiError(error);
|
||||
alert(err.code);
|
||||
toasts.publish({
|
||||
type: 'error',
|
||||
text: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function checkout(priceId: string) {
|
||||
try {
|
||||
const { execute: executeCheckout } = useAsync({
|
||||
async handler(priceId: string) {
|
||||
const result = await apiFetch('/api/account/checkout', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
@@ -62,11 +67,15 @@ async function checkout(priceId: string) {
|
||||
} satisfies ApiAccountCheckoutRequest
|
||||
});
|
||||
await navigateTo(result.url, { external: true });
|
||||
} catch (error: unknown) {
|
||||
},
|
||||
onError(error) {
|
||||
const err = getApiError(error);
|
||||
alert(err.code);
|
||||
toasts.publish({
|
||||
type: 'error',
|
||||
text: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function hasSubscription(priceId: string) {
|
||||
return (
|
||||
@@ -284,7 +293,7 @@ useHead({
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
class="alert"
|
||||
@click="unsubscribe"
|
||||
@click="executeUnsubscribe"
|
||||
>
|
||||
{{ $t("upgrade.unsubConfirm") }}
|
||||
</AlertDialogAction>
|
||||
@@ -314,7 +323,7 @@ useHead({
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
class="action"
|
||||
@click="() => checkout(selectedTier?.priceId || '')"
|
||||
@click="() => executeCheckout(selectedTier?.priceId || '')"
|
||||
>
|
||||
{{ $t("modals.confirm") }}
|
||||
</AlertDialogAction>
|
||||
@@ -330,7 +339,7 @@ useHead({
|
||||
>
|
||||
<button
|
||||
|
||||
@click.prevent="checkout(selectedTier.priceId)"
|
||||
@click.prevent="executeCheckout(selectedTier.priceId)"
|
||||
>
|
||||
Subscribe to {{ selectedTier.name }}
|
||||
</button>
|
||||
|
||||
@@ -20,6 +20,9 @@ export const useToasts = defineStore('toasts', () => {
|
||||
remove(id: number) {
|
||||
toasts.value = toasts.value.filter(v => v.id !== id);
|
||||
},
|
||||
clear() {
|
||||
toasts.value = [];
|
||||
},
|
||||
publish(item: ToastContent) {
|
||||
toasts.value.push({
|
||||
id: idCounter++,
|
||||
|
||||
Reference in New Issue
Block a user