Merge pull request #37 from AlexisNP/features/toast-service
This commit is contained in:
4
app.vue
4
app.vue
@@ -43,6 +43,10 @@ const useIdFunction = () => useId()
|
|||||||
</div>
|
</div>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
|
|
||||||
|
<ClientOnly>
|
||||||
|
<UiToaster />
|
||||||
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,10 @@
|
|||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--success: 156 72% 67%;
|
||||||
|
--success-muted: 156 72% 90%;
|
||||||
|
--success-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--ring: 222.2 84% 4.9%;
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
@@ -64,6 +68,10 @@
|
|||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--success: 142 72% 29%;
|
||||||
|
--success-muted: 142 72% 10%;
|
||||||
|
--success-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--ring: 212.7 26.8% 83.9%;
|
--ring: 212.7 26.8% 83.9%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PhCircleNotch } from "@phosphor-icons/vue";
|
import { PhCircleNotch } from "@phosphor-icons/vue";
|
||||||
|
import { useToast } from "~/components/ui/toast";
|
||||||
import type { Calendar } from "~/models/CalendarConfig";
|
import type { Calendar } from "~/models/CalendarConfig";
|
||||||
|
|
||||||
|
const { toast } = useToast()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modalState: boolean,
|
modalState: boolean,
|
||||||
calendar: Calendar | null
|
calendar: Calendar | null
|
||||||
@@ -19,11 +23,20 @@ async function handleAction(): Promise<void> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await $fetch(`/api/calendars/${props.calendar.id}`, { method: "DELETE" })
|
await $fetch(`/api/calendars/${props.calendar.id}`, { method: "DELETE" })
|
||||||
|
emit("on-close")
|
||||||
|
|
||||||
// isDeleteEventModalOpen.value = false
|
toast({
|
||||||
|
title: t("entity.calendar.deletedToast.title", { calendar: props.calendar.name }),
|
||||||
|
variant: "success",
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
// formErrors.message = err.message
|
toast({
|
||||||
|
title: err.message,
|
||||||
|
variant: "destructive"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PhCircleNotch } from "@phosphor-icons/vue";
|
import { PhCircleNotch } from "@phosphor-icons/vue";
|
||||||
|
import { useToast } from "~/components/ui/toast";
|
||||||
|
|
||||||
const { resetSkeleton, deleteEventFromSkeleton, cancelLatestRequest } = useCalendar()
|
const { resetSkeleton, deleteEventFromSkeleton, cancelLatestRequest } = useCalendar()
|
||||||
const { isDeleteEventModalOpen, eventSkeleton, lastActiveEvent } = storeToRefs(useCalendar())
|
const { isDeleteEventModalOpen, eventSkeleton, lastActiveEvent } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
|
const { toast } = useToast()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const isLoading = ref<boolean>(false)
|
const isLoading = ref<boolean>(false)
|
||||||
|
|
||||||
const formErrors = reactive<{ message: string | null }>({
|
const formErrors = reactive<{ message: string | null }>({
|
||||||
@@ -22,16 +26,24 @@ async function handleAction(): Promise<void> {
|
|||||||
|
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
||||||
|
const eventTitle = eventSkeleton.value.title
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteEventFromSkeleton()
|
await deleteEventFromSkeleton()
|
||||||
|
|
||||||
isDeleteEventModalOpen.value = false
|
isDeleteEventModalOpen.value = false
|
||||||
|
resetSkeleton()
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t("entity.calendar.event.deletedToast.title", { event: eventTitle }),
|
||||||
|
variant: "success",
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
formErrors.message = err.message
|
formErrors.message = err.message
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
resetSkeleton()
|
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
|
|
||||||
import { PhCheckCircle, PhGear, PhGlobeHemisphereWest, PhLaptop, PhMoon, PhPalette, PhSignOut, PhSun, PhTranslate, PhUserCircle } from "@phosphor-icons/vue"
|
import { PhCheckCircle, PhGear, PhGlobeHemisphereWest, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle } from "@phosphor-icons/vue"
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -70,12 +70,12 @@ function pushRoute(to: AvailableRoutes) {
|
|||||||
{{ $t('ui.sidebarMenu.avatarFallback') }}
|
{{ $t('ui.sidebarMenu.avatarFallback') }}
|
||||||
</UiAvatarFallback>
|
</UiAvatarFallback>
|
||||||
</UiAvatar>
|
</UiAvatar>
|
||||||
<UiButton v-else variant="outline" size="icon">
|
<UiButton v-else variant="outline" size="icon" class="rounded-full">
|
||||||
<PhUserCircle size="18" />
|
<PhUserCircle size="24" />
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</UiDropdownMenuTrigger>
|
</UiDropdownMenuTrigger>
|
||||||
|
|
||||||
<UiDropdownMenuContent class="w-72 p-0" :align="'start'" :side="'top'" :side-offset="10" :align-offset="25" :collision-padding="40">
|
<UiDropdownMenuContent class="w-72 p-0 pb-1" :align="'start'" :side="'top'" :side-offset="10" :align-offset="25" :collision-padding="40">
|
||||||
<template v-if="user">
|
<template v-if="user">
|
||||||
<p class="p-2 text-[.7em] opacity-75">
|
<p class="p-2 text-[.7em] opacity-75">
|
||||||
{{ $t('ui.greeting', { user: user?.email }) }}
|
{{ $t('ui.greeting', { user: user?.email }) }}
|
||||||
@@ -91,6 +91,12 @@ function pushRoute(to: AvailableRoutes) {
|
|||||||
<UiDropdownMenuSeparator />
|
<UiDropdownMenuSeparator />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<p class="p-2 text-[.7em] opacity-75">
|
||||||
|
{{ $t('ui.anonymousGreeting') }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
<UiDropdownMenuSub>
|
<UiDropdownMenuSub>
|
||||||
<UiDropdownMenuSubTrigger class="p-0 rounded-none">
|
<UiDropdownMenuSubTrigger class="p-0 rounded-none">
|
||||||
<UiDropdownMenuItem class="flex gap-[.5ch] items-center pointer-events-none">
|
<UiDropdownMenuItem class="flex gap-[.5ch] items-center pointer-events-none">
|
||||||
@@ -195,7 +201,7 @@ function pushRoute(to: AvailableRoutes) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<UiDropdownMenuItem v-if="!user" class="flex gap-[.5ch] items-center rounded-none" @click="handleGoogleLogin">
|
<UiDropdownMenuItem v-if="!user" class="flex gap-[.5ch] items-center rounded-none" @click="handleGoogleLogin">
|
||||||
<PhGear size="20" weight="fill" />
|
<PhSignIn size="18" weight="fill" />
|
||||||
<span>
|
<span>
|
||||||
{{ $t('ui.sidebarMenu.login') }}
|
{{ $t('ui.sidebarMenu.login') }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
28
components/ui/toast/Toast.vue
Normal file
28
components/ui/toast/Toast.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { ToastRoot, type ToastRootEmits, useForwardPropsEmits } from "radix-vue"
|
||||||
|
import { type ToastProps, toastVariants } from "."
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<ToastProps>()
|
||||||
|
|
||||||
|
const emits = defineEmits<ToastRootEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ToastRoot
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn(toastVariants({ variant }), props.class)"
|
||||||
|
@update:open="onOpenChange"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</ToastRoot>
|
||||||
|
</template>
|
||||||
19
components/ui/toast/ToastAction.vue
Normal file
19
components/ui/toast/ToastAction.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from "vue"
|
||||||
|
import { ToastAction, type ToastActionProps } from "radix-vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<ToastActionProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ToastAction v-bind="delegatedProps" :class="cn('inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive', props.class)">
|
||||||
|
<slot />
|
||||||
|
</ToastAction>
|
||||||
|
</template>
|
||||||
22
components/ui/toast/ToastClose.vue
Normal file
22
components/ui/toast/ToastClose.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from "vue"
|
||||||
|
import { ToastClose, type ToastCloseProps } from "radix-vue"
|
||||||
|
import { X } from "lucide-vue-next"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<ToastCloseProps & {
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ToastClose v-bind="delegatedProps" :class="cn('absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600', props.class)">
|
||||||
|
<X class="h-4 w-4" />
|
||||||
|
</ToastClose>
|
||||||
|
</template>
|
||||||
19
components/ui/toast/ToastDescription.vue
Normal file
19
components/ui/toast/ToastDescription.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from "vue"
|
||||||
|
import { ToastDescription, type ToastDescriptionProps } from "radix-vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<ToastDescriptionProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ToastDescription :class="cn('text-sm opacity-90', props.class)" v-bind="delegatedProps">
|
||||||
|
<slot />
|
||||||
|
</ToastDescription>
|
||||||
|
</template>
|
||||||
11
components/ui/toast/ToastProvider.vue
Normal file
11
components/ui/toast/ToastProvider.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ToastProvider, type ToastProviderProps } from "radix-vue"
|
||||||
|
|
||||||
|
const props = defineProps<ToastProviderProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ToastProvider v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</ToastProvider>
|
||||||
|
</template>
|
||||||
19
components/ui/toast/ToastTitle.vue
Normal file
19
components/ui/toast/ToastTitle.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from "vue"
|
||||||
|
import { ToastTitle, type ToastTitleProps } from "radix-vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<ToastTitleProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ToastTitle v-bind="delegatedProps" :class="cn('text-[.85em] font-semibold', props.class)">
|
||||||
|
<slot />
|
||||||
|
</ToastTitle>
|
||||||
|
</template>
|
||||||
17
components/ui/toast/ToastViewport.vue
Normal file
17
components/ui/toast/ToastViewport.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from "vue"
|
||||||
|
import { ToastViewport, type ToastViewportProps } from "radix-vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<ToastViewportProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ToastViewport v-bind="delegatedProps" :class="cn('fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]', props.class)" />
|
||||||
|
</template>
|
||||||
30
components/ui/toast/Toaster.vue
Normal file
30
components/ui/toast/Toaster.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { isVNode } from "vue"
|
||||||
|
import { useToast } from "./use-toast"
|
||||||
|
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "."
|
||||||
|
|
||||||
|
const { toasts } = useToast()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ToastProvider>
|
||||||
|
<Toast v-for="toast in toasts" :key="toast.id" v-bind="toast">
|
||||||
|
<div class="grid gap-1">
|
||||||
|
<ToastTitle v-if="toast.title">
|
||||||
|
{{ toast.title }}
|
||||||
|
</ToastTitle>
|
||||||
|
<template v-if="toast.description">
|
||||||
|
<ToastDescription v-if="isVNode(toast.description)">
|
||||||
|
<component :is="toast.description" />
|
||||||
|
</ToastDescription>
|
||||||
|
<ToastDescription v-else>
|
||||||
|
{{ toast.description }}
|
||||||
|
</ToastDescription>
|
||||||
|
</template>
|
||||||
|
<ToastClose />
|
||||||
|
</div>
|
||||||
|
<component :is="toast.action" />
|
||||||
|
</Toast>
|
||||||
|
<ToastViewport />
|
||||||
|
</ToastProvider>
|
||||||
|
</template>
|
||||||
38
components/ui/toast/index.ts
Normal file
38
components/ui/toast/index.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { ToastRootProps } from "radix-vue"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
|
||||||
|
import { type VariantProps, cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
export { default as Toaster } from "./Toaster.vue"
|
||||||
|
export { default as Toast } from "./Toast.vue"
|
||||||
|
export { default as ToastViewport } from "./ToastViewport.vue"
|
||||||
|
export { default as ToastAction } from "./ToastAction.vue"
|
||||||
|
export { default as ToastClose } from "./ToastClose.vue"
|
||||||
|
export { default as ToastTitle } from "./ToastTitle.vue"
|
||||||
|
export { default as ToastDescription } from "./ToastDescription.vue"
|
||||||
|
export { default as ToastProvider } from "./ToastProvider.vue"
|
||||||
|
export { toast, useToast } from "./use-toast"
|
||||||
|
|
||||||
|
export const toastVariants = cva(
|
||||||
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[--radix-toast-swipe-end-x] data-[swipe=move]:translate-x-[--radix-toast-swipe-move-x] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border bg-background text-foreground",
|
||||||
|
destructive: "destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||||
|
success: "success group border-success bg-success-muted text-success-foreground"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
type ToastVariants = VariantProps<typeof toastVariants>
|
||||||
|
|
||||||
|
export interface ToastProps extends ToastRootProps {
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
variant?: ToastVariants["variant"]
|
||||||
|
onOpenChange?: ((value: boolean) => void) | undefined
|
||||||
|
}
|
||||||
165
components/ui/toast/use-toast.ts
Normal file
165
components/ui/toast/use-toast.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { computed, ref } from "vue"
|
||||||
|
import type { Component, VNode } from "vue"
|
||||||
|
import type { ToastProps } from "."
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 3
|
||||||
|
const TOAST_REMOVE_DELAY = 1000000
|
||||||
|
|
||||||
|
export type StringOrVNode =
|
||||||
|
| string
|
||||||
|
| VNode
|
||||||
|
| (() => VNode)
|
||||||
|
|
||||||
|
type ToasterToast = ToastProps & {
|
||||||
|
id: string
|
||||||
|
title?: string
|
||||||
|
description?: StringOrVNode
|
||||||
|
action?: Component
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionTypes = {
|
||||||
|
ADD_TOAST: "ADD_TOAST",
|
||||||
|
UPDATE_TOAST: "UPDATE_TOAST",
|
||||||
|
DISMISS_TOAST: "DISMISS_TOAST",
|
||||||
|
REMOVE_TOAST: "REMOVE_TOAST",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
count = (count + 1) % Number.MAX_VALUE
|
||||||
|
return count.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType = typeof actionTypes
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {
|
||||||
|
type: ActionType["ADD_TOAST"]
|
||||||
|
toast: ToasterToast
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["UPDATE_TOAST"]
|
||||||
|
toast: Partial<ToasterToast>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["DISMISS_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["REMOVE_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
toasts: ToasterToast[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
|
function addToRemoveQueue(toastId: string) {
|
||||||
|
if (toastTimeouts.has(toastId))
|
||||||
|
return
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
toastTimeouts.delete(toastId)
|
||||||
|
dispatch({
|
||||||
|
type: actionTypes.REMOVE_TOAST,
|
||||||
|
toastId,
|
||||||
|
})
|
||||||
|
}, TOAST_REMOVE_DELAY)
|
||||||
|
|
||||||
|
toastTimeouts.set(toastId, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = ref<State>({
|
||||||
|
toasts: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
function dispatch(action: Action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case actionTypes.ADD_TOAST:
|
||||||
|
state.value.toasts = [action.toast, ...state.value.toasts].slice(0, TOAST_LIMIT)
|
||||||
|
break
|
||||||
|
|
||||||
|
case actionTypes.UPDATE_TOAST:
|
||||||
|
state.value.toasts = state.value.toasts.map(t =>
|
||||||
|
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
case actionTypes.DISMISS_TOAST: {
|
||||||
|
const { toastId } = action
|
||||||
|
|
||||||
|
if (toastId) {
|
||||||
|
addToRemoveQueue(toastId)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
state.value.toasts.forEach((toast) => {
|
||||||
|
addToRemoveQueue(toast.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
state.value.toasts = state.value.toasts.map(t =>
|
||||||
|
t.id === toastId || toastId === undefined
|
||||||
|
? {
|
||||||
|
...t,
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
: t,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case actionTypes.REMOVE_TOAST:
|
||||||
|
if (action.toastId === undefined)
|
||||||
|
state.value.toasts = []
|
||||||
|
else
|
||||||
|
state.value.toasts = state.value.toasts.filter(t => t.id !== action.toastId)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useToast() {
|
||||||
|
return {
|
||||||
|
toasts: computed(() => state.value.toasts),
|
||||||
|
toast,
|
||||||
|
dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toast = Omit<ToasterToast, "id">
|
||||||
|
|
||||||
|
function toast(props: Toast) {
|
||||||
|
const id = genId()
|
||||||
|
|
||||||
|
const update = (props: ToasterToast) =>
|
||||||
|
dispatch({
|
||||||
|
type: actionTypes.UPDATE_TOAST,
|
||||||
|
toast: { ...props, id },
|
||||||
|
})
|
||||||
|
|
||||||
|
const dismiss = () => dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id })
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: actionTypes.ADD_TOAST,
|
||||||
|
toast: {
|
||||||
|
...props,
|
||||||
|
id,
|
||||||
|
open: true,
|
||||||
|
onOpenChange: (open: boolean) => {
|
||||||
|
if (!open)
|
||||||
|
dismiss()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
dismiss,
|
||||||
|
update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { toast, useToast }
|
||||||
@@ -17,6 +17,7 @@ export default defineI18nConfig(() => ({
|
|||||||
edit: "Edit",
|
edit: "Edit",
|
||||||
},
|
},
|
||||||
greeting: "Connected as {user}",
|
greeting: "Connected as {user}",
|
||||||
|
anonymousGreeting: "Preferences",
|
||||||
sidebarMenu: {
|
sidebarMenu: {
|
||||||
appearance: "Appearance",
|
appearance: "Appearance",
|
||||||
language: "Language",
|
language: "Language",
|
||||||
@@ -107,7 +108,10 @@ export default defineI18nConfig(() => ({
|
|||||||
deleteDialog: {
|
deleteDialog: {
|
||||||
title: "Delete this event",
|
title: "Delete this event",
|
||||||
subtitle: "Data associated with this event will be lost and you won't be able to retrieve it !",
|
subtitle: "Data associated with this event will be lost and you won't be able to retrieve it !",
|
||||||
}
|
},
|
||||||
|
deletedToast: {
|
||||||
|
title: "The event \"{event}\" has been successfuly deleted.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
createDialog: {
|
createDialog: {
|
||||||
title: "New calendar",
|
title: "New calendar",
|
||||||
@@ -127,6 +131,9 @@ export default defineI18nConfig(() => ({
|
|||||||
title: "Are you sure you want to delete this calendar ?",
|
title: "Are you sure you want to delete this calendar ?",
|
||||||
subtitle: "Its events won't be accessible anymore and you won't be able to retrieve the deleted data !",
|
subtitle: "Its events won't be accessible anymore and you won't be able to retrieve the deleted data !",
|
||||||
},
|
},
|
||||||
|
deletedToast: {
|
||||||
|
title: "Calendar \"{calendar}\" has been successfuly deleted.",
|
||||||
|
},
|
||||||
millennia: {
|
millennia: {
|
||||||
nameSingular: "Millennia",
|
nameSingular: "Millennia",
|
||||||
nextSingular: "Next millennia",
|
nextSingular: "Next millennia",
|
||||||
@@ -183,6 +190,7 @@ export default defineI18nConfig(() => ({
|
|||||||
edit: "Modifier",
|
edit: "Modifier",
|
||||||
},
|
},
|
||||||
greeting: "Connecté en tant que {user}",
|
greeting: "Connecté en tant que {user}",
|
||||||
|
anonymousGreeting: "Préférences",
|
||||||
sidebarMenu: {
|
sidebarMenu: {
|
||||||
appearance: "Apparence",
|
appearance: "Apparence",
|
||||||
language: "Langue",
|
language: "Langue",
|
||||||
@@ -273,7 +281,10 @@ export default defineI18nConfig(() => ({
|
|||||||
deleteDialog: {
|
deleteDialog: {
|
||||||
title: "Supprimer l'évènement",
|
title: "Supprimer l'évènement",
|
||||||
subtitle: "Les données associés à cet évènement seront supprimées et vous ne pourrez plus les récupérer !",
|
subtitle: "Les données associés à cet évènement seront supprimées et vous ne pourrez plus les récupérer !",
|
||||||
}
|
},
|
||||||
|
deletedToast: {
|
||||||
|
title: "L'évènement \"{event}\" a été supprimé avec succès.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
createDialog: {
|
createDialog: {
|
||||||
title: "Nouveau calendrier",
|
title: "Nouveau calendrier",
|
||||||
@@ -293,6 +304,9 @@ export default defineI18nConfig(() => ({
|
|||||||
title: "Êtes-vous sûr de supprimer ce calendrier ?",
|
title: "Êtes-vous sûr de supprimer ce calendrier ?",
|
||||||
subtitle: "Les évènements ne seront plus accessibles et vous ne pourrez plus récupérer les données !",
|
subtitle: "Les évènements ne seront plus accessibles et vous ne pourrez plus récupérer les données !",
|
||||||
},
|
},
|
||||||
|
deletedToast: {
|
||||||
|
title: "Le calendrier \"{calendar}\" a été supprimé avec succès.",
|
||||||
|
},
|
||||||
millennia: {
|
millennia: {
|
||||||
nameSingular: "Millénaire",
|
nameSingular: "Millénaire",
|
||||||
nextSingular: "Millénaire suivant",
|
nextSingular: "Millénaire suivant",
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ module.exports = {
|
|||||||
DEFAULT: "hsl(var(--destructive))",
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
foreground: "hsl(var(--destructive-foreground))",
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
},
|
},
|
||||||
|
success: {
|
||||||
|
DEFAULT: "hsl(var(--success))",
|
||||||
|
muted: "hsl(var(--success-muted))",
|
||||||
|
foreground: "hsl(var(--success-foreground))",
|
||||||
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: "hsl(var(--muted))",
|
DEFAULT: "hsl(var(--muted))",
|
||||||
foreground: "hsl(var(--muted-foreground))",
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
|||||||
Reference in New Issue
Block a user