Merge branch 'dev'
This commit is contained in:
64
README.md
64
README.md
@@ -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
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -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
8
package-lock.json
generated
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
107
src/components/calendar/CalendarMenuSubnav.vue
Normal file
107
src/components/calendar/CalendarMenuSubnav.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
89
src/components/calendar/search/lists/CharacterCallout.vue
Normal file
89
src/components/calendar/search/lists/CharacterCallout.vue
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Character } from '@/models/Characters'
|
||||||
|
import type { LeimDate } from '@/models/Date'
|
||||||
|
import { useCalendar } from '@/stores/CalendarStore'
|
||||||
|
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { PhArrowSquareOut, PhPlant, PhSkull } from '@phosphor-icons/vue'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
character: Character
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'query:date-jump', payload: LeimDate): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { getFormattedDateTitle } = useCalendar()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="block w-full text-left py-3 px-4 border-[1px] border-slate-700 rounded-sm">
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<h2 class="font-bold">
|
||||||
|
{{ character.name }}
|
||||||
|
</h2>
|
||||||
|
<div v-if="character.wiki">
|
||||||
|
<Button variant="link" size="xs" as-child>
|
||||||
|
<a :href="character.wiki" target="_blank">
|
||||||
|
Wiki
|
||||||
|
<PhArrowSquareOut size="16" weight="fill" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<menu class="flex gap-2 border-[1px] border-slate-700 rounded-sm w-fit">
|
||||||
|
<li v-if="character.birth">
|
||||||
|
<TooltipProvider :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>
|
||||||
114
src/components/calendar/search/lists/EventCallout.vue
Normal file
114
src/components/calendar/search/lists/EventCallout.vue
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { getRelativeString, type LeimDate } from '@/models/Date'
|
||||||
|
import type { CalendarEvent } from '@/models/Events'
|
||||||
|
import { useCalendar } from '@/stores/CalendarStore'
|
||||||
|
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { PhArrowSquareOut, PhHourglassMedium, PhAlarm } from '@phosphor-icons/vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
event: CalendarEvent
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'query:date-jump', payload: LeimDate): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { defaultDate, getFormattedDateTitle } = useCalendar()
|
||||||
|
|
||||||
|
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate)
|
||||||
|
const dateDuration: string | null = props.event.endDate
|
||||||
|
? getRelativeString(props.event.startDate, props.event.endDate, 'compact')
|
||||||
|
: null
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="relative block w-full text-left py-3 px-4 rounded-sm transition-colors"
|
||||||
|
:class="{
|
||||||
|
'text-white bg-slate-600 hover:bg-slate-700': !event.category,
|
||||||
|
'text-white bg-lime-600 hover:bg-lime-700': event.category === 'naissance',
|
||||||
|
'text-white bg-stone-500 hover:bg-stone-700': event.category === 'mort',
|
||||||
|
'text-white bg-orange-600 hover:bg-orange-700': event.category === 'catastrophe',
|
||||||
|
'text-white bg-pink-600 hover:bg-pink-700': event.category === 'catastrophe naturelle',
|
||||||
|
'text-white bg-sky-600 hover:bg-sky-700': event.category === 'législation',
|
||||||
|
'text-white bg-purple-600 hover:bg-purple-700': event.category === 'religion',
|
||||||
|
'text-white bg-emerald-600 hover:bg-emerald-700': event.category === 'joueurs',
|
||||||
|
'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category === 'inauguration',
|
||||||
|
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category === 'invention',
|
||||||
|
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': event.category === 'science',
|
||||||
|
'text-slate-900 bg-white hover:bg-yellow-200': event.category === 'bénédiction',
|
||||||
|
'text-slate-900 bg-purple-200 hover:bg-purple-300': event.category === 'découverte',
|
||||||
|
'text-slate-900 bg-indigo-200 hover:bg-indigo-300': event.category === 'exploration',
|
||||||
|
'text-white bg-amber-600 hover:bg-amber-700': event.category === 'construction',
|
||||||
|
'text-slate-900 bg-violet-200 hover:bg-violet-300': event.category === 'arcanologie',
|
||||||
|
'text-white bg-rose-600 hover:bg-rose-700': event.category === 'criminalité',
|
||||||
|
'text-white bg-stone-600 hover:bg-stone-700': event.category === 'scandale',
|
||||||
|
'text-slate-900 bg-yellow-500 hover:bg-yellow-600': event.category === 'commerce'
|
||||||
|
}"
|
||||||
|
@click="$emit('query:date-jump', event.startDate)"
|
||||||
|
>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<h2 class="font-bold">
|
||||||
|
{{ event.title }}
|
||||||
|
</h2>
|
||||||
|
<div v-if="event.wiki">
|
||||||
|
<Button variant="link" size="xs" as-child class="text-inherit">
|
||||||
|
<a :href="event.wiki" target="_blank">
|
||||||
|
Wiki
|
||||||
|
<PhArrowSquareOut size="16" weight="fill" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-1">
|
||||||
|
<template v-if="!event.endDate">
|
||||||
|
<p class="col-span-2 font-semibold text-sm opacity-75">
|
||||||
|
{{ getFormattedDateTitle(event.startDate, true) }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<p class="col-span-2 font-semibold text-sm opacity-75">
|
||||||
|
Du {{ getFormattedDateTitle(event.startDate, true) }} au
|
||||||
|
{{ getFormattedDateTitle(event.endDate, true) }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-1 flex gap-x-2 items-center">
|
||||||
|
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
|
||||||
|
<PhAlarm size="16" weight="fill" /> {{ dateDifference }}
|
||||||
|
</p>
|
||||||
|
<template v-if="dateDuration">
|
||||||
|
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1">
|
||||||
|
<PhHourglassMedium size="16" weight="fill" /> Pendant {{ dateDuration }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="event.category || event.secondaryCategories" class="absolute top-3 right-4">
|
||||||
|
<ul class="flex gap-1">
|
||||||
|
<li v-if="event.category">
|
||||||
|
<Badge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
|
||||||
|
{{ event.category }}
|
||||||
|
</Badge>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li v-for="cat in event.secondaryCategories" :key="cat">
|
||||||
|
<Badge class="mix-blend-luminosity bg-gray-600" variant="secondary">
|
||||||
|
{{ cat }}
|
||||||
|
</Badge>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="event.description" class="text-sm">
|
||||||
|
<hr class="my-2 border-white opacity-50" />
|
||||||
|
<span class="opacity-75">
|
||||||
|
{{ event.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
1
src/views/meta.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const titleTemplate = { titleTemplate: '%s — TTTools' }
|
||||||
Reference in New Issue
Block a user