Merge pull request #79 from AlexisNP/features/mobile-calendars
Features/mobile calendars
This commit is contained in:
28
app.vue
28
app.vue
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ConfigProvider } from "radix-vue"
|
import { ConfigProvider } from "radix-vue"
|
||||||
|
import { cn } from "./lib/utils";
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
titleTemplate: (titleChunk) => {
|
titleTemplate: (titleChunk) => {
|
||||||
@@ -26,6 +27,8 @@ useHead({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const useIdFunction = () => useId()
|
const useIdFunction = () => useId()
|
||||||
|
|
||||||
|
const { isSidebarOpened } = storeToRefs(useUiStore())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -34,10 +37,18 @@ const useIdFunction = () => useId()
|
|||||||
|
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<ConfigProvider :use-id="useIdFunction">
|
<ConfigProvider :use-id="useIdFunction">
|
||||||
<div class="h-full grid grid-cols-[auto_1fr] dark:bg-black transition-colors">
|
<div
|
||||||
|
class="h-full grid md:grid-cols-[auto_1fr] dark:bg-black shadow-body-light dark:shadow-body-dark transition-colors after:absolute after:transition-colors"
|
||||||
|
:class="cn({
|
||||||
|
'max-md:after:bg-transparent': isSidebarOpened,
|
||||||
|
'has-sidebar max-md:after:bg-black/20 md:after:opacity-0 md:after:pointer-events-none': isSidebarOpened
|
||||||
|
})"
|
||||||
|
>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
||||||
<div class="wrapper shadow-body-light dark:shadow-body-dark transition-all">
|
<div
|
||||||
|
class="wrapper max-h-screen transition-all"
|
||||||
|
>
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,4 +65,17 @@ const useIdFunction = () => useId()
|
|||||||
.wrapper > * {
|
.wrapper > * {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.has-sidebar {
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -146,7 +146,7 @@
|
|||||||
}
|
}
|
||||||
.event-button.is-hidden {
|
.event-button.is-hidden {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: var(--bg-color);
|
color: var(--color-foreground);
|
||||||
border: 1px dashed var(--bg-color);
|
border: 1px dashed var(--bg-color);
|
||||||
transition-property: background-color, color, border-color;
|
transition-property: background-color, color, border-color;
|
||||||
}
|
}
|
||||||
|
|||||||
4
assets/_typography.css
Normal file
4
assets/_typography.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@theme inline {
|
||||||
|
--text-2xs: 0.7rem;
|
||||||
|
--text-2xs--line-height: 0.75rem;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
@config '../tailwind.config.js';
|
@config '../tailwind.config.js';
|
||||||
|
|
||||||
@import "./theme.css" layer(theme);
|
@import "./theme.css" layer(theme);
|
||||||
|
@import "./_typography.css" layer(theme);
|
||||||
@import "./_colors.css" layer(theme);
|
@import "./_colors.css" layer(theme);
|
||||||
@import "./_colors.css" layer(utilities);
|
@import "./_colors.css" layer(utilities);
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ const { isReadOnly } = storeToRefs(useCalendar())
|
|||||||
<UiTooltipProvider :delay-duration="250">
|
<UiTooltipProvider :delay-duration="250">
|
||||||
<UiTooltip>
|
<UiTooltip>
|
||||||
<UiTooltipTrigger as-child>
|
<UiTooltipTrigger as-child>
|
||||||
<UiButton v-if="!isReadOnly" variant="secondary" size="icon" @click="toggleCategoriesModal(true)">
|
<ClientOnly>
|
||||||
<PhTag size="20" weight="light" />
|
<UiButton v-if="!isReadOnly" variant="secondary" size="icon" @click="toggleCategoriesModal(true)">
|
||||||
</UiButton>
|
<PhTag size="20" weight="light" />
|
||||||
|
</UiButton>
|
||||||
|
</ClientOnly>
|
||||||
</UiTooltipTrigger>
|
</UiTooltipTrigger>
|
||||||
<UiTooltipContent>
|
<UiTooltipContent>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -3,10 +3,15 @@ import { storeToRefs } from "pinia"
|
|||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
|
|
||||||
import { PhMapPin } from "@phosphor-icons/vue"
|
import { PhMapPin } from "@phosphor-icons/vue"
|
||||||
|
import { breakpointsTailwind } from "@vueuse/core"
|
||||||
|
|
||||||
const { defaultDate, getFormattedDateTitle, getRelativeString, getDifferenceInDays } = useCalendar()
|
const { defaultDate, getFormattedDateTitle, getRelativeString, getDifferenceInDays } = useCalendar()
|
||||||
const { selectedDate } = storeToRefs(useCalendar())
|
const { selectedDate } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
|
const breakpoints = useBreakpoints(
|
||||||
|
breakpointsTailwind
|
||||||
|
)
|
||||||
|
|
||||||
const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true))
|
const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true))
|
||||||
// const mainDateTitle = computed(() => convertDateToDays(selectedDate.value))
|
// const mainDateTitle = computed(() => convertDateToDays(selectedDate.value))
|
||||||
|
|
||||||
@@ -16,19 +21,24 @@ const isToday = computed(() => getDifferenceInDays(defaultDate, selectedDate.val
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="grid md:flex md:gap-2 items-center">
|
||||||
<h1 class="text-2xl font-bold flex items-center gap-1">
|
<h1 class="text-lg md:text-2xl max-md:leading-tight font-bold flex items-center gap-1">
|
||||||
<PhMapPin size="26" weight="bold" /> {{ mainDateTitle }}
|
<PhMapPin
|
||||||
|
:size="breakpoints.md.value ? 26 : 18"
|
||||||
|
class="max-md:hidden"
|
||||||
|
weight="light"
|
||||||
|
/>
|
||||||
|
{{ mainDateTitle }}
|
||||||
</h1>
|
</h1>
|
||||||
<h2 v-if="!isToday" class="text-xl italic opacity-75">
|
<h2 v-if="!isToday" class="text-sm max-md:leading-tight md:text-xl italic opacity-75">
|
||||||
– {{ dateDifference }}
|
<span class="max-md:hidden">–</span> {{ dateDifference }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<UiSkeleton class="h-8 w-64" />
|
<UiSkeleton class="h-8 w-64 max-md:max-w-full" />
|
||||||
<UiSkeleton class="h-6 w-28" />
|
<UiSkeleton class="max-md:hidden h-6 w-28" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { breakpointsTailwind } from "@vueuse/core"
|
||||||
import { cn } from "~/lib/utils"
|
import { cn } from "~/lib/utils"
|
||||||
import { useCalendar } from "~/stores/CalendarStore"
|
import { useCalendar } from "~/stores/CalendarStore"
|
||||||
import { PhCalendarBlank, PhCheckCircle, PhGear } from "@phosphor-icons/vue"
|
import { PhCalendarBlank, PhCheckCircle, PhDotsThreeVertical, PhGear, PhTag } from "@phosphor-icons/vue"
|
||||||
import { computed } from "vue"
|
|
||||||
|
|
||||||
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar()
|
const breakpoints = useBreakpoints(
|
||||||
|
breakpointsTailwind
|
||||||
|
)
|
||||||
|
|
||||||
|
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType, toggleCategoriesModal } = useCalendar()
|
||||||
const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
|
const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -12,15 +16,29 @@ const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
|
|||||||
<UiDropdownMenu>
|
<UiDropdownMenu>
|
||||||
<UiDropdownMenuTrigger as-child>
|
<UiDropdownMenuTrigger as-child>
|
||||||
<UiButton variant="secondary" size="icon">
|
<UiButton variant="secondary" size="icon">
|
||||||
<PhGear size="20" weight="fill" />
|
<ClientOnly>
|
||||||
|
<PhGear v-if="breakpoints.md.value" size="20" weight="fill" />
|
||||||
|
<PhDotsThreeVertical v-else size="26" weight="bold" />
|
||||||
|
</ClientOnly>
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</UiDropdownMenuTrigger>
|
</UiDropdownMenuTrigger>
|
||||||
|
|
||||||
<UiDropdownMenuContent :side="'bottom'" :align="'start'" :side-offset="10" :align-offset="25" :collision-padding="40" class="text-right">
|
<UiDropdownMenuContent :side="'bottom'" :align="'start'" :side-offset="10" :align-offset="25" :collision-padding="20" class="text-right">
|
||||||
<UiDropdownMenuArrow />
|
<UiDropdownMenuArrow />
|
||||||
<UiDropdownMenuLabel>
|
<UiDropdownMenuLabel>
|
||||||
{{ $t('entity.calendar.seeOptions') }}
|
{{ $t('entity.calendar.seeOptions') }}
|
||||||
</UiDropdownMenuLabel>
|
</UiDropdownMenuLabel>
|
||||||
|
|
||||||
|
<UiDropdownMenuItem
|
||||||
|
v-if="!breakpoints.md.value"
|
||||||
|
class="flex gap-[1ch] justify-end items-center"
|
||||||
|
@click="toggleCategoriesModal(true)"
|
||||||
|
>
|
||||||
|
{{ $t('entity.calendar.seeCategories') }}
|
||||||
|
|
||||||
|
<PhTag size="18" />
|
||||||
|
</UiDropdownMenuItem>
|
||||||
|
|
||||||
<UiDropdownMenuSub>
|
<UiDropdownMenuSub>
|
||||||
<UiDropdownMenuSubTrigger arrow-direction="left" class="p-0 rounded-none">
|
<UiDropdownMenuSubTrigger arrow-direction="left" class="p-0 rounded-none">
|
||||||
<UiDropdownMenuItem class="flex gap-[1ch] justify-end items-center pointer-events-none">
|
<UiDropdownMenuItem class="flex gap-[1ch] justify-end items-center pointer-events-none">
|
||||||
|
|||||||
@@ -1,37 +1,51 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils"
|
||||||
import { PhArchive, PhCalendarDots, PhFile, PhFileDashed, PhFilePlus, PhPencil, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue";
|
import { PhArchive, PhCalendarDots, PhFile, PhFileDashed, PhFilePlus, PhPencil, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue"
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon"
|
||||||
import type { Calendar } from "~/models/CalendarConfig";
|
import type { Calendar } from "~/models/CalendarConfig"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
calendar: Calendar,
|
calendar: Calendar
|
||||||
gmId?: string,
|
gmId?: string
|
||||||
showActions?: boolean,
|
showActions?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits(["on-edit", "on-delete"])
|
const emit = defineEmits(["on-edit", "on-delete"])
|
||||||
|
|
||||||
const { locale } = useI18n();
|
const cardRef = ref(null)
|
||||||
|
const isCardHovered = useElementHover(cardRef)
|
||||||
|
const { focused: isCardFocused } = useFocusWithin(cardRef)
|
||||||
|
|
||||||
const createdAt = DateTime.fromISO(props.calendar.createdAt!).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value });
|
const { locale } = useI18n()
|
||||||
const updatedAt = computed<string>(() => props.calendar.updatedAt ? DateTime.fromISO(props.calendar.updatedAt).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value }) : "");
|
|
||||||
|
|
||||||
const user = useSupabaseUser();
|
const createdAt = DateTime.fromISO(props.calendar.createdAt!).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value })
|
||||||
const isOwner = computed(() => user.value && props.gmId && user.value.id === props.gmId);
|
const updatedAt = computed<string>(() => props.calendar.updatedAt ? DateTime.fromISO(props.calendar.updatedAt).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value }) : "")
|
||||||
const calendarLink = computed(() => isOwner.value ? `/my/calendars/${props.calendar.id}` : `/calendars/${props.calendar.shortId}`);
|
|
||||||
|
const user = useSupabaseUser()
|
||||||
|
const isOwner = computed(() => user.value && props.gmId && user.value.id === props.gmId)
|
||||||
|
const calendarLink = computed(() => isOwner.value ? `/my/calendars/${props.calendar.id}` : `/calendars/${props.calendar.shortId}`)
|
||||||
|
|
||||||
|
const hasActions = computed(() => isOwner.value && props.showActions)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiCard
|
<UiCard
|
||||||
class="w-full h-full flex flex-col transition-all"
|
ref="cardRef"
|
||||||
|
class="w-full h-full flex flex-col"
|
||||||
:link="calendarLink"
|
:link="calendarLink"
|
||||||
:class="cn(
|
:class="cn(
|
||||||
calendar.color ? `card-color element-${calendar.color}` : '',
|
calendar.color ? `group card-color element-${calendar.color}` : '',
|
||||||
)"
|
)"
|
||||||
>
|
>
|
||||||
<UiCardHeader class="gap-4">
|
<UiCardHeader class="gap-4">
|
||||||
<UiCardTitle class="text-xl pr-12">{{ calendar.name }}</UiCardTitle>
|
<UiCardTitle
|
||||||
|
class="text-xl"
|
||||||
|
:class="cn({
|
||||||
|
'pr-18': hasActions
|
||||||
|
})"
|
||||||
|
>
|
||||||
|
{{ calendar.name }}
|
||||||
|
</UiCardTitle>
|
||||||
|
|
||||||
<div v-if="calendar.state === 'published'" class="flex items-center gap-1 text-sm">
|
<div v-if="calendar.state === 'published'" class="flex items-center gap-1 text-sm">
|
||||||
<PhFile size="20" weight="fill" />
|
<PhFile size="20" weight="fill" />
|
||||||
@@ -55,16 +69,19 @@ const calendarLink = computed(() => isOwner.value ? `/my/calendars/${props.calen
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="isOwner && showActions"
|
v-if="hasActions && (isCardHovered || isCardFocused)"
|
||||||
class="flex gap-1 absolute top-4 right-4 z-20"
|
class="flex gap-2 absolute top-4 right-4 z-20"
|
||||||
>
|
>
|
||||||
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-indigo-400 dark:hover:bg-indigo-700" @click="emit('on-edit')">
|
<Transition name="fade" appear>
|
||||||
<PhPencil size="16" />
|
<UiButton size="icon" variant="outline" class="size-8 border-foreground/20 group-hover:border-foreground/30 hover:text-background hover:border-foreground hover:bg-foreground" @click="emit('on-edit')">
|
||||||
</UiButton>
|
<PhPencil size="15" weight="fill" />
|
||||||
|
</UiButton>
|
||||||
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-rose-400 dark:hover:bg-rose-700" @click="emit('on-delete')">
|
</Transition>
|
||||||
<PhTrash size="16" />
|
<Transition name="fade" appear>
|
||||||
</UiButton>
|
<UiButton size="icon" variant="outline" class="size-8 border-foreground/25 group-hover:border-foreground/40 hover:text-background hover:border-rose-500 hover:bg-rose-500" @click="emit('on-delete')">
|
||||||
|
<PhTrash size="15" weight="fill" />
|
||||||
|
</UiButton>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
|
|
||||||
|
|||||||
28
components/calendar/SearchCTA.vue
Normal file
28
components/calendar/SearchCTA.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||||
|
import { PhMagnifyingGlass } from "@phosphor-icons/vue"
|
||||||
|
|
||||||
|
const { revealAdvancedSearch } = useCalendar()
|
||||||
|
|
||||||
|
const breakpoints = useBreakpoints(
|
||||||
|
breakpointsTailwind
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ClientOnly>
|
||||||
|
<Transition name="fade" appear>
|
||||||
|
<UiButton
|
||||||
|
:search-slash="breakpoints.lg.value"
|
||||||
|
:size="breakpoints.lg.value ? 'default' : 'icon'"
|
||||||
|
@click="revealAdvancedSearch()"
|
||||||
|
>
|
||||||
|
<PhMagnifyingGlass size="20" weight="light" />
|
||||||
|
|
||||||
|
<span v-if="breakpoints.lg.value">
|
||||||
|
{{ $t('entity.advancedSearch.title') }}
|
||||||
|
</span>
|
||||||
|
</UiButton>
|
||||||
|
</Transition>
|
||||||
|
</ClientOnly>
|
||||||
|
</template>
|
||||||
@@ -2,8 +2,6 @@
|
|||||||
import type { Category } from "~/models/Category";
|
import type { Category } from "~/models/Category";
|
||||||
import { ScrollAreaRoot, ScrollAreaViewport, ScrollAreaScrollbar, ScrollAreaThumb } from "radix-vue"
|
import { ScrollAreaRoot, ScrollAreaViewport, ScrollAreaScrollbar, ScrollAreaThumb } from "radix-vue"
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const { categories } = defineProps<{
|
const { categories } = defineProps<{
|
||||||
categories: Category[]
|
categories: Category[]
|
||||||
}>()
|
}>()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||||
import { PhPlus } from "@phosphor-icons/vue";
|
import { PhPlus } from "@phosphor-icons/vue";
|
||||||
|
import { VisuallyHidden } from "radix-vue";
|
||||||
|
|
||||||
const isDialogOpen = ref<boolean>(false);
|
const isDialogOpen = ref<boolean>(false);
|
||||||
const { resetSkeleton } = useCalendar();
|
const { resetSkeleton } = useCalendar();
|
||||||
@@ -15,26 +17,46 @@ function toggleDialog() {
|
|||||||
function handleClosing() {
|
function handleClosing() {
|
||||||
setTimeout(() => resetSkeleton(), 100)
|
setTimeout(() => resetSkeleton(), 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const breakpoints = useBreakpoints(
|
||||||
|
breakpointsTailwind
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiButton @click="toggleDialog">
|
<ClientOnly>
|
||||||
<PhPlus size="18" weight="bold" />
|
<Transition name="fade" appear>
|
||||||
|
<UiButton
|
||||||
|
class="max-md:fixed max-md:bottom-8 max-md:right-8 max-md:z-50 max-md:size-14 max-md:rounded-xl"
|
||||||
|
:size="breakpoints.md.value ? 'default' : 'icon'"
|
||||||
|
@click="toggleDialog"
|
||||||
|
>
|
||||||
|
<PhPlus :size="breakpoints.md.value ? 18 : 24" weight="bold" />
|
||||||
|
|
||||||
<strong class="font-semibold">
|
<strong v-if="breakpoints.md.value" class="font-semibold">
|
||||||
{{ $t("entity.calendar.event.newEvent") }}
|
{{ $t("entity.calendar.event.newEvent") }}
|
||||||
</strong>
|
</strong>
|
||||||
</UiButton>
|
</UiButton>
|
||||||
|
</Transition>
|
||||||
|
</ClientOnly>
|
||||||
|
|
||||||
<UiDialog v-model:open="isDialogOpen">
|
<UiDialog v-model:open="isDialogOpen">
|
||||||
<UiDialogContent
|
<UiDialogContent
|
||||||
class="border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
class="max-md:translate-0 max-md:inset-0 max-md:w-full max-md:block"
|
||||||
@escape-key-down.prevent="handleClosing"
|
:trap-focus="true"
|
||||||
|
@escape-key-down="handleClosing"
|
||||||
@pointer-down-outside.prevent="handleClosing"
|
@pointer-down-outside.prevent="handleClosing"
|
||||||
>
|
>
|
||||||
<UiDialogTitle>
|
<UiDialogTitle class="max-md:mb-8">
|
||||||
{{ $t("entity.calendar.event.addSingle") }}
|
{{ $t("entity.calendar.event.addSingle") }}
|
||||||
</UiDialogTitle>
|
</UiDialogTitle>
|
||||||
|
|
||||||
|
<VisuallyHidden>
|
||||||
|
<UiDialogDescription>
|
||||||
|
{{ $t("entity.calendar.event.addSingleDescription") }}
|
||||||
|
</UiDialogDescription>
|
||||||
|
</VisuallyHidden>
|
||||||
|
|
||||||
<CalendarFormCreateEvent @event-created="toggleDialog" />
|
<CalendarFormCreateEvent @event-created="toggleDialog" />
|
||||||
</UiDialogContent>
|
</UiDialogContent>
|
||||||
</UiDialog>
|
</UiDialog>
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ function handleClosing() {
|
|||||||
<UiDialogContent
|
<UiDialogContent
|
||||||
:disable-outside-pointer-events="true"
|
:disable-outside-pointer-events="true"
|
||||||
:trap-focus="true"
|
:trap-focus="true"
|
||||||
class="pl-3 min-w-96 border-border"
|
class="pl-3 md:min-w-96 max-md:translate-0 max-md:inset-0 max-md:w-full max-md:block"
|
||||||
@escape-key-down="handleClosing"
|
@escape-key-down="handleClosing"
|
||||||
@focus-outside="handleClosing"
|
@focus-outside="handleClosing"
|
||||||
@interact-outside="handleClosing"
|
@interact-outside="handleClosing"
|
||||||
@pointer-down-outside="(e) => e.preventDefault()"
|
@pointer-down-outside="(e) => e.preventDefault()"
|
||||||
>
|
>
|
||||||
<header class="pl-8 grid gap-y-2">
|
<header class="pl-8 grid gap-y-2 max-md:mb-8">
|
||||||
<UiDialogTitle>
|
<UiDialogTitle>
|
||||||
{{ $t('entity.calendar.event.editDialog.title') }}
|
{{ $t('entity.calendar.event.editDialog.title') }}
|
||||||
</UiDialogTitle>
|
</UiDialogTitle>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { cn } from "~/lib/utils"
|
|||||||
import type { RPGDate } from "~/models/Date"
|
import type { RPGDate } from "~/models/Date"
|
||||||
import type { CalendarEvent } from "~/models/CalendarEvent"
|
import type { CalendarEvent } from "~/models/CalendarEvent"
|
||||||
import { useCalendar } from "~/stores/CalendarStore"
|
import { useCalendar } from "~/stores/CalendarStore"
|
||||||
|
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PhHourglassMedium,
|
PhHourglassMedium,
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
PhDotsThreeOutlineVertical,
|
PhDotsThreeOutlineVertical,
|
||||||
PhEye
|
PhEye
|
||||||
} from "@phosphor-icons/vue"
|
} from "@phosphor-icons/vue"
|
||||||
|
import type { CollisionPadding } from "~/models/Popover"
|
||||||
|
|
||||||
const { defaultDate, getFormattedDateTitle, jumpToDate, getRelativeString, revealEditEventModal, revealDeleteEventModal } = useCalendar()
|
const { defaultDate, getFormattedDateTitle, jumpToDate, getRelativeString, revealEditEventModal, revealDeleteEventModal } = useCalendar()
|
||||||
const { lastActiveEvent, isReadOnly } = storeToRefs(useCalendar())
|
const { lastActiveEvent, isReadOnly } = storeToRefs(useCalendar())
|
||||||
@@ -24,6 +26,22 @@ const props = defineProps<{
|
|||||||
isEndEvent?: boolean
|
isEndEvent?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const breakpoints = useBreakpoints(
|
||||||
|
breakpointsTailwind
|
||||||
|
)
|
||||||
|
const collisionPadLg: CollisionPadding = {
|
||||||
|
top: 50,
|
||||||
|
bottom: 50,
|
||||||
|
left: 50,
|
||||||
|
right: 50,
|
||||||
|
}
|
||||||
|
const collisionPad: CollisionPadding = {
|
||||||
|
top: 80,
|
||||||
|
bottom: 80,
|
||||||
|
left: 12.5,
|
||||||
|
right: 12.5,
|
||||||
|
}
|
||||||
|
|
||||||
// Ref for the popover
|
// Ref for the popover
|
||||||
const eventDetails = ref<HTMLElement>()
|
const eventDetails = ref<HTMLElement>()
|
||||||
|
|
||||||
@@ -62,12 +80,12 @@ function deployDeleteModal() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiPopoverContent
|
<UiPopoverContent
|
||||||
class="w-96 event-popover"
|
class="max-w-full w-96 event-popover"
|
||||||
:align="'center'"
|
:align="'center'"
|
||||||
:align-offset="50"
|
:align-offset="breakpoints.lg.value ? 50 : 25"
|
||||||
:side="'left'"
|
:side-offset="8"
|
||||||
:collision-padding="60"
|
:side="breakpoints.lg.value ? 'left' : 'top'"
|
||||||
:hide-when-detached="true"
|
:collision-padding="breakpoints.lg.value ? collisionPadLg : collisionPad"
|
||||||
:class="cn(
|
:class="cn(
|
||||||
event.category ? `element-${event.category.color}` : '',
|
event.category ? `element-${event.category.color}` : '',
|
||||||
)"
|
)"
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ onMounted(() => {
|
|||||||
<UiPopover v-model:open="isPopoverDetailsOpen">
|
<UiPopover v-model:open="isPopoverDetailsOpen">
|
||||||
<UiPopoverTrigger as-child>
|
<UiPopoverTrigger as-child>
|
||||||
<button
|
<button
|
||||||
class="event-button text-xs px-2 py-1 block w-full text-left rounded-sm transition-colors outline-offset-1 cursor-pointer"
|
class="event-button text-2xs md:text-xs px-[5px] py-[5px] md:px-2 md:py-1 block w-full text-left rounded-sm transition-colors outline-offset-1 cursor-pointer"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
`element-${event.category?.color}`,
|
`element-${event.category?.color}`,
|
||||||
|
|||||||
@@ -36,8 +36,10 @@ const isUpdatingCalendar = ref<boolean>(false)
|
|||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
isUpdatingCalendar.value = true
|
isUpdatingCalendar.value = true
|
||||||
|
|
||||||
|
const fetchBody = { ...calendarSkeleton.value, worldId: props.world?.id }
|
||||||
|
|
||||||
const { error } = await tryCatch(
|
const { error } = await tryCatch(
|
||||||
$fetch(`/api/calendars/${calendarSkeleton.value.id}`, { method: "PATCH", body: { ...calendarSkeleton.value, worldId: props.world?.id } })
|
$fetch(`/api/calendars/${calendarSkeleton.value.id}`, { method: "PATCH", body: fetchBody })
|
||||||
)
|
)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@@ -1,46 +1,45 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useCalendar } from "~/stores/CalendarStore"
|
import { useCalendar } from "~/stores/CalendarStore"
|
||||||
|
import { breakpointsTailwind } from "@vueuse/core"
|
||||||
|
|
||||||
import { PhMagnifyingGlass } from "@phosphor-icons/vue"
|
const breakpoints = useBreakpoints(
|
||||||
|
breakpointsTailwind
|
||||||
|
)
|
||||||
|
|
||||||
const { revealAdvancedSearch, } = useCalendar()
|
|
||||||
const { isReadOnly, defaultDate } = storeToRefs(useCalendar())
|
const { isReadOnly, defaultDate } = storeToRefs(useCalendar())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="mt-2 grid gap-4 border-border border-b-[1px]">
|
<header class="md:mt-2 grid gap-3 md:gap-4 border-border border-b-[1px] transition-colors">
|
||||||
<div class="px-8 flex items-center justify-between gap-2">
|
<div class="px-5 md:px-8 flex items-center justify-between gap-2">
|
||||||
<menu class="flex items-center gap-2">
|
<menu class="flex items-center md:gap-2">
|
||||||
<li>
|
<li>
|
||||||
<LazyCalendarDialogQuickCreateEvent v-if="!isReadOnly" />
|
<LazyCalendarDialogQuickCreateEvent v-if="!isReadOnly" />
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li class="max-md:hidden">
|
||||||
<LazyCalendarMenuToday v-if="defaultDate" />
|
<LazyCalendarMenuToday v-if="defaultDate" />
|
||||||
</li>
|
</li>
|
||||||
<li class="ml-4">
|
<li class="ml-2 md:ml-4">
|
||||||
<CalendarCurrentDate />
|
<CalendarCurrentDate />
|
||||||
</li>
|
</li>
|
||||||
</menu>
|
</menu>
|
||||||
|
|
||||||
<menu class="flex items-center gap-2">
|
<menu class="flex items-center gap-2">
|
||||||
<li>
|
<li>
|
||||||
<UiButton search-slash @click="revealAdvancedSearch()">
|
<CalendarSearchCTA />
|
||||||
<PhMagnifyingGlass size="20" weight="light" />
|
|
||||||
<span>
|
|
||||||
{{ $t('entity.advancedSearch.title') }}
|
|
||||||
</span>
|
|
||||||
</UiButton>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<CalendarCategoriesCTA />
|
|
||||||
</li>
|
</li>
|
||||||
|
<ClientOnly>
|
||||||
|
<li v-if="breakpoints.md.value">
|
||||||
|
<CalendarCategoriesCTA />
|
||||||
|
</li>
|
||||||
|
</ClientOnly>
|
||||||
<li>
|
<li>
|
||||||
<CalendarOptionsCTA />
|
<CalendarOptionsCTA />
|
||||||
</li>
|
</li>
|
||||||
</menu>
|
</menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-8">
|
<div class="ml-4 md:ml-8">
|
||||||
<CalendarMenuSubnav />
|
<CalendarMenuSubnav />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ function toFutureFar(): void {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div class="grid items-end w-40 px-4 py-2 bg-white dark:bg-black border-border border-x-[1px] border-t-[1px] rounded-t-sm text-sm transition-colors">
|
<div class="grid items-center w-40 px-3 md:px-4 py-2 bg-white dark:bg-black border-border border-x-[1px] border-t-[1px] rounded-t-sm text-sm max-md:text-xs transition-colors">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<span>{{ currentDate.currentDateTitle }}</span>
|
<span>{{ currentDate.currentDateTitle }}</span>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from "vue"
|
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||||
|
import { PhCalendar } from "@phosphor-icons/vue"
|
||||||
|
|
||||||
const { defaultDate, jumpToDefaultDate, getFormattedDateTitle, currentDate } = useCalendar()
|
const { defaultDate, jumpToDefaultDate, getFormattedDateTitle, currentDate } = useCalendar()
|
||||||
|
|
||||||
@@ -8,15 +9,29 @@ const defaultDateFormatted: string = getFormattedDateTitle(defaultDate, true)
|
|||||||
const buttonDisabledState: ComputedRef<boolean> = computed<boolean>(() => {
|
const buttonDisabledState: ComputedRef<boolean> = computed<boolean>(() => {
|
||||||
return currentDate.currentMonth === defaultDate.month && currentDate.currentYear === defaultDate.year
|
return currentDate.currentMonth === defaultDate.month && currentDate.currentYear === defaultDate.year
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const breakpoints = useBreakpoints(
|
||||||
|
breakpointsTailwind
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiTooltipProvider :delay-duration="250">
|
<UiTooltipProvider :delay-duration="250">
|
||||||
<UiTooltip>
|
<UiTooltip>
|
||||||
<UiTooltipTrigger as-child>
|
<UiTooltipTrigger as-child>
|
||||||
<UiButton size="sm" variant="secondary" :disabled="buttonDisabledState" @click="jumpToDefaultDate">
|
<ClientOnly>
|
||||||
{{ $t('entity.calendar.date.today') }}
|
<UiButton
|
||||||
</UiButton>
|
:size="breakpoints.md.value ? 'default' : 'icon'"
|
||||||
|
variant="secondary"
|
||||||
|
:disabled="buttonDisabledState" @click="jumpToDefaultDate"
|
||||||
|
>
|
||||||
|
<PhCalendar v-if="!breakpoints.md.value" size="20" weight="fill" />
|
||||||
|
|
||||||
|
<span v-if="breakpoints.md.value">
|
||||||
|
{{ $t('entity.calendar.date.today') }}
|
||||||
|
</span>
|
||||||
|
</UiButton>
|
||||||
|
</ClientOnly>
|
||||||
</UiTooltipTrigger>
|
</UiTooltipTrigger>
|
||||||
<UiTooltipContent>
|
<UiTooltipContent>
|
||||||
<p>{{ defaultDateFormatted }}</p>
|
<p>{{ defaultDateFormatted }}</p>
|
||||||
|
|||||||
@@ -265,10 +265,10 @@ function handleCategoryUnselect(e: Category) {
|
|||||||
<template>
|
<template>
|
||||||
<UiDialog v-model:open="isAdvancedSearchOpen" @update:open="resetSearch()">
|
<UiDialog v-model:open="isAdvancedSearchOpen" @update:open="resetSearch()">
|
||||||
<UiDialogContent
|
<UiDialogContent
|
||||||
class="flex flex-col flex-nowrap top-10 -translate-y-0 data-[state=closed]:slide-out-to-top-[5%] data-[state=open]:slide-in-from-top-[5%]"
|
class="flex flex-col flex-nowrap top-10 -translate-y-0 data-[state=closed]:slide-out-to-top-[5%] data-[state=open]:slide-in-from-top-[5%] max-md:rounded-none"
|
||||||
:class="{
|
:class="cn({
|
||||||
'bottom-10': searchResults.length > 0
|
'max-md:w-full bottom-0 md:bottom-10 top-0 md:top-10': searchResults.length > 0
|
||||||
}"
|
})"
|
||||||
>
|
>
|
||||||
<VisuallyHidden>
|
<VisuallyHidden>
|
||||||
<UiDialogTitle> {{ $t('entity.advancedSearch.title') }} </UiDialogTitle>
|
<UiDialogTitle> {{ $t('entity.advancedSearch.title') }} </UiDialogTitle>
|
||||||
@@ -297,7 +297,7 @@ function handleCategoryUnselect(e: Category) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-8">
|
<div class="flex items-center justify-between gap-8">
|
||||||
<div>
|
<div class="hidden">
|
||||||
<UiToggleGroup
|
<UiToggleGroup
|
||||||
v-model="selectedEntity"
|
v-model="selectedEntity"
|
||||||
type="single"
|
type="single"
|
||||||
@@ -315,7 +315,7 @@ function handleCategoryUnselect(e: Category) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grow flex justify-end items-center gap-1">
|
<div class="grow flex justify-end items-center gap-1">
|
||||||
<UiTagsInput class="grow px-0 gap-y-1 w-80">
|
<UiTagsInput class="grow px-0 gap-y-1 md:w-80">
|
||||||
<div v-if="selectedCategories.length > 0" class="flex gap-2 flex-wrap items-center px-3">
|
<div v-if="selectedCategories.length > 0" class="flex gap-2 flex-wrap items-center px-3">
|
||||||
<UiTagsInputItem v-for="cat in selectedCategories" :key="cat.id" :value="cat.name">
|
<UiTagsInputItem v-for="cat in selectedCategories" :key="cat.id" :value="cat.name">
|
||||||
<button
|
<button
|
||||||
@@ -447,7 +447,7 @@ function handleCategoryUnselect(e: Category) {
|
|||||||
as-child
|
as-child
|
||||||
>
|
>
|
||||||
<UiButton
|
<UiButton
|
||||||
class="w-10 h-10 p-0"
|
class="size-8 md:size-10 p-0"
|
||||||
:variant="item.value === currentPage ? 'default' : 'outline'"
|
:variant="item.value === currentPage ? 'default' : 'outline'"
|
||||||
>
|
>
|
||||||
{{ item.value }}
|
{{ item.value }}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const dateDuration = computed<string | null>(() => props.event.endDate ? getRela
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
class="event-callout group relative block w-full text-left py-3 px-4 cursor-pointer transition-colors"
|
class="event-callout group relative block w-full text-left py-2 px-3 md:py-3 md:px-4 cursor-pointer transition-colors"
|
||||||
:class="cn(
|
:class="cn(
|
||||||
event.category ? `element-${event.category.color}` : '',
|
event.category ? `element-${event.category.color}` : '',
|
||||||
{
|
{
|
||||||
@@ -30,7 +30,7 @@ const dateDuration = computed<string | null>(() => props.event.endDate ? getRela
|
|||||||
@click="$emit('query:date-jump', event.startDate)"
|
@click="$emit('query:date-jump', event.startDate)"
|
||||||
>
|
>
|
||||||
<div class="flex gap-2 items-center mb-1">
|
<div class="flex gap-2 items-center mb-1">
|
||||||
<h2 class="font-bold text-lg underline-offset-4 group-hover:underline">
|
<h2 class="font-semibold md:font-bold md:text-lg underline-offset-4 group-hover:underline">
|
||||||
{{ event.title }}
|
{{ event.title }}
|
||||||
</h2>
|
</h2>
|
||||||
<div v-if="event.wiki">
|
<div v-if="event.wiki">
|
||||||
@@ -43,7 +43,7 @@ const dateDuration = computed<string | null>(() => props.event.endDate ? getRela
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 items-center justify-between mb-1">
|
<div class="flex gap-2 items-center justify-between mb-2 md:mb-1">
|
||||||
<template v-if="!event.endDate">
|
<template v-if="!event.endDate">
|
||||||
<p class="font-semibold text-sm opacity-75">
|
<p class="font-semibold text-sm opacity-75">
|
||||||
{{ getFormattedDateTitle(event.startDate, true) }}
|
{{ getFormattedDateTitle(event.startDate, true) }}
|
||||||
@@ -81,15 +81,15 @@ const dateDuration = computed<string | null>(() => props.event.endDate ? getRela
|
|||||||
|
|
||||||
<div class="mb-1 flex gap-x-2 items-center">
|
<div class="mb-1 flex gap-x-2 items-center">
|
||||||
<template v-if="event.location">
|
<template v-if="event.location">
|
||||||
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
|
<p class="w-fit text-xs md:text-sm italic opacity-75 flex items-center gap-1">
|
||||||
<PhMapPinArea size="16" weight="fill" /> {{ event.location }}
|
<PhMapPinArea size="16" weight="fill" /> {{ event.location }}
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
|
<p class="w-fit text-xs md:text-sm italic opacity-75 flex items-center gap-1">
|
||||||
<PhAlarm size="16" weight="fill" /> {{ dateDifference }}
|
<PhAlarm size="16" weight="fill" /> {{ dateDifference }}
|
||||||
</p>
|
</p>
|
||||||
<template v-if="dateDuration">
|
<template v-if="dateDuration">
|
||||||
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
|
<p class="w-fit text-xs md:text-sm italic opacity-75 flex items-center gap-1">
|
||||||
<PhHourglassMedium size="16" weight="fill" /> {{ $t('entity.calendar.date.while', { duration: dateDuration } )}}
|
<PhHourglassMedium size="16" weight="fill" /> {{ $t('entity.calendar.date.while', { duration: dateDuration } )}}
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
@@ -111,8 +111,8 @@ const dateDuration = computed<string | null>(() => props.event.endDate ? getRela
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="event.description" class="text-sm">
|
<div v-if="event.description" class="text-xs md:text-sm">
|
||||||
<hr class="my-2 border-white opacity-50" >
|
<hr class="my-3 md:my-2 border-white opacity-50" >
|
||||||
<span class="opacity-75">
|
<span class="opacity-75">
|
||||||
{{ event.description }}
|
{{ event.description }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const pagedResults = computed(() => sortedResults.value.slice(props.startAt, pro
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ul class="grid gap-4 pl-8 pr-12">
|
<ul class="grid gap-3 md:gap-4 pl-4 pr-6 md:pl-8 md:pr-12">
|
||||||
<li v-for="r in pagedResults" :key="isCalendarEvent(r) ? r.title : r.name">
|
<li v-for="r in pagedResults" :key="isCalendarEvent(r) ? r.title : r.name">
|
||||||
<EventCallout v-if="isCalendarEvent(r)" :event="r" @query:date-jump="handleJumpToDate" />
|
<EventCallout v-if="isCalendarEvent(r)" :event="r" @query:date-jump="handleJumpToDate" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { RPGDate } from "~/models/Date"
|
import type { RPGDate } from "~/models/Date"
|
||||||
import type { CalendarEvent } from "~/models/CalendarEvent"
|
import type { CalendarEvent } from "~/models/CalendarEvent"
|
||||||
import { useElementBounding } from "@vueuse/core"
|
import { breakpointsTailwind, useElementBounding } from "@vueuse/core"
|
||||||
import { storeToRefs } from "pinia"
|
import { storeToRefs } from "pinia"
|
||||||
import { computed, ref, type ComputedRef } from "vue"
|
import { computed, ref, type ComputedRef } from "vue"
|
||||||
|
|
||||||
@@ -12,12 +12,20 @@ const props = defineProps<{
|
|||||||
faded?: boolean
|
faded?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "on-open-create-dialog", date: RPGDate): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const calendarTile = ref()
|
const calendarTile = ref()
|
||||||
const calendarEventsList = ref()
|
const calendarEventsList = ref()
|
||||||
|
|
||||||
const { defaultDate, selectDate, areDatesIdentical } = useCalendar()
|
const { defaultDate, selectDate, areDatesIdentical, getFormattedDateTitle } = useCalendar()
|
||||||
const { selectedDate, currentEvents, isReadOnly } = storeToRefs(useCalendar())
|
const { selectedDate, currentEvents, isReadOnly } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
|
const breakpoints = useBreakpoints(
|
||||||
|
breakpointsTailwind
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All events with a startDate / endDate that starts or ends on the tile
|
* All events with a startDate / endDate that starts or ends on the tile
|
||||||
*/
|
*/
|
||||||
@@ -73,19 +81,19 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="calendarTile"
|
ref="calendarTile"
|
||||||
class="tile relative text-xs p-2"
|
class="tile relative p-1 md:p-2 transition-colors"
|
||||||
:class="{
|
:class="{
|
||||||
'text-slate-300 dark:text-slate-500': props.faded,
|
'text-slate-300 dark:text-slate-500': props.faded,
|
||||||
'text-slate-500 dark:text-slate-300': !props.faded
|
'text-slate-500 dark:text-slate-300': !props.faded
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="relative z-10 group block w-full text-center cursor-pointer"
|
class="relative z-10 group block w-full text-2xs md:text-xs text-center cursor-pointer"
|
||||||
@click="selectDate(date)"
|
@click="selectDate(date)"
|
||||||
>
|
>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<span
|
<span
|
||||||
class="inline-flex w-8 h-8 aspect-square items-center justify-center rounded-full border-2 border-transparent font-bold transition-colors group-hover:border-indigo-300 dark:group-hover:border-indigo-700"
|
class="inline-flex size-7 md:size-8 aspect-square items-center justify-center rounded-full border-1 md:border-2 border-transparent font-bold transition-colors group-hover:border-indigo-300 dark:group-hover:border-indigo-700"
|
||||||
:class="{
|
:class="{
|
||||||
'text-white bg-slate-600 dark:bg-slate-800': isDefaultDate && !isSelectedDate,
|
'text-white bg-slate-600 dark:bg-slate-800': isDefaultDate && !isSelectedDate,
|
||||||
'text-white bg-indigo-500': isSelectedDate
|
'text-white bg-indigo-500': isSelectedDate
|
||||||
@@ -99,7 +107,7 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
|||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<ul
|
<ul
|
||||||
ref="calendarEventsList"
|
ref="calendarEventsList"
|
||||||
class="absolute top-12 bottom-2 inset-x-2 grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity"
|
class="absolute top-9 md:top-12 bottom-1 md:bottom-2 inset-x-2 grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity"
|
||||||
:class="{
|
:class="{
|
||||||
'opacity-40': props.faded && !isSelectedDate
|
'opacity-40': props.faded && !isSelectedDate
|
||||||
}"
|
}"
|
||||||
@@ -114,17 +122,23 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
|||||||
<UiPopover>
|
<UiPopover>
|
||||||
<UiPopoverTrigger as-child>
|
<UiPopoverTrigger as-child>
|
||||||
<button
|
<button
|
||||||
class="text-xs px-2 py-1 block w-full text-left font-bold rounded-sm whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer transition-colors hover:bg-slate-800"
|
class="text-2xs md:text-xs px-[5px] py-[5px] md:px-2 md:py-1 block w-full text-left font-bold rounded-sm whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer transition-colors hover:text-foreground hover:bg-foreground/10"
|
||||||
>
|
>
|
||||||
{{ eventsNotDisplayed }} autre{{ eventsNotDisplayed > 1 ? 's' : '' }}
|
{{ eventsNotDisplayed }} autre{{ eventsNotDisplayed > 1 ? 's' : '' }}
|
||||||
</button>
|
</button>
|
||||||
</UiPopoverTrigger>
|
</UiPopoverTrigger>
|
||||||
<UiPopoverContent class="w-80" :align="'center'" :side="'right'">
|
<UiPopoverContent
|
||||||
<div class="text-center mb-4">
|
class="w-80"
|
||||||
|
:align="'center'"
|
||||||
|
:align-offset="breakpoints.lg.value ? 50 : 25"
|
||||||
|
:collision-padding="breakpoints.lg.value ? 50 : 25"
|
||||||
|
:side="breakpoints.lg.value ? 'left' : 'bottom'"
|
||||||
|
>
|
||||||
|
<div class="text-center mb-2">
|
||||||
<span
|
<span
|
||||||
class="inline-flex w-12 h-12 aspect-square items-center justify-center text-lg font-semibold text-slate-300 bg-slate-800 rounded-full"
|
class="text-lg font-semibold rounded-full"
|
||||||
>
|
>
|
||||||
{{ date.day }}
|
{{ getFormattedDateTitle(date, true) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity">
|
<ul class="grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity">
|
||||||
@@ -143,14 +157,17 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
|||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
|
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<LazyCalendarDialogCreateEvent v-if="!isReadOnly" :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
|
<LazyCalendarDialogCreateEvent v-if="!isReadOnly && breakpoints.lg.value" :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
|
||||||
|
<button
|
||||||
|
v-else-if="!isReadOnly && !breakpoints.lg.value"
|
||||||
|
class="absolute inset-0 w-full h-full cursor-default z-0"
|
||||||
|
@click="emit('on-open-create-dialog', props.date)"
|
||||||
|
/>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
// .tile are the direct children of the grid
|
|
||||||
// We need to get, between each element, a 1px solid line to separate them, for columns and rows
|
|
||||||
.tile {
|
.tile {
|
||||||
border-right-width: 1px;
|
border-right-width: 1px;
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useCalendar } from "~/stores/CalendarStore"
|
import { useCalendar } from "~/stores/CalendarStore"
|
||||||
import { useThrottleFn } from "@vueuse/core"
|
import { useThrottleFn } from "@vueuse/core"
|
||||||
|
import type { RPGDate } from "~/models/Date"
|
||||||
|
import { VisuallyHidden } from "radix-vue"
|
||||||
|
|
||||||
const { currentDate, decrementViewMonth, incrementViewMonth } = useCalendar()
|
const { currentDate, decrementViewMonth, incrementViewMonth, resetSkeleton } = useCalendar()
|
||||||
const { currentMonthData } = storeToRefs(useCalendar())
|
const { currentMonthData, operationInProgress, eventSkeleton } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
function handleWheel(e: WheelEvent) {
|
function handleWheel(e: WheelEvent) {
|
||||||
|
// Prevent scrolling
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
const isMovingUp = e.deltaY < 0
|
const isMovingUp = e.deltaY < 0
|
||||||
if (isMovingUp) {
|
if (isMovingUp) {
|
||||||
moveCalendarLeft()
|
moveCalendarLeft()
|
||||||
@@ -21,10 +26,43 @@ const moveCalendarLeft = useThrottleFn(() => {
|
|||||||
const moveCalendarRight = useThrottleFn(() => {
|
const moveCalendarRight = useThrottleFn(() => {
|
||||||
incrementViewMonth()
|
incrementViewMonth()
|
||||||
}, 100)
|
}, 100)
|
||||||
|
|
||||||
|
const isDialogOpen = ref<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens event creation's popover
|
||||||
|
*/
|
||||||
|
function handleDialogOpen(date: RPGDate) {
|
||||||
|
// If another operation is in progress, whether it's another create popup or a modal, don't bother opening it
|
||||||
|
if (operationInProgress.value) {
|
||||||
|
isDialogOpen.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSkeleton()
|
||||||
|
|
||||||
|
isDialogOpen.value = true
|
||||||
|
|
||||||
|
// Set skeleton initial startDate if it's known
|
||||||
|
if (date) {
|
||||||
|
eventSkeleton.value.startDate = date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDialog() {
|
||||||
|
isDialogOpen.value = !isDialogOpen.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents the modal from closing if's still loading
|
||||||
|
*/
|
||||||
|
function handleClosing() {
|
||||||
|
setTimeout(() => resetSkeleton(), 100)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="grid grid-cols-10" @wheel="handleWheel">
|
<div class="grid grid-cols-5 md:grid-cols-7 lg:grid-cols-10" @wheel="handleWheel">
|
||||||
<template v-if="currentMonthData">
|
<template v-if="currentMonthData">
|
||||||
<CalendarStateMonthlyDayTile
|
<CalendarStateMonthlyDayTile
|
||||||
v-for="day in currentMonthData?.days"
|
v-for="day in currentMonthData?.days"
|
||||||
@@ -34,7 +72,29 @@ const moveCalendarRight = useThrottleFn(() => {
|
|||||||
month: currentDate.currentMonth,
|
month: currentDate.currentMonth,
|
||||||
year: currentDate.currentYear
|
year: currentDate.currentYear
|
||||||
}"
|
}"
|
||||||
|
@on-open-create-dialog="handleDialogOpen"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UiDialog v-model:open="isDialogOpen">
|
||||||
|
<UiDialogContent
|
||||||
|
class="max-md:translate-0 max-md:inset-0 max-md:w-full max-md:block"
|
||||||
|
:trap-focus="true"
|
||||||
|
@escape-key-down="handleClosing"
|
||||||
|
@pointer-down-outside.prevent="null"
|
||||||
|
>
|
||||||
|
<UiDialogTitle class="max-md:mb-8">
|
||||||
|
{{ $t("entity.calendar.event.addSingle") }}
|
||||||
|
</UiDialogTitle>
|
||||||
|
|
||||||
|
<VisuallyHidden>
|
||||||
|
<UiDialogDescription>
|
||||||
|
{{ $t("entity.calendar.event.addSingleDescription") }}
|
||||||
|
</UiDialogDescription>
|
||||||
|
</VisuallyHidden>
|
||||||
|
|
||||||
|
<CalendarFormCreateEvent @event-created="toggleDialog" />
|
||||||
|
</UiDialogContent>
|
||||||
|
</UiDialog>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,46 +15,52 @@ defineProps<{
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiBreadcrumb>
|
<div class="flex gap-3 items-center">
|
||||||
<UiBreadcrumbList>
|
<div class="md:hidden">
|
||||||
<UiBreadcrumbItem v-if="route.path !== '/my'">
|
<SidebarToggle />
|
||||||
<UiBreadcrumbLink as-child>
|
</div>
|
||||||
<NuxtLink to="/my">
|
|
||||||
{{ $t("breadcrumbs.profile") }}
|
|
||||||
</NuxtLink>
|
|
||||||
</UiBreadcrumbLink>
|
|
||||||
</UiBreadcrumbItem>
|
|
||||||
<UiBreadcrumbItem v-else>
|
|
||||||
<UiBreadcrumbPage>
|
|
||||||
{{ $t("breadcrumbs.profile") }}
|
|
||||||
</UiBreadcrumbPage>
|
|
||||||
</UiBreadcrumbItem>
|
|
||||||
|
|
||||||
<template v-for="(item, index) in items" :key="index">
|
<UiBreadcrumb>
|
||||||
<UiBreadcrumbSeparator>
|
<UiBreadcrumbList>
|
||||||
<PhCaretRight />
|
<UiBreadcrumbItem v-if="route.path !== '/my'">
|
||||||
</UiBreadcrumbSeparator>
|
<UiBreadcrumbLink as-child>
|
||||||
<UiBreadcrumbItem>
|
<NuxtLink to="/my">
|
||||||
<UiBreadcrumbLink v-if="item.to" as-child>
|
{{ $t("breadcrumbs.profile") }}
|
||||||
<NuxtLink :to="item.to">
|
</NuxtLink>
|
||||||
|
</UiBreadcrumbLink>
|
||||||
|
</UiBreadcrumbItem>
|
||||||
|
<UiBreadcrumbItem v-else>
|
||||||
|
<UiBreadcrumbPage>
|
||||||
|
{{ $t("breadcrumbs.profile") }}
|
||||||
|
</UiBreadcrumbPage>
|
||||||
|
</UiBreadcrumbItem>
|
||||||
|
|
||||||
|
<template v-for="(item, index) in items" :key="index">
|
||||||
|
<UiBreadcrumbSeparator>
|
||||||
|
<PhCaretRight />
|
||||||
|
</UiBreadcrumbSeparator>
|
||||||
|
<UiBreadcrumbItem>
|
||||||
|
<UiBreadcrumbLink v-if="item.to" as-child>
|
||||||
|
<NuxtLink :to="item.to">
|
||||||
|
<template v-if="item.label">
|
||||||
|
{{ item.label }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t(`breadcrumbs.${item.translateKey}`) }}
|
||||||
|
</template>
|
||||||
|
</NuxtLink>
|
||||||
|
</UiBreadcrumbLink>
|
||||||
|
<UiBreadcrumbPage v-else>
|
||||||
<template v-if="item.label">
|
<template v-if="item.label">
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ $t(`breadcrumbs.${item.translateKey}`) }}
|
{{ $t(`breadcrumbs.${item.translateKey}`) }}
|
||||||
</template>
|
</template>
|
||||||
</NuxtLink>
|
</UiBreadcrumbPage>
|
||||||
</UiBreadcrumbLink>
|
</UiBreadcrumbItem>
|
||||||
<UiBreadcrumbPage v-else>
|
</template>
|
||||||
<template v-if="item.label">
|
</UiBreadcrumbList>
|
||||||
{{ item.label }}
|
</UiBreadcrumb>
|
||||||
</template>
|
</div>
|
||||||
<template v-else>
|
|
||||||
{{ $t(`breadcrumbs.${item.translateKey}`) }}
|
|
||||||
</template>
|
|
||||||
</UiBreadcrumbPage>
|
|
||||||
</UiBreadcrumbItem>
|
|
||||||
</template>
|
|
||||||
</UiBreadcrumbList>
|
|
||||||
</UiBreadcrumb>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PhCompass, PhGlobeHemisphereEast, PhHurricane, PhList } from "@phosphor-icons/vue"
|
import { PhCompass, PhGlobeHemisphereEast, PhHurricane, PhX } from "@phosphor-icons/vue"
|
||||||
import type { SidebarMenuActionType, SidebarMenuIcon } from "./SidebarProps";
|
import type { SidebarMenuActionType, SidebarMenuIcon } from "./SidebarProps";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { breakpointsTailwind } from "@vueuse/core"
|
||||||
|
|
||||||
const { revealAdvancedSearch } = useCalendar()
|
const { revealAdvancedSearch } = useCalendar()
|
||||||
const { currentMenu } = storeToRefs(useUiStore())
|
const { toggleSidebar } = useUiStore()
|
||||||
|
const { currentMenu, isSidebarOpened } = storeToRefs(useUiStore())
|
||||||
|
|
||||||
function handleMenuItemAction(actionType: SidebarMenuActionType) {
|
function handleMenuItemAction(actionType: SidebarMenuActionType) {
|
||||||
if (actionType === "event-search") {
|
if (actionType === "event-search") {
|
||||||
@@ -21,26 +24,68 @@ function computeMenuItemIcon(iconString: SidebarMenuIcon) {
|
|||||||
return PhCompass
|
return PhCompass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const breakpoints = useBreakpoints(
|
||||||
|
breakpointsTailwind
|
||||||
|
)
|
||||||
|
|
||||||
|
// const sidebarRef = ref(null)
|
||||||
|
|
||||||
|
// onClickOutside(sidebarRef, () => {
|
||||||
|
// isSidebarOpened.value = false
|
||||||
|
// })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="w-16 py-6 border-r-[1px] bg-indigo-700 dark:bg-black text-white border-r-indigo-700 dark:border-r-indigo-950 grid grid-rows-[1fr_auto] justify-center transition-colors after:opacity-50 after:contrast-125 dark:after:opacity-75 dark:after:contrast-175 after:-hue-rotate-60">
|
<nav
|
||||||
<menu class="flex flex-col gap-4">
|
ref="sidebarRef"
|
||||||
<li class="mb-12">
|
:class="cn(
|
||||||
<UiButton variant="ghost" size="icon" class="rounded-full" @click="console.log">
|
['md:relative md:isolate w-16 py-6 grid gap-4 grid-rows-[1fr_auto] justify-center transition-all'], // Base appearance
|
||||||
<PhList size="27" />
|
['after:opacity-50 after:contrast-125 dark:after:opacity-75 dark:after:contrast-175 after:-hue-rotate-60'], // After styling
|
||||||
|
['border-r-[1px] bg-indigo-700 dark:bg-black text-white border-r-indigo-700 dark:border-r-indigo-950 shadow-navbar-light dark:shadow-navbar-dark'], // Colours
|
||||||
|
['max-md:justify-stretch max-md:px-4 max-md:py-4 max-md:absolute max-md:left-0 max-md:inset-0 max-md:z-50 max-md:w-30 max-md:max-w-full'], // Responsive behaviours
|
||||||
|
{
|
||||||
|
'max-md:-translate-x-30': !isSidebarOpened,
|
||||||
|
'max-md:-translate-x-0 shadow-navbar-dark dark:bg-slate-950': isSidebarOpened
|
||||||
|
}
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<menu class="flex flex-col gap-4 max-md:items-center">
|
||||||
|
<li class="mb-12 mt-4 max-md:self-start">
|
||||||
|
<UiButton
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
class="md:hidden size-9 border-background/30"
|
||||||
|
@click="toggleSidebar"
|
||||||
|
>
|
||||||
|
<PhX size="19" />
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li class="max-md:self-start">
|
||||||
<UiTooltipProvider :delay-duration="50">
|
<UiTooltipProvider :delay-duration="50" :disabled="!breakpoints.md.value">
|
||||||
<UiTooltip>
|
<UiTooltip>
|
||||||
<UiTooltipTrigger as-child>
|
<UiTooltipTrigger as-child>
|
||||||
<UiButton variant="ghost" size="icon" class="rounded-full" as-child>
|
<UiButton
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="rounded-full max-md:hidden"
|
||||||
|
as-child
|
||||||
|
>
|
||||||
<RouterLink to="/explore">
|
<RouterLink to="/explore">
|
||||||
<PhCompass size="24" weight="fill" />
|
<PhCompass size="24" weight="fill" />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</UiButton>
|
</UiButton>
|
||||||
|
<RouterLink
|
||||||
|
to="/explore"
|
||||||
|
class="md:hidden flex items-center gap-[.6ch] underline-offset-4 hover:underline"
|
||||||
|
>
|
||||||
|
<PhCompass size="22" weight="fill" />
|
||||||
|
|
||||||
|
<span class="text-[.9em]">
|
||||||
|
{{ $t('pages.explore.menuLabel') }}
|
||||||
|
</span>
|
||||||
|
</RouterLink>
|
||||||
</UiTooltipTrigger>
|
</UiTooltipTrigger>
|
||||||
<UiTooltipContent :side="'right'" :side-offset="6">
|
<UiTooltipContent :side="'right'" :side-offset="6">
|
||||||
<p>
|
<p>
|
||||||
@@ -92,9 +137,6 @@ function computeMenuItemIcon(iconString: SidebarMenuIcon) {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
nav {
|
nav {
|
||||||
position: relative;
|
|
||||||
isolation: isolate;
|
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
display: block;
|
display: block;
|
||||||
content: '';
|
content: '';
|
||||||
|
|||||||
15
components/global/sidebar/SidebarToggle.vue
Normal file
15
components/global/sidebar/SidebarToggle.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { PhList } from "@phosphor-icons/vue"
|
||||||
|
|
||||||
|
const { toggleSidebar } = useUiStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UiButton
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
@click="toggleSidebar"
|
||||||
|
>
|
||||||
|
<PhList size="19" weight="light" />
|
||||||
|
</UiButton>
|
||||||
|
</template>
|
||||||
@@ -31,7 +31,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
v-bind="forwarded"
|
v-bind="forwarded"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-3xl -translate-x-1/2 -translate-y-1/2 gap-4 border border-indigo-200 bg-background dark:bg-slate-950 dark:border-indigo-950 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% data-[state=open]:slide-in-from-top-48% sm:rounded-lg',
|
'fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-2rem)] max-w-3xl -translate-x-1/2 -translate-y-1/2 gap-4 border border-indigo-200 bg-background dark:bg-slate-950 dark:border-indigo-950 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% data-[state=open]:slide-in-from-top-48% rounded-lg',
|
||||||
props.class,
|
props.class,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -15,21 +15,22 @@ const emit = defineEmits(["on-click"])
|
|||||||
<div
|
<div
|
||||||
:class="
|
:class="
|
||||||
cn('rounded-lg border bg-card text-card-foreground shadow-xs transition-all isolate', props.class, {
|
cn('rounded-lg border bg-card text-card-foreground shadow-xs transition-all isolate', props.class, {
|
||||||
'relative outline outline-2 outline-offset-4 outline-transparent hover:-translate-y-[.2rem]':
|
'relative hover:-translate-y-[.2rem]':
|
||||||
props.link || props.hasClick
|
props.link || props.hasClick
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<slot />
|
|
||||||
|
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="props.link"
|
v-if="props.link"
|
||||||
:to="props.link"
|
:to="props.link"
|
||||||
class="absolute inset-0 z-10 focus-visible:outline-hidden cursor-pointer"
|
class="rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 absolute inset-0 z-10 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="props.hasClick"
|
v-if="props.hasClick"
|
||||||
class="absolute inset-0 z-10 focus-visible:outline-hidden cursor-pointer"
|
class="rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 absolute inset-0 z-10 cursor-pointer"
|
||||||
@click="emit('on-click')" />
|
@click="emit('on-click')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
v-bind="forwarded"
|
v-bind="forwarded"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-2rem)] max-w-4xl -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background border-indigo-200 dark:bg-slate-950 dark:border-indigo-950 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% data-[state=open]:slide-in-from-top-48% sm:rounded-lg',
|
'fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-2rem)] max-w-4xl -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background border-indigo-200 dark:bg-slate-950 dark:border-indigo-950 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% data-[state=open]:slide-in-from-top-48% rounded-lg',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
<DialogContent
|
<DialogContent
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
|
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 rounded-lg md:w-full',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const delegatedProps = computed(() => {
|
|||||||
<template>
|
<template>
|
||||||
<PaginationEllipsis
|
<PaginationEllipsis
|
||||||
v-bind="delegatedProps"
|
v-bind="delegatedProps"
|
||||||
:class="cn('size-9 flex items-center justify-center', props.class)"
|
:class="cn('size-8 md:size-10 flex items-center justify-center', props.class)"
|
||||||
>
|
>
|
||||||
<slot>
|
<slot>
|
||||||
<PhArrowsHorizontal />
|
<PhArrowsHorizontal />
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const delegatedProps = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PaginationFirst v-bind="delegatedProps">
|
<PaginationFirst v-bind="delegatedProps">
|
||||||
<Button :class="cn('w-10 h-10 p-0', props.class)" variant="outline">
|
<Button :class="cn('size-8 md:size-10 p-0', props.class)" variant="outline">
|
||||||
<slot>
|
<slot>
|
||||||
<PhCaretDoubleLeft class="size-4" />
|
<PhCaretDoubleLeft class="size-4" />
|
||||||
</slot>
|
</slot>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const delegatedProps = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PaginationLast v-bind="delegatedProps">
|
<PaginationLast v-bind="delegatedProps">
|
||||||
<Button :class="cn('size-10 p-0', props.class)" variant="outline">
|
<Button :class="cn('size-8 md:size-10 p-0', props.class)" variant="outline">
|
||||||
<slot>
|
<slot>
|
||||||
<PhCaretDoubleRight class="size-4" />
|
<PhCaretDoubleRight class="size-4" />
|
||||||
</slot>
|
</slot>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const delegatedProps = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PaginationNext v-bind="delegatedProps">
|
<PaginationNext v-bind="delegatedProps">
|
||||||
<Button :class="cn('size-10 p-0', props.class)" variant="outline">
|
<Button :class="cn('size-8 md:size-10 p-0', props.class)" variant="outline">
|
||||||
<slot>
|
<slot>
|
||||||
<PhCaretRight class="size-4" />
|
<PhCaretRight class="size-4" />
|
||||||
</slot>
|
</slot>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const delegatedProps = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PaginationPrev v-bind="delegatedProps">
|
<PaginationPrev v-bind="delegatedProps">
|
||||||
<Button :class="cn('size-10 p-0', props.class)" variant="outline">
|
<Button :class="cn('size-8 md:size-10 p-0', props.class)" variant="outline">
|
||||||
<slot>
|
<slot>
|
||||||
<PhCaretLeft class="size-4" />
|
<PhCaretLeft class="size-4" />
|
||||||
</slot>
|
</slot>
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PhArchive, PhFile, PhFileDashed, PhFilePlus, PhPencil, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue";
|
import { PhArchive, PhFile, PhFileDashed, PhFilePlus, PhPencil, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue"
|
||||||
import type { World } from "~/models/World";
|
import type { World } from "~/models/World"
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon"
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
world: World
|
world: World
|
||||||
}>();
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits(["on-edit", "on-delete"]);
|
const emit = defineEmits(["on-edit", "on-delete"])
|
||||||
|
|
||||||
const { locale } = useI18n();
|
const cardRef = ref(null)
|
||||||
const createdAt = DateTime.fromISO(props.world.createdAt!).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value });
|
const isCardHovered = useElementHover(cardRef)
|
||||||
const updatedAt = computed<string>(() => props.world.updatedAt ? DateTime.fromISO(props.world.updatedAt).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value }) : "");
|
const { focused: isCardFocused } = useFocusWithin(cardRef)
|
||||||
|
|
||||||
|
const { locale } = useI18n()
|
||||||
|
const createdAt = DateTime.fromISO(props.world.createdAt!).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value })
|
||||||
|
const updatedAt = computed<string>(() => props.world.updatedAt ? DateTime.fromISO(props.world.updatedAt).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value }) : "")
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiCard
|
<UiCard
|
||||||
class="w-full h-full flex flex-col transition-all"
|
ref="cardRef"
|
||||||
|
class="w-full h-full flex flex-col"
|
||||||
:link="`/my/worlds/${world.id}`"
|
:link="`/my/worlds/${world.id}`"
|
||||||
:class="cn(
|
:class="cn(
|
||||||
world.color ? `card-color element-${world.color}` : '',
|
world.color ? `card-color element-${world.color}` : '',
|
||||||
@@ -44,14 +49,20 @@ const updatedAt = computed<string>(() => props.world.updatedAt ? DateTime.fromIS
|
|||||||
<UiCardContent class="grow">
|
<UiCardContent class="grow">
|
||||||
<p class="italic">{{ world.description }}</p>
|
<p class="italic">{{ world.description }}</p>
|
||||||
|
|
||||||
<div class="flex gap-1 absolute top-4 right-4 z-20">
|
<div
|
||||||
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-indigo-400 dark:hover:bg-indigo-700" @click="emit('on-edit')">
|
v-if="isCardHovered || isCardFocused"
|
||||||
<PhPencil size="16" />
|
class="flex gap-2 absolute top-4 right-4 z-20"
|
||||||
</UiButton>
|
>
|
||||||
|
<Transition name="fade" appear>
|
||||||
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-rose-400 dark:hover:bg-rose-700" @click="emit('on-delete')">
|
<UiButton size="icon" variant="outline" class="size-8 border-foreground/20 group-hover:border-foreground/30 hover:text-background hover:border-foreground hover:bg-foreground" @click="emit('on-edit')">
|
||||||
<PhTrash size="16" />
|
<PhPencil size="15" weight="fill" />
|
||||||
</UiButton>
|
</UiButton>
|
||||||
|
</Transition>
|
||||||
|
<Transition name="fade" appear>
|
||||||
|
<UiButton size="icon" variant="outline" class="size-8 border-foreground/25 group-hover:border-foreground/40 hover:text-background hover:border-rose-500 hover:bg-rose-500" @click="emit('on-delete')">
|
||||||
|
<PhTrash size="15" weight="fill" />
|
||||||
|
</UiButton>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</UiCardContent>
|
</UiCardContent>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default defineI18nConfig(() => ({
|
|||||||
en: {
|
en: {
|
||||||
head: {
|
head: {
|
||||||
title: "Fantasy calendars for TTRPGs",
|
title: "Fantasy calendars for TTRPGs",
|
||||||
description: "Tools for players and game masters to help them visualize fantasy worlds better.",
|
description: "Tools for players and game masters to help them visualize their own worlds.",
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
default: {
|
default: {
|
||||||
@@ -168,7 +168,7 @@ export default defineI18nConfig(() => ({
|
|||||||
isLoading: "Calendar is loading…",
|
isLoading: "Calendar is loading…",
|
||||||
hasXEvent: "{count} available event",
|
hasXEvent: "{count} available event",
|
||||||
hasXEvents: "{count} available events",
|
hasXEvents: "{count} available events",
|
||||||
seeCategories: "Modify categories",
|
seeCategories: "Categories",
|
||||||
seeOptions: "Calendar options",
|
seeOptions: "Calendar options",
|
||||||
date: {
|
date: {
|
||||||
start: "Start date",
|
start: "Start date",
|
||||||
@@ -201,6 +201,7 @@ export default defineI18nConfig(() => ({
|
|||||||
nameSingular: "Event",
|
nameSingular: "Event",
|
||||||
namePlural: "Events",
|
namePlural: "Events",
|
||||||
addSingle: "Add an event",
|
addSingle: "Add an event",
|
||||||
|
addSingleDescription: "This will add the event to the calendar",
|
||||||
newEvent: "New event",
|
newEvent: "New event",
|
||||||
title: "Event title",
|
title: "Event title",
|
||||||
isStart: "Start",
|
isStart: "Start",
|
||||||
@@ -491,7 +492,7 @@ export default defineI18nConfig(() => ({
|
|||||||
isLoading: "Chargement du calendrier…",
|
isLoading: "Chargement du calendrier…",
|
||||||
hasXEvent: "{count} évènement disponible",
|
hasXEvent: "{count} évènement disponible",
|
||||||
hasXEvents: "{count} évènements disponibles",
|
hasXEvents: "{count} évènements disponibles",
|
||||||
seeCategories: "Gestion des catégories",
|
seeCategories: "Catégories",
|
||||||
seeOptions: "Options du calendrier",
|
seeOptions: "Options du calendrier",
|
||||||
date: {
|
date: {
|
||||||
start: "Date de début",
|
start: "Date de début",
|
||||||
@@ -524,6 +525,7 @@ export default defineI18nConfig(() => ({
|
|||||||
nameSingular: "Évènement",
|
nameSingular: "Évènement",
|
||||||
namePlural: "Évènements",
|
namePlural: "Évènements",
|
||||||
addSingle: "Ajouter un évènement",
|
addSingle: "Ajouter un évènement",
|
||||||
|
addSingleDescription: "L'évènement sera ajouté au calendrier en cours",
|
||||||
newEvent: "Nouvel évènement",
|
newEvent: "Nouvel évènement",
|
||||||
title: "Titre de l'évènement",
|
title: "Titre de l'évènement",
|
||||||
isStart: "Début",
|
isStart: "Début",
|
||||||
|
|||||||
5
middleware/sidebar.global.ts
Normal file
5
middleware/sidebar.global.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default defineNuxtRouteMiddleware(() => {
|
||||||
|
const { closeSidebar } = useUiStore()
|
||||||
|
|
||||||
|
closeSidebar()
|
||||||
|
})
|
||||||
1
models/Popover.ts
Normal file
1
models/Popover.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type CollisionPadding = Partial<Record<"top" | "right" | "bottom" | "left", number>>
|
||||||
@@ -11,7 +11,7 @@ const isLoading = computed(() => calendarStatus.value === "pending")
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="p-8">
|
<main class="py-8 px-5 md:px-8">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>{{ $t("pages.explore.title") }}</Title>
|
<Title>{{ $t("pages.explore.title") }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
@@ -26,10 +26,10 @@ const isLoading = computed(() => calendarStatus.value === "pending")
|
|||||||
{{ $t("entity.calendar.namePublicPlural") }}
|
{{ $t("entity.calendar.namePublicPlural") }}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<div v-if="isLoading" class="grid md:grid-cols-3 gap-2">
|
<div v-if="isLoading" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
|
||||||
<LoadingCard />
|
<LoadingCard />
|
||||||
</div>
|
</div>
|
||||||
<ul v-else-if="availableCalendars?.data" class="grid md:grid-cols-3 gap-2">
|
<ul v-else-if="availableCalendars?.data" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
|
||||||
<li v-for="calendar in availableCalendars.data" :key="calendar.shortId">
|
<li v-for="calendar in availableCalendars.data" :key="calendar.shortId">
|
||||||
<CalendarPreviewCard :calendar="calendar" :gm-id="calendar.world?.gmId" />
|
<CalendarPreviewCard :calendar="calendar" :gm-id="calendar.world?.gmId" />
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ definePageMeta({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="p-8">
|
<main class="py-8 px-5 md:px-8">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>
|
<Title>
|
||||||
TTTools — {{ $t('head.title') }}
|
TTTools — {{ $t('head.title') }}
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ watch([calendar], () => {
|
|||||||
<Title>{{ calendar.data.name }}</Title>
|
<Title>{{ calendar.data.name }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<div class="h-full grid grid-rows-[auto_1fr] pt-8 gap-y-2">
|
<div class="h-full grid grid-rows-[auto_1fr] pt-8 md:gap-y-2">
|
||||||
<div class="px-8">
|
<div class="px-5 md:px-8 max-md:mb-2">
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
v-if="calendar.data.world"
|
v-if="calendar.data.world"
|
||||||
:items="[
|
:items="[
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ definePageMeta({
|
|||||||
middleware: ["auth-guard", "reset-menu"]
|
middleware: ["auth-guard", "reset-menu"]
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: worlds } = await useLazyFetch<{ data: World[] }>("/api/worlds/query", { query: { gmId: user?.value!.id } })
|
const { data: worlds, status: worldStatus } = await useLazyFetch<{ data: World[] }>("/api/worlds/query", { query: { gmId: user?.value!.id }, key: "user-worlds", keepalive: true })
|
||||||
const sortedWorlds = computed(() => worlds.value?.data ? [...worlds.value.data].sort((a, b) => (a.id ?? 0) - (b.id ?? 0)) : [])
|
const sortedWorlds = computed(() => worlds.value?.data ? [...worlds.value.data].sort((a, b) => (a.id ?? 0) - (b.id ?? 0)) : [])
|
||||||
|
|
||||||
|
const isLoading = computed(() => worldStatus.value === "idle")
|
||||||
|
|
||||||
// Redirect user back home when they log out on the page
|
// Redirect user back home when they log out on the page
|
||||||
watch(user, (n) => {
|
watch(user, (n) => {
|
||||||
if (!n) {
|
if (!n) {
|
||||||
@@ -110,14 +112,20 @@ function hideEditModal() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="p-8">
|
<main class="py-8 px-5 md:px-8">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>{{ $t("pages.profile.metaTitle") }}</Title>
|
<Title>{{ $t("pages.profile.metaTitle") }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Heading level="h1">
|
<div class="flex items-center gap-3">
|
||||||
{{ $t("pages.profile.title", { user: user?.user_metadata.full_name }) }}
|
<div class="md:hidden">
|
||||||
</Heading>
|
<SidebarToggle />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Heading level="h1">
|
||||||
|
{{ $t("pages.profile.title", { user: user?.user_metadata.full_name }) }}
|
||||||
|
</Heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="mt-4">
|
<section class="mt-4">
|
||||||
<Spacing size="lg">
|
<Spacing size="lg">
|
||||||
@@ -127,7 +135,10 @@ function hideEditModal() {
|
|||||||
</Heading>
|
</Heading>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-if="worlds?.data" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
|
<div v-if="isLoading" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
|
||||||
|
<LoadingCard />
|
||||||
|
</div>
|
||||||
|
<ul v-else-if="worlds?.data" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
|
||||||
<li v-for="world in sortedWorlds" :key="world.id">
|
<li v-for="world in sortedWorlds" :key="world.id">
|
||||||
<WorldPreviewCard :world="world" @on-edit="() => deployEditModal(world)" @on-delete="() => deployDeleteModal(world)" />
|
<WorldPreviewCard :world="world" @on-edit="() => deployEditModal(world)" @on-delete="() => deployDeleteModal(world)" />
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ function hideEditModal() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="p-8">
|
<main class="py-8 px-5 md:px-8">
|
||||||
<template v-if="status === 'pending'">
|
<template v-if="status === 'pending'">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>{{ $t("entity.world.namePlural") }}</Title>
|
<Title>{{ $t("entity.world.namePlural") }}</Title>
|
||||||
@@ -188,7 +188,9 @@ function hideEditModal() {
|
|||||||
|
|
||||||
<div class="lg:w-1/2">
|
<div class="lg:w-1/2">
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<Heading level="h1">{{ world.data.name }}</Heading>
|
<div class="flex items-center gap-3">
|
||||||
|
<Heading level="h1">{{ world.data.name }}</Heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
<UiTooltipProvider :delay-duration="250">
|
<UiTooltipProvider :delay-duration="250">
|
||||||
<UiTooltip>
|
<UiTooltip>
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
|
import { breakpointsTailwind } from "@vueuse/core"
|
||||||
import type { SidebarMenuItem } from "~/components/global/sidebar/SidebarProps"
|
import type { SidebarMenuItem } from "~/components/global/sidebar/SidebarProps"
|
||||||
|
|
||||||
|
const breakpoints = useBreakpoints(
|
||||||
|
breakpointsTailwind
|
||||||
|
)
|
||||||
|
|
||||||
export const useUiStore = defineStore("ui", () => {
|
export const useUiStore = defineStore("ui", () => {
|
||||||
const currentMenu: Ref<SidebarMenuItem[]> = ref<SidebarMenuItem[]>([])
|
const currentMenu = ref<SidebarMenuItem[]>([])
|
||||||
|
const isSidebarOpened = ref<boolean>(false)
|
||||||
|
|
||||||
function setCurrentMenu(items: SidebarMenuItem[]) {
|
function setCurrentMenu(items: SidebarMenuItem[]) {
|
||||||
currentMenu.value = items
|
currentMenu.value = items
|
||||||
@@ -11,9 +17,35 @@ export const useUiStore = defineStore("ui", () => {
|
|||||||
currentMenu.value = []
|
currentMenu.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openSidebar() {
|
||||||
|
if (breakpoints.isGreater("md")) return
|
||||||
|
|
||||||
|
isSidebarOpened.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSidebar() {
|
||||||
|
if (breakpoints.isGreater("md")) return
|
||||||
|
|
||||||
|
isSidebarOpened.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSidebar() {
|
||||||
|
if (breakpoints.isGreater("md")) return
|
||||||
|
|
||||||
|
isSidebarOpened.value = !isSidebarOpened.value
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(breakpoints.md, () => {
|
||||||
|
isSidebarOpened.value = false
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentMenu,
|
currentMenu,
|
||||||
setCurrentMenu,
|
setCurrentMenu,
|
||||||
resetMenu
|
resetMenu,
|
||||||
|
isSidebarOpened,
|
||||||
|
openSidebar,
|
||||||
|
closeSidebar,
|
||||||
|
toggleSidebar
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ module.exports = {
|
|||||||
boxShadow: {
|
boxShadow: {
|
||||||
"body-dark": "hsla(250, 50%, 10%, 0.75) 0 0 32px 2px inset",
|
"body-dark": "hsla(250, 50%, 10%, 0.75) 0 0 32px 2px inset",
|
||||||
"body-light": "hsl(250, 60%, 85%) 0 0 18px 3px inset",
|
"body-light": "hsl(250, 60%, 85%) 0 0 18px 3px inset",
|
||||||
|
"navbar-dark": "hsla(250, 50%, 10%, 0.75) 0 0 32px 2px",
|
||||||
|
"navbar-light": "hsl(250, 60%, 85%) 0 0 18px 3px",
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
xl: "calc(var(--radius) + 4px)",
|
xl: "calc(var(--radius) + 4px)",
|
||||||
|
|||||||
Reference in New Issue
Block a user