Base nuxt move after refactoring until no warnings
There still exists an issue with one Adobe led library ; @international-dates don't seem to compile their sourcemaps correctly. I don't think this is something I can fix however, and it may require hacks and workarounds to solve from what I've gathered
This commit is contained in:
39
components/calendar/Calendar.vue
Normal file
39
components/calendar/Calendar.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { computed, type Component, type ComputedRef } from 'vue'
|
||||
|
||||
import CalendarMenu from './CalendarMenu.vue'
|
||||
|
||||
import MonthlyLayout from './state/monthly/Layout.vue'
|
||||
import CenturyLayout from './state/centennially/Layout.vue'
|
||||
import DecadeLayout from './state/decennially/Layout.vue'
|
||||
import YearLayout from './state/yearly/Layout.vue'
|
||||
|
||||
const { currentConfig } = useCalendar()
|
||||
|
||||
const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
return MonthlyLayout
|
||||
|
||||
case 'year':
|
||||
return YearLayout
|
||||
|
||||
case 'decade':
|
||||
return DecadeLayout
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
return CenturyLayout
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full grid grid-rows-[auto,1fr]">
|
||||
<CalendarMenu />
|
||||
<KeepAlive>
|
||||
<component :is="currentViewComponent" />
|
||||
</KeepAlive>
|
||||
</div>
|
||||
</template>
|
||||
24
components/calendar/CalendarCurrentDate.vue
Normal file
24
components/calendar/CalendarCurrentDate.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import { getRelativeString } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { PhMapPin } from '@phosphor-icons/vue'
|
||||
|
||||
const { defaultDate, getFormattedDateTitle } = useCalendar()
|
||||
const { selectedDate } = storeToRefs(useCalendar())
|
||||
|
||||
const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true))
|
||||
|
||||
const dateDifference = computed(() => getRelativeString(defaultDate, selectedDate.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1 class="text-2xl font-bold flex items-center gap-1">
|
||||
<PhMapPin size="26" weight="bold" /> {{ mainDateTitle }}
|
||||
</h1>
|
||||
<h2 class="text-lg italic opacity-75">
|
||||
{{ dateDifference }}
|
||||
</h2>
|
||||
</template>
|
||||
71
components/calendar/CalendarEvent.vue
Normal file
71
components/calendar/CalendarEvent.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { areDatesIdentical, type LeimDate } from '@/models/Date'
|
||||
|
||||
import type { CalendarEvent } from '@/models/Events'
|
||||
import CalendarEventDetails from './CalendarEventDetails.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
event: CalendarEvent
|
||||
tileDate: LeimDate
|
||||
}>()
|
||||
|
||||
const spansMultipleDays = Boolean(props.event.startDate && props.event.endDate)
|
||||
|
||||
const isStartEvent = spansMultipleDays && areDatesIdentical(props.tileDate, props.event.startDate)
|
||||
const isEndEvent =
|
||||
spansMultipleDays && props.event.endDate && areDatesIdentical(props.tileDate, props.event.endDate)
|
||||
|
||||
const isPopoverOpen = ref<boolean>(false)
|
||||
|
||||
function handleClosePopover() {
|
||||
isPopoverOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopover v-model:open="isPopoverOpen">
|
||||
<UiPopoverTrigger as-child>
|
||||
<button
|
||||
class="text-xs px-2 py-1 block w-full text-left rounded-sm whitespace-nowrap overflow-hidden text-ellipsis"
|
||||
:class="
|
||||
cn({
|
||||
'text-white bg-slate-600 hover:bg-slate-700': !event.category,
|
||||
'text-white bg-lime-600 hover:bg-lime-700': event.category === 'naissance',
|
||||
'text-white bg-stone-500 hover:bg-stone-700': event.category === 'mort',
|
||||
'text-white bg-orange-600 hover:bg-orange-700': event.category === 'catastrophe',
|
||||
'text-white bg-pink-600 hover:bg-pink-700': event.category === 'catastrophe naturelle',
|
||||
'text-white bg-sky-600 hover:bg-sky-700': event.category === 'législation',
|
||||
'text-white bg-purple-600 hover:bg-purple-700': event.category === 'religion',
|
||||
'text-white bg-emerald-600 hover:bg-emerald-700': event.category === 'joueurs',
|
||||
'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category === 'inauguration',
|
||||
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category === 'invention',
|
||||
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': event.category === 'science',
|
||||
'text-slate-900 bg-white hover:bg-yellow-200': event.category === 'bénédiction',
|
||||
'text-slate-900 bg-purple-200 hover:bg-purple-300': event.category === 'découverte',
|
||||
'text-slate-900 bg-indigo-200 hover:bg-indigo-300': event.category === 'exploration',
|
||||
'text-white bg-amber-600 hover:bg-amber-700': event.category === 'construction',
|
||||
'text-slate-900 bg-violet-200 hover:bg-violet-300': event.category === 'arcanologie',
|
||||
'text-white bg-rose-600 hover:bg-rose-700': event.category === 'criminalité',
|
||||
'text-white bg-stone-600 hover:bg-stone-700': event.category === 'scandale',
|
||||
'text-slate-900 bg-yellow-500 hover:bg-yellow-600': event.category === 'commerce',
|
||||
'rounded-r-none': isStartEvent,
|
||||
'rounded-l-none': isEndEvent
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ event.title }}
|
||||
</button>
|
||||
</UiPopoverTrigger>
|
||||
|
||||
<CalendarEventDetails
|
||||
v-once
|
||||
:event
|
||||
:spans-multiple-days
|
||||
:is-start-event
|
||||
:is-end-event
|
||||
@query:close-popover="handleClosePopover"
|
||||
/>
|
||||
</UiPopover>
|
||||
</template>
|
||||
245
components/calendar/CalendarEventDetails.vue
Normal file
245
components/calendar/CalendarEventDetails.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<script lang="ts" setup>
|
||||
import { getRelativeString, type LeimDate } from '@/models/Date'
|
||||
import type { CalendarEvent } from '@/models/Events'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useCalendarEvents } from '@/stores/EventStore'
|
||||
|
||||
import {
|
||||
PhHourglassMedium,
|
||||
PhAlarm,
|
||||
PhHourglassHigh,
|
||||
PhHourglassLow,
|
||||
PhArrowBendDoubleUpLeft,
|
||||
PhArrowBendDoubleUpRight
|
||||
} from '@phosphor-icons/vue'
|
||||
|
||||
const { defaultDate, getFormattedDateTitle, jumpToDate } = useCalendar()
|
||||
const { getRelativeEventFromEvent } = useCalendarEvents()
|
||||
|
||||
const props = defineProps<{
|
||||
event: CalendarEvent
|
||||
spansMultipleDays: boolean
|
||||
isStartEvent?: boolean
|
||||
isEndEvent?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'query:close-popover'): void
|
||||
}>()
|
||||
|
||||
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate)
|
||||
const dateDuration: string | null = props.event.endDate
|
||||
? getRelativeString(props.event.startDate, props.event.endDate, 'compact')
|
||||
: null
|
||||
|
||||
function handleJumpToDate(date: LeimDate) {
|
||||
jumpToDate(date)
|
||||
emit('query:close-popover')
|
||||
}
|
||||
|
||||
function handleGotoRelativeEvent(position: 'next' | 'prev' = 'next') {
|
||||
try {
|
||||
const { targetDate } = getRelativeEventFromEvent(props.event, position, props.isEndEvent)
|
||||
|
||||
handleJumpToDate(targetDate)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopoverContent
|
||||
class="event-details w-96"
|
||||
:align="'center'"
|
||||
:align-offset="50"
|
||||
:side="'left'"
|
||||
:collision-padding="60"
|
||||
:hide-when-detached="true"
|
||||
:class="{
|
||||
'border-slate-800': !event.category,
|
||||
'border-lime-800': event.category === 'naissance',
|
||||
'border-stone-600': event.category === 'mort',
|
||||
'border-orange-800': event.category === 'catastrophe',
|
||||
'border-pink-800': event.category === 'catastrophe naturelle',
|
||||
'border-sky-800': event.category === 'législation',
|
||||
'border-purple-800': event.category === 'religion',
|
||||
'border-emerald-800': event.category === 'joueurs',
|
||||
'border-amber-800': event.category === 'inauguration',
|
||||
'border-green-800': event.category === 'invention',
|
||||
'border-cyan-800': event.category === 'science',
|
||||
'border-slate-600': event.category === 'bénédiction',
|
||||
'border-purple-700': event.category === 'découverte',
|
||||
'border-indigo-700': event.category === 'exploration',
|
||||
'border-amber-700': event.category === 'construction',
|
||||
'border-violet-700': event.category === 'arcanologie',
|
||||
'border-rose-800': event.category === 'criminalité',
|
||||
'border-stone-700': event.category === 'scandale',
|
||||
'border-yellow-600': event.category === 'commerce'
|
||||
}"
|
||||
>
|
||||
<div class="grid gap-1">
|
||||
<div class="text-lg font-semibold">
|
||||
{{ event.title }}
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<template v-if="!event.endDate">
|
||||
<p class="font-semibold">{{ getFormattedDateTitle(event.startDate, true) }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="font-semibold">
|
||||
Du {{ getFormattedDateTitle(event.startDate, true) }} au
|
||||
{{ getFormattedDateTitle(event.endDate, true) }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mb-1 space-y-1">
|
||||
<p class="text-sm italic opacity-75 flex items-center gap-1">
|
||||
<PhAlarm size="16" weight="fill" /> {{ dateDifference }}
|
||||
</p>
|
||||
<template v-if="dateDuration">
|
||||
<p class="text-sm italic opacity-75 flex items-center gap-1">
|
||||
<PhHourglassMedium size="16" weight="fill" /> Pendant {{ dateDuration }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="event.category || event.secondaryCategories">
|
||||
<ul class="flex gap-1">
|
||||
<li v-if="event.category">
|
||||
<UiBadge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
|
||||
{{ event.category }}
|
||||
</UiBadge>
|
||||
</li>
|
||||
|
||||
<li v-for="cat in event.secondaryCategories" :key="cat">
|
||||
<UiBadge class="mix-blend-luminosity bg-gray-600" variant="secondary">
|
||||
{{ cat }}
|
||||
</UiBadge>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<template v-if="event.description">
|
||||
<hr class="border-slate-500 mt-2" >
|
||||
|
||||
<div class="mt-2 text-sm text-slate-300">
|
||||
{{ event.description }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<nav v-if="event.startDate && event.endDate" class="absolute -top-2 right-2 flex gap-2">
|
||||
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child>
|
||||
<button
|
||||
class="flex gap-1"
|
||||
title="Naviguer au début"
|
||||
@click="handleJumpToDate(event.startDate!)"
|
||||
>
|
||||
<PhHourglassHigh size="16" weight="fill" /> Début
|
||||
</button>
|
||||
</UiBadge>
|
||||
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child title="Naviguer à la fin">
|
||||
<button
|
||||
class="flex gap-1"
|
||||
title="Naviguer à la fin"
|
||||
@click="handleJumpToDate(event.endDate!)"
|
||||
>
|
||||
<PhHourglassLow size="16" weight="fill" /> Fin
|
||||
</button>
|
||||
</UiBadge>
|
||||
</nav>
|
||||
|
||||
<nav class="mt-2 flex gap-2">
|
||||
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child>
|
||||
<button
|
||||
class="flex gap-1"
|
||||
title="Évènement précédent"
|
||||
@click="handleGotoRelativeEvent('prev')"
|
||||
>
|
||||
<PhArrowBendDoubleUpLeft size="16" weight="fill" /> Précédent
|
||||
</button>
|
||||
</UiBadge>
|
||||
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child title="Naviguer à la fin">
|
||||
<button
|
||||
class="flex gap-1"
|
||||
title="Évènement suivant"
|
||||
@click="handleGotoRelativeEvent('next')"
|
||||
>
|
||||
<PhArrowBendDoubleUpRight size="16" weight="fill" /> Suivant
|
||||
</button>
|
||||
</UiBadge>
|
||||
</nav>
|
||||
</UiPopoverContent>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.border-slate-800 {
|
||||
--base-color: var(--color-slate-800);
|
||||
}
|
||||
.border-lime-800 {
|
||||
--base-color: var(--color-lime-800);
|
||||
}
|
||||
.border-stone-600 {
|
||||
--base-color: var(--color-stone-600);
|
||||
}
|
||||
.border-orange-800 {
|
||||
--base-color: var(--color-orange-800);
|
||||
}
|
||||
.border-pink-800 {
|
||||
--base-color: var(--color-pink-800);
|
||||
}
|
||||
.border-sky-800 {
|
||||
--base-color: var(--color-sky-800);
|
||||
}
|
||||
.border-purple-800 {
|
||||
--base-color: var(--color-purple-800);
|
||||
}
|
||||
.border-emerald-800 {
|
||||
--base-color: var(--color-emerald-800);
|
||||
}
|
||||
.border-amber-800 {
|
||||
--base-color: var(--color-amber-800);
|
||||
}
|
||||
.border-green-800 {
|
||||
--base-color: var(--color-green-800);
|
||||
}
|
||||
.border-cyan-800 {
|
||||
--base-color: var(--color-cyan-800);
|
||||
}
|
||||
.border-slate-600 {
|
||||
--base-color: var(--color-slate-600);
|
||||
}
|
||||
.border-purple-700 {
|
||||
--base-color: var(--color-purple-700);
|
||||
}
|
||||
.border-indigo-700 {
|
||||
--base-color: var(--color-indigo-700);
|
||||
}
|
||||
.border-amber-700 {
|
||||
--base-color: var(--color-amber-700);
|
||||
}
|
||||
.border-violet-700 {
|
||||
--base-color: var(--color-violet-700);
|
||||
}
|
||||
.border-rose-800 {
|
||||
--base-color: var(--color-rose-800);
|
||||
}
|
||||
.border-stone-700 {
|
||||
--base-color: var(--color-stone-700);
|
||||
}
|
||||
.border-yellow-600 {
|
||||
--base-color: var(--color-yellow-600);
|
||||
}
|
||||
|
||||
.event-details {
|
||||
--bg-color: color-mix(in srgb, var(--base-color), var(--color-slate-950) 85%);
|
||||
background-color: var(--bg-color);
|
||||
|
||||
hr {
|
||||
border-color: var(--base-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
46
components/calendar/CalendarMenu.vue
Normal file
46
components/calendar/CalendarMenu.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
|
||||
import { PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||
import CalendarMenuNav from './CalendarMenuNav.vue'
|
||||
import CalendarMenuSubnav from './CalendarMenuSubnav.vue'
|
||||
import CalendarMenuToday from './CalendarMenuToday.vue'
|
||||
import CalendarSwitch from './CalendarSwitch.vue'
|
||||
import CalendarCurrentDate from './CalendarCurrentDate.vue'
|
||||
|
||||
const { revealAdvancedSearch } = useCalendar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="pt-4 border-slate-700 border-b-[1px]">
|
||||
<div class="px-6 flex justify-between">
|
||||
<menu class="flex items-center gap-2">
|
||||
<li class="flex items-center">
|
||||
<CalendarMenuToday />
|
||||
</li>
|
||||
<li>
|
||||
<CalendarMenuNav />
|
||||
</li>
|
||||
<li class="ml-6">
|
||||
<CalendarCurrentDate />
|
||||
</li>
|
||||
</menu>
|
||||
|
||||
<menu class="flex items-center gap-2">
|
||||
<li>
|
||||
<UiButton search-slash @click="revealAdvancedSearch()">
|
||||
<PhMagnifyingGlass size="20" weight="light" />
|
||||
Recherche avancée
|
||||
</UiButton>
|
||||
</li>
|
||||
<li>
|
||||
<CalendarSwitch />
|
||||
</li>
|
||||
</menu>
|
||||
</div>
|
||||
|
||||
<div class="ml-6">
|
||||
<CalendarMenuSubnav />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
198
components/calendar/CalendarMenuNav.vue
Normal file
198
components/calendar/CalendarMenuNav.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, type ComputedRef } from 'vue'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
|
||||
import {
|
||||
PhCaretDoubleLeft,
|
||||
PhCaretDoubleRight,
|
||||
PhCaretLeft,
|
||||
PhCaretRight
|
||||
} from '@phosphor-icons/vue'
|
||||
|
||||
interface DirectionLabels {
|
||||
pastFar: string
|
||||
pastNear: string
|
||||
futureNear: string
|
||||
futureFar: string
|
||||
}
|
||||
|
||||
const { currentConfig, decrementMonth, incrementMonth, decrementYear, incrementYear } =
|
||||
useCalendar()
|
||||
|
||||
const activeDirectionLabels: ComputedRef<DirectionLabels> = computed(() => {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
return {
|
||||
pastFar: 'Année précédente',
|
||||
pastNear: 'Mois précédent',
|
||||
futureNear: 'Mois suivant',
|
||||
futureFar: 'Année suivante'
|
||||
}
|
||||
|
||||
case 'year':
|
||||
return {
|
||||
pastFar: 'Décennie précédente',
|
||||
pastNear: 'Année précédente',
|
||||
futureNear: 'Année suivante',
|
||||
futureFar: 'Décennie suivante'
|
||||
}
|
||||
|
||||
case 'decade':
|
||||
return {
|
||||
pastFar: 'Siècle précédent',
|
||||
pastNear: 'Décennie précédente',
|
||||
futureNear: 'Décennie suivante',
|
||||
futureFar: 'Siècle suivant'
|
||||
}
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
return {
|
||||
pastFar: 'Millénaire précédent',
|
||||
pastNear: 'Siècle précédent',
|
||||
futureNear: 'Siècle suivant',
|
||||
futureFar: 'Millénaire suivant'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function toPastFar(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
decrementYear()
|
||||
break
|
||||
|
||||
case 'year':
|
||||
decrementYear(10)
|
||||
break
|
||||
|
||||
case 'decade':
|
||||
decrementYear(100)
|
||||
break
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
decrementYear(1000)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function toPastNear(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
decrementMonth()
|
||||
break
|
||||
|
||||
case 'year':
|
||||
decrementYear()
|
||||
break
|
||||
|
||||
case 'decade':
|
||||
decrementYear(10)
|
||||
break
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
decrementYear(100)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function toFutureNear(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
incrementMonth()
|
||||
break
|
||||
|
||||
case 'year':
|
||||
incrementYear()
|
||||
break
|
||||
|
||||
case 'decade':
|
||||
incrementYear(10)
|
||||
break
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
incrementYear(100)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function toFutureFar(): void {
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
incrementYear()
|
||||
break
|
||||
|
||||
case 'year':
|
||||
incrementYear(10)
|
||||
break
|
||||
|
||||
case 'decade':
|
||||
incrementYear(100)
|
||||
break
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
incrementYear(1000)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton variant="outline" size="icon" @click="toPastFar()">
|
||||
<PhCaretDoubleLeft size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.pastFar }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton variant="outline" size="icon" @click="toPastNear()">
|
||||
<PhCaretLeft size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.pastNear }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton variant="outline" size="icon" @click="toFutureNear()">
|
||||
<PhCaretRight size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.futureNear }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton variant="outline" size="icon" @click="toFutureFar()">
|
||||
<PhCaretDoubleRight size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ activeDirectionLabels.futureFar }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
</template>
|
||||
105
components/calendar/CalendarMenuSubnav.vue
Normal file
105
components/calendar/CalendarMenuSubnav.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script lang="ts" setup>
|
||||
import { daysPerMonth, monthsPerYear, type LeimDate } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useCalendarEvents } from '@/stores/EventStore'
|
||||
|
||||
import { PhArrowLineLeft, PhArrowLineRight } from '@phosphor-icons/vue'
|
||||
|
||||
const { currentDate, currentConfig, jumpToDate } = useCalendar()
|
||||
const { getRelativeEventFromDate } = useCalendarEvents()
|
||||
|
||||
function handleGotoPreviousEventPage(position: 'next' | 'prev' = 'next') {
|
||||
let fromDate: LeimDate
|
||||
const toDay = position === 'next' ? daysPerMonth : 1
|
||||
const toMonth = position === 'next' ? monthsPerYear : 0
|
||||
|
||||
switch (currentConfig.viewType) {
|
||||
case 'month':
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
|
||||
case 'year':
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: toMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
|
||||
case 'decade':
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
|
||||
case 'century':
|
||||
default:
|
||||
fromDate = {
|
||||
day: toDay,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
try {
|
||||
const { targetDate } = getRelativeEventFromDate(fromDate, position)
|
||||
|
||||
jumpToDate(targetDate)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-2">
|
||||
<div class="w-40 px-4 py-2 border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
|
||||
<span class="text-sm">{{ currentDate.currentDateTitle }}</span>
|
||||
</div>
|
||||
<div class="border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="rounded-t-sm rounded-b-none"
|
||||
@click="handleGotoPreviousEventPage('prev')"
|
||||
>
|
||||
<PhArrowLineLeft size="22" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>Précédente page à évènements</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
<div class="border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="rounded-t-sm rounded-b-none"
|
||||
@click="handleGotoPreviousEventPage('next')"
|
||||
>
|
||||
<PhArrowLineRight size="22" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>Prochaine page à évènements</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
31
components/calendar/CalendarMenuToday.vue
Normal file
31
components/calendar/CalendarMenuToday.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { areDatesIdentical } from '@/models/Date'
|
||||
|
||||
const { defaultDate, selectedDate } = storeToRefs(useCalendar())
|
||||
const { jumpToDefaultDate, getFormattedDateTitle } = useCalendar()
|
||||
|
||||
const defaultDateFormatted = getFormattedDateTitle(defaultDate.value, true)
|
||||
|
||||
const isDefaultDate = computed(() => {
|
||||
return areDatesIdentical(selectedDate.value, defaultDate.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton size="sm" :disabled="isDefaultDate" @click="jumpToDefaultDate">
|
||||
Aujourd'hui
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>{{ defaultDateFormatted }}</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</template>
|
||||
32
components/calendar/CalendarSwitch.vue
Normal file
32
components/calendar/CalendarSwitch.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { PhCalendarBlank } from '@phosphor-icons/vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar()
|
||||
|
||||
const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiButton size="sm" variant="secondary">
|
||||
<PhCalendarBlank size="18" weight="fill" />
|
||||
|
||||
{{ viewTypeTitle }}
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent :side="'bottom'" :collision-padding="30">
|
||||
<UiDropdownMenuLabel>Mode d'affichage</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem
|
||||
v-for="option in viewTypeOptions"
|
||||
:key="option"
|
||||
@click="setViewType(option)"
|
||||
>
|
||||
{{ getViewTypeTitle(option) }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</template>
|
||||
3
components/calendar/SearchMode.ts
Normal file
3
components/calendar/SearchMode.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const searchUnifier = new RegExp(/[^a-zA-Z0-9\-'']/g)
|
||||
|
||||
export type SearchMode = 'characters' | 'events' | undefined
|
||||
56
components/calendar/Sidebar.vue
Normal file
56
components/calendar/Sidebar.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
|
||||
import { PhList, PhHouse, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import Button from '../ui/button/Button.vue'
|
||||
|
||||
const { revealAdvancedSearch } = useCalendar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="w-16 py-6 border-r-[1px] border-l-slate-500 grid justify-center">
|
||||
<menu class="flex flex-col gap-4">
|
||||
<li class="mb-12">
|
||||
<Button variant="ghost" size="icon" class="rounded-full" @click="console.log">
|
||||
<PhList size="27" />
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<TooltipProvider :delay-duration="100">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="rounded-full" as-child>
|
||||
<RouterLink to="/">
|
||||
<PhHouse size="24" weight="fill" />
|
||||
</RouterLink>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent :side="'right'">
|
||||
<p>Retourner aux outils</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</li>
|
||||
<li>
|
||||
<TooltipProvider :delay-duration="100">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="rounded-full"
|
||||
@click="revealAdvancedSearch()"
|
||||
>
|
||||
<PhMagnifyingGlass size="24" weight="fill" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent :side="'right'">
|
||||
<p>Recherche avancée</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</li>
|
||||
</menu>
|
||||
</nav>
|
||||
</template>
|
||||
426
components/calendar/search/CalendarSearch.vue
Normal file
426
components/calendar/search/CalendarSearch.vue
Normal file
@@ -0,0 +1,426 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
characterCategories,
|
||||
isCharacter,
|
||||
type Character,
|
||||
type CharacterCategory
|
||||
} from '@/models/Characters'
|
||||
import type { LeimDateOrder } from '@/models/Date'
|
||||
import {
|
||||
calendarEventCategories,
|
||||
isCalendarEvent,
|
||||
type CalendarEvent,
|
||||
type CalendarEventCategory
|
||||
} from '@/models/Events'
|
||||
import { useCharacters } from '@/stores/CharacterStore'
|
||||
import { useCalendarEvents } from '@/stores/EventStore'
|
||||
import { capitalize } from '@/utils/Strings'
|
||||
import { useMagicKeys, useScroll, useStorage, whenever } from '@vueuse/core'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { searchUnifier, type SearchMode } from '../SearchMode'
|
||||
|
||||
import { PhClockClockwise, PhClockCounterClockwise, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||
import {
|
||||
ComboboxAnchor,
|
||||
ComboboxInput,
|
||||
ComboboxPortal,
|
||||
ComboboxRoot,
|
||||
VisuallyHidden
|
||||
} from 'radix-vue'
|
||||
|
||||
import SearchList from './lists/SearchList.vue'
|
||||
|
||||
const { characters } = useCharacters()
|
||||
const { allEvents } = useCalendarEvents()
|
||||
|
||||
const modalOpen = defineModel({ default: false })
|
||||
|
||||
const searchQuery = ref<string>('')
|
||||
// const searchEnough = computed<boolean>(() => searchQuery.value.length >= 2)
|
||||
|
||||
const selectedEntity = useStorage('se', 'events' as SearchMode)
|
||||
|
||||
// Order
|
||||
const selectedOrder = ref<LeimDateOrder>('asc')
|
||||
function setOrderAsc() {
|
||||
selectedOrder.value = 'asc'
|
||||
resetPage()
|
||||
}
|
||||
|
||||
function setOrderDesc() {
|
||||
selectedOrder.value = 'desc'
|
||||
resetPage()
|
||||
}
|
||||
|
||||
// Limit
|
||||
const currentPage = ref<number>(1)
|
||||
const itemsPerPage: number = 20
|
||||
const startOfList = computed<number>(() => (currentPage.value - 1) * itemsPerPage)
|
||||
const endOfList = computed<number>(() => startOfList.value + itemsPerPage)
|
||||
|
||||
/**
|
||||
* Resets the pagination
|
||||
*/
|
||||
function resetPage() {
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const searchResults = computed<(Character | CalendarEvent)[]>(() => {
|
||||
let results: (Character | CalendarEvent)[] = []
|
||||
let dataToFilter: Character[] | CalendarEvent[] | (Character | CalendarEvent)[]
|
||||
const shouldFilterCategories = selectedCategories.value.length > 0
|
||||
|
||||
// Assign data to loop over and filter
|
||||
// They are assigned this way for readability
|
||||
if (selectedEntity.value === 'events') {
|
||||
dataToFilter = allEvents
|
||||
} else if (selectedEntity.value === 'characters') {
|
||||
dataToFilter = characters
|
||||
} else {
|
||||
dataToFilter = [...allEvents, ...characters]
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Refactor the categories logic, basically extract the return out of the ifs, like above
|
||||
*/
|
||||
results = dataToFilter.filter((item) => {
|
||||
// Filter calendar events
|
||||
if (isCalendarEvent(item)) {
|
||||
const queryString = new String(searchQuery.value)
|
||||
.replace(searchUnifier, '')
|
||||
.toLocaleLowerCase()
|
||||
|
||||
const hitTitle = item.title
|
||||
.replace(searchUnifier, '')
|
||||
.toLocaleLowerCase()
|
||||
.includes(queryString)
|
||||
const hitDesc = item.description
|
||||
?.replace(searchUnifier, '')
|
||||
.toLocaleLowerCase()
|
||||
.includes(queryString)
|
||||
|
||||
if (!shouldFilterCategories) {
|
||||
return hitTitle || hitDesc
|
||||
}
|
||||
|
||||
// Handle categories logic
|
||||
let hitCategories: boolean = false
|
||||
const allCategories: CalendarEventCategory[] = []
|
||||
|
||||
if (item.category) {
|
||||
allCategories.push(item.category)
|
||||
}
|
||||
|
||||
if (item.secondaryCategories && item.secondaryCategories?.length > 0) {
|
||||
allCategories.push(...item.secondaryCategories)
|
||||
}
|
||||
|
||||
hitCategories = selectedCategories.value.every((selectedCat) => {
|
||||
return allCategories.includes(selectedCat as CalendarEventCategory)
|
||||
})
|
||||
|
||||
return (hitTitle || hitDesc) && hitCategories
|
||||
}
|
||||
|
||||
// Filter characters
|
||||
if (isCharacter(item)) {
|
||||
const queryString = new String(searchQuery.value)
|
||||
.replace(searchUnifier, '')
|
||||
.toLocaleLowerCase()
|
||||
|
||||
const hitTitle = item.name
|
||||
.replace(searchUnifier, '')
|
||||
.toLocaleLowerCase()
|
||||
.includes(queryString)
|
||||
|
||||
if (!shouldFilterCategories) {
|
||||
return hitTitle
|
||||
}
|
||||
|
||||
// Handle categories logic
|
||||
let hitCategories: boolean = false
|
||||
const allCategories: CharacterCategory[] = []
|
||||
|
||||
if (item.category) {
|
||||
allCategories.push(item.category)
|
||||
}
|
||||
|
||||
if (item.secondaryCategories && item.secondaryCategories?.length > 0) {
|
||||
allCategories.push(...item.secondaryCategories)
|
||||
}
|
||||
|
||||
hitCategories = selectedCategories.value.every((selectedCat) => {
|
||||
return allCategories.includes(selectedCat as CharacterCategory)
|
||||
})
|
||||
|
||||
return hitTitle && hitCategories
|
||||
}
|
||||
})
|
||||
|
||||
return results
|
||||
})
|
||||
|
||||
/**
|
||||
* Removes the search query, resets the pagination and removes all selected categories
|
||||
*/
|
||||
function resetSearch() {
|
||||
searchQuery.value = ''
|
||||
resetPage()
|
||||
selectedCategories.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the search Uidialog
|
||||
*/
|
||||
function openUiDialog() {
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the search Uidialog
|
||||
*/
|
||||
function closeUiDialog() {
|
||||
modalOpen.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches the selectedEntity
|
||||
*/
|
||||
function handleEntitySwitch() {
|
||||
resetPage()
|
||||
selectedCategories.value = []
|
||||
}
|
||||
|
||||
// Key combos to deploy modal
|
||||
const keys = useMagicKeys()
|
||||
|
||||
whenever(keys.control_period, () => {
|
||||
openUiDialog()
|
||||
})
|
||||
|
||||
const searchResultsRef = ref<HTMLElement | null>(null)
|
||||
const { y: searchResultsY } = useScroll(searchResultsRef)
|
||||
|
||||
watch(currentPage, () => {
|
||||
searchResultsY.value = 0
|
||||
})
|
||||
|
||||
// Compute categories based on current selectedEntity
|
||||
const currentCategories = computed(() => {
|
||||
if (selectedEntity.value === 'characters') {
|
||||
return [...characterCategories]
|
||||
} else {
|
||||
return [...calendarEventCategories]
|
||||
}
|
||||
})
|
||||
|
||||
const selectedCategories = ref<(CharacterCategory | CalendarEventCategory)[]>([])
|
||||
const categoryFilterOpened = ref<boolean>(false)
|
||||
const searchCategory = ref<string>('')
|
||||
|
||||
const filteredCategories = computed(() =>
|
||||
currentCategories.value.filter((i) => !selectedCategories.value.includes(i))
|
||||
)
|
||||
|
||||
/**
|
||||
* Handles the category selections from the TagInput component
|
||||
*
|
||||
* @param e Radix Change Event
|
||||
*/
|
||||
function handleCategorySelect(e: (CharacterCategory | CalendarEventCategory)) {
|
||||
if (typeof e === 'string') {
|
||||
searchCategory.value = ''
|
||||
selectedCategories.value.push(e)
|
||||
}
|
||||
|
||||
if (filteredCategories.value.length === 0) {
|
||||
categoryFilterOpened.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDialog v-model:open="modalOpen" @update:open="resetSearch()">
|
||||
<UiDialogContent
|
||||
class="flex flex-col flex-nowrap top-16 -translate-y-0 data-[state=closed]:slide-out-to-top-[5%] data-[state=open]:slide-in-from-top-[5%]"
|
||||
:class="{
|
||||
'bottom-16': searchResults.length > 0
|
||||
}"
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<UiDialogTitle> Recherche avancée </UiDialogTitle>
|
||||
</VisuallyHidden>
|
||||
<VisuallyHidden>
|
||||
<UiDialogDescription>
|
||||
Rechercher les données disponibles sur le calendrier
|
||||
</UiDialogDescription>
|
||||
</VisuallyHidden>
|
||||
|
||||
<!-- UiDialog header -->
|
||||
<div id="searchForm" class="grid gap-3">
|
||||
<div class="relative w-full h-fit">
|
||||
<UiInput
|
||||
id="search"
|
||||
v-model:model-value="searchQuery"
|
||||
type="text"
|
||||
placeholder="Rechercher le calendrier"
|
||||
class="pl-10 py-6 text-lg"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<span class="absolute start-1 inset-y-0 flex items-center justify-center px-2 opacity-50">
|
||||
<PhMagnifyingGlass size="20" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<UiToggleGroup
|
||||
v-model="selectedEntity"
|
||||
type="single"
|
||||
class="justify-start"
|
||||
@update:model-value="handleEntitySwitch()"
|
||||
>
|
||||
<UiToggleGroupItem value="events" aria-label="Uniquement les évènements">
|
||||
Évènements
|
||||
</UiToggleGroupItem>
|
||||
<UiToggleGroupItem value="characters" aria-label="Uniquement les personnages">
|
||||
Personnages
|
||||
</UiToggleGroupItem>
|
||||
</UiToggleGroup>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<UiTagsInput class="px-0 gap-0 w-72" :model-value="selectedCategories">
|
||||
<div class="flex gap-2 flex-wrap items-center px-3">
|
||||
<UiTagsInputItem v-for="item in selectedCategories" :key="item" :value="item">
|
||||
<UiTagsInputItemText class="capitalize" />
|
||||
<UiTagsInputItemDelete />
|
||||
</UiTagsInputItem>
|
||||
</div>
|
||||
|
||||
<ComboboxRoot
|
||||
v-model="selectedCategories"
|
||||
v-model:open="categoryFilterOpened"
|
||||
v-model:searchTerm="searchCategory"
|
||||
class="w-full"
|
||||
>
|
||||
<ComboboxAnchor as-child>
|
||||
<ComboboxInput placeholder="Catégories" as-child>
|
||||
<UiTagsInputInput
|
||||
class="w-full px-3"
|
||||
:class="selectedCategories.length > 0 ? 'mt-2' : ''"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</ComboboxInput>
|
||||
</ComboboxAnchor>
|
||||
|
||||
<ComboboxPortal :to="'#searchForm'">
|
||||
<UiCommandList
|
||||
position="popper"
|
||||
class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50"
|
||||
:dismissable="true"
|
||||
>
|
||||
<UiCommandEmpty />
|
||||
<UiCommandGroup>
|
||||
<UiCommandItem
|
||||
v-for="cat in filteredCategories"
|
||||
:key="cat"
|
||||
:value="cat"
|
||||
@select.prevent="handleCategorySelect(cat)"
|
||||
>
|
||||
{{ capitalize(cat) }}
|
||||
</UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
</UiCommandList>
|
||||
</ComboboxPortal>
|
||||
</ComboboxRoot>
|
||||
</UiTagsInput>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
:variant="selectedOrder === 'desc' ? 'secondary' : 'outline'"
|
||||
size="icon"
|
||||
@click="setOrderDesc()"
|
||||
>
|
||||
<PhClockCounterClockwise size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>Plus ancien</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
:variant="selectedOrder === 'asc' ? 'secondary' : 'outline'"
|
||||
size="icon"
|
||||
@click="setOrderAsc()"
|
||||
>
|
||||
<PhClockClockwise size="18" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>Plus récent</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr >
|
||||
|
||||
<div v-if="searchResults.length > 0" ref="searchResultsRef" class="grow overflow-y-auto">
|
||||
<SearchList
|
||||
:results="searchResults"
|
||||
:current-entity="selectedEntity"
|
||||
:order="selectedOrder"
|
||||
:start-at="startOfList"
|
||||
:end-at="endOfList"
|
||||
@jumped-to-date="closeUiDialog()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<UiPagination
|
||||
v-model:page="currentPage"
|
||||
:total="searchResults.length"
|
||||
:items-per-page="itemsPerPage"
|
||||
:sibling-count="2"
|
||||
show-edges
|
||||
:default-page="1"
|
||||
>
|
||||
<UiPaginationList v-slot="{ items }" class="flex items-center gap-1">
|
||||
<UiPaginationFirst />
|
||||
<UiPaginationPrev />
|
||||
|
||||
<template v-for="(item, index) in items">
|
||||
<UiPaginationListItem
|
||||
v-if="item.type === 'page'"
|
||||
:key="index"
|
||||
:value="item.value"
|
||||
as-child
|
||||
>
|
||||
<UiButton
|
||||
class="w-10 h-10 p-0"
|
||||
:variant="item.value === currentPage ? 'default' : 'outline'"
|
||||
>
|
||||
{{ item.value }}
|
||||
</UiButton>
|
||||
</UiPaginationListItem>
|
||||
<UiPaginationEllipsis v-else :key="item.type" :index="index" />
|
||||
</template>
|
||||
|
||||
<UiPaginationNext />
|
||||
<UiPaginationLast />
|
||||
</UiPaginationList>
|
||||
</UiPagination>
|
||||
</div>
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</template>
|
||||
89
components/calendar/search/lists/CharacterCallout.vue
Normal file
89
components/calendar/search/lists/CharacterCallout.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Character } from '@/models/Characters'
|
||||
import type { LeimDate } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { PhArrowSquareOut, PhPlant, PhSkull } from '@phosphor-icons/vue'
|
||||
|
||||
defineProps<{
|
||||
character: Character
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'query:date-jump', payload: LeimDate): void
|
||||
}>()
|
||||
|
||||
const { getFormattedDateTitle } = useCalendar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="block w-full text-left py-3 px-4 border-[1px] border-slate-700 rounded-sm">
|
||||
<div class="grid gap-2">
|
||||
<div class="flex gap-2">
|
||||
<h2 class="font-bold">
|
||||
{{ character.name }}
|
||||
</h2>
|
||||
<div v-if="character.wiki">
|
||||
<Button variant="link" size="xs" as-child>
|
||||
<a :href="character.wiki" target="_blank">
|
||||
Wiki
|
||||
<PhArrowSquareOut size="16" weight="fill" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<menu class="flex gap-2 border-[1px] border-slate-700 rounded-sm w-fit">
|
||||
<li v-if="character.birth">
|
||||
<TooltipProvider :delay-duration="100">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
@click="$emit('query:date-jump', character.birth!)"
|
||||
>
|
||||
<PhPlant size="16" weight="fill" />
|
||||
{{ getFormattedDateTitle(character.birth, true) }}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent :side="'bottom'" :align="'end'" :align-offset="20">
|
||||
<p>Date de naissance</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</li>
|
||||
<span v-if="character.birth && character.death">-</span>
|
||||
<li v-if="character.death">
|
||||
<TooltipProvider :delay-duration="100">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
@click="$emit('query:date-jump', character.death!)"
|
||||
>
|
||||
<PhSkull size="16" weight="fill" />
|
||||
{{ getFormattedDateTitle(character.death, true) }}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent :side="'bottom'" :align="'start'" :align-offset="20">
|
||||
<p>Date de décès</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</li>
|
||||
</menu>
|
||||
|
||||
<hr v-if="character.description" class="border-white opacity-25" >
|
||||
|
||||
<div v-if="character.description" class="text-sm">
|
||||
<span class="opacity-75">
|
||||
{{ character.description }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
114
components/calendar/search/lists/EventCallout.vue
Normal file
114
components/calendar/search/lists/EventCallout.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<script lang="ts" setup>
|
||||
import { getRelativeString, type LeimDate } from '@/models/Date'
|
||||
import type { CalendarEvent } from '@/models/Events'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { PhArrowSquareOut, PhHourglassMedium, PhAlarm } from '@phosphor-icons/vue'
|
||||
|
||||
const props = defineProps<{
|
||||
event: CalendarEvent
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'query:date-jump', payload: LeimDate): void
|
||||
}>()
|
||||
|
||||
const { defaultDate, getFormattedDateTitle } = useCalendar()
|
||||
|
||||
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate)
|
||||
const dateDuration: string | null = props.event.endDate
|
||||
? getRelativeString(props.event.startDate, props.event.endDate, 'compact')
|
||||
: null
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="relative block w-full text-left py-3 px-4 rounded-sm transition-colors"
|
||||
:class="{
|
||||
'text-white bg-slate-600 hover:bg-slate-700': !event.category,
|
||||
'text-white bg-lime-600 hover:bg-lime-700': event.category === 'naissance',
|
||||
'text-white bg-stone-500 hover:bg-stone-700': event.category === 'mort',
|
||||
'text-white bg-orange-600 hover:bg-orange-700': event.category === 'catastrophe',
|
||||
'text-white bg-pink-600 hover:bg-pink-700': event.category === 'catastrophe naturelle',
|
||||
'text-white bg-sky-600 hover:bg-sky-700': event.category === 'législation',
|
||||
'text-white bg-purple-600 hover:bg-purple-700': event.category === 'religion',
|
||||
'text-white bg-emerald-600 hover:bg-emerald-700': event.category === 'joueurs',
|
||||
'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category === 'inauguration',
|
||||
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category === 'invention',
|
||||
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': event.category === 'science',
|
||||
'text-slate-900 bg-white hover:bg-yellow-200': event.category === 'bénédiction',
|
||||
'text-slate-900 bg-purple-200 hover:bg-purple-300': event.category === 'découverte',
|
||||
'text-slate-900 bg-indigo-200 hover:bg-indigo-300': event.category === 'exploration',
|
||||
'text-white bg-amber-600 hover:bg-amber-700': event.category === 'construction',
|
||||
'text-slate-900 bg-violet-200 hover:bg-violet-300': event.category === 'arcanologie',
|
||||
'text-white bg-rose-600 hover:bg-rose-700': event.category === 'criminalité',
|
||||
'text-white bg-stone-600 hover:bg-stone-700': event.category === 'scandale',
|
||||
'text-slate-900 bg-yellow-500 hover:bg-yellow-600': event.category === 'commerce'
|
||||
}"
|
||||
@click="$emit('query:date-jump', event.startDate)"
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<h2 class="font-bold">
|
||||
{{ event.title }}
|
||||
</h2>
|
||||
<div v-if="event.wiki">
|
||||
<Button variant="link" size="xs" as-child class="text-inherit">
|
||||
<a :href="event.wiki" target="_blank">
|
||||
Wiki
|
||||
<PhArrowSquareOut size="16" weight="fill" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<template v-if="!event.endDate">
|
||||
<p class="col-span-2 font-semibold text-sm opacity-75">
|
||||
{{ getFormattedDateTitle(event.startDate, true) }}
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="col-span-2 font-semibold text-sm opacity-75">
|
||||
Du {{ getFormattedDateTitle(event.startDate, true) }} au
|
||||
{{ getFormattedDateTitle(event.endDate, true) }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mb-1 flex gap-x-2 items-center">
|
||||
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
|
||||
<PhAlarm size="16" weight="fill" /> {{ dateDifference }}
|
||||
</p>
|
||||
<template v-if="dateDuration">
|
||||
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
|
||||
<PhHourglassMedium size="16" weight="fill" /> Pendant {{ dateDuration }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="event.category || event.secondaryCategories" class="absolute top-3 right-4">
|
||||
<ul class="flex gap-1">
|
||||
<li v-if="event.category">
|
||||
<Badge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
|
||||
{{ event.category }}
|
||||
</Badge>
|
||||
</li>
|
||||
|
||||
<li v-for="cat in event.secondaryCategories" :key="cat">
|
||||
<Badge class="mix-blend-luminosity bg-gray-600" variant="secondary">
|
||||
{{ cat }}
|
||||
</Badge>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="event.description" class="text-sm">
|
||||
<hr class="my-2 border-white opacity-50" >
|
||||
<span class="opacity-75">
|
||||
{{ event.description }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
74
components/calendar/search/lists/SearchList.vue
Normal file
74
components/calendar/search/lists/SearchList.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script lang="ts" setup>
|
||||
import { isCharacter, type Character } from '@/models/Characters'
|
||||
import { compareDates, type LeimDate, type LeimDateOrder } from '@/models/Date'
|
||||
import { isCalendarEvent, type CalendarEvent } from '@/models/Events'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { computed } from 'vue'
|
||||
import type { SearchMode } from '../../SearchMode'
|
||||
|
||||
import CharacterCallout from './CharacterCallout.vue'
|
||||
import EventCallout from './EventCallout.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
results: (Character | CalendarEvent)[]
|
||||
currentEntity: SearchMode
|
||||
order: LeimDateOrder
|
||||
startAt: number
|
||||
endAt: number
|
||||
limit?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['jumpedToDate'])
|
||||
|
||||
const { jumpToDate } = useCalendar()
|
||||
|
||||
function handleJumpToDate(date?: LeimDate) {
|
||||
if (!date) return
|
||||
|
||||
jumpToDate(date)
|
||||
emit('jumpedToDate')
|
||||
}
|
||||
|
||||
// Initial sorting of the results
|
||||
const sortedResults = computed(() => {
|
||||
return [...props.results].sort((a, b) => {
|
||||
let firstDate: LeimDate
|
||||
let secondDate: LeimDate
|
||||
|
||||
if (isCalendarEvent(a)) {
|
||||
firstDate = a.startDate
|
||||
} else if (isCharacter(a) && a.birth) {
|
||||
firstDate = a.birth
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (isCalendarEvent(b)) {
|
||||
secondDate = b.startDate
|
||||
} else if (isCharacter(b) && b.birth) {
|
||||
secondDate = b.birth
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
|
||||
return compareDates(firstDate, secondDate, props.order)
|
||||
})
|
||||
})
|
||||
|
||||
// Page the sorted results
|
||||
const pagedResults = computed(() => sortedResults.value.slice(props.startAt, props.endAt))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="grid gap-4">
|
||||
<li v-for="r in pagedResults" :key="isCalendarEvent(r) ? r.title : r.name">
|
||||
<EventCallout v-if="isCalendarEvent(r)" :event="r" @query:date-jump="handleJumpToDate" />
|
||||
|
||||
<CharacterCallout
|
||||
v-else-if="isCharacter(r)"
|
||||
:character="r"
|
||||
@query:date-jump="handleJumpToDate"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
5
components/calendar/state/centennially/Layout.vue
Normal file
5
components/calendar/state/centennially/Layout.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<div>Century</div>
|
||||
</template>
|
||||
3
components/calendar/state/decennially/Layout.vue
Normal file
3
components/calendar/state/decennially/Layout.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>Decade</div>
|
||||
</template>
|
||||
141
components/calendar/state/monthly/DayTile.vue
Normal file
141
components/calendar/state/monthly/DayTile.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script lang="ts" setup>
|
||||
import { areDatesIdentical, type LeimDate } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useCalendarEvents } from '@/stores/EventStore'
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref, type ComputedRef } from 'vue'
|
||||
|
||||
import CalendarEventButton from '../../CalendarEvent.vue'
|
||||
import type { CalendarEvent } from '@/models/Events'
|
||||
|
||||
const props = defineProps<{
|
||||
date: LeimDate
|
||||
faded?: boolean
|
||||
}>()
|
||||
|
||||
const calendarTile = ref()
|
||||
const calendarEventsList = ref()
|
||||
|
||||
const { defaultDate, selectDate } = useCalendar()
|
||||
const { selectedDate } = storeToRefs(useCalendar())
|
||||
const { currentEvents } = storeToRefs(useCalendarEvents())
|
||||
|
||||
const eventsForTheDay = computed(() => {
|
||||
return currentEvents.value.filter((currentEvent) => {
|
||||
return (
|
||||
areDatesIdentical(currentEvent.startDate, props.date) ||
|
||||
areDatesIdentical(currentEvent.endDate!, props.date)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const isDefaultDate = computed(() => {
|
||||
return areDatesIdentical(props.date, defaultDate)
|
||||
})
|
||||
|
||||
const isSelectedDate = computed(() => {
|
||||
return areDatesIdentical(props.date, selectedDate.value)
|
||||
})
|
||||
|
||||
// Get bounding elements for both tile and events list
|
||||
const { height: tileHeight, top: tileTop } = useElementBounding(calendarTile)
|
||||
const { top: tileListTop } = useElementBounding(calendarEventsList)
|
||||
|
||||
// Compute the available number of events that can be displayed from refs heights
|
||||
const numberOfEventsToFit: ComputedRef<number> = computed(() => {
|
||||
if (!eventsForTheDay.value.length) return 0
|
||||
|
||||
return Math.trunc((tileHeight.value - (tileListTop.value - tileTop.value)) / 40)
|
||||
})
|
||||
|
||||
const eventsToDisplay: ComputedRef<CalendarEvent[]> = computed(() => {
|
||||
return [...eventsForTheDay.value].splice(0, numberOfEventsToFit.value)
|
||||
})
|
||||
const eventsNotDisplayed = computed(
|
||||
() => eventsForTheDay.value.length - eventsToDisplay.value.length
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="calendarTile"
|
||||
class="tile relative text-xs p-2 border-slate-700"
|
||||
:class="{
|
||||
'text-slate-500': props.faded,
|
||||
'text-slate-300': !props.faded
|
||||
}"
|
||||
>
|
||||
<!-- Used for "display all events" -->
|
||||
<button class="absolute inset-0 w-full h-full cursor-default z-0" />
|
||||
|
||||
<button
|
||||
class="relative z-10 group block w-full text-center cursor-pointer"
|
||||
@click="selectDate(date)"
|
||||
>
|
||||
<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-slate-800"
|
||||
:class="{
|
||||
'bg-slate-800': isDefaultDate && !isSelectedDate,
|
||||
'text-white bg-blue-500': isSelectedDate
|
||||
}"
|
||||
>
|
||||
{{ date.day }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ul
|
||||
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="{
|
||||
'opacity-40': props.faded && !isSelectedDate
|
||||
}"
|
||||
>
|
||||
<li v-for="event in eventsToDisplay" :key="event.title" class="grid pointer-events-auto">
|
||||
<CalendarEventButton :event :tile-date="date" />
|
||||
</li>
|
||||
|
||||
<li v-if="eventsNotDisplayed > 0" class="pointer-events-auto">
|
||||
<UiPopover>
|
||||
<UiPopoverTrigger as-child>
|
||||
<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"
|
||||
>
|
||||
{{ eventsNotDisplayed }} autre{{ eventsNotDisplayed > 1 ? 's' : '' }}
|
||||
</button>
|
||||
</UiPopoverTrigger>
|
||||
<UiPopoverContent class="w-80" :align="'center'" :side="'right'">
|
||||
<div class="text-center mb-4">
|
||||
<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"
|
||||
>
|
||||
{{ date.day }}
|
||||
</span>
|
||||
</div>
|
||||
<ul class="grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity">
|
||||
<li
|
||||
v-for="event in eventsForTheDay"
|
||||
:key="event.title"
|
||||
class="grid pointer-events-auto"
|
||||
>
|
||||
<CalendarEventButton :event :tile-date="date" />
|
||||
</li>
|
||||
</ul>
|
||||
</UiPopoverContent>
|
||||
</UiPopover>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tile {
|
||||
&:not(:nth-child(10n)) {
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
&:nth-child(n + 11) {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
73
components/calendar/state/monthly/Layout.vue
Normal file
73
components/calendar/state/monthly/Layout.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LeimDate } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
|
||||
import DayTile from './DayTile.vue'
|
||||
|
||||
const { staticConfig, currentDate, decrementMonth, incrementMonth } = useCalendar()
|
||||
|
||||
const daysPerMonth = staticConfig.daysPerMonth
|
||||
|
||||
function getNextMonthDate(day: number): LeimDate {
|
||||
let nextDay = day
|
||||
let nextMonth = currentDate.currentMonth + 1
|
||||
let nextYear = currentDate.currentYear
|
||||
let nextPeriod = currentDate.currentPeriod
|
||||
|
||||
// If the new value would exceed the max number of month per year
|
||||
if (nextMonth >= staticConfig.monthsPerYear) {
|
||||
nextMonth = 0
|
||||
// Increment the year
|
||||
nextYear++
|
||||
}
|
||||
|
||||
if (nextYear >= 0) {
|
||||
nextPeriod = 'nante'
|
||||
}
|
||||
|
||||
return {
|
||||
day: nextDay,
|
||||
month: nextMonth,
|
||||
year: nextYear,
|
||||
period: nextPeriod
|
||||
}
|
||||
}
|
||||
|
||||
function handleWheel(e: WheelEvent) {
|
||||
const isMovingUp = e.deltaY < 0
|
||||
if (isMovingUp) {
|
||||
moveCalendarLeft()
|
||||
} else {
|
||||
moveCalendarRight()
|
||||
}
|
||||
}
|
||||
|
||||
const moveCalendarLeft = useThrottleFn(() => {
|
||||
decrementMonth()
|
||||
}, 100)
|
||||
|
||||
const moveCalendarRight = useThrottleFn(() => {
|
||||
incrementMonth()
|
||||
}, 100)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-10" @wheel="handleWheel">
|
||||
<DayTile
|
||||
v-for="day in daysPerMonth"
|
||||
:key="day"
|
||||
:date="{
|
||||
day: day,
|
||||
month: currentDate.currentMonth,
|
||||
year: currentDate.currentYear
|
||||
}"
|
||||
/>
|
||||
<DayTile
|
||||
v-for="nextMonthDay in 8"
|
||||
:key="nextMonthDay"
|
||||
faded
|
||||
:date="getNextMonthDate(nextMonthDay)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
56
components/calendar/state/yearly/DayTile.vue
Normal file
56
components/calendar/state/yearly/DayTile.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
import { areDatesIdentical, type LeimDate } from '@/models/Date'
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useCalendarEvents } from '@/stores/EventStore'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, type ComputedRef } from 'vue'
|
||||
|
||||
const { currentDate, defaultDate, selectDate } = useCalendar()
|
||||
const { selectedDate } = storeToRefs(useCalendar())
|
||||
const { currentEvents } = storeToRefs(useCalendarEvents())
|
||||
|
||||
const props = defineProps<{
|
||||
monthNumber: number
|
||||
dayNumber: number
|
||||
}>()
|
||||
|
||||
const tileDate: ComputedRef<LeimDate> = computed(() => {
|
||||
return {
|
||||
day: props.dayNumber,
|
||||
month: props.monthNumber,
|
||||
year: currentDate.currentYear
|
||||
}
|
||||
})
|
||||
|
||||
const isDefaultDate = computed<boolean>(() => {
|
||||
return areDatesIdentical(tileDate.value, defaultDate)
|
||||
})
|
||||
|
||||
const isSelectedDate = computed<boolean>(() => {
|
||||
return areDatesIdentical(tileDate.value, selectedDate.value)
|
||||
})
|
||||
|
||||
const hasAtLeastOneEvent = computed<boolean>(() => {
|
||||
return Boolean(
|
||||
currentEvents.value.find((currentEvent) => {
|
||||
return areDatesIdentical(currentEvent.startDate, tileDate.value)
|
||||
})
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="relative grid place-items-center aspect-square rounded-full border-2 border-transparent transition-colors after:content-[''] after:absolute after:top-1 after:right-1 after:w-[.3rem] after:h-[.3rem] after:rounded-full after:transition-colors"
|
||||
:class="{
|
||||
'hover:bg-slate-800 hover:border-slate-800': !isDefaultDate && !isSelectedDate,
|
||||
'bg-slate-800 hover:bg-slate-600 hover:border-slate-600': isDefaultDate && !isSelectedDate,
|
||||
'text-white bg-blue-500 hover:bg-blue-600 hover:border-blue-600': isSelectedDate,
|
||||
'after:bg-green-600': hasAtLeastOneEvent,
|
||||
'after:bg-slate-950': hasAtLeastOneEvent && isSelectedDate
|
||||
}"
|
||||
@click="selectDate(tileDate)"
|
||||
>
|
||||
<span ref="tileRef" class="text-slate-300 text-[.85em]">{{ dayNumber }}</span>
|
||||
</button>
|
||||
</template>
|
||||
37
components/calendar/state/yearly/Layout.vue
Normal file
37
components/calendar/state/yearly/Layout.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
|
||||
import MonthTile from './MonthTile.vue'
|
||||
|
||||
const { staticConfig, decrementYear, incrementYear } = useCalendar()
|
||||
|
||||
function handleWheel(e: WheelEvent) {
|
||||
const isMovingUp = e.deltaY < 0
|
||||
if (isMovingUp) {
|
||||
moveCalendarLeft()
|
||||
} else {
|
||||
moveCalendarRight()
|
||||
}
|
||||
}
|
||||
|
||||
const moveCalendarLeft = useThrottleFn(() => {
|
||||
decrementYear()
|
||||
}, 100)
|
||||
|
||||
const moveCalendarRight = useThrottleFn(() => {
|
||||
incrementYear()
|
||||
}, 100)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mt-[10vh] mb-auto" @wheel="handleWheel">
|
||||
<div ref="test" class="grid grid-cols-5 gap-x-8 gap-y-16">
|
||||
<MonthTile
|
||||
v-for="month in staticConfig.monthsPerYear"
|
||||
:key="month"
|
||||
:month-number="month - 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
29
components/calendar/state/yearly/MonthTile.vue
Normal file
29
components/calendar/state/yearly/MonthTile.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCalendar } from '@/stores/CalendarStore'
|
||||
|
||||
import DayTile from './DayTile.vue'
|
||||
|
||||
const { staticConfig, getMonthName } = useCalendar()
|
||||
|
||||
const props = defineProps<{
|
||||
monthNumber: number
|
||||
}>()
|
||||
|
||||
const tileMonthName: string = getMonthName(props.monthNumber)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="font-medium">
|
||||
{{ tileMonthName }}
|
||||
</div>
|
||||
<div class="grid grid-cols-7">
|
||||
<DayTile
|
||||
v-for="day in staticConfig.daysPerMonth"
|
||||
:key="day"
|
||||
:month-number="monthNumber"
|
||||
:day-number="day"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user