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:
Alexis
2024-05-12 22:11:28 +02:00
parent 174b488319
commit 5772ab7aa3
157 changed files with 10627 additions and 7136 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,3 @@
export const searchUnifier = new RegExp(/[^a-zA-Z0-9\-'']/g)
export type SearchMode = 'characters' | 'events' | undefined

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,5 @@
<script lang="ts" setup></script>
<template>
<div>Century</div>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<div>Decade</div>
</template>

View 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>

View 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>

View 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>

View 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>

View 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>