chg: [front] Added support of toaster
This commit is contained in:
parent
b040f85d59
commit
0370ad08a7
9 changed files with 239 additions and 3 deletions
|
@ -4,6 +4,7 @@ import TheThemeButton from './components/TheThemeButton.vue'
|
|||
import TheAdminPanel from './components/TheAdminPanel.vue'
|
||||
import TheSocketConnectionState from './components/TheSocketConnectionState.vue'
|
||||
import TheDahboard from './TheDahboard.vue'
|
||||
import Toaster from '@/components/elements/Toaster.vue'
|
||||
import { socketConnected } from "@/socket";
|
||||
import { darkModeEnabled } from "@/settings.js"
|
||||
|
||||
|
@ -34,6 +35,7 @@ onMounted(() => {
|
|||
<div class="mt-12">
|
||||
<TheDahboard></TheDahboard>
|
||||
</div>
|
||||
<Toaster></Toaster>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { exercises, selected_exercises, diagnostic, fullReload, resetAllExerciseProgress, resetAll, resetLiveLogs, changeExerciseSelection, debouncedGetDiangostic, remediateSetting } from "@/socket";
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { faScrewdriverWrench, faTrash, faSuitcaseMedical, faGraduationCap, faBan, faRotate, faHammer, faCheck } from '@fortawesome/free-solid-svg-icons'
|
||||
import { toast } from '@/utils.js'
|
||||
|
||||
const admin_modal = ref(null)
|
||||
const clickedButtons = ref([])
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
<template>
|
||||
<div class="my-2 --ml-1 bg-slate-50 dark:bg-slate-600 py-1 pl-1 pr-3 rounded-md relative flex flex-col">
|
||||
<div :class="`${!hasActivity ? 'hidden' : 'absolute'} h-10 -mt-1 w-full z-30`">
|
||||
<div class="text-xxs flex justify-between h-full items-center text-slate-500 dark:text-slate-300 select-none">
|
||||
<div class="text-2xs flex justify-between h-full items-center text-slate-500 dark:text-slate-300 select-none">
|
||||
<span class="-rotate-90 w-8 -ml-3">- {{ notificationHistoryConfig.buffer_timestamp_min }}min</span>
|
||||
<span class="-rotate-90 w-8 text-xs">–</span>
|
||||
<span class="-rotate-90 w-8 text-lg">–</span>
|
||||
|
|
70
src/components/elements/Alert.vue
Normal file
70
src/components/elements/Alert.vue
Normal file
|
@ -0,0 +1,70 @@
|
|||
<script setup>
|
||||
import {
|
||||
faCircleCheck,
|
||||
faCircleExclamation,
|
||||
faCircleInfo,
|
||||
faTriangleExclamation
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
message: [Array, String],
|
||||
variant: {
|
||||
type: String,
|
||||
validator(value, props) {
|
||||
return ['success', 'warning', 'danger', 'info'].includes(value)
|
||||
},
|
||||
default: 'info'
|
||||
}
|
||||
})
|
||||
|
||||
const icon = computed(() => {
|
||||
if (props.variant == 'success') {
|
||||
return faCircleCheck
|
||||
} else if (props.variant == 'warning') {
|
||||
return faCircleExclamation
|
||||
} else if (props.variant == 'danger') {
|
||||
return faTriangleExclamation
|
||||
}
|
||||
return faCircleInfo
|
||||
})
|
||||
|
||||
const variantClass = computed(() => {
|
||||
if (props.variant == 'success') {
|
||||
return 'green'
|
||||
} else if (props.variant == 'warning') {
|
||||
return 'amber'
|
||||
} else if (props.variant == 'danger') {
|
||||
return 'red'
|
||||
}
|
||||
return 'blue'
|
||||
})
|
||||
|
||||
const messages = computed(() => {
|
||||
return Array.isArray(props.message)
|
||||
? props.message
|
||||
: props.message !== undefined
|
||||
? [props.message]
|
||||
: []
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="`mb-2 p-2 border-l-4 text-left rounded dark:bg-${variantClass}-300 dark:text-slate-900 dark:border-${variantClass}-700 bg-${variantClass}-200 text-slate-900 border-${variantClass}-700`"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="icon"
|
||||
:class="`text-${variantClass}-700 text-lg mx-3`"
|
||||
></FontAwesomeIcon>
|
||||
<strong class="mr-2">{{ props.title }}</strong>
|
||||
<p
|
||||
class="text-slate-700 p-1 font-light whitespace-pre"
|
||||
v-for="(message, i) in messages"
|
||||
:key="i"
|
||||
>
|
||||
{{ message }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
97
src/components/elements/Toast.vue
Normal file
97
src/components/elements/Toast.vue
Normal file
|
@ -0,0 +1,97 @@
|
|||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { removeToast } from '@/utils.js'
|
||||
import {
|
||||
faCancel,
|
||||
faCheck,
|
||||
faCircleCheck,
|
||||
faCircleExclamation,
|
||||
faCircleInfo,
|
||||
faClose,
|
||||
faTriangleExclamation
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const props = defineProps({
|
||||
id: Number,
|
||||
title: String,
|
||||
message: String,
|
||||
variant: {
|
||||
type: String,
|
||||
validator(value, props) {
|
||||
return ['success', 'warning', 'danger', 'info'].includes(value)
|
||||
},
|
||||
default: 'info'
|
||||
},
|
||||
confirm: Boolean,
|
||||
confirmCb: Function
|
||||
})
|
||||
|
||||
const duration = 7000
|
||||
|
||||
const icon = computed(() => {
|
||||
if (props.variant == 'success') {
|
||||
return faCircleCheck
|
||||
} else if (props.variant == 'warning') {
|
||||
return faCircleExclamation
|
||||
} else if (props.variant == 'danger') {
|
||||
return faTriangleExclamation
|
||||
}
|
||||
return faCircleInfo
|
||||
})
|
||||
|
||||
const variantClass = computed(() => {
|
||||
if (props.variant == 'success') {
|
||||
return 'green'
|
||||
} else if (props.variant == 'warning') {
|
||||
return 'amber'
|
||||
} else if (props.variant == 'danger') {
|
||||
return 'red'
|
||||
}
|
||||
return 'blue'
|
||||
})
|
||||
|
||||
function close() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function callConfirmCb() {
|
||||
props.confirmCb()
|
||||
close()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
removeToast(props.id)
|
||||
}, duration)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="`flex flex-col min-w-72 py-2 px-3 rounded bg-${variantClass}-100`">
|
||||
<div :class="`text-${variantClass}-800 flex items-center`">
|
||||
<FontAwesomeIcon :icon="icon" class="mr-2"></FontAwesomeIcon>
|
||||
<span class="font-semibold">{{ props.title }}</span>
|
||||
<button class="ml-auto w-6 btn btn-link !p-0">
|
||||
<FontAwesomeIcon
|
||||
:icon="faClose"
|
||||
class="text-gray-500 hover:text-gray-700"
|
||||
fixed-width
|
||||
@click="close()"
|
||||
></FontAwesomeIcon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-slate-600 p-1 font-light" v-if="props.message !== undefined && props.message.length > 0">
|
||||
{{ props.message }}
|
||||
</div>
|
||||
<div v-if="props.confirm" class="flex gap-1">
|
||||
<button class="btn btn-info" @click="callConfirmCb()">
|
||||
<FontAwesomeIcon :icon="faCheck" class="fa-fw"></FontAwesomeIcon> Confirm
|
||||
</button>
|
||||
<button class="btn" @click="close()">
|
||||
<FontAwesomeIcon :icon="faCancel" class="fa-fw"></FontAwesomeIcon> Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
47
src/components/elements/Toaster.vue
Normal file
47
src/components/elements/Toaster.vue
Normal file
|
@ -0,0 +1,47 @@
|
|||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { allToasts, removeToast } from '@/utils.js'
|
||||
import Toast from '@/components/elements/Toast.vue'
|
||||
|
||||
const container = ref()
|
||||
const toasts = computed(() => allToasts.value)
|
||||
|
||||
function remove(toast_id) {
|
||||
removeToast(toast_id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div ref="container" class="fixed m-3 top-0 right-0">
|
||||
<transition-group name="list" tag="div" class="flex flex-col-reverse gap-2">
|
||||
<Toast
|
||||
v-for="toast of toasts"
|
||||
:key="toast.id"
|
||||
:id="toast.id"
|
||||
:title="toast.title"
|
||||
:message="toast.message"
|
||||
:variant="toast.variant"
|
||||
:confirm="toast.confirm"
|
||||
:confirmCb="toast.confirmCb"
|
||||
@close="remove(toast.id)"
|
||||
/>
|
||||
</transition-group>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
.list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
}
|
||||
</style>
|
|
@ -1,17 +1,19 @@
|
|||
import './assets/main.css'
|
||||
import VueApexCharts from "vue3-apexcharts";
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createApp, ref } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import Modal from "@/components/elements/Modal.vue"
|
||||
import Loading from "@/components/elements/Loading.vue"
|
||||
import Alert from "@/components/elements/Alert.vue"
|
||||
|
||||
const app = createApp(App)
|
||||
app.component('FontAwesomeIcon', FontAwesomeIcon)
|
||||
app.component('Modal', Modal)
|
||||
app.component('Loading', Loading)
|
||||
app.component('Alert', Alert)
|
||||
app.use(VueApexCharts)
|
||||
|
||||
app.mount('#app')
|
|
@ -1,5 +1,8 @@
|
|||
import { computed, ref } from "vue"
|
||||
|
||||
let toastID = 0
|
||||
export const toastBuffer = ref([])
|
||||
export const allToasts = computed(() => toastBuffer.value)
|
||||
export function toast(toast) {
|
||||
toastID += 1
|
||||
toast.id = toastID
|
||||
|
|
|
@ -8,6 +8,15 @@ export default {
|
|||
{
|
||||
pattern: /bg-blue+/, // Includes bg of all colors and shades
|
||||
},
|
||||
{
|
||||
pattern: /bg-(red|green|blue|amber)-(100|200|300|800)/, // Includes bg of all colors and shades
|
||||
},
|
||||
{
|
||||
pattern: /text-(red|green|blue|amber)-(100|700|800)/, // Includes bg of all colors and shades
|
||||
},
|
||||
{
|
||||
pattern: /border-(red|green|blue|amber)-(700)/, // Includes bg of all colors and shades
|
||||
},
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
|
@ -18,8 +27,13 @@ export default {
|
|||
'3xl': '1800px',
|
||||
},
|
||||
fontSize: {
|
||||
'xxs': '0.6rem',
|
||||
'2xs': '0.66rem',
|
||||
},
|
||||
maxWidth: {
|
||||
'8xl': '88rem',
|
||||
'9xl': '96rem',
|
||||
'10xl': '104rem',
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
|
Loading…
Reference in a new issue