feat: add loading indicators to every async action

This commit is contained in:
mrjvs
2026-08-16 14:45:39 +02:00
parent 32d686ee1a
commit 4a68c78224
8 changed files with 52 additions and 165 deletions

View File

@@ -33,7 +33,7 @@ form.account p {
margin: 12px 0;
}
form.account div {
form.account > div {
margin-top: 24px;
}

View File

@@ -1,131 +0,0 @@
.account-form-wrapper {
height: 80vh;
display: flex;
justify-content: center;
align-content: center;
flex-direction: column;
margin: 15vh auto;
width: fit-content;
overflow: hidden;
}
form.account {
display: block;
padding: 40px 48px;
background-color: var(--bg-shade-2);
color: var(--text-shade-1);
border-radius: 12px;
width: min(480px, 90vw);
box-sizing: border-box;
}
form.account h2 {
margin: 0;
color: var(--text-shade-3);
}
form.account p {
margin: 12px 0;
}
form.account div {
margin-top: 24px;
}
form.account label {
display: block;
margin-bottom: 6px;
text-transform: uppercase;
font-size: 12px;
}
form.account button {
width: 100%;
background: var(--accent-shade-0);
}
form.account a {
text-decoration: none;
display: block;
color: var(--text-shade-1);
text-align: right;
margin: 6px 0;
width: fit-content;
}
form.account a:hover {
color: var(--text-shade-3);
}
form.account a.pwdreset {
margin-left: auto;
font-size: 14px;
}
form.account a.register {
margin: auto;
margin-top: 18px;
}
@keyframes banner-notice {
0% {
top: -150px;
}
20% {
top: 35px;
}
80% {
top: 35px;
}
100% {
top: -150px;
}
}
.banner-notice {
display: flex;
justify-content: center;
position: fixed;
top: -150px;
width: 100%;
animation: banner-notice 5s;
}
.banner-notice div {
padding: 4px 36px;
border-radius: 5px;
z-index: 3;
}
.banner-notice.success div {
background: var(--green-shade-0);
}
.banner-notice.error div {
background: var(--red-shade-1);
}
form.account.register {
display: grid;
grid-template-columns: repeat(2, 1fr);
width: min(780px, 90vw);
column-gap: 24px;
margin-bottom: 48px;
}
form.account.forgot-password div.h-captcha {
grid-column: 1 / span 2;
display: flex;
justify-content: center;
}
form.account.register p,
form.account.register div.email,
form.account.register div.buttons {
grid-column: 1 / span 2;
}
@media screen and (max-width: 720px) {
form.account.register {
grid-template-columns: 1fr;
}
form.account.register div.h-captcha,
form.account.register p,
form.account.register div.email,
form.account.register div.buttons {
grid-column: unset;
}
}

View File

@@ -7,7 +7,7 @@ const captchaRef = useTemplateRef('captcha');
const form = reactive({ emailOrUsername: '' });
const { execute } = useAsync({
const { execute, isLoading } = useAsync({
async handler() {
let captchaResponse: string | null = null;
if (captchaRef.value) {
@@ -68,7 +68,8 @@ const { execute } = useAsync({
/>
<div class="buttons">
<button type="submit">
{{ t('account.forgotPassword.submit') }}
<Loader v-if="isLoading" />
<span v-else>{{ $t("account.forgotPassword.submit") }}</span>
</button>
</div>
</form>

View File

@@ -39,7 +39,7 @@ const dialogContainer = ref(null);
const deleteModalOpen = ref(false);
const editModalOpen = ref(false);
const { execute: executeUpdateServerEnvironment } = useAsync({
const { execute: executeUpdateServerEnvironment, isLoading: isLoadingUpdateServerEnv } = useAsync({
async handler(env: ApiAccountUpdateRequest['environment']) {
await apiFetch('/api/account/update', {
method: 'PATCH',
@@ -58,7 +58,7 @@ const { execute: executeUpdateServerEnvironment } = useAsync({
}
});
const { execute: executeDeleteAccount } = useAsync({
const { execute: executeDeleteAccount, isLoading: isLoadingDelete } = useAsync({
async handler() {
await apiFetch('/api/account/delete', {
method: 'POST'
@@ -75,7 +75,7 @@ const { execute: executeDeleteAccount } = useAsync({
}
});
const { execute: executeLinkDiscord } = useAsync({
const { execute: executeLinkDiscord, isLoading: isLoadingLink } = useAsync({
async handler() {
const result = await apiFetch('/api/account/discord-link', {
method: 'GET'
@@ -91,7 +91,7 @@ const { execute: executeLinkDiscord } = useAsync({
}
});
const { execute: executeUnlinkDiscord } = useAsync({
const { execute: executeUnlinkDiscord, isLoading: isLoadingUnlink } = useAsync({
async handler() {
await apiFetch('/api/account/discord-unlink', {
method: 'POST'
@@ -194,15 +194,19 @@ useHead({
</p>
</AlertDialog.Description>
<div class="modal-button-wrapper">
<AlertDialog.Cancel class="cancel">
<AlertDialog.Cancel
:disabled="isLoadingDelete"
class="cancel"
>
{{ $t("modals.cancel") }}
</AlertDialog.Cancel>
<AlertDialog.Action
class="alert."
<button
class="alert"
@click="executeDeleteAccount"
>
{{ $t("account.settings.delete.modalConfirm") }}
</AlertDialog.Action>
<Loader v-if="isLoadingDelete" />
<span v-else>{{ $t("account.settings.delete.modalConfirm") }}</span>
</button>
</div>
</AlertDialog.Content>
</AlertDialog.Portal>
@@ -338,7 +342,8 @@ useHead({
class="button secondary"
@click.prevent="() => executeUpdateServerEnvironment(selectedServerEnv)"
>
Save
<Loader v-if="isLoadingUpdateServerEnv" />
<span v-else>Save</span>
</button>
<p
v-html="
@@ -425,7 +430,8 @@ useHead({
class="button secondary"
@click="executeUnlinkDiscord"
>
{{ $t("account.settings.settingCards.removeDiscord") }}
<Loader v-if="isLoadingUnlink" />
<span v-else>{{ $t("account.settings.settingCards.removeDiscord") }}</span>
</button>
<p v-else>
{{ $t("account.settings.settingCards.noDiscordLinked") }}
@@ -433,7 +439,8 @@ useHead({
:style="{cursor: 'pointer'}"
@click="executeLinkDiscord"
>
{{ $t("account.settings.settingCards.linkDiscord") }}
<Loader v-if="isLoadingLink" />
<span v-else>{{ $t("account.settings.settingCards.linkDiscord") }}</span>
</NuxtLink>
</p>
</div>

View File

@@ -16,7 +16,7 @@ const registerURI = computed(() => {
const loginForm = reactive({ username: '', password: '' });
const { execute } = useAsync({
const { execute, isLoading } = useAsync({
async handler() {
const res = await $fetch('/api/auth/login', {
method: 'POST',
@@ -75,8 +75,8 @@ const { execute } = useAsync({
</div>
<div class="buttons">
<button type="submit">
<!-- TODO: loading state on button. ... style maybe? -->
{{ $t("account.loginForm.login") }}
<Loader v-if="isLoading" />
<span v-else>{{ $t("account.loginForm.login") }}</span>
</button>
<a
:href="registerURI"

View File

@@ -19,7 +19,7 @@ const loginURI = computed(() => {
const registerForm = reactive({ email: '', username: '', mii_name: '', password: '', password_confirm: '' });
const captchaRef = useTemplateRef('captcha');
const { execute } = useAsync({
const { execute, isLoading } = useAsync({
async handler() {
let captchaResponse: string | null = null;
if (captchaRef.value) {
@@ -128,7 +128,8 @@ const { execute } = useAsync({
/>
<div class="buttons">
<button type="submit">
{{ $t("account.loginForm.register") }}
<Loader v-if="isLoading" />
<span v-else>{{ $t("account.loginForm.register") }}</span>
</button>
<a
:href="loginURI"

View File

@@ -6,7 +6,7 @@ const toasts = useToasts();
const { t } = useI18n();
const form = reactive({ password: '', passwordConfirm: '' });
const { execute } = useAsync({
const { execute, isLoading } = useAsync({
async handler() {
const resetToken = route.query.token?.toString();
if (!resetToken) {
@@ -65,7 +65,8 @@ const { execute } = useAsync({
</div>
<div class="buttons">
<button type="submit">
{{ t('account.resetPassword.submit') }}
<Loader v-if="isLoading" />
<span v-else>{{ $t("account.resetPassword.submit") }}</span>
</button>
</div>
</form>

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
/* eslint-disable vue/no-v-html -- locale files still have raw html */
import {
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
@@ -42,7 +41,7 @@ const goalTextVars = computed(() => {
};
});
const { execute: executeUnsubscribe } = useAsync({
const { execute: executeUnsubscribe, isLoading: isLoadingUnsubscribe } = useAsync({
async handler() {
await apiFetch('/api/account/unsubscribe', {
method: 'POST'
@@ -58,7 +57,7 @@ const { execute: executeUnsubscribe } = useAsync({
}
});
const { execute: executeCheckout } = useAsync({
const { execute: executeCheckout, isLoading: isLoadingCheckout } = useAsync({
async handler(priceId: string) {
const result = await apiFetch('/api/account/checkout', {
method: 'POST',
@@ -288,15 +287,19 @@ useHead({
<span v-html="$t('upgrade.unsubPrompt').replace('tiername', profile?.stripeTier?.tierName ?? '')" />
</AlertDialogDescription>
<div class="modal-button-wrapper">
<AlertDialogCancel class="cancel">
<AlertDialogCancel
:disabled="isLoadingUnsubscribe"
class="cancel"
>
{{ $t("modals.cancel") }}
</AlertDialogCancel>
<AlertDialogAction
<button
class="alert"
@click="executeUnsubscribe"
>
{{ $t("upgrade.unsubConfirm") }}
</AlertDialogAction>
<Loader v-if="isLoadingUnsubscribe" />
<span v-else>{{ $t("upgrade.unsubConfirm") }}</span>
</button>
</div>
</AlertDialogContent>
</AlertDialogPortal>
@@ -318,15 +321,19 @@ useHead({
<span v-html="$t('upgrade.changeTierPrompt').replace('oldtiername', profile.stripeTier.tierName).replace('newtiername', selectedTier?.name ?? '')" />
</AlertDialogDescription>
<div class="modal-button-wrapper">
<AlertDialogCancel class="cancel">
<AlertDialogCancel
:disabled="isLoadingCheckout"
class="cancel"
>
{{ $t("modals.cancel") }}
</AlertDialogCancel>
<AlertDialogAction
<button
class="action"
@click="() => executeCheckout(selectedTier?.priceId || '')"
>
{{ $t("modals.confirm") }}
</AlertDialogAction>
<Loader v-if="isLoadingCheckout" />
<span v-else>{{ $t("modals.confirm") }}</span>
</button>
</div>
</AlertDialogContent>
</AlertDialogPortal>
@@ -341,7 +348,8 @@ useHead({
@click.prevent="executeCheckout(selectedTier.priceId)"
>
Subscribe to {{ selectedTier.name }}
<Loader v-if="isLoadingCheckout" />
<span v-else>Subscribe to {{ selectedTier.name }}</span>
</button>
</div>
<div