feat : middleware and refresh token

This commit is contained in:
Yusron alamsyah
2026-04-06 13:55:31 +07:00
parent d438fb0f5f
commit 4325bae76f
16 changed files with 1127 additions and 476 deletions
+98
View File
@@ -0,0 +1,98 @@
<script setup lang="ts">
type ConfirmDialogMode = {
modelValue: boolean;
title?: string;
message?: string;
confirmText?: string;
cancelText?: string;
confirmColor?: string;
cancelColor?: string;
persistent?: boolean;
maxWidth?: number | string;
loading?: boolean;
// Kalau true: klik "Ya" langsung tutup.
// Kalau false: parent yang nutup (cocok untuk async delete/update).
closeOnConfirm?: boolean;
};
const props = withDefaults(defineProps<ConfirmDialogMode>(), {
title: 'Konfirmasi',
message: 'Apakah Anda yakin?',
confirmText: 'Ya',
cancelText: 'Tidak',
confirmColor: 'success',
cancelColor: 'error',
persistent: true,
maxWidth: 500,
loading: false,
closeOnConfirm: true,
});
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'confirm'): void;
(e: 'cancel'): void;
}>();
const close = () => emit('update:modelValue', false);
const onCancel = () => {
emit('cancel');
close();
};
const onConfirm = () => {
emit('confirm');
if (props.closeOnConfirm) close();
};
</script>
<template>
<v-dialog
location="top"
transition="dialog-top-transition"
:model-value="props.modelValue"
@update:model-value="emit('update:modelValue', $event)"
:persistent="props.persistent"
:max-width="props.maxWidth"
>
<v-card class="pa-6">
<v-card-title class="text-h5">
<slot name="title">{{ props.title }}</slot>
</v-card-title>
<v-card-text>
<slot>{{ props.message }}</slot>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
:color="props.cancelColor"
variant="tonal"
flat
:disabled="props.loading"
@click="onCancel"
>
{{ props.cancelText }}
</v-btn>
<v-btn
:color="props.confirmColor"
variant="tonal"
flat
:loading="props.loading"
:disabled="props.loading"
@click="onConfirm"
>
{{ props.confirmText }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
+127
View File
@@ -0,0 +1,127 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
interface Props {
loading?: boolean;
empty?: boolean;
error?: boolean;
loadingText?: string;
emptyText?: string;
emptyIcon?: string;
errorText?: string;
errorIcon?: string;
size?: 'small' | 'default' | 'large';
minHeight?: string;
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
empty: false,
error: false,
loadingText: 'Memuat data...',
emptyText: 'Tidak ada data',
emptyIcon: 'solar:folder-open-outline',
errorText: 'Terjadi kesalahan saat memuat data',
errorIcon: 'solar:danger-circle-outline',
size: 'default',
minHeight: '200px'
});
const emit = defineEmits<{
(e: 'retry'): void;
}>();
const spinnerSize = computed(() => {
switch (props.size) {
case 'small': return 48;
case 'large': return 80;
default: return 64;
}
});
const iconSize = computed(() => {
switch (props.size) {
case 'small': return 48;
case 'large': return 80;
default: return 64;
}
});
const textClass = computed(() => {
switch (props.size) {
case 'small': return 'text-body-1';
case 'large': return 'text-h5';
default: return 'text-subtitle-1';
}
});
</script>
<template>
<!-- Loading State -->
<div v-if="loading" class="loading-state-container" :style="{ minHeight: minHeight }">
<div class="state-content">
<v-progress-circular
indeterminate
color="primary"
:size="spinnerSize"
></v-progress-circular>
<p :class="['mt-4', textClass, 'text-medium-emphasis']">
{{ loadingText }}
</p>
</div>
</div>
<!-- Empty State -->
<div v-else-if="empty" class="loading-state-container" :style="{ minHeight: minHeight }">
<div class="state-content">
<Icon :icon="emptyIcon" :height="iconSize" class="text-medium-emphasis" />
<p :class="['mt-4', textClass, 'text-medium-emphasis']">
{{ emptyText }}
</p>
<slot name="empty-action"></slot>
</div>
</div>
<!-- Error State -->
<div v-else-if="error" class="loading-state-container" :style="{ minHeight: minHeight }">
<div class="state-content">
<Icon :icon="errorIcon" :height="iconSize" class="text-error" />
<p :class="['mt-4', textClass, 'text-error']">
{{ errorText }}
</p>
<v-btn
color="primary"
variant="tonal"
class="mt-4"
@click="emit('retry')"
>
<Icon icon="solar:refresh-outline" height="20" class="mr-2" />
Coba Lagi
</v-btn>
<slot name="error-action"></slot>
</div>
</div>
<!-- Content Slot -->
<slot v-else></slot>
</template>
<style scoped>
.loading-state-container {
width: 100%;
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
.state-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
width: 100%;
}
</style>