feat : middleware and refresh token
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user