Merge branch 'dev'

This commit is contained in:
Alexis
2024-05-05 16:48:54 +02:00
28 changed files with 957 additions and 388 deletions

View File

@@ -1,61 +1,5 @@
# vue-project # Leim Tools
A congregate of tools used to visualize TTRPG elements and help content creators (mostly me for now tbh) build their worlds.
This template should help get you started developing with Vue 3 in Vite. ## Timeline
Visual helper for time-related data, such as events, eras and characters. Helpful to know how old a character is while something is happening in the world, or how far two events are separated in the timeline.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Run Headed Component Tests with [Cypress Component Testing](https://on.cypress.io/component)
```sh
npm run test:unit:dev # or `npm run test:unit` for headless testing
```
### Run End-to-End Tests with [Cypress](https://www.cypress.io/)
```sh
npm run test:e2e:dev
```
This runs the end-to-end tests against the Vite development server.
It is much faster than the production build.
But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments):
```sh
npm run build
npm run test:e2e
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

View File

@@ -9,8 +9,6 @@
<meta name="description" content="Outil de visualisation chronologique des évènements de Léim" /> <meta name="description" content="Outil de visualisation chronologique des évènements de Léim" />
<meta name="generator" content="VueJS" /> <meta name="generator" content="VueJS" />
<title>Chronologie — Leim Wiki</title>
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon.png">
@@ -21,9 +19,9 @@
<meta name="msapplication-TileColor" content="#00aba9"> <meta name="msapplication-TileColor" content="#00aba9">
<meta name="theme-color" content="#a9d1eb"> <meta name="theme-color" content="#a9d1eb">
<meta property="og:title" content="Chronologie — Leim Wiki"/> <meta property="og:title" content="Outils — Leim Wiki"/>
<meta property="og:type" content="timeline" /> <meta property="og:type" content="tabletop-tools" />
<meta property="og:url" content="timeline.alexcreates.fr" /> <meta property="og:url" content="ttt.alexcreates.fr" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

8
package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "vue-project", "name": "vue-project",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@phosphor-icons/vue": "^2.1.6", "@phosphor-icons/vue": "^2.2.1",
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.9.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
@@ -890,9 +890,9 @@
} }
}, },
"node_modules/@phosphor-icons/vue": { "node_modules/@phosphor-icons/vue": {
"version": "2.1.6", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@phosphor-icons/vue/-/vue-2.1.6.tgz", "resolved": "https://registry.npmjs.org/@phosphor-icons/vue/-/vue-2.2.1.tgz",
"integrity": "sha512-j4iVvNT4JbDR2udIVfakKZHd14i3jCvO4b24OC0W/1rasftU8qVg1Tnb3QDwAT0teNU+UxvaM+kCWxfGHC2dUg==", "integrity": "sha512-3RNg1utc2Z5RwPKWFkW3eXI/0BfQAwXgtFxPUPeSzi55jGYUq16b+UqcgbKLazWFlwg5R92OCLKjDiJjeiJcnA==",
"engines": { "engines": {
"node": ">=14" "node": ">=14"
}, },

View File

@@ -1,6 +1,6 @@
{ {
"name": "vue-project", "name": "leim-tool",
"version": "0.0.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -17,7 +17,8 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@phosphor-icons/vue": "^2.1.6", "@phosphor-icons/vue": "^2.2.1",
"lucide-vue-next": "^0.364.0",
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.9.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",

View File

@@ -1,38 +1,72 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'
import { cn } from '@/lib/utils'
import { areDatesIdentical, type LeimDate } from '@/models/Date'
import { Popover, PopoverTrigger } from '@/components/ui/popover' import { Popover, PopoverTrigger } from '@/components/ui/popover'
import type { CalendarEvent } from '@/models/Events' import type { CalendarEvent } from '@/models/Events'
import CalendarEventDetails from './CalendarEventDetails.vue' import CalendarEventDetails from './CalendarEventDetails.vue'
defineProps<{ const props = defineProps<{
event: CalendarEvent 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> </script>
<template> <template>
<Popover> <Popover v-model:open="isPopoverOpen">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<button <button
class="text-xs px-2 py-1 block w-full text-left rounded-sm whitespace-nowrap overflow-hidden text-ellipsis" class="text-xs px-2 py-1 block w-full text-left rounded-sm whitespace-nowrap overflow-hidden text-ellipsis"
:class="{ :class="
'text-white bg-slate-600 hover:bg-slate-700': !event.category, cn({
'text-white bg-lime-600 hover:bg-lime-700': event.category === 'naissance', 'text-white bg-slate-600 hover:bg-slate-700': !event.category,
'text-white bg-stone-500 hover:bg-stone-700': event.category === 'mort', 'text-white bg-lime-600 hover:bg-lime-700': event.category === 'naissance',
'text-white bg-orange-600 hover:bg-orange-700': event.category === 'catastrophe', 'text-white bg-stone-500 hover:bg-stone-700': event.category === 'mort',
'text-white bg-pink-600 hover:bg-pink-700': event.category === 'catastrophe naturelle', 'text-white bg-orange-600 hover:bg-orange-700': event.category === 'catastrophe',
'text-white bg-sky-600 hover:bg-sky-700': event.category === 'législation', 'text-white bg-pink-600 hover:bg-pink-700': event.category === 'catastrophe naturelle',
'text-white bg-purple-600 hover:bg-purple-700': event.category === 'religion', 'text-white bg-sky-600 hover:bg-sky-700': event.category === 'législation',
'text-white bg-emerald-600 hover:bg-emerald-700': event.category === 'joueurs', 'text-white bg-purple-600 hover:bg-purple-700': event.category === 'religion',
'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category === 'inauguration', 'text-white bg-emerald-600 hover:bg-emerald-700': event.category === 'joueurs',
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category === 'invention', 'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category === 'inauguration',
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': event.category === 'science', 'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category === 'invention',
'text-slate-900 bg-yellow-100 hover:bg-yellow-200': event.category === 'bénédiction' '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 }} {{ event.title }}
</button> </button>
</PopoverTrigger> </PopoverTrigger>
<CalendarEventDetails :event v-once /> <CalendarEventDetails
:event
:spans-multiple-days
:is-start-event
:is-end-event
@query:close-popover="handleClosePopover"
v-once
/>
</Popover> </Popover>
</template> </template>

View File

@@ -1,29 +1,67 @@
<script lang="ts" setup> <script lang="ts" setup>
import { getRelativeString } from '@/models/Date' import { getRelativeString, type LeimDate } from '@/models/Date'
import type { CalendarEvent } from '@/models/Events' import type { CalendarEvent } from '@/models/Events'
import { useCalendar } from '@/stores/CalendarStore' import { useCalendar } from '@/stores/CalendarStore'
import { useCalendarEvents } from '@/stores/EventStore'
import { PhHourglassMedium } from '@phosphor-icons/vue' import {
PhHourglassMedium,
PhAlarm,
PhHourglassHigh,
PhHourglassLow,
PhArrowBendDoubleUpLeft,
PhArrowBendDoubleUpRight
} from '@phosphor-icons/vue'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { PopoverContent } from '@/components/ui/popover' import { PopoverContent } from '@/components/ui/popover'
const { defaultDate, getFormattedDateTitle } = useCalendar() const { defaultDate, getFormattedDateTitle, jumpToDate } = useCalendar()
const { getRelativeEventFromEvent } = useCalendarEvents()
const props = defineProps<{ event: CalendarEvent }>() const props = defineProps<{
event: CalendarEvent
spansMultipleDays: boolean
isStartEvent?: boolean
isEndEvent?: boolean
}>()
const dateDifference: string = getRelativeString(defaultDate, props.event.date) 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> </script>
<template> <template>
<PopoverContent <PopoverContent
class="w-96" class="event-details w-96"
align="start" :align="'center'"
:align-offset="50" :align-offset="50"
:collision-padding="20" :side="'left'"
:collision-padding="60"
:hide-when-detached="true"
:class="{ :class="{
'border-slate-800': !event.category, 'border-slate-800': !event.category,
'border-lime-800': event.category === 'naissance', 'border-lime-800': event.category === 'naissance',
'border-stone-800': event.category === 'mort', 'border-stone-600': event.category === 'mort',
'border-orange-800': event.category === 'catastrophe', 'border-orange-800': event.category === 'catastrophe',
'border-pink-800': event.category === 'catastrophe naturelle', 'border-pink-800': event.category === 'catastrophe naturelle',
'border-sky-800': event.category === 'législation', 'border-sky-800': event.category === 'législation',
@@ -32,7 +70,14 @@ const dateDifference: string = getRelativeString(defaultDate, props.event.date)
'border-amber-800': event.category === 'inauguration', 'border-amber-800': event.category === 'inauguration',
'border-green-800': event.category === 'invention', 'border-green-800': event.category === 'invention',
'border-cyan-800': event.category === 'science', 'border-cyan-800': event.category === 'science',
'border-yellow-800': event.category === 'bénédiction' '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="grid gap-1">
@@ -40,14 +85,30 @@ const dateDifference: string = getRelativeString(defaultDate, props.event.date)
{{ event.title }} {{ event.title }}
</div> </div>
<div class="mb-1 space-y-1"> <div class="mb-1">
<p class="font-semibold">{{ getFormattedDateTitle(event.date, true) }}</p> <template v-if="!event.endDate">
<p class="text-sm italic opacity-75 flex items-center gap-1"> <p class="font-semibold">{{ getFormattedDateTitle(event.startDate, true) }}</p>
<PhHourglassMedium size="16" weight="fill" /> {{ dateDifference }} </template>
</p> <template v-else>
<p class="font-semibold">
Du {{ getFormattedDateTitle(event.startDate, true) }} au
{{ getFormattedDateTitle(event.endDate, true) }}
</p>
</template>
</div> </div>
<div v-if="event.category || event.secondaryCategories"> <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"> <ul class="flex gap-1">
<li v-if="event.category"> <li v-if="event.category">
<Badge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary"> <Badge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
@@ -61,50 +122,126 @@ const dateDifference: string = getRelativeString(defaultDate, props.event.date)
</Badge> </Badge>
</li> </li>
</ul> </ul>
</div> </template>
<div class="mt-2 italic text-sm text-slate-500"> <template v-if="event.description">
{{ event.description }} <hr class="border-slate-500 mt-2" />
</div>
<div class="mt-2 text-sm text-slate-300">
{{ event.description }}
</div>
</template>
</div> </div>
<nav v-if="event.startDate && event.endDate" class="absolute -top-2 right-2 flex gap-2">
<Badge class="hover:opacity-100 hover:bg-slate-300" as-child>
<button
class="flex gap-1"
@click="handleJumpToDate(event.startDate!)"
title="Naviguer au début"
>
<PhHourglassHigh size="16" weight="fill" /> Début
</button>
</Badge>
<Badge class="hover:opacity-100 hover:bg-slate-300" as-child title="Naviguer à la fin">
<button
class="flex gap-1"
@click="handleJumpToDate(event.endDate!)"
title="Naviguer à la fin"
>
<PhHourglassLow size="16" weight="fill" /> Fin
</button>
</Badge>
</nav>
<nav class="mt-2 flex gap-2">
<Badge class="hover:opacity-100 hover:bg-slate-300" as-child>
<button
class="flex gap-1"
@click="handleGotoRelativeEvent('prev')"
title="Évènement précédent"
>
<PhArrowBendDoubleUpLeft size="16" weight="fill" /> Précédent
</button>
</Badge>
<Badge class="hover:opacity-100 hover:bg-slate-300" as-child title="Naviguer à la fin">
<button
class="flex gap-1"
@click="handleGotoRelativeEvent('next')"
title="Évènement suivant"
>
<PhArrowBendDoubleUpRight size="16" weight="fill" /> Suivant
</button>
</Badge>
</nav>
</PopoverContent> </PopoverContent>
</template> </template>
<style lang="scss"> <style lang="scss">
.border-slate-800 { .border-slate-800 {
background-color: color-mix(in srgb, var(--color-slate-800), var(--color-slate-950) 85%); --base-color: var(--color-slate-800);
} }
.border-lime-800 { .border-lime-800 {
background-color: color-mix(in srgb, var(--color-lime-800), var(--color-slate-950) 85%); --base-color: var(--color-lime-800);
} }
.border-stone-800 { .border-stone-600 {
background-color: color-mix(in srgb, var(--color-stone-800), var(--color-slate-950) 85%); --base-color: var(--color-stone-600);
} }
.border-orange-800 { .border-orange-800 {
background-color: color-mix(in srgb, var(--color-orange-800), var(--color-slate-950) 85%); --base-color: var(--color-orange-800);
} }
.border-pink-800 { .border-pink-800 {
background-color: color-mix(in srgb, var(--color-pink-800), var(--color-slate-950) 85%); --base-color: var(--color-pink-800);
} }
.border-sky-800 { .border-sky-800 {
background-color: color-mix(in srgb, var(--color-sky-800), var(--color-slate-950) 85%); --base-color: var(--color-sky-800);
} }
.border-purple-800 { .border-purple-800 {
background-color: color-mix(in srgb, var(--color-purple-800), var(--color-slate-950) 85%); --base-color: var(--color-purple-800);
} }
.border-emerald-800 { .border-emerald-800 {
background-color: color-mix(in srgb, var(--color-emerald-800), var(--color-slate-950) 85%); --base-color: var(--color-emerald-800);
} }
.border-amber-800 { .border-amber-800 {
background-color: color-mix(in srgb, var(--color-amber-800), var(--color-slate-950) 85%); --base-color: var(--color-amber-800);
} }
.border-green-800 { .border-green-800 {
background-color: color-mix(in srgb, var(--color-green-800), var(--color-slate-950) 85%); --base-color: var(--color-green-800);
} }
.border-cyan-800 { .border-cyan-800 {
background-color: color-mix(in srgb, var(--color-cyan-800), var(--color-slate-950) 85%); --base-color: var(--color-cyan-800);
} }
.border-yellow-800 { .border-slate-600 {
background-color: color-mix(in srgb, var(--color-yellow-800), var(--color-slate-950) 85%); --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> </style>

View File

@@ -2,12 +2,14 @@
import { useCalendar } from '@/stores/CalendarStore' import { useCalendar } from '@/stores/CalendarStore'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { PhMagnifyingGlass } from '@phosphor-icons/vue'
import CalendarMenuNav from './CalendarMenuNav.vue' import CalendarMenuNav from './CalendarMenuNav.vue'
import CalendarMenuSubnav from './CalendarMenuSubnav.vue'
import CalendarMenuToday from './CalendarMenuToday.vue' import CalendarMenuToday from './CalendarMenuToday.vue'
import CalendarSwitch from './CalendarSwitch.vue' import CalendarSwitch from './CalendarSwitch.vue'
import CalendarCurrentDate from './CalendarCurrentDate.vue' import CalendarCurrentDate from './CalendarCurrentDate.vue'
const { currentDate, revealAdvancedSearch } = useCalendar() const { revealAdvancedSearch } = useCalendar()
</script> </script>
<template> <template>
@@ -37,10 +39,9 @@ const { currentDate, revealAdvancedSearch } = useCalendar()
</li> </li>
</menu> </menu>
</div> </div>
<div class="ml-12 flex">
<div class="px-4 py-2 border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm"> <div class="ml-6">
<span class="text-sm">{{ currentDate.currentDateTitle }}</span> <CalendarMenuSubnav />
</div>
</div> </div>
</header> </header>
</template> </template>

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, type ComputedRef } from 'vue'
import { useCalendar } from '@/stores/CalendarStore' import { useCalendar } from '@/stores/CalendarStore'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
@@ -8,8 +9,7 @@ import {
PhCaretLeft, PhCaretLeft,
PhCaretRight PhCaretRight
} from '@phosphor-icons/vue' } from '@phosphor-icons/vue'
import Button from '../ui/button/Button.vue' import Button from '@/components/ui/button/Button.vue'
import { computed, type ComputedRef } from 'vue'
interface DirectionLabels { interface DirectionLabels {
pastFar: string pastFar: string
@@ -147,7 +147,7 @@ function toFutureFar(): void {
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<TooltipProvider :delayDuration="250"> <TooltipProvider :delayDuration="250">
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="toPastFar()"> <Button variant="outline" size="icon" @click="toPastFar()">
<PhCaretDoubleLeft size="18" /> <PhCaretDoubleLeft size="18" />
</Button> </Button>
@@ -160,7 +160,7 @@ function toFutureFar(): void {
<TooltipProvider :delayDuration="250"> <TooltipProvider :delayDuration="250">
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="toPastNear()"> <Button variant="outline" size="icon" @click="toPastNear()">
<PhCaretLeft size="18" /> <PhCaretLeft size="18" />
</Button> </Button>
@@ -173,7 +173,7 @@ function toFutureFar(): void {
<TooltipProvider :delayDuration="250"> <TooltipProvider :delayDuration="250">
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="toFutureNear()"> <Button variant="outline" size="icon" @click="toFutureNear()">
<PhCaretRight size="18" /> <PhCaretRight size="18" />
</Button> </Button>
@@ -186,7 +186,7 @@ function toFutureFar(): void {
<TooltipProvider :delayDuration="250"> <TooltipProvider :delayDuration="250">
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="toFutureFar()"> <Button variant="outline" size="icon" @click="toFutureFar()">
<PhCaretDoubleRight size="18" /> <PhCaretDoubleRight size="18" />
</Button> </Button>

View File

@@ -0,0 +1,107 @@
<script lang="ts" setup>
import { daysPerMonth, monthsPerYear, type LeimDate } from '@/models/Date'
import { useCalendar } from '@/stores/CalendarStore'
import { useCalendarEvents } from '@/stores/EventStore'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { PhArrowLineLeft, PhArrowLineRight } from '@phosphor-icons/vue'
import Button from '@/components/ui/button/Button.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">
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon"
class="rounded-t-sm rounded-b-none"
@click="handleGotoPreviousEventPage('prev')"
>
<PhArrowLineLeft size="22" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Précédente page à évènements</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div class="border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon"
class="rounded-t-sm rounded-b-none"
@click="handleGotoPreviousEventPage('next')"
>
<PhArrowLineRight size="22" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Prochaine page à évènements</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</template>

View File

@@ -20,7 +20,7 @@ const isDefaultDate = computed(() => {
<template> <template>
<TooltipProvider :delayDuration="250"> <TooltipProvider :delayDuration="250">
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger as-child>
<Button @click="jumpToDefaultDate" size="sm" :disabled="isDefaultDate"> <Button @click="jumpToDefaultDate" size="sm" :disabled="isDefaultDate">
Aujourd'hui Aujourd'hui
</Button> </Button>

View File

@@ -20,14 +20,14 @@ const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
<template> <template>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger as-child>
<Button size="sm" variant="secondary"> <Button size="sm" variant="secondary">
<PhCalendarBlank size="18" weight="fill" /> <PhCalendarBlank size="18" weight="fill" />
{{ viewTypeTitle }} {{ viewTypeTitle }}
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent :side="'bottom'" :collision-padding="30">
<DropdownMenuLabel>Mode d'affichage</DropdownMenuLabel> <DropdownMenuLabel>Mode d'affichage</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem

View File

@@ -19,7 +19,7 @@ const { revealAdvancedSearch } = useCalendar()
<li> <li>
<TooltipProvider :delayDuration="100"> <TooltipProvider :delayDuration="100">
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="rounded-full" as-child> <Button variant="ghost" size="icon" class="rounded-full" as-child>
<RouterLink to="/"> <RouterLink to="/">
<PhHouse size="24" weight="fill" /> <PhHouse size="24" weight="fill" />
@@ -35,7 +35,7 @@ const { revealAdvancedSearch } = useCalendar()
<li> <li>
<TooltipProvider :delayDuration="100"> <TooltipProvider :delayDuration="100">
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger as-child>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"

View File

@@ -1,33 +1,27 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
characterCategories,
isCharacter, isCharacter,
type Character, type Character,
characterCategories,
type CharacterCategory type CharacterCategory
} from '@/models/Characters' } from '@/models/Characters'
import type { LeimDateOrder } from '@/models/Date' import type { LeimDateOrder } from '@/models/Date'
import { import {
calendarEventCategories,
isCalendarEvent, isCalendarEvent,
type CalendarEvent, type CalendarEvent,
calendarEventCategories,
type CalendarEventCategory type CalendarEventCategory
} from '@/models/Events' } from '@/models/Events'
import { capitalize } from '@/utils/Strings'
import { useCharacters } from '@/stores/CharacterStore' import { useCharacters } from '@/stores/CharacterStore'
import { useCalendarEvents } from '@/stores/EventStore' import { useCalendarEvents } from '@/stores/EventStore'
import { useMagicKeys, useStorage, whenever } from '@vueuse/core' import { capitalize } from '@/utils/Strings'
import { computed, ref } from 'vue' import { useMagicKeys, useScroll, useStorage, whenever } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import { searchUnifier, type SearchMode } from '../Search' import { searchUnifier, type SearchMode } from '../Search'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/components/ui/command' import { CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/components/ui/command'
import { import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog'
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { import {
Pagination, Pagination,
@@ -60,7 +54,7 @@ import {
import SearchList from './lists/SearchList.vue' import SearchList from './lists/SearchList.vue'
const { characters } = useCharacters() const { characters } = useCharacters()
const { baseEvents } = useCalendarEvents() const { allEvents } = useCalendarEvents()
const modalOpen = defineModel({ default: false }) const modalOpen = defineModel({ default: false })
@@ -87,6 +81,9 @@ const itemsPerPage: number = 20
const startOfList = computed<number>(() => (currentPage.value - 1) * itemsPerPage) const startOfList = computed<number>(() => (currentPage.value - 1) * itemsPerPage)
const endOfList = computed<number>(() => startOfList.value + itemsPerPage) const endOfList = computed<number>(() => startOfList.value + itemsPerPage)
/**
* Resets the pagination
*/
function resetPage() { function resetPage() {
currentPage.value = 1 currentPage.value = 1
} }
@@ -99,11 +96,11 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Assign data to loop over and filter // Assign data to loop over and filter
// They are assigned this way for readability // They are assigned this way for readability
if (selectedEntity.value === 'events') { if (selectedEntity.value === 'events') {
dataToFilter = baseEvents dataToFilter = allEvents
} else if (selectedEntity.value === 'characters') { } else if (selectedEntity.value === 'characters') {
dataToFilter = characters dataToFilter = characters
} else { } else {
dataToFilter = [...baseEvents, ...characters] dataToFilter = [...allEvents, ...characters]
} }
/** /**
@@ -142,7 +139,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
} }
hitCategories = selectedCategories.value.every((selectedCat) => { hitCategories = selectedCategories.value.every((selectedCat) => {
return allCategories.includes(selectedCat) return allCategories.includes(selectedCat as CalendarEventCategory)
}) })
return (hitTitle || hitDesc) && hitCategories return (hitTitle || hitDesc) && hitCategories
@@ -165,7 +162,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Handle categories logic // Handle categories logic
let hitCategories: boolean = false let hitCategories: boolean = false
let allCategories: CalendarEventCategory[] = [] let allCategories: CharacterCategory[] = []
if (item.category) { if (item.category) {
allCategories.push(item.category) allCategories.push(item.category)
@@ -176,7 +173,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
} }
hitCategories = selectedCategories.value.every((selectedCat) => { hitCategories = selectedCategories.value.every((selectedCat) => {
return allCategories.includes(selectedCat) return allCategories.includes(selectedCat as CharacterCategory)
}) })
return hitTitle && hitCategories return hitTitle && hitCategories
@@ -186,20 +183,32 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
return results return results
}) })
/**
* Removes the search query, resets the pagination and removes all selected categories
*/
function resetSearch() { function resetSearch() {
searchQuery.value = '' searchQuery.value = ''
resetPage() resetPage()
selectedCategories.value = [] selectedCategories.value = []
} }
/**
* Opens the search dialog
*/
function openDialog() { function openDialog() {
modalOpen.value = true modalOpen.value = true
} }
/**
* Closes the search dialog
*/
function closeDialog() { function closeDialog() {
modalOpen.value = false modalOpen.value = false
} }
/**
* Switches the selectedEntity
*/
function handleEntitySwitch() { function handleEntitySwitch() {
resetPage() resetPage()
selectedCategories.value = [] selectedCategories.value = []
@@ -212,7 +221,14 @@ whenever(keys.control_period, () => {
openDialog() openDialog()
}) })
// Categories 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(() => { const currentCategories = computed(() => {
if (selectedEntity.value === 'characters') { if (selectedEntity.value === 'characters') {
return [...characterCategories] return [...characterCategories]
@@ -225,17 +241,22 @@ const selectedCategories = ref<(CharacterCategory | CalendarEventCategory)[]>([]
const categoryFilterOpened = ref<boolean>(false) const categoryFilterOpened = ref<boolean>(false)
const searchCategory = ref<string>('') const searchCategory = ref<string>('')
const filteredFrameworks = computed(() => const filteredCategories = computed(() =>
currentCategories.value.filter((i) => !selectedCategories.value.includes(i)) currentCategories.value.filter((i) => !selectedCategories.value.includes(i))
) )
/**
* Handles the category selections from the TagInput component
*
* @param e Radix Change Event
*/
function handleCategorySelect(e: any) { function handleCategorySelect(e: any) {
if (typeof e.detail.value === 'string') { if (typeof e.detail.value === 'string') {
searchCategory.value = '' searchCategory.value = ''
selectedCategories.value.push(e.detail.value) selectedCategories.value.push(e.detail.value)
} }
if (filteredFrameworks.value.length === 0) { if (filteredCategories.value.length === 0) {
categoryFilterOpened.value = false categoryFilterOpened.value = false
} }
} }
@@ -292,7 +313,7 @@ function handleCategorySelect(e: any) {
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<TagsInput class="px-0 gap-0 w-52" :model-value="selectedCategories"> <TagsInput class="px-0 gap-0 w-72" :model-value="selectedCategories">
<div class="flex gap-2 flex-wrap items-center px-3"> <div class="flex gap-2 flex-wrap items-center px-3">
<TagsInputItem v-for="item in selectedCategories" :key="item" :value="item"> <TagsInputItem v-for="item in selectedCategories" :key="item" :value="item">
<TagsInputItemText class="capitalize" /> <TagsInputItemText class="capitalize" />
@@ -325,7 +346,7 @@ function handleCategorySelect(e: any) {
<CommandEmpty /> <CommandEmpty />
<CommandGroup> <CommandGroup>
<CommandItem <CommandItem
v-for="framework in filteredFrameworks" v-for="framework in filteredCategories"
:key="framework" :key="framework"
:value="framework" :value="framework"
@select.prevent="handleCategorySelect" @select.prevent="handleCategorySelect"
@@ -340,7 +361,7 @@ function handleCategorySelect(e: any) {
<TooltipProvider :delayDuration="250"> <TooltipProvider :delayDuration="250">
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger as-child>
<Button <Button
:variant="selectedOrder === 'desc' ? 'secondary' : 'outline'" :variant="selectedOrder === 'desc' ? 'secondary' : 'outline'"
size="icon" size="icon"
@@ -357,7 +378,7 @@ function handleCategorySelect(e: any) {
<TooltipProvider :delayDuration="250"> <TooltipProvider :delayDuration="250">
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger as-child>
<Button <Button
:variant="selectedOrder === 'asc' ? 'secondary' : 'outline'" :variant="selectedOrder === 'asc' ? 'secondary' : 'outline'"
size="icon" size="icon"
@@ -377,7 +398,7 @@ function handleCategorySelect(e: any) {
<hr /> <hr />
<div v-if="searchResults.length > 0" class="grow overflow-y-auto"> <div v-if="searchResults.length > 0" class="grow overflow-y-auto" ref="searchResultsRef">
<SearchList <SearchList
:results="searchResults" :results="searchResults"
:current-entity="selectedEntity" :current-entity="selectedEntity"

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 :delayDuration="100">
<Tooltip>
<TooltipTrigger as-child>
<Button
@click="$emit('query:date-jump', character.birth!)"
variant="ghost"
size="xs"
>
<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 :delayDuration="100">
<Tooltip>
<TooltipTrigger as-child>
<Button
@click="$emit('query:date-jump', character.death!)"
variant="ghost"
size="xs"
>
<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

@@ -1,15 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { isCharacter, type Character } from '@/models/Characters' import { isCharacter, type Character } from '@/models/Characters'
import { compareDates, getRelativeString, type LeimDate, type LeimDateOrder } from '@/models/Date' import { compareDates, type LeimDate, type LeimDateOrder } from '@/models/Date'
import { isCalendarEvent, type CalendarEvent } from '@/models/Events' import { isCalendarEvent, type CalendarEvent } from '@/models/Events'
import { useCalendar } from '@/stores/CalendarStore' import { useCalendar } from '@/stores/CalendarStore'
import { computed } from 'vue' import { computed, ref } from 'vue'
import type { SearchMode } from '../../Search' import type { SearchMode } from '../../Search'
import { PhHourglassMedium, PhPlant, PhSkull, PhArrowSquareOut } from '@phosphor-icons/vue' import CharacterCallout from './CharacterCallout.vue'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import EventCallout from './EventCallout.vue'
import { Button } from '@/components/ui/button' import { useScroll } from '@vueuse/core'
import { Badge } from '@/components/ui/badge'
const props = defineProps<{ const props = defineProps<{
results: (Character | CalendarEvent)[] results: (Character | CalendarEvent)[]
@@ -22,7 +21,7 @@ const props = defineProps<{
const emit = defineEmits(['jumpedToDate']) const emit = defineEmits(['jumpedToDate'])
const { defaultDate, getFormattedDateTitle, jumpToDate } = useCalendar() const { jumpToDate } = useCalendar()
function handleJumpToDate(date?: LeimDate) { function handleJumpToDate(date?: LeimDate) {
if (!date) return if (!date) return
@@ -38,7 +37,7 @@ const sortedResults = computed(() => {
let secondDate: LeimDate let secondDate: LeimDate
if (isCalendarEvent(a)) { if (isCalendarEvent(a)) {
firstDate = a.date firstDate = a.startDate
} else if (isCharacter(a) && a.birth) { } else if (isCharacter(a) && a.birth) {
firstDate = a.birth firstDate = a.birth
} else { } else {
@@ -46,7 +45,7 @@ const sortedResults = computed(() => {
} }
if (isCalendarEvent(b)) { if (isCalendarEvent(b)) {
secondDate = b.date secondDate = b.startDate
} else if (isCharacter(b) && b.birth) { } else if (isCharacter(b) && b.birth) {
secondDate = b.birth secondDate = b.birth
} else { } else {
@@ -64,133 +63,13 @@ const pagedResults = computed(() => sortedResults.value.slice(props.startAt, pro
<template> <template>
<ul class="grid gap-4"> <ul class="grid gap-4">
<li v-for="r in pagedResults" :key="isCalendarEvent(r) ? r.title : r.name"> <li v-for="r in pagedResults" :key="isCalendarEvent(r) ? r.title : r.name">
<button <EventCallout v-if="isCalendarEvent(r)" :event="r" @query:date-jump="handleJumpToDate" />
v-if="isCalendarEvent(r)"
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': !r.category,
'text-white bg-lime-600 hover:bg-lime-700': r.category === 'naissance',
'text-white bg-stone-500 hover:bg-stone-700': r.category === 'mort',
'text-white bg-orange-600 hover:bg-orange-700': r.category === 'catastrophe',
'text-white bg-pink-600 hover:bg-pink-700': r.category === 'catastrophe naturelle',
'text-white bg-sky-600 hover:bg-sky-700': r.category === 'législation',
'text-white bg-purple-600 hover:bg-purple-700': r.category === 'religion',
'text-white bg-emerald-600 hover:bg-emerald-700': r.category === 'joueurs',
'text-slate-900 bg-amber-300 hover:bg-amber-400': r.category === 'inauguration',
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': r.category === 'invention',
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': r.category === 'science',
'text-slate-900 bg-yellow-100 hover:bg-yellow-200': r.category === 'bénédiction'
}"
@click="handleJumpToDate(r.date)"
>
<div class="flex gap-2 items-center">
<h2 class="font-bold">
{{ r.title }}
</h2>
<div v-if="r.wiki">
<Button variant="link" size="xs" as-child class="text-inherit">
<a :href="r.wiki" target="_blank">
Wiki
<PhArrowSquareOut size="16" weight="fill" />
</a>
</Button>
</div>
</div>
<div class="mb-1 flex gap-4 items-center"> <CharacterCallout
<p class="opacity-75">{{ getFormattedDateTitle(r.date, true) }}</p>
<p class="text-sm italic opacity-75 flex items-center gap-1">
<PhHourglassMedium size="16" weight="fill" />
{{ getRelativeString(defaultDate, r.date) }}
</p>
</div>
<div v-if="r.category || r.secondaryCategories" class="absolute top-3 right-4">
<ul class="flex gap-1">
<li v-if="r.category">
<Badge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
{{ r.category }}
</Badge>
</li>
<li v-for="cat in r.secondaryCategories" :key="cat">
<Badge class="mix-blend-luminosity bg-gray-600" variant="secondary">
{{ cat }}
</Badge>
</li>
</ul>
</div>
<div v-if="r.description" class="text-sm">
<hr class="my-2 border-white opacity-50" />
<span class="opacity-75">
{{ r.description }}
</span>
</div>
</button>
<div
v-else-if="isCharacter(r)" v-else-if="isCharacter(r)"
class="block w-full text-left py-3 px-4 border-[1px] border-slate-700 rounded-sm" :character="r"
> @query:date-jump="handleJumpToDate"
<div class="grid gap-2"> />
<div class="flex gap-2">
<h2 class="font-bold">
{{ r.name }}
</h2>
<div v-if="r.wiki">
<Button variant="link" size="xs" as-child>
<a :href="r.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="r.birth">
<TooltipProvider :delayDuration="100">
<Tooltip>
<TooltipTrigger>
<Button @click="handleJumpToDate(r.birth)" variant="ghost" size="xs">
<PhPlant size="16" weight="fill" />
{{ getFormattedDateTitle(r.birth, true) }}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Date de naissance</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</li>
<span v-if="r.birth && r.death">-</span>
<li v-if="r.death">
<TooltipProvider :delayDuration="100">
<Tooltip>
<TooltipTrigger>
<Button @click="handleJumpToDate(r.death)" variant="ghost" size="xs">
<PhSkull size="16" weight="fill" />
{{ getFormattedDateTitle(r.death, true) }}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Date de décès</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</li>
</menu>
<hr v-if="r.description" class="border-white opacity-25" />
<div v-if="r.description" class="text-sm">
<span class="opacity-75">
{{ r.description }}
</span>
</div>
</div>
</div>
</li> </li>
</ul> </ul>
</template> </template>

View File

@@ -1,8 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup></script>
import { useCalendarEvents } from '@/stores/EventStore'
const { currentEvents } = useCalendarEvents()
</script>
<template> <template>
<div>Century</div> <div>Century</div>

View File

@@ -24,7 +24,10 @@ const { currentEvents } = storeToRefs(useCalendarEvents())
const eventsForTheDay = computed(() => { const eventsForTheDay = computed(() => {
return currentEvents.value.filter((currentEvent) => { return currentEvents.value.filter((currentEvent) => {
return areDatesIdentical(currentEvent.date, props.date) return (
areDatesIdentical(currentEvent.startDate, props.date) ||
areDatesIdentical(currentEvent.endDate!, props.date)
)
}) })
}) })
@@ -90,7 +93,7 @@ const eventsNotDisplayed = computed(
}" }"
> >
<li v-for="event in eventsToDisplay" :key="event.title" class="grid pointer-events-auto"> <li v-for="event in eventsToDisplay" :key="event.title" class="grid pointer-events-auto">
<CalendarEventButton :event /> <CalendarEventButton :event :tile-date="date" />
</li> </li>
<li v-if="eventsNotDisplayed > 0" class="pointer-events-auto"> <li v-if="eventsNotDisplayed > 0" class="pointer-events-auto">
@@ -116,7 +119,7 @@ const eventsNotDisplayed = computed(
:key="event.title" :key="event.title"
class="grid pointer-events-auto" class="grid pointer-events-auto"
> >
<CalendarEventButton :event /> <CalendarEventButton :event :tile-date="date" />
</li> </li>
</ul> </ul>
</PopoverContent> </PopoverContent>

View File

@@ -33,7 +33,7 @@ const isSelectedDate = computed<boolean>(() => {
const hasAtLeastOneEvent = computed<boolean>(() => { const hasAtLeastOneEvent = computed<boolean>(() => {
return Boolean( return Boolean(
currentEvents.value.find((currentEvent) => { currentEvents.value.find((currentEvent) => {
return areDatesIdentical(currentEvent.date, tileDate.value) return areDatesIdentical(currentEvent.startDate, tileDate.value)
}) })
) )
}) })

View File

@@ -33,7 +33,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
v-bind="forwarded" v-bind="forwarded"
:class=" :class="
cn( cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-4xl -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', 'fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-2rem)] max-w-4xl -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class props.class
) )
" "

View File

@@ -174,6 +174,15 @@ export const charactersList: Character[] = [
wiki: 'https://alexcreates.fr/leim/index.php/Räzal' wiki: 'https://alexcreates.fr/leim/index.php/Räzal'
}, },
// Legends
{
name: 'Jorhas Kirendre',
birth: { day: 2, month: 9, year: -452 },
death: { day: 2, month: 2, year: -419 },
category: 'sentinelle',
wiki: 'https://alexcreates.fr/leim/index.php/Jorhas_Kirendre'
},
// "Les Milles Cages" // "Les Milles Cages"
{ {
name: 'Ernestin Pomel', name: 'Ernestin Pomel',

View File

@@ -1,10 +1,51 @@
import type { CalendarEvent } from '@/models/Events' import type { CalendarEvent } from '@/models/Events'
export const regularEvents: CalendarEvent[] = [ export const initialEvents: CalendarEvent[] = [
// Histoire Antique // Histoire Antique
{
title: "Laurdieu devient la première cité de l'empire de Kaliatos",
startDate: { day: 3, month: 4, year: -1932 },
description:
"L'empire de Kaliatos établi sa capitale dans la cité de Laurdieu, qui connaitra une prospérité nouvelle au sein d'Aldys.",
category: 'législation',
secondaryCategories: ['inauguration'],
wiki: 'https://alexcreates.fr/leim/index.php/Laurdieu',
hidden: true
},
{
title: "Apparition d'Asménys",
startDate: { day: 19, month: 7, year: -1358 },
description:
"La défunte chanteuse Asménys apparaît à un barde pendant son jeune public, démarrant la religion des Prêtresses d'Asménys.",
category: 'religion',
secondaryCategories: ['bénédiction'],
wiki: 'https://alexcreates.fr/leim/index.php/Pr%C3%AAtresses_d%27Asm%C3%A9nys',
hidden: true
},
{
title: 'La Rupture',
startDate: { day: 26, month: 5, year: -756 },
endDate: { day: 4, month: 9, year: 29 },
description:
"Les Abysses se déversent à la surface de Léim, à travers plusieurs brèches. Plusieurs hordes de démons se rapprochent des villes, et ce qu'on appellera l'Âge des Abysses commencent alors sur la planète entière.",
category: 'catastrophe',
secondaryCategories: ['catastrophe naturelle'],
wiki: 'https://alexcreates.fr/leim/index.php/Seconde_Rupture',
hidden: true
},
{
title: 'Marche du sang',
startDate: { day: 18, month: 9, year: -420 },
endDate: { day: 27, month: 1, year: -419 },
description:
"L'empereur de Kaliatos ordonne personnellement la traque de Jorhas Kirendre pour connivence avec les démons. Plusieurs bataillons sont affectés à la chasse de Jorhas, qui se terminera par son incarcération ainsi que la mort de plusieurs centaines de soldats.",
category: 'criminalité',
wiki: 'https://alexcreates.fr/leim/index.php/Jorhas_Kirendre',
hidden: true
},
{ {
title: 'Exécution de Tyhos', title: 'Exécution de Tyhos',
date: { day: 1, month: 0, year: 0 }, startDate: { day: 1, month: 0, year: 0 },
description: description:
"Le léviathan Tyhos rend l'âme après un combat de plusieurs années contre Lystos, le dieu du Soleil.", "Le léviathan Tyhos rend l'âme après un combat de plusieurs années contre Lystos, le dieu du Soleil.",
category: 'bénédiction', category: 'bénédiction',
@@ -14,28 +55,84 @@ export const regularEvents: CalendarEvent[] = [
}, },
{ {
title: 'Traité de Kadel', title: 'Traité de Kadel',
date: { day: 20, month: 4, year: 100 }, startDate: { day: 29, month: 4, year: 100 },
description: '', description: '',
category: 'inauguration', category: 'inauguration',
secondaryCategories: ['législation'], secondaryCategories: ['législation'],
wiki: 'https://alexcreates.fr/leim/index.php/Alliance_Kald%C3%A9lienne#Trait%C3%A9_de_Kadel' wiki: 'https://alexcreates.fr/leim/index.php/Alliance_Kald%C3%A9lienne#Trait%C3%A9_de_Kadel'
}, },
{
title: 'Découverte des Plaines de Poussières',
startDate: { day: 17, month: 7, year: 305 },
description:
"Les troupes de la reconquête aldienne explorent le littoral d'une immense étendue grise et inerte.",
category: 'découverte',
secondaryCategories: ['exploration'],
wiki: 'https://alexcreates.fr/leim/index.php/Plaines_de_poussi%C3%A8re',
hidden: true
},
{
title: 'Construction du Rempart de Laurdieu',
startDate: { day: 30, month: 2, year: 340 },
endDate: { day: 27, month: 9, year: 355 },
description:
"Le Grand Conseil Kaldélien ordonne la construction d'une muraille autour des Plaines de Poussières, afin de contenir les démons y sortant.",
category: 'construction',
wiki: 'https://alexcreates.fr/leim/index.php/Plaines_de_poussi%C3%A8re',
hidden: true
},
// Histoire Récente // Histoire Récente
{
title: "Inauguration de l'Académie Artistique Arcanique",
startDate: { day: 11, month: 6, year: 2545 },
description:
"Scäd Sceni ouvre son institut artistique dédié à l'apprentissage des arts arcaniques",
category: 'inauguration',
secondaryCategories: ['arcanologie'],
wiki: 'https://alexcreates.fr/leim/index.php/Buse_(arme)'
},
{ {
title: 'Création de la buse kaldélienne', title: 'Création de la buse kaldélienne',
date: { day: 4, month: 2, year: 3113 }, startDate: { day: 4, month: 2, year: 3113 },
description: description:
'Sophia de Rougefer invente la buse kaldélienne, une arme à feu souple à deux canons.', 'Sophia de Rougefer invente la buse kaldélienne, une arme à feu souple à deux canons.',
category: 'invention', category: 'invention',
secondaryCategories: ['science'], secondaryCategories: ['science'],
wiki: 'https://alexcreates.fr/leim/index.php/Buse_(arme)' wiki: 'https://alexcreates.fr/leim/index.php/Buse_(arme)'
}, },
{
title: 'Feux dans les champs de Bamast',
startDate: { day: 9, month: 5, year: 3200 },
description:
"Plusieurs incendies criminels se propagent à travers les champs de sérille des fermiers des littoraux de Bamast. Aucun suspect ni coupable ne sera trouvé et l'enquête sera baclée.",
category: 'catastrophe',
secondaryCategories: ['criminalité'],
wiki: 'https://alexcreates.fr/leim/index.php/Vascylly'
},
{
title: 'Meurtre de Darléon Typhos',
startDate: { day: 21, month: 6, year: 3200 },
description:
'Darléon Typhos surprend Vascylly fouillant sa demeure peu après la tombée de la nuit. Le majordome de la famille Typhos découvre le corps sans vie de son maître le lendemain.',
category: 'criminalité',
secondaryCategories: ['joueurs'],
wiki: 'https://alexcreates.fr/leim/index.php/Vascylly'
},
{
title: 'Scandale Rodhus',
startDate: { day: 25, month: 9, year: 3208 },
description:
"Tivian Rodhus, un professeur estimé de l'Académie Centrale Kaldélienne, est emprisonné pour corruption et aggression sexuelle. Le corps enseignant y est remanié sur ordre de Lazarus Tymos, comte de Nacride.",
category: 'criminalité',
secondaryCategories: ['scandale'],
wiki: 'https://alexcreates.fr/leim/index.php/Tivian_Rodhus'
},
// "Les Milles Cages" // "Les Milles Cages"
{ {
title: "Arrivée d'aventuriers à Borélis", title: "Arrivée d'aventuriers à Borélis",
date: { day: 12, month: 7, year: 3209 }, startDate: { day: 12, month: 7, year: 3209 },
description: description:
'Tara Belyus, Vascylly et Adol Sulvan livrent 3 condamnés à Handany. Ils partent pour la mer durant la journée.', 'Tara Belyus, Vascylly et Adol Sulvan livrent 3 condamnés à Handany. Ils partent pour la mer durant la journée.',
category: 'joueurs' category: 'joueurs'
@@ -44,25 +141,25 @@ export const regularEvents: CalendarEvent[] = [
title: "Naufrage de l'Éclipse", title: "Naufrage de l'Éclipse",
description: description:
"L'Éclipse, le navire de la garde contenant des condamnés à destination des Cages Handaniennes, s'échoue au large des côtes montagneuses de la Lance d'Aldys.", "L'Éclipse, le navire de la garde contenant des condamnés à destination des Cages Handaniennes, s'échoue au large des côtes montagneuses de la Lance d'Aldys.",
date: { day: 14, month: 7, year: 3209 }, startDate: { day: 14, month: 7, year: 3209 },
category: 'catastrophe' category: 'catastrophe'
}, },
{ {
title: 'Emprisonnement de Tivian Rodhus', title: 'Emprisonnement de Tivian Rodhus',
description: "Celui qu'on surnomme la Bête d'Ambrose arrive à Handany, où il purgera sa peine.", description: "Celui qu'on surnomme la Bête d'Ambrose arrive à Handany, où il purgera sa peine.",
date: { day: 14, month: 7, year: 3209 }, startDate: { day: 14, month: 7, year: 3209 },
category: 'législation' category: 'législation'
}, },
{ {
title: 'Sulvan et Anastael atteignent Bamast', title: 'Sulvan et Anastael atteignent Bamast',
date: { day: 19, month: 2, year: 3210 }, startDate: { day: 19, month: 2, year: 3210 },
category: 'joueurs' category: 'joueurs'
}, },
{ {
title: 'Jugement de Bormis Griloup', title: 'Jugement de Bormis Griloup',
description: description:
"Bromis Griloup est jugé coupable d'escroquerie et sabotage aux Cours d'Acier de Tourgrise. Il purgera une peine de 10 ans au sein des prisons royales.", "Bromis Griloup est jugé coupable d'escroquerie et sabotage aux Cours d'Acier de Tourgrise. Il purgera une peine de 10 ans au sein des prisons royales.",
date: { day: 4, month: 8, year: 3209 }, startDate: { day: 4, month: 8, year: 3209 },
category: 'législation' category: 'législation'
}, },
@@ -70,42 +167,42 @@ export const regularEvents: CalendarEvent[] = [
{ {
title: 'Inauguration de la Cloche du Pilier', title: 'Inauguration de la Cloche du Pilier',
description: "Le Moine Premier inaugure la grande cloche d'argent au sommet du Pilier d'Ikami.", description: "Le Moine Premier inaugure la grande cloche d'argent au sommet du Pilier d'Ikami.",
date: { day: 29, month: 5, year: 3209 }, startDate: { day: 29, month: 5, year: 3209 },
category: 'religion', category: 'religion',
hidden: true secondaryCategories: ['inauguration']
}, },
{ {
title: '1ère disparation à Cantane', title: '1ère disparation à Cantane',
description: "Taleb Vaht décède dans une grotte à la suite d'une attaque d'ischiels enragées.", description: "Taleb Vaht décède dans une grotte à la suite d'une attaque d'ischiels enragées.",
date: { day: 28, month: 7, year: 3209 }, startDate: { day: 28, month: 7, year: 3209 },
category: 'mort', category: 'mort',
hidden: true hidden: true
}, },
{ {
title: '2ème disparation à Cantane', title: '2ème disparation à Cantane',
description: 'Donovane le mineur kéturien disparait sans laisser de traces.', description: 'Donovane le mineur kéturien disparait sans laisser de traces.',
date: { day: 32, month: 7, year: 3209 }, startDate: { day: 32, month: 7, year: 3209 },
category: 'mort', category: 'mort',
hidden: true hidden: true
}, },
{ {
title: '3ème disparation à Cantane', title: '3ème disparation à Cantane',
description: 'Disparition de Sébastien, gredin sauride.', description: 'Disparition de Sébastien, gredin sauride.',
date: { day: 10, month: 8, year: 3209 }, startDate: { day: 10, month: 8, year: 3209 },
category: 'mort', category: 'mort',
hidden: true hidden: true
}, },
{ {
title: '4ème disparation à Cantane', title: '4ème disparation à Cantane',
description: 'Disparition de Thérence, patrouilleur sauride de la Vieille Garde.', description: 'Disparition de Thérence, patrouilleur sauride de la Vieille Garde.',
date: { day: 19, month: 8, year: 3209 }, startDate: { day: 19, month: 8, year: 3209 },
category: 'mort', category: 'mort',
hidden: true hidden: true
}, },
{ {
title: '5ème disparation à Cantane', title: '5ème disparation à Cantane',
description: 'Disparition de Mathilda Boulais, vendeuse de pierres.', description: 'Disparition de Mathilda Boulais, vendeuse de pierres.',
date: { day: 22, month: 8, year: 3209 }, startDate: { day: 22, month: 8, year: 3209 },
category: 'mort', category: 'mort',
hidden: true hidden: true
}, },
@@ -113,7 +210,8 @@ export const regularEvents: CalendarEvent[] = [
title: 'Grande Banque Minérale de Cantane', title: 'Grande Banque Minérale de Cantane',
description: description:
'Les artisans et mineurs de Rougefer se réunissent à Cantane pour vendre le fruit de leur dur labeur.', 'Les artisans et mineurs de Rougefer se réunissent à Cantane pour vendre le fruit de leur dur labeur.',
date: { day: 23, month: 8, year: 3209 }, startDate: { day: 23, month: 8, year: 3209 },
category: 'inauguration' endDate: { day: 26, month: 8, year: 3209 },
category: 'commerce'
} }
] ]

View File

@@ -90,7 +90,11 @@ export function getDifferenceInDays(baseDate: LeimDate, relativeDate: LeimDate):
* @param relativeDate The year to compare it to * @param relativeDate The year to compare it to
* @returns A string with info on how the relative date differs to the base date * @returns A string with info on how the relative date differs to the base date
*/ */
export function getRelativeString(baseDate: LeimDate, relativeDate: LeimDate): string { export function getRelativeString(
baseDate: LeimDate,
relativeDate: LeimDate,
formatting: 'compact' | 'complex' = 'complex'
): string {
const differenceInDays: number = getDifferenceInDays(baseDate, relativeDate) const differenceInDays: number = getDifferenceInDays(baseDate, relativeDate)
let output: string = '' let output: string = ''
let direction: 'past' | 'present' | 'future' = 'present' let direction: 'past' | 'present' | 'future' = 'present'
@@ -103,31 +107,33 @@ export function getRelativeString(baseDate: LeimDate, relativeDate: LeimDate): s
direction = 'past' direction = 'past'
} }
// Handle if it's the same date if (formatting === 'complex') {
if (direction === 'present') { // Handle if it's the same date
return "Aujourd'hui" if (direction === 'present') {
} return "Aujourd'hui"
if (differenceInDays === -2) { }
return 'Avant-hier' if (differenceInDays === -2) {
} return 'Avant-hier'
if (differenceInDays === -1) { }
return 'Hier' if (differenceInDays === -1) {
} return 'Hier'
if (differenceInDays === 1) { }
return 'Demain' if (differenceInDays === 1) {
} return 'Demain'
if (differenceInDays === 2) { }
return 'Après-demain' if (differenceInDays === 2) {
} return 'Après-demain'
}
// Get relevant prefix for the string // Get relevant prefix for the string
if (direction === 'future') { if (direction === 'future') {
directionPrefix = 'Dans' directionPrefix = 'Dans '
} else if (direction === 'past') { } else if (direction === 'past') {
directionPrefix = 'Il y a' directionPrefix = 'Il y a '
} }
output += directionPrefix output += directionPrefix
}
const yearPackets: number = Math.abs(Math.trunc(differenceInDays / daysPerYear)) const yearPackets: number = Math.abs(Math.trunc(differenceInDays / daysPerYear))
const monthPackets: number = Math.abs(Math.trunc(differenceInDays / daysPerMonth) % monthsPerYear) const monthPackets: number = Math.abs(Math.trunc(differenceInDays / daysPerMonth) % monthsPerYear)
@@ -138,9 +144,9 @@ export function getRelativeString(baseDate: LeimDate, relativeDate: LeimDate): s
// Assign year part // Assign year part
if (yearPackets) { if (yearPackets) {
if (yearPackets === 1) { if (yearPackets === 1) {
output += ` ${yearPackets} an` output += `${yearPackets} an`
} else { } else {
output += ` ${yearPackets} ans` output += `${yearPackets} ans`
} }
} }

View File

@@ -2,7 +2,8 @@ import type { LeimDate } from './Date'
export interface CalendarEvent { export interface CalendarEvent {
title: string title: string
date: LeimDate startDate: LeimDate
endDate?: LeimDate
description?: string description?: string
category?: CalendarEventCategory category?: CalendarEventCategory
secondaryCategories?: CalendarEventCategory[] secondaryCategories?: CalendarEventCategory[]
@@ -21,11 +22,18 @@ export const calendarEventCategories = [
'invention', 'invention',
'science', 'science',
'bénédiction', 'bénédiction',
'joueurs' 'joueurs',
] 'découverte',
'exploration',
'construction',
'arcanologie',
'criminalité',
'scandale',
'commerce'
] as const
export type CalendarEventCategory = (typeof calendarEventCategories)[number] export type CalendarEventCategory = (typeof calendarEventCategories)[number]
export function isCalendarEvent(object: any): object is CalendarEvent { export function isCalendarEvent(object: any): object is CalendarEvent {
return 'date' in object return 'startDate' in object
} }

View File

@@ -1,35 +1,37 @@
import { regularEvents } from '@/data/Events' import { initialEvents } from '@/data/Events'
import { convertDateToDays, daysPerMonth } from '@/models/Date' import { compareDates, convertDateToDays, daysPerMonth, type LeimDate } from '@/models/Date'
import type { CalendarEvent } from '@/models/Events' import type { CalendarEvent } from '@/models/Events'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, watch, type Ref } from 'vue' import { ref, watch, type Ref } from 'vue'
import { useCalendar } from './CalendarStore' import { useCalendar } from './CalendarStore'
import { useCharacters } from './CharacterStore' // import { useCharacters } from './CharacterStore'
export const useCalendarEvents = defineStore('calendar-events', () => { export const useCalendarEvents = defineStore('calendar-events', () => {
const { currentDate, currentConfig } = useCalendar() const { currentDate, currentConfig } = useCalendar()
const { charactersWithBirthData, charactersWithDeathData } = useCharacters() // const { charactersWithBirthData, charactersWithDeathData } = useCharacters()
const baseEvents: CalendarEvent[] = regularEvents const baseEvents: CalendarEvent[] = initialEvents
const characterBirthEvents = charactersWithBirthData.map((character) => { // const characterBirthEvents = charactersWithBirthData.map((character) => {
return { // return {
title: `Naissance de ${character.name}`, // title: `Naissance de ${character.name}`,
date: character.birth, // startDate: character.birth,
category: 'naissance' // category: 'naissance'
} as CalendarEvent // } as CalendarEvent
// })
// const characterDeathEvents = charactersWithDeathData.map((character) => {
// return {
// title: `Décès de ${character.name}`,
// startDate: character.death,
// category: 'mort'
// } as CalendarEvent
// })
const allEvents = [...baseEvents].sort((a, b) => {
return compareDates(a.startDate, b.startDate, 'desc')
}) })
const characterDeathEvents = charactersWithDeathData.map((character) => {
return {
title: `Décès de ${character.name}`,
date: character.death,
category: 'mort'
} as CalendarEvent
})
const allEvents = [...characterBirthEvents, ...characterDeathEvents, ...baseEvents]
// Gets all current event in its default state // Gets all current event in its default state
const currentEvents: Ref<CalendarEvent[]> = ref(computeCurrentEvents()) const currentEvents: Ref<CalendarEvent[]> = ref(computeCurrentEvents())
@@ -47,9 +49,15 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
* @returns Whether the event should appear in the current view * @returns Whether the event should appear in the current view
*/ */
function shouldEventBeDisplayed(event: CalendarEvent): boolean { function shouldEventBeDisplayed(event: CalendarEvent): boolean {
const eventDateToDays = convertDateToDays(event.date) const eventStartDateToDays = convertDateToDays(event.startDate)
const eventEndDateToDays: number = event.endDate ? convertDateToDays(event.endDate) : 0
const isEventOnCurrentScreen = const isEventOnCurrentScreen =
event.date.year === currentDate.currentYear && event.date.month === currentDate.currentMonth (event.startDate.year === currentDate.currentYear &&
event.startDate.month === currentDate.currentMonth) ||
(event.endDate &&
event.endDate.year === currentDate.currentYear &&
event.endDate.month === currentDate.currentMonth)
// Check whether the event is on the last 8 tiles // Check whether the event is on the last 8 tiles
// This is to allow leap events from appearing on the last 8 tiles // This is to allow leap events from appearing on the last 8 tiles
@@ -62,25 +70,28 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
const last8Tiles = lastDayOfCurrentMonth + 8 const last8Tiles = lastDayOfCurrentMonth + 8
const isEventOnNext8Tiles = const isEventOnNext8Tiles =
eventDateToDays <= last8Tiles && eventDateToDays >= lastDayOfCurrentMonth (eventStartDateToDays <= last8Tiles && eventStartDateToDays >= lastDayOfCurrentMonth) ||
(Boolean(event.endDate) &&
eventEndDateToDays <= last8Tiles &&
eventEndDateToDays >= lastDayOfCurrentMonth)
switch (currentConfig.viewType) { switch (currentConfig.viewType) {
case 'month': case 'month':
return isEventOnCurrentScreen || isEventOnNext8Tiles return isEventOnCurrentScreen || isEventOnNext8Tiles
case 'year': case 'year':
return event.date.year === currentDate.currentYear return event.startDate.year === currentDate.currentYear
case 'decade': case 'decade':
return ( return (
event.date.year >= currentDate.currentYear && event.startDate.year >= currentDate.currentYear &&
event.date.year <= currentDate.currentYear + 10 event.startDate.year <= currentDate.currentYear + 10
) )
case 'century': case 'century':
return ( return (
event.date.year >= currentDate.currentYear && event.startDate.year >= currentDate.currentYear &&
event.date.year <= currentDate.currentYear + 100 event.startDate.year <= currentDate.currentYear + 100
) )
default: default:
@@ -97,5 +108,108 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
return allEvents.filter((event) => shouldEventBeDisplayed(event)) return allEvents.filter((event) => shouldEventBeDisplayed(event))
} }
return { baseEvents, allEvents, currentEvents } /**
* From a base event, gets the next or previous in the timeline
* @todo **This should probably be extracted to function FIRST with dates only, as the initialIsEnd param is only used at the beggining to establish a pivot**
*
* @param event The event at a given position in the data
* @param position Whether we should get the next or previous event
* @returns The next event in chronological order
*/
function getRelativeEventFromEvent(
event: CalendarEvent,
position: 'next' | 'prev' = 'next',
initialIsEnd: boolean = false
): { event: CalendarEvent; targetDate: LeimDate } {
let dateToParse: LeimDate // Day value of the date that the user interacted with
if (initialIsEnd && event.endDate) {
dateToParse = event.endDate
} else {
dateToParse = event.startDate
}
return getRelativeEventFromDate(dateToParse, position)
}
function getRelativeEventFromDate(
date: LeimDate,
position: 'next' | 'prev' = 'next'
): { event: CalendarEvent; targetDate: LeimDate } {
const pivotValue = convertDateToDays(date)
let t: { eventData: CalendarEvent; distance: number; targetKey: 'startDate' | 'endDate' }[] = []
// Loop over all event once to convert the structure to a usable one
for (let i = 0; i < allEvents.length; i++) {
const e: CalendarEvent = allEvents[i]
// Estimate distance from pivot
const startDateDays: number = convertDateToDays(e.startDate)
const startDistance: number = startDateDays - pivotValue
// Push startDate to comparator array
t.push({
eventData: e,
distance: startDistance,
targetKey: 'startDate'
})
// Check the same things for endDate
if (e.endDate) {
const endDateDays: number = convertDateToDays(e.endDate)
const endDistance: number = endDateDays - pivotValue
// Push optional endDate to comparator array
t.push({
eventData: e,
distance: endDistance,
targetKey: 'endDate'
})
}
}
// // Special case : If we query the first one but it already is
// if (position === 'prev' && t[0].distance === 0) {
// const targetDate =
// t[0].targetKey === 'startDate' ? t[0].eventData.startDate : t[0].eventData.endDate!
// return {
// event: t[0].eventData,
// targetDate: targetDate
// }
// }
// // Special case : If we query the last one but it already is
// if (position === 'next' && t[t.length - 1].distance === 0) {
// const targetDate =
// t[t.length - 1].targetKey === 'startDate'
// ? t[t.length - 1].eventData.startDate
// : t[t.length - 1].eventData.endDate!
// return {
// event: t[t.length - 1].eventData,
// targetDate: targetDate
// }
// }
// Based on the direction, either ignore negative distance (past) or positive distance (future)
t = t.filter((i) => {
return position === 'next' ? i.distance > 0 : i.distance < 0
})
if (!t.length) {
throw new Error(
"Aucun évènement suivant ou précédent trouvé ; Peut-être l'évènement se situe au début ou à la fin du calendrier ?"
)
}
// Get event with remaining minimum distance
const closestEvent = t.reduce((a, b) => {
return Math.abs(b.distance) < Math.abs(a.distance) ? b : a
})
return {
event: closestEvent.eventData,
targetDate: closestEvent.eventData[closestEvent.targetKey]!
}
}
return { allEvents, currentEvents, getRelativeEventFromDate, getRelativeEventFromEvent }
}) })

View File

@@ -2,6 +2,8 @@
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { onMounted } from 'vue' import { onMounted } from 'vue'
import { useCalendar } from '@/stores/CalendarStore' import { useCalendar } from '@/stores/CalendarStore'
import { useTitle } from '@vueuse/core'
import { titleTemplate } from './meta'
import Calendar from '@/components/calendar/Calendar.vue' import Calendar from '@/components/calendar/Calendar.vue'
import Sidebar from '@/components/calendar/Sidebar.vue' import Sidebar from '@/components/calendar/Sidebar.vue'
@@ -11,6 +13,8 @@ const { validateUrlParams } = useCalendar()
const { isAdvancedSearchOpen } = storeToRefs(useCalendar()) const { isAdvancedSearchOpen } = storeToRefs(useCalendar())
onMounted(() => validateUrlParams()) onMounted(() => validateUrlParams())
useTitle('Calendrier', titleTemplate)
</script> </script>
<template> <template>

View File

@@ -1,6 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { useTitle } from '@vueuse/core'
import { titleTemplate } from './meta'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
useTitle('Outils', titleTemplate)
</script> </script>
<template> <template>

1
src/views/meta.ts Normal file
View File

@@ -0,0 +1 @@
export const titleTemplate = { titleTemplate: '%s — TTTools' }