Create sign-in and sign-out workflow in SPA
This commit is contained in:
parent
08a1b5b81d
commit
b6d1bd9ee2
15 changed files with 761 additions and 460 deletions
123
ts/components/common/ButtonLoading.vue
Normal file
123
ts/components/common/ButtonLoading.vue
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<style scoped>
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<button
|
||||
:class="buttonClasses"
|
||||
:disabled="disabled || loading"
|
||||
@click="handleClick"
|
||||
>
|
||||
<!-- Loading Spinner -->
|
||||
<span
|
||||
v-if="loading"
|
||||
class="spinner-border spinner-border-sm me-2"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
|
||||
<!-- Icon (only show when not loading) -->
|
||||
<i v-if="icon && !loading" :class="iconClasses"></i>
|
||||
|
||||
<!-- Button Text -->
|
||||
<span v-if="text">{{ text }}</span>
|
||||
|
||||
<!-- Slot for additional content -->
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
// Define props
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
validator: (value) =>
|
||||
[
|
||||
"primary",
|
||||
"secondary",
|
||||
"success",
|
||||
"danger",
|
||||
"warning",
|
||||
"info",
|
||||
"light",
|
||||
"dark",
|
||||
"link",
|
||||
"outline-primary",
|
||||
"outline-secondary",
|
||||
"outline-success",
|
||||
"outline-danger",
|
||||
"outline-warning",
|
||||
"outline-info",
|
||||
"outline-light",
|
||||
"outline-dark",
|
||||
].includes(value),
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "",
|
||||
validator: (value) => ["", "sm", "lg"].includes(value),
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Define emits
|
||||
const emit = defineEmits(["click"]);
|
||||
|
||||
// Computed classes for button
|
||||
const buttonClasses = computed(() => {
|
||||
return [
|
||||
"btn",
|
||||
`btn-${props.variant}`,
|
||||
{
|
||||
[`btn-${props.size}`]: props.size,
|
||||
"w-100": props.block,
|
||||
disabled: props.loading,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// Computed classes for icon
|
||||
const iconClasses = computed(() => {
|
||||
return [
|
||||
props.icon,
|
||||
{ "me-2": props.text }, // Add margin if there's text
|
||||
];
|
||||
});
|
||||
|
||||
// Handle click event
|
||||
const handleClick = (event) => {
|
||||
if (!props.loading && !props.disabled) {
|
||||
emit("click", event);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
<template>
|
||||
<div id="content">
|
||||
<div v-if="session.loading">Loading...</div>
|
||||
<div v-else-if="session.error">Error: {{ session.error }}</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// No imports needed for this simple component
|
||||
import { useSessionStore } from "@/store/session";
|
||||
|
||||
const session = useSessionStore();
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -197,6 +197,9 @@
|
|||
label="Configuration"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationLink to="/signout" icon="door-open" label="Signout" />
|
||||
</li>
|
||||
<li>
|
||||
<NavigationLink to="/_/sudo" icon="god" label="Sudo" />
|
||||
</li>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue