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.
## 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
```
## 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.

View File

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

8
package-lock.json generated
View File

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

View File

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

View File

@@ -1,38 +1,72 @@
<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 type { CalendarEvent } from '@/models/Events'
import CalendarEventDetails from './CalendarEventDetails.vue'
defineProps<{
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>
<Popover>
<Popover v-model:open="isPopoverOpen">
<PopoverTrigger as-child>
<button
class="text-xs px-2 py-1 block w-full text-left rounded-sm whitespace-nowrap overflow-hidden text-ellipsis"
: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-yellow-100 hover:bg-yellow-200': event.category === 'bénédiction'
}"
: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>
</PopoverTrigger>
<CalendarEventDetails :event v-once />
<CalendarEventDetails
:event
:spans-multiple-days
:is-start-event
:is-end-event
@query:close-popover="handleClosePopover"
v-once
/>
</Popover>
</template>

View File

@@ -1,29 +1,67 @@
<script lang="ts" setup>
import { getRelativeString } from '@/models/Date'
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 } from '@phosphor-icons/vue'
import {
PhHourglassMedium,
PhAlarm,
PhHourglassHigh,
PhHourglassLow,
PhArrowBendDoubleUpLeft,
PhArrowBendDoubleUpRight
} from '@phosphor-icons/vue'
import { Badge } from '@/components/ui/badge'
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>
<template>
<PopoverContent
class="w-96"
align="start"
class="event-details w-96"
:align="'center'"
:align-offset="50"
:collision-padding="20"
:side="'left'"
:collision-padding="60"
:hide-when-detached="true"
:class="{
'border-slate-800': !event.category,
'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-pink-800': event.category === 'catastrophe naturelle',
'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-green-800': event.category === 'invention',
'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">
@@ -40,14 +85,30 @@ const dateDifference: string = getRelativeString(defaultDate, props.event.date)
{{ event.title }}
</div>
<div class="mb-1 space-y-1">
<p class="font-semibold">{{ getFormattedDateTitle(event.date, true) }}</p>
<p class="text-sm italic opacity-75 flex items-center gap-1">
<PhHourglassMedium size="16" weight="fill" /> {{ dateDifference }}
</p>
<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 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">
<li v-if="event.category">
<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>
</li>
</ul>
</div>
</template>
<div class="mt-2 italic text-sm text-slate-500">
{{ event.description }}
</div>
<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">
<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>
</template>
<style lang="scss">
.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 {
background-color: color-mix(in srgb, var(--color-lime-800), var(--color-slate-950) 85%);
--base-color: var(--color-lime-800);
}
.border-stone-800 {
background-color: color-mix(in srgb, var(--color-stone-800), var(--color-slate-950) 85%);
.border-stone-600 {
--base-color: var(--color-stone-600);
}
.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 {
background-color: color-mix(in srgb, var(--color-pink-800), var(--color-slate-950) 85%);
--base-color: var(--color-pink-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 {
background-color: color-mix(in srgb, var(--color-purple-800), var(--color-slate-950) 85%);
--base-color: var(--color-purple-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 {
background-color: color-mix(in srgb, var(--color-amber-800), var(--color-slate-950) 85%);
--base-color: var(--color-amber-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 {
background-color: color-mix(in srgb, var(--color-cyan-800), var(--color-slate-950) 85%);
--base-color: var(--color-cyan-800);
}
.border-yellow-800 {
background-color: color-mix(in srgb, var(--color-yellow-800), var(--color-slate-950) 85%);
.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

@@ -2,12 +2,14 @@
import { useCalendar } from '@/stores/CalendarStore'
import { Button } from '@/components/ui/button'
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 { currentDate, revealAdvancedSearch } = useCalendar()
const { revealAdvancedSearch } = useCalendar()
</script>
<template>
@@ -37,10 +39,9 @@ const { currentDate, revealAdvancedSearch } = useCalendar()
</li>
</menu>
</div>
<div class="ml-12 flex">
<div class="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="ml-6">
<CalendarMenuSubnav />
</div>
</header>
</template>

View File

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

View File

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

View File

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

View File

@@ -1,33 +1,27 @@
<script lang="ts" setup>
import {
characterCategories,
isCharacter,
type Character,
characterCategories,
type CharacterCategory
} from '@/models/Characters'
import type { LeimDateOrder } from '@/models/Date'
import {
calendarEventCategories,
isCalendarEvent,
type CalendarEvent,
calendarEventCategories,
type CalendarEventCategory
} from '@/models/Events'
import { capitalize } from '@/utils/Strings'
import { useCharacters } from '@/stores/CharacterStore'
import { useCalendarEvents } from '@/stores/EventStore'
import { useMagicKeys, useStorage, whenever } from '@vueuse/core'
import { computed, ref } from 'vue'
import { capitalize } from '@/utils/Strings'
import { useMagicKeys, useScroll, useStorage, whenever } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import { searchUnifier, type SearchMode } from '../Search'
import { Button } from '@/components/ui/button'
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/components/ui/command'
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog'
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import {
Pagination,
@@ -60,7 +54,7 @@ import {
import SearchList from './lists/SearchList.vue'
const { characters } = useCharacters()
const { baseEvents } = useCalendarEvents()
const { allEvents } = useCalendarEvents()
const modalOpen = defineModel({ default: false })
@@ -87,6 +81,9 @@ 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
}
@@ -99,11 +96,11 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Assign data to loop over and filter
// They are assigned this way for readability
if (selectedEntity.value === 'events') {
dataToFilter = baseEvents
dataToFilter = allEvents
} else if (selectedEntity.value === 'characters') {
dataToFilter = characters
} else {
dataToFilter = [...baseEvents, ...characters]
dataToFilter = [...allEvents, ...characters]
}
/**
@@ -142,7 +139,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
}
hitCategories = selectedCategories.value.every((selectedCat) => {
return allCategories.includes(selectedCat)
return allCategories.includes(selectedCat as CalendarEventCategory)
})
return (hitTitle || hitDesc) && hitCategories
@@ -165,7 +162,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Handle categories logic
let hitCategories: boolean = false
let allCategories: CalendarEventCategory[] = []
let allCategories: CharacterCategory[] = []
if (item.category) {
allCategories.push(item.category)
@@ -176,7 +173,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
}
hitCategories = selectedCategories.value.every((selectedCat) => {
return allCategories.includes(selectedCat)
return allCategories.includes(selectedCat as CharacterCategory)
})
return hitTitle && hitCategories
@@ -186,20 +183,32 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
return results
})
/**
* Removes the search query, resets the pagination and removes all selected categories
*/
function resetSearch() {
searchQuery.value = ''
resetPage()
selectedCategories.value = []
}
/**
* Opens the search dialog
*/
function openDialog() {
modalOpen.value = true
}
/**
* Closes the search dialog
*/
function closeDialog() {
modalOpen.value = false
}
/**
* Switches the selectedEntity
*/
function handleEntitySwitch() {
resetPage()
selectedCategories.value = []
@@ -212,7 +221,14 @@ whenever(keys.control_period, () => {
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(() => {
if (selectedEntity.value === 'characters') {
return [...characterCategories]
@@ -225,17 +241,22 @@ const selectedCategories = ref<(CharacterCategory | CalendarEventCategory)[]>([]
const categoryFilterOpened = ref<boolean>(false)
const searchCategory = ref<string>('')
const filteredFrameworks = computed(() =>
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: any) {
if (typeof e.detail.value === 'string') {
searchCategory.value = ''
selectedCategories.value.push(e.detail.value)
}
if (filteredFrameworks.value.length === 0) {
if (filteredCategories.value.length === 0) {
categoryFilterOpened.value = false
}
}
@@ -292,7 +313,7 @@ function handleCategorySelect(e: any) {
</div>
<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">
<TagsInputItem v-for="item in selectedCategories" :key="item" :value="item">
<TagsInputItemText class="capitalize" />
@@ -325,7 +346,7 @@ function handleCategorySelect(e: any) {
<CommandEmpty />
<CommandGroup>
<CommandItem
v-for="framework in filteredFrameworks"
v-for="framework in filteredCategories"
:key="framework"
:value="framework"
@select.prevent="handleCategorySelect"
@@ -340,7 +361,7 @@ function handleCategorySelect(e: any) {
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger>
<TooltipTrigger as-child>
<Button
:variant="selectedOrder === 'desc' ? 'secondary' : 'outline'"
size="icon"
@@ -357,7 +378,7 @@ function handleCategorySelect(e: any) {
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger>
<TooltipTrigger as-child>
<Button
:variant="selectedOrder === 'asc' ? 'secondary' : 'outline'"
size="icon"
@@ -377,7 +398,7 @@ function handleCategorySelect(e: any) {
<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
:results="searchResults"
: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>
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 { useCalendar } from '@/stores/CalendarStore'
import { computed } from 'vue'
import { computed, ref } from 'vue'
import type { SearchMode } from '../../Search'
import { PhHourglassMedium, PhPlant, PhSkull, PhArrowSquareOut } from '@phosphor-icons/vue'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import CharacterCallout from './CharacterCallout.vue'
import EventCallout from './EventCallout.vue'
import { useScroll } from '@vueuse/core'
const props = defineProps<{
results: (Character | CalendarEvent)[]
@@ -22,7 +21,7 @@ const props = defineProps<{
const emit = defineEmits(['jumpedToDate'])
const { defaultDate, getFormattedDateTitle, jumpToDate } = useCalendar()
const { jumpToDate } = useCalendar()
function handleJumpToDate(date?: LeimDate) {
if (!date) return
@@ -38,7 +37,7 @@ const sortedResults = computed(() => {
let secondDate: LeimDate
if (isCalendarEvent(a)) {
firstDate = a.date
firstDate = a.startDate
} else if (isCharacter(a) && a.birth) {
firstDate = a.birth
} else {
@@ -46,7 +45,7 @@ const sortedResults = computed(() => {
}
if (isCalendarEvent(b)) {
secondDate = b.date
secondDate = b.startDate
} else if (isCharacter(b) && b.birth) {
secondDate = b.birth
} else {
@@ -64,133 +63,13 @@ const pagedResults = computed(() => sortedResults.value.slice(props.startAt, pro
<template>
<ul class="grid gap-4">
<li v-for="r in pagedResults" :key="isCalendarEvent(r) ? r.title : r.name">
<button
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>
<EventCallout v-if="isCalendarEvent(r)" :event="r" @query:date-jump="handleJumpToDate" />
<div class="mb-1 flex gap-4 items-center">
<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
<CharacterCallout
v-else-if="isCharacter(r)"
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">
{{ 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>
:character="r"
@query:date-jump="handleJumpToDate"
/>
</li>
</ul>
</template>

View File

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

View File

@@ -24,7 +24,10 @@ const { currentEvents } = storeToRefs(useCalendarEvents())
const eventsForTheDay = computed(() => {
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">
<CalendarEventButton :event />
<CalendarEventButton :event :tile-date="date" />
</li>
<li v-if="eventsNotDisplayed > 0" class="pointer-events-auto">
@@ -116,7 +119,7 @@ const eventsNotDisplayed = computed(
:key="event.title"
class="grid pointer-events-auto"
>
<CalendarEventButton :event />
<CalendarEventButton :event :tile-date="date" />
</li>
</ul>
</PopoverContent>

View File

@@ -33,7 +33,7 @@ const isSelectedDate = computed<boolean>(() => {
const hasAtLeastOneEvent = computed<boolean>(() => {
return Boolean(
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"
:class="
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
)
"

View File

@@ -174,6 +174,15 @@ export const charactersList: Character[] = [
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"
{
name: 'Ernestin Pomel',

View File

@@ -1,10 +1,51 @@
import type { CalendarEvent } from '@/models/Events'
export const regularEvents: CalendarEvent[] = [
export const initialEvents: CalendarEvent[] = [
// 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',
date: { day: 1, month: 0, year: 0 },
startDate: { day: 1, month: 0, year: 0 },
description:
"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',
@@ -14,28 +55,84 @@ export const regularEvents: CalendarEvent[] = [
},
{
title: 'Traité de Kadel',
date: { day: 20, month: 4, year: 100 },
startDate: { day: 29, month: 4, year: 100 },
description: '',
category: 'inauguration',
secondaryCategories: ['législation'],
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
{
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',
date: { day: 4, month: 2, year: 3113 },
startDate: { day: 4, month: 2, year: 3113 },
description:
'Sophia de Rougefer invente la buse kaldélienne, une arme à feu souple à deux canons.',
category: 'invention',
secondaryCategories: ['science'],
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"
{
title: "Arrivée d'aventuriers à Borélis",
date: { day: 12, month: 7, year: 3209 },
startDate: { day: 12, month: 7, year: 3209 },
description:
'Tara Belyus, Vascylly et Adol Sulvan livrent 3 condamnés à Handany. Ils partent pour la mer durant la journée.',
category: 'joueurs'
@@ -44,25 +141,25 @@ export const regularEvents: CalendarEvent[] = [
title: "Naufrage de l'Éclipse",
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.",
date: { day: 14, month: 7, year: 3209 },
startDate: { day: 14, month: 7, year: 3209 },
category: 'catastrophe'
},
{
title: 'Emprisonnement de Tivian Rodhus',
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'
},
{
title: 'Sulvan et Anastael atteignent Bamast',
date: { day: 19, month: 2, year: 3210 },
startDate: { day: 19, month: 2, year: 3210 },
category: 'joueurs'
},
{
title: 'Jugement de Bormis Griloup',
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.",
date: { day: 4, month: 8, year: 3209 },
startDate: { day: 4, month: 8, year: 3209 },
category: 'législation'
},
@@ -70,42 +167,42 @@ export const regularEvents: CalendarEvent[] = [
{
title: 'Inauguration de la Cloche du Pilier',
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',
hidden: true
secondaryCategories: ['inauguration']
},
{
title: '1ère disparation à Cantane',
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',
hidden: true
},
{
title: '2ème disparation à Cantane',
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',
hidden: true
},
{
title: '3ème disparation à Cantane',
description: 'Disparition de Sébastien, gredin sauride.',
date: { day: 10, month: 8, year: 3209 },
startDate: { day: 10, month: 8, year: 3209 },
category: 'mort',
hidden: true
},
{
title: '4ème disparation à Cantane',
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',
hidden: true
},
{
title: '5ème disparation à Cantane',
description: 'Disparition de Mathilda Boulais, vendeuse de pierres.',
date: { day: 22, month: 8, year: 3209 },
startDate: { day: 22, month: 8, year: 3209 },
category: 'mort',
hidden: true
},
@@ -113,7 +210,8 @@ export const regularEvents: CalendarEvent[] = [
title: 'Grande Banque Minérale de Cantane',
description:
'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 },
category: 'inauguration'
startDate: { day: 23, month: 8, year: 3209 },
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
* @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)
let output: string = ''
let direction: 'past' | 'present' | 'future' = 'present'
@@ -103,31 +107,33 @@ export function getRelativeString(baseDate: LeimDate, relativeDate: LeimDate): s
direction = 'past'
}
// Handle if it's the same date
if (direction === 'present') {
return "Aujourd'hui"
}
if (differenceInDays === -2) {
return 'Avant-hier'
}
if (differenceInDays === -1) {
return 'Hier'
}
if (differenceInDays === 1) {
return 'Demain'
}
if (differenceInDays === 2) {
return 'Après-demain'
}
if (formatting === 'complex') {
// Handle if it's the same date
if (direction === 'present') {
return "Aujourd'hui"
}
if (differenceInDays === -2) {
return 'Avant-hier'
}
if (differenceInDays === -1) {
return 'Hier'
}
if (differenceInDays === 1) {
return 'Demain'
}
if (differenceInDays === 2) {
return 'Après-demain'
}
// Get relevant prefix for the string
if (direction === 'future') {
directionPrefix = 'Dans'
} else if (direction === 'past') {
directionPrefix = 'Il y a'
}
// Get relevant prefix for the string
if (direction === 'future') {
directionPrefix = 'Dans '
} else if (direction === 'past') {
directionPrefix = 'Il y a '
}
output += directionPrefix
output += directionPrefix
}
const yearPackets: number = Math.abs(Math.trunc(differenceInDays / daysPerYear))
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
if (yearPackets) {
if (yearPackets === 1) {
output += ` ${yearPackets} an`
output += `${yearPackets} an`
} else {
output += ` ${yearPackets} ans`
output += `${yearPackets} ans`
}
}

View File

@@ -2,7 +2,8 @@ import type { LeimDate } from './Date'
export interface CalendarEvent {
title: string
date: LeimDate
startDate: LeimDate
endDate?: LeimDate
description?: string
category?: CalendarEventCategory
secondaryCategories?: CalendarEventCategory[]
@@ -21,11 +22,18 @@ export const calendarEventCategories = [
'invention',
'science',
'bénédiction',
'joueurs'
]
'joueurs',
'découverte',
'exploration',
'construction',
'arcanologie',
'criminalité',
'scandale',
'commerce'
] as const
export type CalendarEventCategory = (typeof calendarEventCategories)[number]
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 { convertDateToDays, daysPerMonth } from '@/models/Date'
import { initialEvents } from '@/data/Events'
import { compareDates, convertDateToDays, daysPerMonth, type LeimDate } from '@/models/Date'
import type { CalendarEvent } from '@/models/Events'
import { defineStore } from 'pinia'
import { ref, watch, type Ref } from 'vue'
import { useCalendar } from './CalendarStore'
import { useCharacters } from './CharacterStore'
// import { useCharacters } from './CharacterStore'
export const useCalendarEvents = defineStore('calendar-events', () => {
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) => {
return {
title: `Naissance de ${character.name}`,
date: character.birth,
category: 'naissance'
} as CalendarEvent
// const characterBirthEvents = charactersWithBirthData.map((character) => {
// return {
// title: `Naissance de ${character.name}`,
// startDate: character.birth,
// category: 'naissance'
// } 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
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
*/
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 =
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
// 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 isEventOnNext8Tiles =
eventDateToDays <= last8Tiles && eventDateToDays >= lastDayOfCurrentMonth
(eventStartDateToDays <= last8Tiles && eventStartDateToDays >= lastDayOfCurrentMonth) ||
(Boolean(event.endDate) &&
eventEndDateToDays <= last8Tiles &&
eventEndDateToDays >= lastDayOfCurrentMonth)
switch (currentConfig.viewType) {
case 'month':
return isEventOnCurrentScreen || isEventOnNext8Tiles
case 'year':
return event.date.year === currentDate.currentYear
return event.startDate.year === currentDate.currentYear
case 'decade':
return (
event.date.year >= currentDate.currentYear &&
event.date.year <= currentDate.currentYear + 10
event.startDate.year >= currentDate.currentYear &&
event.startDate.year <= currentDate.currentYear + 10
)
case 'century':
return (
event.date.year >= currentDate.currentYear &&
event.date.year <= currentDate.currentYear + 100
event.startDate.year >= currentDate.currentYear &&
event.startDate.year <= currentDate.currentYear + 100
)
default:
@@ -97,5 +108,108 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
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 { onMounted } from 'vue'
import { useCalendar } from '@/stores/CalendarStore'
import { useTitle } from '@vueuse/core'
import { titleTemplate } from './meta'
import Calendar from '@/components/calendar/Calendar.vue'
import Sidebar from '@/components/calendar/Sidebar.vue'
@@ -11,6 +13,8 @@ const { validateUrlParams } = useCalendar()
const { isAdvancedSearchOpen } = storeToRefs(useCalendar())
onMounted(() => validateUrlParams())
useTitle('Calendrier', titleTemplate)
</script>
<template>

View File

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

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

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