38 Commits

Author SHA1 Message Date
Alexis
46f2b0bbed Merge branch 'features/nuxt' into dev 2024-06-05 13:32:57 +02:00
Alexis
be4eb1a7c0 Merge branch 'features/nuxt-supabase' into features/nuxt 2024-06-05 13:32:51 +02:00
Alexis
c4b5ad724c Added delete functions for events 2024-06-04 18:47:54 +02:00
Alexis
373484f17c Added transition to updating event 2024-06-04 18:31:26 +02:00
Alexis
5f6a8e46c6 Added update route for event 2024-06-04 12:01:19 +02:00
Alexis
9802266b8c Added reactivity when adding an event 2024-06-03 21:39:19 +02:00
Alexis
5332b4c9c4 Updated form component
I'll need to refactor this to a Calendar level component
2024-06-02 10:35:18 +02:00
Alexis
a92edd6215 Added post component for events
This needs RLS disabled, will work on that later, I don't like postgre
2024-06-01 11:07:07 +02:00
Alexis
f2d5277c5f Merge branch 'features/nuxt-supabase' into features/nuxt 2024-05-24 20:03:21 +02:00
Alexis
1b801504f2 Reworked relative date operations
This is the most straightforward way to deal with months that _can_ be not the same length. I'm tired
2024-05-24 18:06:58 +02:00
Alexis
c0940c4985 Working event base 2024-05-22 19:27:43 +02:00
Alexis
030e3cdc12 Fixed negative number issue 2024-05-22 19:25:28 +02:00
Alexis
f361fdeb49 Fixed issue with past dates
Some past dates would render as the wrong year packet number
2024-05-20 18:55:09 +02:00
Alexis
a1d0ae0f35 Fixed getRelativeString function
We don't need to have a precise day for display (for now) but it sure would be nice to have
2024-05-20 18:17:13 +02:00
Alexis
784b5cf421 Fixed compareDate function for flexible months
This breaks the year layout, but it's 100% caused by the props of the screen, it's not caused by the date comparison
2024-05-20 12:07:13 +02:00
Alexis
ce2dbbc8a4 Added dummy data as temp bandaid 2024-05-18 21:30:12 +02:00
Alexis
13b8be9399 This broke most things tbh, the date functions need to be reworked to account for differently formatted calendars (how idk). This would fix most issues that stem from it in other screens 2024-05-18 20:56:38 +02:00
Alexis
73e4b0e3d7 Added default date config in calendar data 2024-05-18 18:01:49 +02:00
Alexis
3de59116aa Added secondary categories to events / characters 2024-05-18 17:23:27 +02:00
Alexis
471df5e559 Fixed loading error with calendar store 2024-05-18 15:45:17 +02:00
Alexis
059edd6fc1 Added loading logic to calendar page 2024-05-17 20:50:45 +02:00
Alexis
5dd06ea3f8 Fixed errors caused by new event model 2024-05-17 17:03:56 +02:00
Alexis
67f5a270af Huge refactor for backend and client
This is barely functionnal, but at least it can display some data without crashing
Most other functionnalities other than displaying events are broken, and so are the relative date operations, they need to be fixed asap
2024-05-16 22:58:19 +02:00
Alexis
49e523485b Remodeled database and api to prepare for move 2024-05-15 17:18:17 +02:00
Alexis
3a8e8b0947 Adjusted role permissions 2024-05-14 12:29:31 +02:00
Alexis
0588cb775e Merge branch 'features/nuxt-auth' into features/nuxt 2024-05-14 12:07:11 +02:00
Alexis
f77950b5be Added auth guard middleware 2024-05-14 12:07:05 +02:00
Alexis
cd5822def4 Profile page and sidebar layouts 2024-05-14 11:53:20 +02:00
Alexis
4034104c9e Updated sidebar to be global 2024-05-14 11:21:08 +02:00
Alexis
da6b30f00c Downgraded nuxt to prevent supabase errors
The issues would happen randomly when login from google, it doesn't seem to happen again with nuxt 3.7.0 ?
2024-05-13 22:10:48 +02:00
Alexis
133404ff72 Added supabase auth to project
This design is minimalist for now, it might be extended (at least with a basic profil page or command list)
2024-05-13 20:45:52 +02:00
Alexis
df14fad98a Fixed hydration errors
Moved some code to client only (it makes sense given the localstorage stuff)
2024-05-13 16:06:57 +02:00
Alexis
b49b570fe2 Moved data to api server route 2024-05-13 14:41:28 +02:00
Alexis
bb88074215 Added back exposed colours 2024-05-12 23:50:22 +02:00
Alexis
4e9bc4137b Added back meta from old index.html 2024-05-12 22:24:17 +02:00
Alexis
5772ab7aa3 Base nuxt move after refactoring until no warnings
There still exists an issue with one Adobe led library ; @international-dates don't seem to compile their sourcemaps correctly.
I don't think this is something I can fix however, and it may require hacks and workarounds to solve from what I've gathered
2024-05-12 22:11:28 +02:00
Alexis
174b488319 Removed cypress 2024-05-12 14:11:27 +02:00
Alexis
521b3530a3 Fixed URLs guards for calendar page 2024-05-06 16:56:29 +02:00
210 changed files with 14246 additions and 9110 deletions

View File

@@ -1,35 +0,0 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
overrides: [
{
files: [
'**/__tests__/*.{cy,spec}.{js,ts,jsx,tsx}',
'cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}',
'cypress/support/**/*.{js,ts,jsx,tsx}'
],
extends: ['plugin:cypress/recommended']
}
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
indent: ['error', 2, { SwitchCase: 1 }],
'prettier/prettier': [
'error',
{
endOfLine: 'auto'
}
],
'vue/multi-word-component-names': 'off'
}
}

44
.gitignore vendored
View File

@@ -1,30 +1,28 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
# Misc
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.fleet
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
# Local env files
.env
.env.*
!.env.example
# Supabase
.branches
.temp

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
# https://pnpm.io/npmrc#shamefully-hoist
# Fixes: WARN Failed to resolve dependency: @supabase/gotrue-js, present in 'optimizeDeps.include'
shamefully-hoist=true

View File

@@ -1,7 +0,0 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"eslint.experimental.useFlatConfig": true,
"editor.indentSize": "tabSize",
"editor.tabSize": 2,
}

View File

@@ -1,5 +1,75 @@
# Leim Tools
A congregate of tools used to visualize TTRPG elements and help content creators (mostly me for now tbh) build their worlds.
# Nuxt 3 Minimal Starter
## 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.
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install the dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm run dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm run build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm run preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

42
app.vue Normal file
View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import { ConfigProvider } from 'radix-vue'
useHead({
titleTemplate: (titleChunk) => {
return titleChunk ? `${titleChunk} — TTTools` : 'TTTools';
},
meta: [
{ name: "charset", content: "UTF-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1.0" },
{ name: "author", content: "Alexis Pelé" },
{ name: "description", content: "Tools destined to players and game master, helping them visualize their worlds better." },
{ name: "generator", content: "Nuxt" },
{ name: "msapplication-TileColor", content: "00aba9" },
{ name: "theme-color", content: "00aba9" },
{ name: "og:type", content: "tabletop-tools" },
{ name: "og:url", content: "ttt.alexcreates.fr" },
{ name: 'robots', content: 'noindex, nofollow'}
],
link: [
{ rel: 'apple-touch-icon', sizes: "76x76", href: '/apple-touch-icon.png' },
{ rel: 'icon', sizes: "32x32", type: 'image/png', href: '/favicon-32x32.png' },
{ rel: 'icon', sizes: "16x16", type: 'image/png', href: '/favicon-16x16.png' },
{ rel: 'manifest', href: '/site.webmanifest' },
{ rel: 'mask-icon', href: '/safari-pinned-tab.svg', color: '#6595b4' },
]
})
const useIdFunction = () => useId()
</script>
<template>
<div class="h-screen">
<NuxtLoadingIndicator />
<NuxtLayout>
<ConfigProvider :use-id="useIdFunction">
<NuxtPage/>
</ConfigProvider>
</NuxtLayout>
</div>
</template>

View File

@@ -2,70 +2,68 @@
@tailwind components;
@tailwind utilities;
@layer base {
:root {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
:root.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
@@ -75,4 +73,4 @@
body {
@apply bg-background text-foreground;
}
}
}

View File

@@ -4,11 +4,11 @@
"typescript": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/assets/main.css",
"css": "assets/main.css",
"baseColor": "slate",
"cssVariables": true
},
"framework": "vite",
"framework": "nuxt",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"

View File

@@ -0,0 +1,115 @@
<script lang="ts" setup>
import { useCalendar } from '@/stores/CalendarStore'
import { computed, type Component, type ComputedRef } from 'vue'
import MonthlyLayout from './state/monthly/Layout.vue'
import CenturyLayout from './state/centennially/Layout.vue'
import DecadeLayout from './state/decennially/Layout.vue'
import YearLayout from './state/yearly/Layout.vue'
const { isAdvancedSearchOpen, isEditEventModalOpen } = storeToRefs(useCalendar())
const route = useRoute()
const worldId = route.params.id
const { setCalendarId, setMonths, setDefaultDate, currentConfig, selectedDate, jumpToDate } = useCalendar()
const { setEvents } = useCalendarEvents()
const { setCharacters } = useCharacters()
const { data: calendar, pending: calPending, refresh: calRefresh } = await useLazyFetch(`/api/calendars/query?world_id=${worldId}`)
const { data: characters, pending: charPending, refresh: charRefresh } = await useLazyFetch(`/api/characters/query?world_id=${worldId}`)
if (!calendar.value) {
await calRefresh()
} else {
if (calendar.value?.data?.id) {
setCalendarId(calendar.value?.data?.id)
}
if (calendar.value?.data?.months) {
setMonths(calendar.value?.data?.months)
}
if (calendar.value?.data?.months) {
setMonths(calendar.value?.data?.months)
}
if (calendar.value?.data?.today) {
setDefaultDate(calendar.value?.data?.today)
}
if (calendar.value?.data?.events) {
setEvents(calendar.value?.data?.events)
}
}
if (!characters.value) {
await charRefresh()
} else {
if (characters.value?.data) {
setCharacters(characters.value?.data)
}
}
watch(calPending, (newStatus) => {
if (!newStatus) {
if (calendar.value?.data?.id) {
setCalendarId(calendar.value?.data?.id)
}
if (calendar.value?.data?.months) {
setMonths(calendar.value?.data?.months)
}
if (calendar.value?.data?.today) {
setDefaultDate(calendar.value?.data?.today)
}
if (calendar.value?.data?.events) {
setEvents(calendar.value?.data?.events)
}
}
})
watch(charPending, (newStatus) => {
if (!newStatus) {
if (characters.value?.data) {
setCharacters(characters.value?.data)
}
}
})
const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
switch (currentConfig.viewType) {
case 'month':
return MonthlyLayout
case 'year':
return YearLayout
case 'decade':
return DecadeLayout
case 'century':
default:
return CenturyLayout
}
})
onMounted(() => {
jumpToDate(selectedDate)
})
</script>
<template>
<div class="h-full">
<template v-if="calPending || charPending">
<div class="h-full grid place-items-center">
Loading notamment
</div>
</template>
<template v-else>
<div class="h-full grid grid-rows-[auto,1fr]">
<CalendarMenu />
<KeepAlive>
<component :is="currentViewComponent"/>
</KeepAlive>
</div>
</template>
<CalendarSearch v-model:model-value="isAdvancedSearchOpen" />
<CalendarFormUpdateEvent v-model:model-value="isEditEventModalOpen" />
</div>
</template>

View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { PhMapPin } from '@phosphor-icons/vue'
const { defaultDate, getFormattedDateTitle, getRelativeString } = useCalendar()
const { selectedDate } = storeToRefs(useCalendar())
const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true))
// const mainDateTitle = computed(() => convertDateToDays(selectedDate.value))
const dateDifference = computed(() => getRelativeString(defaultDate, selectedDate.value))
</script>
<template>
<ClientOnly>
<h1 class="text-2xl font-bold flex items-center gap-1">
<PhMapPin size="26" weight="bold" /> {{ mainDateTitle }}
</h1>
<h2 class="text-lg italic opacity-75">
{{ dateDifference }}
</h2>
<template #fallback>
<div class="grid gap-1">
<UiSkeleton class="h-8 w-64" />
<UiSkeleton class="h-6 w-28" />
</div>
</template>
</ClientOnly>
</template>

View File

@@ -1,56 +1,55 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { cn } from '@/lib/utils'
import { areDatesIdentical, type LeimDate } from '@/models/Date'
import { Popover, PopoverTrigger } from '@/components/ui/popover'
import type { CalendarEvent } from '@/models/Events'
import CalendarEventDetails from './CalendarEventDetails.vue'
import type { RPGDate } from '@/models/Date'
import type { CalendarEvent } from '~/models/CalendarEvent'
const props = defineProps<{
event: CalendarEvent
tileDate: LeimDate
tileDate: RPGDate
}>()
const { areDatesIdentical } = useCalendar()
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)
const isPopoverDetailsOpen = ref<boolean>(false)
function handleClosePopover() {
isPopoverOpen.value = false
isPopoverDetailsOpen.value = false
}
</script>
<template>
<Popover v-model:open="isPopoverOpen">
<PopoverTrigger as-child>
<UiPopover v-model:open="isPopoverDetailsOpen">
<UiPopoverTrigger as-child>
<button
class="text-xs px-2 py-1 block w-full text-left rounded-sm whitespace-nowrap overflow-hidden text-ellipsis"
:class="
cn({
'text-white bg-slate-600 hover:bg-slate-700': !event.category,
'text-white bg-lime-600 hover:bg-lime-700': event.category === 'naissance',
'text-white bg-stone-500 hover:bg-stone-700': event.category === 'mort',
'text-white bg-orange-600 hover:bg-orange-700': event.category === 'catastrophe',
'text-white bg-pink-600 hover:bg-pink-700': event.category === 'catastrophe naturelle',
'text-white bg-sky-600 hover:bg-sky-700': event.category === 'législation',
'text-white bg-purple-600 hover:bg-purple-700': event.category === 'religion',
'text-white bg-emerald-600 hover:bg-emerald-700': event.category === 'joueurs',
'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category === 'inauguration',
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category === 'invention',
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': event.category === 'science',
'text-slate-900 bg-white hover:bg-yellow-200': event.category === 'bénédiction',
'text-slate-900 bg-purple-200 hover:bg-purple-300': event.category === 'découverte',
'text-slate-900 bg-indigo-200 hover:bg-indigo-300': event.category === 'exploration',
'text-white bg-amber-600 hover:bg-amber-700': event.category === 'construction',
'text-slate-900 bg-violet-200 hover:bg-violet-300': event.category === 'arcanologie',
'text-white bg-rose-600 hover:bg-rose-700': event.category === 'criminalité',
'text-white bg-stone-600 hover:bg-stone-700': event.category === 'scandale',
'text-slate-900 bg-yellow-500 hover:bg-yellow-600': event.category === 'commerce',
'text-white bg-lime-600 hover:bg-lime-700': event.category?.name === 'naissance',
'text-white bg-stone-500 hover:bg-stone-700': event.category?.name === 'mort',
'text-white bg-orange-600 hover:bg-orange-700': event.category?.name === 'catastrophe',
'text-white bg-pink-600 hover:bg-pink-700': event.category?.name === 'catastrophe naturelle',
'text-white bg-sky-600 hover:bg-sky-700': event.category?.name === 'législation',
'text-white bg-purple-600 hover:bg-purple-700': event.category?.name === 'religion',
'text-white bg-emerald-600 hover:bg-emerald-700': event.category?.name === 'joueurs',
'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category?.name === 'inauguration',
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category?.name === 'invention',
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': event.category?.name === 'science',
'text-slate-900 bg-white hover:bg-yellow-200': event.category?.name === 'bénédiction',
'text-slate-900 bg-purple-200 hover:bg-purple-300': event.category?.name === 'découverte',
'text-slate-900 bg-indigo-200 hover:bg-indigo-300': event.category?.name === 'exploration',
'text-white bg-amber-600 hover:bg-amber-700': event.category?.name === 'construction',
'text-slate-900 bg-violet-200 hover:bg-violet-300': event.category?.name === 'arcanologie',
'text-white bg-rose-600 hover:bg-rose-700': event.category?.name === 'criminalité',
'text-white bg-stone-600 hover:bg-stone-700': event.category?.name === 'scandale',
'text-slate-900 bg-yellow-500 hover:bg-yellow-600': event.category?.name === 'commerce',
'rounded-r-none': isStartEvent,
'rounded-l-none': isEndEvent
})
@@ -58,7 +57,7 @@ function handleClosePopover() {
>
{{ event.title }}
</button>
</PopoverTrigger>
</UiPopoverTrigger>
<CalendarEventDetails
:event
@@ -66,7 +65,6 @@ function handleClosePopover() {
:is-start-event
:is-end-event
@query:close-popover="handleClosePopover"
v-once
/>
</Popover>
</UiPopover>
</template>

View File

@@ -0,0 +1,247 @@
<script lang="ts" setup>
import type { RPGDate } from '@/models/Date'
import type { CalendarEvent } from '@/models/CalendarEvent'
import { useCalendar } from '@/stores/CalendarStore'
import {
PhHourglassMedium,
PhAlarm,
PhHourglassHigh,
PhHourglassLow,
PhDotsThreeOutlineVertical
} from '@phosphor-icons/vue'
const { defaultDate, getFormattedDateTitle, jumpToDate, revealEditEventModal, getRelativeString } = useCalendar()
const { deleteEvent } = useCalendarEvents()
const { lastActiveEvent } = storeToRefs(useCalendarEvents())
const props = defineProps<{
event: CalendarEvent
spansMultipleDays: boolean
isStartEvent?: boolean
isEndEvent?: boolean
}>()
// Ref for the popover
const eventDetails = ref<HTMLElement>()
const emit = defineEmits(['query:close-popover'])
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: RPGDate) {
jumpToDate(date)
emit('query:close-popover')
}
/**
* Edit event
*/
const commandMenuOpened = ref(false)
function deployEditModal() {
lastActiveEvent.value = { ...props.event }
revealEditEventModal()
commandMenuOpened.value = false
}
</script>
<template>
<UiPopoverContent
class="event-details w-96 bg-slate-900"
:align="'center'"
:align-offset="50"
:side="'left'"
:collision-padding="60"
:hide-when-detached="true"
:class="{
'border-slate-800': !event.category,
'border-lime-800': event.category?.name === 'naissance',
'border-stone-600': event.category?.name === 'mort',
'border-orange-800': event.category?.name === 'catastrophe',
'border-pink-800': event.category?.name === 'catastrophe naturelle',
'border-sky-800': event.category?.name === 'législation',
'border-purple-800': event.category?.name === 'religion',
'border-emerald-800': event.category?.name === 'joueurs',
'border-amber-800': event.category?.name === 'inauguration',
'border-green-800': event.category?.name === 'invention',
'border-cyan-800': event.category?.name === 'science',
'border-slate-600': event.category?.name === 'bénédiction',
'border-purple-700': event.category?.name === 'découverte',
'border-indigo-700': event.category?.name === 'exploration',
'border-amber-700': event.category?.name === 'construction',
'border-violet-700': event.category?.name === 'arcanologie',
'border-rose-800': event.category?.name === 'criminalité',
'border-stone-700': event.category?.name === 'scandale',
'border-yellow-600': event.category?.name === 'commerce'
}"
>
<div ref="eventDetails" class="grid gap-1">
<header class="pr-12">
<div class="text-lg font-semibold">
{{ event.title }}
</div>
<div class="mb-1">
<template v-if="!event.endDate">
<p class="font-semibold">{{ getFormattedDateTitle(event.startDate, true) }}</p>
</template>
<template v-else>
<p class="font-semibold">
Du {{ getFormattedDateTitle(event.startDate, true) }} au
{{ getFormattedDateTitle(event.endDate, true) }}
</p>
</template>
</div>
</header>
<div class="mb-1 space-y-1">
<p class="text-sm italic opacity-75 flex items-center gap-1">
<PhAlarm size="16" weight="fill" /> {{ dateDifference }}
</p>
<template v-if="dateDuration">
<p class="text-sm italic opacity-75 flex items-center gap-1">
<PhHourglassMedium size="16" weight="fill" /> Pendant {{ dateDuration }}
</p>
</template>
</div>
<template v-if="event.category || event.secondaryCategories">
<ul class="flex gap-1">
<li v-if="event.category">
<UiBadge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
{{ event.category?.name }}
</UiBadge>
</li>
<li v-for="cat in event.secondaryCategories" :key="cat.id">
<UiBadge class="mix-blend-luminosity bg-gray-600" variant="secondary">
{{ cat.name }}
</UiBadge>
</li>
</ul>
</template>
<template v-if="event.description">
<hr class="border-slate-500 mt-2" >
<div class="mt-2 text-sm text-slate-300">
{{ event.description }}
</div>
</template>
</div>
<menu class="absolute top-4 right-4">
<UiPopover v-model:open="commandMenuOpened">
<UiPopoverTrigger as-child>
<UiButton size="icon" variant="ghost">
<PhDotsThreeOutlineVertical size="20" weight="fill" />
</UiButton>
</UiPopoverTrigger>
<UiPopoverContent class="w-fit p-0" align="start" side="right" :collision-padding="20" >
<UiCommand>
<UiCommandList>
<UiCommandGroup>
<UiCommandItem value="edit-event" @select="deployEditModal"> Modifier </UiCommandItem>
<UiCommandItem value="delete-event" @select="deleteEvent(event.id!)"> Supprimer </UiCommandItem>
</UiCommandGroup>
</UiCommandList>
</UiCommand>
</UiPopoverContent>
</UiPopover>
</menu>
<nav v-if="event.startDate && event.endDate" class="mt-2 flex gap-2">
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child>
<button
class="flex gap-1"
title="Naviguer au début"
@click="handleJumpToDate(event.startDate!)"
>
<PhHourglassHigh size="16" weight="fill" /> Début
</button>
</UiBadge>
<UiBadge class="hover:opacity-100 hover:bg-slate-300" as-child title="Naviguer à la fin">
<button
class="flex gap-1"
title="Naviguer à la fin"
@click="handleJumpToDate(event.endDate!)"
>
<PhHourglassLow size="16" weight="fill" /> Fin
</button>
</UiBadge>
</nav>
</UiPopoverContent>
</template>
<style lang="scss">
.border-slate-800 {
--base-color: var(--color-slate-800);
}
.border-lime-800 {
--base-color: var(--color-lime-800);
}
.border-stone-600 {
--base-color: var(--color-stone-600);
}
.border-orange-800 {
--base-color: var(--color-orange-800);
}
.border-pink-800 {
--base-color: var(--color-pink-800);
}
.border-sky-800 {
--base-color: var(--color-sky-800);
}
.border-purple-800 {
--base-color: var(--color-purple-800);
}
.border-emerald-800 {
--base-color: var(--color-emerald-800);
}
.border-amber-800 {
--base-color: var(--color-amber-800);
}
.border-green-800 {
--base-color: var(--color-green-800);
}
.border-cyan-800 {
--base-color: var(--color-cyan-800);
}
.border-slate-600 {
--base-color: var(--color-slate-600);
}
.border-purple-700 {
--base-color: var(--color-purple-700);
}
.border-indigo-700 {
--base-color: var(--color-indigo-700);
}
.border-amber-700 {
--base-color: var(--color-amber-700);
}
.border-violet-700 {
--base-color: var(--color-violet-700);
}
.border-rose-800 {
--base-color: var(--color-rose-800);
}
.border-stone-700 {
--base-color: var(--color-stone-700);
}
.border-yellow-600 {
--base-color: var(--color-yellow-600);
}
.event-details {
--bg-color: color-mix(in srgb, var(--base-color), var(--color-slate-950) 85%);
background-color: var(--bg-color);
hr {
border-color: var(--base-color);
}
}
</style>

View File

@@ -1,13 +1,7 @@
<script lang="ts" setup>
import { useCalendar } from '@/stores/CalendarStore'
import { Button } from '@/components/ui/button'
import { PhMagnifyingGlass } from '@phosphor-icons/vue'
import CalendarMenuNav from './CalendarMenuNav.vue'
import CalendarMenuSubnav from './CalendarMenuSubnav.vue'
import CalendarMenuToday from './CalendarMenuToday.vue'
import CalendarSwitch from './CalendarSwitch.vue'
import CalendarCurrentDate from './CalendarCurrentDate.vue'
const { revealAdvancedSearch } = useCalendar()
</script>
@@ -29,10 +23,10 @@ const { revealAdvancedSearch } = useCalendar()
<menu class="flex items-center gap-2">
<li>
<Button search-slash @click="revealAdvancedSearch()">
<UiButton search-slash @click="revealAdvancedSearch()">
<PhMagnifyingGlass size="20" weight="light" />
Recherche avancée
</Button>
</UiButton>
</li>
<li>
<CalendarSwitch />

View File

@@ -2,14 +2,12 @@
import { computed, type ComputedRef } from 'vue'
import { useCalendar } from '@/stores/CalendarStore'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import {
PhCaretDoubleLeft,
PhCaretDoubleRight,
PhCaretLeft,
PhCaretRight
} from '@phosphor-icons/vue'
import Button from '@/components/ui/button/Button.vue'
interface DirectionLabels {
pastFar: string
@@ -145,56 +143,56 @@ function toFutureFar(): void {
<template>
<div class="flex items-center gap-2">
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="toPastFar()">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton variant="outline" size="icon" @click="toPastFar()">
<PhCaretDoubleLeft size="18" />
</Button>
</TooltipTrigger>
<TooltipContent>
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ activeDirectionLabels.pastFar }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="toPastNear()">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton variant="outline" size="icon" @click="toPastNear()">
<PhCaretLeft size="18" />
</Button>
</TooltipTrigger>
<TooltipContent>
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ activeDirectionLabels.pastNear }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="toFutureNear()">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton variant="outline" size="icon" @click="toFutureNear()">
<PhCaretRight size="18" />
</Button>
</TooltipTrigger>
<TooltipContent>
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ activeDirectionLabels.futureNear }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button variant="outline" size="icon" @click="toFutureFar()">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton variant="outline" size="icon" @click="toFutureFar()">
<PhCaretDoubleRight size="18" />
</Button>
</TooltipTrigger>
<TooltipContent>
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ activeDirectionLabels.futureFar }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
</template>

View File

@@ -1,17 +1,20 @@
<script lang="ts" setup>
import { daysPerMonth, monthsPerYear, type LeimDate } from '@/models/Date'
import { type RPGDate } 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
let fromDate: RPGDate
// To modify, obviously
const daysPerMonth = 32
const monthsPerYear = 10
const toDay = position === 'next' ? daysPerMonth : 1
const toMonth = position === 'next' ? monthsPerYear : 0
@@ -62,46 +65,54 @@ function handleGotoPreviousEventPage(position: 'next' | 'prev' = 'next') {
<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 class="grid items-end w-40 px-4 py-2 border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm text-sm">
<ClientOnly>
<span>{{ currentDate.currentDateTitle }}</span>
<template #fallback>
<span class="inline-block">
<UiSkeleton class="h-[19px] w-full rounded-sm" />
</span>
</template>
</ClientOnly>
</div>
<div class="border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
variant="ghost"
size="icon"
class="rounded-t-sm rounded-b-none"
@click="handleGotoPreviousEventPage('prev')"
>
<PhArrowLineLeft size="22" />
</Button>
</TooltipTrigger>
<TooltipContent>
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>Précédente page à évènements</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
<div class="border-slate-700 border-x-[1px] border-t-[1px] rounded-t-sm">
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
variant="ghost"
size="icon"
class="rounded-t-sm rounded-b-none"
@click="handleGotoPreviousEventPage('next')"
>
<PhArrowLineRight size="22" />
</Button>
</TooltipTrigger>
<TooltipContent>
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>Prochaine page à évènements</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
</div>
</template>

View File

@@ -0,0 +1,36 @@
<script lang="ts" setup>
import { useCalendar } from '@/stores/CalendarStore'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
const { defaultDate, areDatesIdentical } = useCalendar()
const { selectedDate } = storeToRefs(useCalendar())
const { jumpToDefaultDate, getFormattedDateTitle } = useCalendar()
const defaultDateFormatted: string = getFormattedDateTitle(defaultDate, true)
const isDefaultDate: ComputedRef<boolean> = computed<boolean>(() => areDatesIdentical(selectedDate.value, defaultDate))
</script>
<template>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<ClientOnly>
<UiButton size="sm" :disabled="isDefaultDate" @click="jumpToDefaultDate">
Aujourd'hui
</UiButton>
<template #fallback>
<UiButton size="sm">
Aujourd'hui
</UiButton>
</template>
</ClientOnly>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ defaultDateFormatted }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</template>

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import { useCalendar } from '@/stores/CalendarStore'
import { PhCalendarBlank } from '@phosphor-icons/vue'
import { computed } from 'vue'
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar()
const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
</script>
<template>
<UiDropdownMenu>
<UiDropdownMenuTrigger as-child>
<UiButton size="sm" variant="secondary">
<PhCalendarBlank size="18" weight="fill" />
{{ viewTypeTitle }}
</UiButton>
</UiDropdownMenuTrigger>
<UiDropdownMenuContent :side="'bottom'" :collision-padding="30">
<UiDropdownMenuLabel>Mode d'affichage</UiDropdownMenuLabel>
<UiDropdownMenuSeparator />
<UiDropdownMenuItem
v-for="option in viewTypeOptions"
:key="option"
@click="setViewType(option)"
>
{{ getViewTypeTitle(option) }}
</UiDropdownMenuItem>
</UiDropdownMenuContent>
</UiDropdownMenu>
</template>

View File

@@ -0,0 +1,126 @@
<script lang="ts" setup>
import type { RPGDate } from '~/models/Date';
import {
PhAlarm
} from '@phosphor-icons/vue'
const { eventSkeleton } = storeToRefs(useCalendarEvents())
const { resetSkeleton, submitSkeleton } = useCalendarEvents()
const popoverOpen = ref(false)
const formErrors = reactive<{ message: string | null }>({
message: null
})
const props = defineProps<{
date?: RPGDate
btnClass?: string
}>()
/**
* Opens event creation's popover
*/
function openEventCreatePopover() {
resetSkeleton()
popoverOpen.value = true
// Set skeleton initial startDate if it's known
if (props.date) {
eventSkeleton.value.startDate = { ...props.date } // We need to clone it otherwise the props ends up mutating (?)
}
}
// Watch the popover state
// And reset the skeleton if it has been dismissed
watch(popoverOpen, (hasOpened, _o) => {
if (!hasOpened) {
resetSkeleton()
}
})
async function handleSubmit() {
try {
await submitSkeleton()
popoverOpen.value = false
} catch (err) {
if (err instanceof Error) {
formErrors.message = err.message
}
}
}
</script>
<template>
<UiPopover v-model:open="popoverOpen">
<UiPopoverTrigger as-child>
<button :class="btnClass" @click="openEventCreatePopover()" />
</UiPopoverTrigger>
<UiPopoverContent
:align="'center'"
:side="'right'"
:collision-padding="60"
class="pl-3 min-w-96 bg-slate-900 border-slate-800"
>
<form @submit.prevent="handleSubmit">
<div class="grid grid-cols-2 gap-y-4">
<div class="col-span-2 pl-8">
<input
id="new-event-title"
v-model="eventSkeleton.title"
type="text"
name="new-event-title"
required
placeholder="Titre de l'évènement"
class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600">
</div>
<div class="col-span-2 pl-8">
<textarea
id="new-event-description"
v-model="eventSkeleton.description"
name="new-event-description"
placeholder="Description brève de l'évènement"
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"
/>
</div>
<div class="col-span-2">
<div class="flex items-center gap-2">
<PhAlarm size="18" weight="fill" />
<CalendarInputRPGDate
v-model:model-value="eventSkeleton.startDate"
placeholder="Date de début"
:initial-date="props.date"
:required="true"
/>
<span></span>
<CalendarInputRPGDate
v-model:model-value="eventSkeleton.endDate"
placeholder="Date de fin"
:initial-date="props.date"
/>
</div>
</div>
<div class="text-red-500 pl-8">
<span class="text-sm">
{{ formErrors.message }}
</span>
</div>
<div class="text-right">
<UiButton size="sm">
Sauvegarder
</UiButton>
</div>
</div>
</form>
</UiPopoverContent>
</UiPopover>
</template>

View File

@@ -0,0 +1,112 @@
<script lang="ts" setup>
import { PhAlarm } from '@phosphor-icons/vue'
import { VisuallyHidden } from 'radix-vue'
const isModalOpened = defineModel<boolean>({ default: false })
const { resetSkeleton, updateEventFromSkeleton } = useCalendarEvents()
const { eventSkeleton, lastActiveEvent } = storeToRefs(useCalendarEvents())
const formErrors = reactive<{ message: string | null }>({
message: null
})
// Watch the popover state
watch(isModalOpened, (hasOpened, _o) => {
if (hasOpened && lastActiveEvent.value) {
eventSkeleton.value = { ...lastActiveEvent.value }
} else {
resetSkeleton()
}
})
async function handleSubmit() {
try {
await updateEventFromSkeleton()
isModalOpened.value = false
} catch (err) {
if (err instanceof Error) {
formErrors.message = err.message
}
}
}
</script>
<template>
<UiDialog v-model:open="isModalOpened" :modal="true">
<UiDialogContent
:align="'center'"
:side="'right'"
:collision-padding="60"
:disable-outside-pointer-events="true"
:trap-focus="true"
class="pl-3 min-w-96 bg-slate-900 border-slate-800"
>
<VisuallyHidden>
<UiDialogTitle> Modifier l'évènement</UiDialogTitle>
<UiDialogDescription>
Mettre à jour les données de l'évènement
</UiDialogDescription>
</VisuallyHidden>
<form @submit.prevent="handleSubmit">
<div class="grid grid-cols-2 gap-y-4">
<div class="col-span-2 ml-8">
<input
id="new-event-title"
v-model="eventSkeleton.title"
type="text"
name="new-event-title"
required
placeholder="Titre de l'évènement"
class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600">
</div>
<div class="col-span-2 ml-8">
<textarea
id="new-event-description"
v-model="eventSkeleton.description"
name="new-event-description"
placeholder="Description brève de l'évènement"
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"
/>
</div>
<div class="col-span-2">
<div class="flex items-center gap-2">
<PhAlarm size="18" weight="fill" />
<CalendarInputRPGDate
v-model:model-value="eventSkeleton.startDate"
placeholder="Date de début"
:initial-date="lastActiveEvent?.startDate"
:required="true"
/>
<span></span>
<CalendarInputRPGDate
v-model:model-value="eventSkeleton.endDate"
placeholder="Date de fin"
:initial-date="lastActiveEvent?.endDate"
/>
</div>
</div>
<div class="text-red-500 ml-8">
<span class="text-sm">
{{ formErrors.message }}
</span>
</div>
<div class="text-right">
<UiButton size="sm" type="submit">
Sauvegarder
</UiButton>
</div>
</div>
</form>
</UiDialogContent>
</UiDialog>
</template>

View File

@@ -0,0 +1,166 @@
<script lang="ts" setup>
import type { RPGDate } from '~/models/Date';
import {
PhXCircle
} from '@phosphor-icons/vue'
const model = defineModel<RPGDate | null>()
const id = useId()
const props = defineProps<{
initialDate?: RPGDate
placeholder?: string
required?: boolean
}>()
const { getFormattedDateTitle, getMonthName, defaultDate } = useCalendar()
const { sortedMonths } = storeToRefs(useCalendar())
const popoverOpened = ref<boolean>(false)
const inputPlaceholder = computed<string>(() => {
if (model.value) {
return getFormattedDateTitle(model.value, true)
} else if (props.placeholder) {
return props.placeholder
}
return ''
})
const initialDateValue = props.initialDate ? { ...props.initialDate } : null
const monthTitle = computed<string>(() => {
if (model.value) {
return getMonthName(model.value.month)
}
return ''
})
const hasValue = computed<boolean>(() => !!model.value)
const monthData = computed(() => {
if (model.value) {
return sortedMonths.value[model.value.month]
}
return null
})
function setModelDay(day: number) {
if (model.value) {
model.value.day = day
}
}
function handleMonthChange(e: string) {
const monthId = Number(e)
if (model.value) {
model.value.month = monthId
}
}
/**
* Handle click if the date is null
*/
function handleNullClick() {
// Early return
if (model.value) return
if (!model.value) {
if (props.initialDate) {
model.value = initialDateValue
} else {
model.value = { ...defaultDate }
}
}
}
function handleValueReset() {
// Early return
if (!model.value) return
model.value = null
}
</script>
<template>
<UiPopover v-model:open="popoverOpened">
<UiPopoverTrigger>
<div class="flex">
<UiButton
size='sm'
variant="outline"
:class="{
'text-slate-500': !hasValue,
'rounded-tr-none rounded-br-none': model
}"
@click.prevent="handleNullClick"
>
{{ inputPlaceholder }}
</UiButton>
<UiButton
v-if="model && !required"
size='sm'
variant="outline"
class="rounded-tl-none rounded-bl-none px-2"
@click.prevent="handleValueReset"
>
<PhXCircle size="14" />
</UiButton>
</div>
</UiPopoverTrigger>
<UiPopoverContent
v-if="model"
align="start"
side="bottom"
class="bg-slate-900 border-slate-800"
>
<div class="grid grid-cols-2 items-center gap-x-2 gap-y-3">
<UiSelect @update:model-value="handleMonthChange">
<UiSelectTrigger>
<UiSelectValue :placeholder="monthTitle" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectGroup class="max-h-[50vh]">
<UiSelectItem v-for="(month, i) in sortedMonths" :key="`popover-select-item-${id}-${i}`" :value="i.toString()">
{{ month.name }}
</UiSelectItem>
</UiSelectGroup>
</UiSelectContent>
</UiSelect>
<UiInput v-model="model.year" type="number" />
<!-- <nav class="flex items-center justify-end gap-2">
<UiButton variant="outline" size="icon" class="w-8 h-8" @click="decrementModelMonth()">
<PhCaretLeft size="14" />
</UiButton>
<UiButton variant="outline" size="icon" class="w-8 h-8" @click="incrementModelMonth()">
<PhCaretRight size="14" />
</UiButton>
</nav> -->
<hr class="col-span-2" >
<div class="col-span-2">
<div v-if="monthData" class="grid grid-cols-7 gap-1">
<button
v-for="day in monthData.days"
:key="`popover-day-grid-${id}-${day}`"
class="aspect-square rounded-full text-[.8em] transition-colors hover:bg-slate-700"
:class="{'bg-blue-500 hover:bg-blue-700': day === model.day}"
@click="setModelDay(day)"
>
{{ day }}
</button>
</div>
</div>
</div>
</UiPopoverContent>
</UiPopover>
</template>

View File

@@ -1,47 +1,20 @@
<script lang="ts" setup>
import {
characterCategories,
isCharacter,
type Character,
type CharacterCategory
} from '@/models/Characters'
import type { LeimDateOrder } from '@/models/Date'
import type { RPGDateOrder } from '@/models/Date'
import {
calendarEventCategories,
isCalendarEvent,
type CalendarEvent,
type CalendarEventCategory
} from '@/models/Events'
} from '~/models/CalendarEvent'
import { useCharacters } from '@/stores/CharacterStore'
import { useCalendarEvents } from '@/stores/EventStore'
import { capitalize } from '@/utils/Strings'
import { useMagicKeys, useScroll, useStorage, whenever } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import { searchUnifier, type SearchMode } from '../Search'
import { searchUnifier, type SearchMode } from '../SearchMode'
import { Button } from '@/components/ui/button'
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/components/ui/command'
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import {
Pagination,
PaginationEllipsis,
PaginationFirst,
PaginationLast,
PaginationList,
PaginationListItem,
PaginationNext,
PaginationPrev
} from '@/components/ui/pagination'
import {
TagsInput,
TagsInputInput,
TagsInputItem,
TagsInputItemDelete,
TagsInputItemText
} from '@/components/ui/tags-input'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { PhClockClockwise, PhClockCounterClockwise, PhMagnifyingGlass } from '@phosphor-icons/vue'
import {
ComboboxAnchor,
@@ -52,11 +25,12 @@ import {
} from 'radix-vue'
import SearchList from './lists/SearchList.vue'
import type { Category } from '~/models/Category'
const { characters } = useCharacters()
const { allEvents } = useCalendarEvents()
const { characters } = storeToRefs(useCharacters())
const { allEvents } = storeToRefs(useCalendarEvents())
const modalOpen = defineModel({ default: false })
const modalOpen = defineModel<boolean>({ default: false })
const searchQuery = ref<string>('')
// const searchEnough = computed<boolean>(() => searchQuery.value.length >= 2)
@@ -64,7 +38,7 @@ const searchQuery = ref<string>('')
const selectedEntity = useStorage('se', 'events' as SearchMode)
// Order
const selectedOrder = ref<LeimDateOrder>('asc')
const selectedOrder = ref<RPGDateOrder>('asc')
function setOrderAsc() {
selectedOrder.value = 'asc'
resetPage()
@@ -96,11 +70,11 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Assign data to loop over and filter
// They are assigned this way for readability
if (selectedEntity.value === 'events') {
dataToFilter = allEvents
dataToFilter = allEvents.value
} else if (selectedEntity.value === 'characters') {
dataToFilter = characters
dataToFilter = characters.value
} else {
dataToFilter = [...allEvents, ...characters]
dataToFilter = [...allEvents.value, ...characters.value]
}
/**
@@ -128,7 +102,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Handle categories logic
let hitCategories: boolean = false
let allCategories: CalendarEventCategory[] = []
const allCategories: Category[] = []
if (item.category) {
allCategories.push(item.category)
@@ -139,7 +113,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
}
hitCategories = selectedCategories.value.every((selectedCat) => {
return allCategories.includes(selectedCat as CalendarEventCategory)
return allCategories.includes(selectedCat as Category)
})
return (hitTitle || hitDesc) && hitCategories
@@ -162,7 +136,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Handle categories logic
let hitCategories: boolean = false
let allCategories: CharacterCategory[] = []
const allCategories: Category[] = []
if (item.category) {
allCategories.push(item.category)
@@ -173,7 +147,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
}
hitCategories = selectedCategories.value.every((selectedCat) => {
return allCategories.includes(selectedCat as CharacterCategory)
return allCategories.includes(selectedCat as Category)
})
return hitTitle && hitCategories
@@ -193,16 +167,16 @@ function resetSearch() {
}
/**
* Opens the search dialog
* Opens the search Uidialog
*/
function openDialog() {
function openUiDialog() {
modalOpen.value = true
}
/**
* Closes the search dialog
* Closes the search Uidialog
*/
function closeDialog() {
function closeUiDialog() {
modalOpen.value = false
}
@@ -218,26 +192,22 @@ function handleEntitySwitch() {
const keys = useMagicKeys()
whenever(keys.control_period, () => {
openDialog()
openUiDialog()
})
const searchResultsRef = ref<HTMLElement | null>(null)
const { y: searchResultsY } = useScroll(searchResultsRef)
watch(currentPage, () => {
watch([currentPage, selectedEntity], () => {
searchResultsY.value = 0
})
// Compute categories based on current selectedEntity
const currentCategories = computed(() => {
if (selectedEntity.value === 'characters') {
return [...characterCategories]
} else {
return [...calendarEventCategories]
}
return []
})
const selectedCategories = ref<(CharacterCategory | CalendarEventCategory)[]>([])
const selectedCategories = ref<(Category)[]>([])
const categoryFilterOpened = ref<boolean>(false)
const searchCategory = ref<string>('')
@@ -250,10 +220,10 @@ const filteredCategories = computed(() =>
*
* @param e Radix Change Event
*/
function handleCategorySelect(e: any) {
if (typeof e.detail.value === 'string') {
function handleCategorySelect(e: (Category)) {
if (typeof e === 'string') {
searchCategory.value = ''
selectedCategories.value.push(e.detail.value)
selectedCategories.value.push(e)
}
if (filteredCategories.value.length === 0) {
@@ -263,31 +233,31 @@ function handleCategorySelect(e: any) {
</script>
<template>
<Dialog v-model:open="modalOpen" @update:open="resetSearch()">
<DialogContent
<UiDialog v-model:open="modalOpen" @update:open="resetSearch()">
<UiDialogContent
class="flex flex-col flex-nowrap top-16 -translate-y-0 data-[state=closed]:slide-out-to-top-[5%] data-[state=open]:slide-in-from-top-[5%]"
:class="{
'bottom-16': searchResults.length > 0
}"
>
<VisuallyHidden>
<DialogTitle> Recherche avancée </DialogTitle>
<UiDialogTitle> Recherche avancée </UiDialogTitle>
</VisuallyHidden>
<VisuallyHidden>
<DialogDescription>
<UiDialogDescription>
Rechercher les données disponibles sur le calendrier
</DialogDescription>
</UiDialogDescription>
</VisuallyHidden>
<!-- Dialog header -->
<div class="grid gap-3" id="searchForm">
<!-- UiDialog header -->
<div id="searchForm" class="grid gap-3">
<div class="relative w-full h-fit">
<Input
<UiInput
id="search"
v-model:model-value="searchQuery"
type="text"
placeholder="Rechercher le calendrier"
class="pl-10 py-6 text-lg"
v-model:model-value="searchQuery"
autocomplete="off"
/>
<span class="absolute start-1 inset-y-0 flex items-center justify-center px-2 opacity-50">
@@ -297,28 +267,28 @@ function handleCategorySelect(e: any) {
<div class="flex items-center justify-between">
<div>
<ToggleGroup
<UiToggleGroup
v-model="selectedEntity"
type="single"
class="justify-start"
v-model="selectedEntity"
@update:model-value="handleEntitySwitch()"
>
<ToggleGroupItem value="events" aria-label="Uniquement les évènements" v-once>
<UiToggleGroupItem value="events" aria-label="Uniquement les évènements">
Évènements
</ToggleGroupItem>
<ToggleGroupItem value="characters" aria-label="Uniquement les personnages" v-once>
</UiToggleGroupItem>
<UiToggleGroupItem value="characters" aria-label="Uniquement les personnages">
Personnages
</ToggleGroupItem>
</ToggleGroup>
</UiToggleGroupItem>
</UiToggleGroup>
</div>
<div class="flex items-center gap-1">
<TagsInput class="px-0 gap-0 w-72" :model-value="selectedCategories">
<UiTagsInput class="px-0 gap-0 w-72">
<div class="flex gap-2 flex-wrap items-center px-3">
<TagsInputItem v-for="item in selectedCategories" :key="item" :value="item">
<TagsInputItemText class="capitalize" />
<TagsInputItemDelete />
</TagsInputItem>
<UiTagsInputItem v-for="item in selectedCategories" :key="item.id" :value="item.name">
<UiTagsInputItemText class="capitalize" />
<UiTagsInputItemDelete />
</UiTagsInputItem>
</div>
<ComboboxRoot
@@ -329,7 +299,7 @@ function handleCategorySelect(e: any) {
>
<ComboboxAnchor as-child>
<ComboboxInput placeholder="Catégories" as-child>
<TagsInputInput
<UiTagsInputInput
class="w-full px-3"
:class="selectedCategories.length > 0 ? 'mt-2' : ''"
@keydown.enter.prevent
@@ -338,79 +308,79 @@ function handleCategorySelect(e: any) {
</ComboboxAnchor>
<ComboboxPortal :to="'#searchForm'">
<CommandList
<UiCommandList
position="popper"
class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50"
:dismissable="true"
>
<CommandEmpty />
<CommandGroup>
<CommandItem
v-for="framework in filteredCategories"
:key="framework"
:value="framework"
@select.prevent="handleCategorySelect"
<UiCommandEmpty />
<UiCommandGroup>
<UiCommandItem
v-for="cat in filteredCategories"
:key="cat"
:value="cat"
@select.prevent="handleCategorySelect(cat)"
>
{{ capitalize(framework) }}
</CommandItem>
</CommandGroup>
</CommandList>
{{ capitalize(cat) }}
</UiCommandItem>
</UiCommandGroup>
</UiCommandList>
</ComboboxPortal>
</ComboboxRoot>
</TagsInput>
</UiTagsInput>
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
:variant="selectedOrder === 'desc' ? 'secondary' : 'outline'"
size="icon"
@click="setOrderDesc()"
>
<PhClockCounterClockwise size="18" />
</Button>
</TooltipTrigger>
<TooltipContent>
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>Plus ancien</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<TooltipProvider :delayDuration="250">
<Tooltip>
<TooltipTrigger as-child>
<Button
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
:variant="selectedOrder === 'asc' ? 'secondary' : 'outline'"
size="icon"
@click="setOrderAsc()"
>
<PhClockClockwise size="18" />
</Button>
</TooltipTrigger>
<TooltipContent>
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>Plus récent</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
</div>
</div>
<hr />
<hr >
<div v-if="searchResults.length > 0" class="grow overflow-y-auto" ref="searchResultsRef">
<div v-if="searchResults.length > 0" ref="searchResultsRef" class="grow overflow-y-auto">
<SearchList
:results="searchResults"
:current-entity="selectedEntity"
:order="selectedOrder"
:start-at="startOfList"
:end-at="endOfList"
@jumped-to-date="closeDialog()"
@jumped-to-date="closeUiDialog()"
/>
</div>
<div class="flex justify-end">
<Pagination
<UiPagination
v-model:page="currentPage"
:total="searchResults.length"
:items-per-page="itemsPerPage"
@@ -418,32 +388,32 @@ function handleCategorySelect(e: any) {
show-edges
:default-page="1"
>
<PaginationList v-slot="{ items }" class="flex items-center gap-1">
<PaginationFirst />
<PaginationPrev />
<UiPaginationList v-slot="{ items }" class="flex items-center gap-1">
<UiPaginationFirst />
<UiPaginationPrev />
<template v-for="(item, index) in items">
<PaginationListItem
<UiPaginationListItem
v-if="item.type === 'page'"
:key="index"
:value="item.value"
as-child
>
<Button
<UiButton
class="w-10 h-10 p-0"
:variant="item.value === currentPage ? 'default' : 'outline'"
>
{{ item.value }}
</Button>
</PaginationListItem>
<PaginationEllipsis v-else :key="item.type" :index="index" />
</UiButton>
</UiPaginationListItem>
<UiPaginationEllipsis v-else :key="item.type" :index="index" />
</template>
<PaginationNext />
<PaginationLast />
</PaginationList>
</Pagination>
<UiPaginationNext />
<UiPaginationLast />
</UiPaginationList>
</UiPagination>
</div>
</DialogContent>
</Dialog>
</UiDialogContent>
</UiDialog>
</template>

View File

@@ -1,21 +1,23 @@
<script lang="ts" setup>
import type { Character } from '@/models/Characters'
import type { LeimDate } from '@/models/Date'
import type { RPGDate } 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<{
const props = defineProps<{
character: Character
}>()
defineEmits<{
(e: 'query:date-jump', payload: LeimDate): void
(e: 'query:date-jump', payload: RPGDate): void
}>()
const { getFormattedDateTitle } = useCalendar()
console.log(props.character.birth)
</script>
<template>
@@ -37,13 +39,13 @@ const { getFormattedDateTitle } = useCalendar()
<menu class="flex gap-2 border-[1px] border-slate-700 rounded-sm w-fit">
<li v-if="character.birth">
<TooltipProvider :delayDuration="100">
<TooltipProvider :delay-duration="100">
<Tooltip>
<TooltipTrigger as-child>
<Button
@click="$emit('query:date-jump', character.birth!)"
variant="ghost"
size="xs"
@click="$emit('query:date-jump', character.birth!)"
>
<PhPlant size="16" weight="fill" />
{{ getFormattedDateTitle(character.birth, true) }}
@@ -57,13 +59,13 @@ const { getFormattedDateTitle } = useCalendar()
</li>
<span v-if="character.birth && character.death">-</span>
<li v-if="character.death">
<TooltipProvider :delayDuration="100">
<TooltipProvider :delay-duration="100">
<Tooltip>
<TooltipTrigger as-child>
<Button
@click="$emit('query:date-jump', character.death!)"
variant="ghost"
size="xs"
@click="$emit('query:date-jump', character.death!)"
>
<PhSkull size="16" weight="fill" />
{{ getFormattedDateTitle(character.death, true) }}
@@ -77,7 +79,7 @@ const { getFormattedDateTitle } = useCalendar()
</li>
</menu>
<hr v-if="character.description" class="border-white opacity-25" />
<hr v-if="character.description" class="border-white opacity-25" >
<div v-if="character.description" class="text-sm">
<span class="opacity-75">

View File

@@ -1,10 +1,8 @@
<script lang="ts" setup>
import { getRelativeString, type LeimDate } from '@/models/Date'
import type { CalendarEvent } from '@/models/Events'
import type { RPGDate } from '@/models/Date'
import type { CalendarEvent } from '@/models/CalendarEvent'
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<{
@@ -12,9 +10,11 @@ const props = defineProps<{
}>()
defineEmits<{
(e: 'query:date-jump', payload: LeimDate): void
(e: 'query:date-jump', payload: RPGDate): void
}>()
const { getRelativeString } = useCalendar()
const { defaultDate, getFormattedDateTitle } = useCalendar()
const dateDifference: string = getRelativeString(defaultDate, props.event.startDate)
@@ -28,24 +28,24 @@ const dateDuration: string | null = props.event.endDate
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'
'text-white bg-lime-600 hover:bg-lime-700': event.category?.name === 'naissance',
'text-white bg-stone-500 hover:bg-stone-700': event.category?.name === 'mort',
'text-white bg-orange-600 hover:bg-orange-700': event.category?.name === 'catastrophe',
'text-white bg-pink-600 hover:bg-pink-700': event.category?.name === 'catastrophe naturelle',
'text-white bg-sky-600 hover:bg-sky-700': event.category?.name === 'législation',
'text-white bg-purple-600 hover:bg-purple-700': event.category?.name === 'religion',
'text-white bg-emerald-600 hover:bg-emerald-700': event.category?.name === 'joueurs',
'text-slate-900 bg-amber-300 hover:bg-amber-400': event.category?.name === 'inauguration',
'text-slate-900 bg-emerald-200 hover:bg-emerald-300': event.category?.name === 'invention',
'text-slate-900 bg-cyan-300 hover:bg-cyan-400': event.category?.name === 'science',
'text-slate-900 bg-white hover:bg-yellow-200': event.category?.name === 'bénédiction',
'text-slate-900 bg-purple-200 hover:bg-purple-300': event.category?.name === 'découverte',
'text-slate-900 bg-indigo-200 hover:bg-indigo-300': event.category?.name === 'exploration',
'text-white bg-amber-600 hover:bg-amber-700': event.category?.name === 'construction',
'text-slate-900 bg-violet-200 hover:bg-violet-300': event.category?.name === 'arcanologie',
'text-white bg-rose-600 hover:bg-rose-700': event.category?.name === 'criminalité',
'text-white bg-stone-600 hover:bg-stone-700': event.category?.name === 'scandale',
'text-slate-900 bg-yellow-500 hover:bg-yellow-600': event.category?.name === 'commerce'
}"
@click="$emit('query:date-jump', event.startDate)"
>
@@ -54,12 +54,12 @@ const dateDuration: string | null = props.event.endDate
{{ event.title }}
</h2>
<div v-if="event.wiki">
<Button variant="link" size="xs" as-child class="text-inherit">
<UiButton variant="link" size="xs" as-child class="text-inherit">
<a :href="event.wiki" target="_blank">
Wiki
<PhArrowSquareOut size="16" weight="fill" />
</a>
</Button>
</UiButton>
</div>
</div>
@@ -91,21 +91,21 @@ const dateDuration: string | null = props.event.endDate
<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>
<UiBadge class="mix-blend-luminosity font-bold bg-gray-600" variant="secondary">
{{ event.category?.name }}
</UiBadge>
</li>
<li v-for="cat in event.secondaryCategories" :key="cat">
<Badge class="mix-blend-luminosity bg-gray-600" variant="secondary">
{{ cat }}
</Badge>
<li v-for="cat in event.secondaryCategories" :key="cat.id">
<UiBadge class="mix-blend-luminosity bg-gray-600" variant="secondary">
{{ cat.name }}
</UiBadge>
</li>
</ul>
</div>
<div v-if="event.description" class="text-sm">
<hr class="my-2 border-white opacity-50" />
<hr class="my-2 border-white opacity-50" >
<span class="opacity-75">
{{ event.description }}
</span>

View File

@@ -1,19 +1,18 @@
<script lang="ts" setup>
import { isCharacter, type Character } from '@/models/Characters'
import { compareDates, type LeimDate, type LeimDateOrder } from '@/models/Date'
import { isCalendarEvent, type CalendarEvent } from '@/models/Events'
import type { RPGDate, RPGDateOrder } from '@/models/Date'
import { useCalendar } from '@/stores/CalendarStore'
import { computed, ref } from 'vue'
import type { SearchMode } from '../../Search'
import { computed } from 'vue'
import { isCalendarEvent, type CalendarEvent } from '~/models/CalendarEvent'
import type { SearchMode } from '../../SearchMode'
import CharacterCallout from './CharacterCallout.vue'
import EventCallout from './EventCallout.vue'
import { useScroll } from '@vueuse/core'
const props = defineProps<{
results: (Character | CalendarEvent)[]
currentEntity: SearchMode
order: LeimDateOrder
order: RPGDateOrder
startAt: number
endAt: number
limit?: number
@@ -21,9 +20,9 @@ const props = defineProps<{
const emit = defineEmits(['jumpedToDate'])
const { jumpToDate } = useCalendar()
const { jumpToDate, compareDates } = useCalendar()
function handleJumpToDate(date?: LeimDate) {
function handleJumpToDate(date?: RPGDate) {
if (!date) return
jumpToDate(date)
@@ -33,8 +32,8 @@ function handleJumpToDate(date?: LeimDate) {
// Initial sorting of the results
const sortedResults = computed(() => {
return [...props.results].sort((a, b) => {
let firstDate: LeimDate
let secondDate: LeimDate
let firstDate: RPGDate
let secondDate: RPGDate
if (isCalendarEvent(a)) {
firstDate = a.startDate

View File

@@ -0,0 +1,181 @@
<script lang="ts" setup>
import type { RPGDate } from '@/models/Date'
import type { CalendarEvent } from '@/models/CalendarEvent'
import { useCalendar } from '@/stores/CalendarStore'
import { useCalendarEvents } from '@/stores/EventStore'
import { useElementBounding } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, ref, type ComputedRef } from 'vue'
import CalendarEventButton from '../../CalendarEvent.vue'
const props = defineProps<{
date: RPGDate
faded?: boolean
}>()
const calendarTile = ref()
const calendarEventsList = ref()
const { defaultDate, selectDate, areDatesIdentical } = useCalendar()
const { selectedDate } = storeToRefs(useCalendar())
const { currentEvents } = storeToRefs(useCalendarEvents())
/**
* All events with a startDate / endDate that starts or ends on the tile
*/
const eventsForTheDay = computed(() => {
return currentEvents.value.filter((currentEvent) => {
return (
areDatesIdentical(currentEvent.startDate, props.date) ||
areDatesIdentical(currentEvent.endDate!, props.date)
)
})
})
/**
* Is the tile on today's date ?
*/
const isDefaultDate = computed(() => {
return areDatesIdentical(props.date, defaultDate)
})
/**
* Is the tile on the hightlighted date ?
*/
const isSelectedDate = computed(() => {
return areDatesIdentical(props.date, selectedDate.value)
})
// Get bounding elements for both tile and events list
const { height: tileHeight, top: tileTop } = useElementBounding(calendarTile)
const { top: tileListTop } = useElementBounding(calendarEventsList)
// Compute the available number of events that can be displayed from refs heights
const numberOfEventsToFit: ComputedRef<number> = computed(() => {
if (!eventsForTheDay.value.length) return 0
return Math.trunc((tileHeight.value - (tileListTop.value - tileTop.value)) / 40)
})
/**
* Events that can fit in the tile's space
*/
const eventsToDisplay: ComputedRef<CalendarEvent[]> = computed<CalendarEvent[]>(() => {
return [...eventsForTheDay.value].splice(0, numberOfEventsToFit.value)
})
/**
* Remaining events that are not displayed
*
* Used if not all events can be seen in the tile's space
*/
const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsForTheDay.value.length - eventsToDisplay.value.length)
</script>
<template>
<div
ref="calendarTile"
class="tile relative text-xs p-2 border-slate-700"
:class="{
'text-slate-500': props.faded,
'text-slate-300': !props.faded
}"
>
<button
class="relative z-10 group block w-full text-center cursor-pointer"
@click="selectDate(date)"
>
<ClientOnly>
<span
class="inline-flex w-8 h-8 aspect-square items-center justify-center rounded-full border-2 border-transparent font-bold transition-colors group-hover:border-slate-800"
:class="{
'bg-slate-800': isDefaultDate && !isSelectedDate,
'text-white bg-blue-500': isSelectedDate
}"
>
{{ date.day }}
</span>
</ClientOnly>
</button>
<ClientOnly>
<ul
ref="calendarEventsList"
class="absolute top-12 bottom-2 inset-x-2 grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity"
:class="{
'opacity-40': props.faded && !isSelectedDate
}"
>
<TransitionGroup name="event">
<li v-for="event in eventsToDisplay" :key="event.title" class="grid pointer-events-auto">
<CalendarEventButton :event :tile-date="date" />
</li>
</TransitionGroup>
<li v-if="eventsNotDisplayed > 0" class="pointer-events-auto">
<UiPopover>
<UiPopoverTrigger as-child>
<button
class="text-xs px-2 py-1 block w-full text-left font-bold rounded-sm whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer transition-colors hover:bg-slate-800"
>
{{ eventsNotDisplayed }} autre{{ eventsNotDisplayed > 1 ? 's' : '' }}
</button>
</UiPopoverTrigger>
<UiPopoverContent class="w-80" :align="'center'" :side="'right'">
<div class="text-center mb-4">
<span
class="inline-flex w-12 h-12 aspect-square items-center justify-center text-lg font-semibold text-slate-300 bg-slate-800 rounded-full"
>
{{ date.day }}
</span>
</div>
<ul class="grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity">
<li
v-for="event in eventsForTheDay"
:key="event.title"
class="grid pointer-events-auto"
>
<CalendarEventButton :event :tile-date="date" />
</li>
</ul>
</UiPopoverContent>
</UiPopover>
</li>
</ul>
</ClientOnly>
<ClientOnly>
<CalendarFormCreateEvent :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
</ClientOnly>
</div>
</template>
<style lang="scss" scoped>
.tile {
&:not(:nth-child(10n)) {
border-right-width: 1px;
}
&:nth-child(n + 11) {
border-top-width: 1px;
}
}
.event-enter-active {
transition: all 120ms ease-in-out;
}
.event-leave-active {
transition: all 180ms ease-in-out;
}
.event-enter-from,
.event-leave-to {
opacity: 0;
}
.event-enter-from {
transform: translateX(.15rem);
}
.event-leave-to {
transform: translateX(-.5rem);
}
</style>

View File

@@ -0,0 +1,38 @@
<script lang="ts" setup>
import { useCalendar } from '@/stores/CalendarStore'
import { useThrottleFn } from '@vueuse/core'
const { currentDate, decrementMonth, incrementMonth } = useCalendar()
const { currentMonthData } = storeToRefs(useCalendar())
function handleWheel(e: WheelEvent) {
const isMovingUp = e.deltaY < 0
if (isMovingUp) {
moveCalendarLeft()
} else {
moveCalendarRight()
}
}
const moveCalendarLeft = useThrottleFn(() => {
decrementMonth()
}, 100)
const moveCalendarRight = useThrottleFn(() => {
incrementMonth()
}, 100)
</script>
<template>
<div class="grid grid-cols-10" @wheel="handleWheel">
<CalendarStateMonthlyDayTile
v-for="day in currentMonthData.days"
:key="`layout-month-grid-${day}`"
:date="{
day: day,
month: currentDate.currentMonth,
year: currentDate.currentYear
}"
/>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { areDatesIdentical, type LeimDate } from '@/models/Date'
import type { RPGDate } from '@/models/Date'
import { useCalendar } from '@/stores/CalendarStore'
import { useCalendarEvents } from '@/stores/EventStore'
import { storeToRefs } from 'pinia'
@@ -14,7 +14,9 @@ const props = defineProps<{
dayNumber: number
}>()
const tileDate: ComputedRef<LeimDate> = computed(() => {
const { areDatesIdentical } = useCalendar()
const tileDate: ComputedRef<RPGDate> = computed(() => {
return {
day: props.dayNumber,
month: props.monthNumber,

View File

@@ -2,9 +2,8 @@
import { useCalendar } from '@/stores/CalendarStore'
import { useThrottleFn } from '@vueuse/core'
import MonthTile from './MonthTile.vue'
const { staticConfig, decrementYear, incrementYear } = useCalendar()
const { decrementYear, incrementYear } = useCalendar()
const { sortedMonths: months } = storeToRefs(useCalendar())
function handleWheel(e: WheelEvent) {
const isMovingUp = e.deltaY < 0
@@ -26,11 +25,11 @@ const moveCalendarRight = useThrottleFn(() => {
<template>
<div class="container mt-[10vh] mb-auto" @wheel="handleWheel">
<div ref="test" class="grid grid-cols-5 gap-x-8 gap-y-16">
<MonthTile
v-for="month in staticConfig.monthsPerYear"
:key="month"
:month-number="month - 1"
<div class="grid grid-cols-5 gap-x-8 gap-y-16">
<CalendarStateYearlyMonthTile
v-for="month in months"
:key="month.id"
:month
/>
</div>
</div>

View File

@@ -0,0 +1,23 @@
<script lang="ts" setup>
import type { CalendarMonth } from '~/models/CalendarMonth';
defineProps<{
month: CalendarMonth
}>()
</script>
<template>
<div>
<div class="font-medium">
{{ month.name }}
</div>
<div class="grid grid-cols-7">
<CalendarStateYearlyDayTile
v-for="day in month.days"
:key="day"
:month-number="month.position"
:day-number="day"
/>
</div>
</div>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<h1 class="text-2xl font-bold flex">
<slot />
</h1>
</template>

View File

@@ -0,0 +1,77 @@
<script lang="ts" setup>
import { PhHouse, PhList } from '@phosphor-icons/vue'
import type { SidebarProps } from './SidebarProps';
defineProps<SidebarProps>()
</script>
<template>
<nav class="w-16 py-6 border-r-[1px] border-l-slate-500 grid grid-rows-[1fr_auto] justify-center">
<menu class="flex flex-col gap-4">
<li class="mb-12">
<UiButton variant="ghost" size="icon" class="rounded-full" @click="console.log">
<PhList size="27" />
</UiButton>
</li>
<li v-if="!isHome">
<UiTooltipProvider :delay-duration="100">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton variant="ghost" size="icon" class="rounded-full" as-child>
<RouterLink to="/">
<PhHouse size="24" weight="fill" />
</RouterLink>
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent :side="'right'">
<p>Retourner aux outils</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</li>
<li v-for="(item, i) in menuItems" :key="i">
<UiTooltipProvider :delay-duration="100">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton v-if="item.to" variant="ghost" size="icon" class="rounded-full" as-child>
<RouterLink :to="item.to">
<component :is="item.phIcon" size="24" weight="fill" />
</RouterLink>
</UiButton>
<UiButton v-if="item.clickHandler" variant="ghost" size="icon" class="rounded-full" @click="item.clickHandler">
<component :is="item.phIcon" size="24" weight="fill" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent :side="'right'">
<p>{{ item.tooltip }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</li>
<!-- <li>
<UiTooltipProvider :delay-duration="100">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
variant="ghost"
size="icon"
class="rounded-full"
@click="revealAdvancedSearch()"
>
<PhMagnifyingGlass size="24" weight="fill" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent :side="'right'">
<p>Recherche avancée</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</li> -->
</menu>
<UserCTA />
</nav>
</template>

View File

@@ -0,0 +1,11 @@
export interface MenuItem {
phIcon: Component
tooltip: string
clickHandler?: () => void
to?: string
}
export interface SidebarProps {
menuItems: MenuItem[],
isHome?: boolean
}

View File

@@ -0,0 +1,81 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { PhUserCircle } from '@phosphor-icons/vue'
const router = useRouter()
const { auth } = useSupabaseClient()
const user = useSupabaseUser()
const userMeta = computed(() => user.value?.user_metadata)
const menuOpened = ref<boolean>(false)
function closeMenu() {
menuOpened.value = false
}
watch(user, closeMenu)
async function handleGoogleLogin() {
try {
auth.signInWithOAuth({
provider: 'google',
options: {
queryParams: {
access_type: 'offline',
prompt: 'consent'
}
}
})
} catch (err) {
console.log(err)
}
}
async function handleLogout() {
try {
const { error } = await auth.signOut()
if (error) throw error
} catch (err) {
console.log(err)
}
}
function gotoProfilePage() {
router.push({ path: '/profile' })
}
</script>
<template>
<ClientOnly>
<UiPopover v-model:open="menuOpened">
<UiPopoverTrigger>
<UiAvatar v-if="user" class="cursor-pointer">
<UiAvatarImage
:src="userMeta?.avatar_url"
:alt="userMeta?.full_name"
referrerpolicy="no-referrer"
/>
<UiAvatarFallback>Me</UiAvatarFallback>
</UiAvatar>
<UiButton v-else variant="outline" size="icon">
<PhUserCircle size="18" />
</UiButton>
</UiPopoverTrigger>
<UiPopoverContent class="w-fit p-0" :align="'start'" :side="'top'" :collision-padding="20">
<UiCommand>
<UiCommandList v-if="user">
<UiCommandGroup :heading="`Connecté en tant que ${user?.email}`">
<UiCommandItem value="profile" @select="gotoProfilePage"> Profil </UiCommandItem>
<UiCommandItem value="logout" @select="handleLogout"> Déconnexion </UiCommandItem>
</UiCommandGroup>
</UiCommandList>
<UiCommandList v-else>
<UiCommandItem value="logout" @select="handleGoogleLogin"> Connexion </UiCommandItem>
</UiCommandList>
</UiCommand>
</UiPopoverContent>
</UiPopover>
</ClientOnly>
</template>

View File

@@ -0,0 +1,55 @@
<script lang="ts" setup>
const user = useSupabaseUser()
const { data: worlds } = await useLazyFetch('/api/worlds')
</script>
<template>
<main class="p-8">
<Heading>{{ user?.user_metadata.full_name }}</Heading>
<section v-if="worlds?.data" class="mt-4">
<h2 class="mb-4 text-lg font-bold">
Mondes
</h2>
<ul class="grid md:grid-cols-3 gap-2">
<li v-for="(world, i) in worlds.data" :key="i">
<UiCard
class="w-full transition-all"
:link="`/calendar/${world.id}`"
:class="{
'hover:bg-slate-50 dark:hover:bg-sky-950 dark:focus-within:outline-sky-900': !world.color,
'bg-red-100 dark:bg-red-950 border-red-200 hover:bg-red-50 dark:hover:bg-red-900 dark:border-red-900 dark:focus-within:outline-red-900': world.color === 'red',
'bg-orange-100 dark:bg-orange-950 border-orange-200 hover:bg-orange-50 dark:hover:bg-orange-900 dark:border-orange-900 dark:focus-within:outline-orange-900': world.color === 'orange',
'bg-amber-100 dark:bg-amber-950 border-amber-200 hover:bg-amber-50 dark:hover:bg-amber-900 dark:border-amber-900 dark:focus-within:outline-amber-900': world.color === 'amber',
'bg-yellow-100 dark:bg-yellow-950 border-yellow-200 hover:bg-yellow-50 dark:hover:bg-yellow-900 dark:border-yellow-900 dark:focus-within:outline-yellow-900': world.color === 'yellow',
'bg-lime-100 dark:bg-lime-950 border-lime-200 hover:bg-lime-50 dark:hover:bg-lime-900 dark:border-lime-900 dark:focus-within:outline-lime-900': world.color === 'lime',
' bg-green-100 dark:bg-green-950 border-green-200 hover:bg-green-50 dark:hover:bg-green-900 dark:border-green-900 dark:focus-within:outline-green-900': world.color === 'green',
'bg-emerald-100 dark:bg-emerald-950 border-emerald-200 hover:bg-emerald-50 dark:hover:bg-emerald-900 dark:border-emerald-900 dark:focus-within:outline-emerald-900': world.color === 'emerald',
'bg-teal-100 dark:bg-teal-950 border-teal-200 hover:bg-teal-50 dark:hover:bg-teal-900 dark:border-teal-900 dark:focus-within:outline-teal-900': world.color === 'teal',
'bg-cyan-100 dark:bg-cyan-950 border-cyan-200 hover:bg-cyan-50 dark:hover:bg-cyan-900 dark:border-cyan-900 dark:focus-within:outline-cyan-900': world.color === 'cyan',
'bg-sky-100 dark:bg-sky-950 border-sky-200 hover:bg-sky-50 dark:hover:bg-sky-900 dark:border-sky-900 dark:focus-within:outline-sky-900': world.color === 'sky',
'bg-blue-100 dark:bg-blue-950 border-blue-200 hover:bg-blue-50 dark:hover:bg-blue-900 dark:border-blue-900 dark:focus-within:outline-blue-900': world.color === 'blue',
'bg-indigo-100 dark:bg-indigo-950 border-indigo-200 hover:bg-indigo-50 dark:hover:bg-indigo-900 dark:border-indigo-900 dark:focus-within:outline-indigo-900': world.color === 'indigo',
'bg-violet-100 dark:bg-violet-950 border-violet-200 hover:bg-violet-50 dark:hover:bg-violet-900 dark:border-violet-900 dark:focus-within:outline-violet-900': world.color === 'violet',
'bg-purple-100 dark:bg-purple-950 border-purple-200 hover:bg-purple-50 dark:hover:bg-purple-900 dark:border-purple-900 dark:focus-within:outline-purple-900': world.color === 'purple',
'bg-fuchsia-100 dark:bg-fuchsia-950 border-fuchsia-200 hover:bg-fuchsia-50 dark:hover:bg-fuchsia-900 dark:border-fuchsia-900 dark:focus-within:outline-fuchsia-900': world.color === 'fuchsia',
'bg-pink-100 dark:bg-pink-950 border-pink-200 hover:bg-pink-50 dark:hover:bg-pink-900 dark:border-pink-900 dark:focus-within:outline-pink-900': world.color === 'pink',
'bg-pink-100 dark:bg-rose-950 border-rose-200 hover:bg-rose-50 dark:hover:bg-rose-900 dark:border-rose-900 dark:focus-within:outline-rose-900': world.color === 'rose',
'text-slate-100 bg-slate-900 border-slate-700 hover:bg-slate-700 dark:hover:bg-slate-800 dark:border-slate-900 dark:focus-within:outline-slate-900': world.color === 'black',
' hover:bg-slate-50 dark:hover:text-slate-900 dark:hover:bg-slate-300 dark:border-slate-500 dark:focus-within:outline-slate-100': world.color === 'white',
}"
>
<UiCardHeader>
<UiCardTitle>{{ world.name }}</UiCardTitle>
</UiCardHeader>
<UiCardContent>
<p class="italic">{{ world.description }}</p>
</UiCardContent>
</UiCard>
</li>
</ul>
</section>
</main>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { AvatarRoot } from 'radix-vue'
import { type AvatarVariants, avatarVariant } from '.'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<{
class?: HTMLAttributes['class']
size?: AvatarVariants['size']
shape?: AvatarVariants['shape']
}>(), {
size: 'sm',
shape: 'circle',
})
</script>
<template>
<AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)">
<slot />
</AvatarRoot>
</template>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { AvatarFallback, type AvatarFallbackProps } from 'radix-vue'
const props = defineProps<AvatarFallbackProps>()
</script>
<template>
<AvatarFallback v-bind="props">
<slot />
</AvatarFallback>
</template>

View File

@@ -0,0 +1,9 @@
<script setup lang="ts">
import { AvatarImage, type AvatarImageProps } from 'radix-vue'
const props = defineProps<AvatarImageProps>()
</script>
<template>
<AvatarImage v-bind="props" class="h-full w-full object-cover" />
</template>

View File

@@ -0,0 +1,24 @@
import { type VariantProps, cva } from 'class-variance-authority'
export { default as Avatar } from './Avatar.vue'
export { default as AvatarImage } from './AvatarImage.vue'
export { default as AvatarFallback } from './AvatarFallback.vue'
export const avatarVariant = cva(
'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',
{
variants: {
size: {
sm: 'h-10 w-10 text-xs',
base: 'h-16 w-16 text-2xl',
lg: 'h-32 w-32 text-5xl',
},
shape: {
circle: 'rounded-full',
square: 'rounded-md',
},
},
},
)
export type AvatarVariants = VariantProps<typeof avatarVariant>

View File

@@ -7,18 +7,19 @@ export const badgeVariants = cva(
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground'
}
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default'
}
}
variant: 'default',
},
},
)
export type BadgeVariants = VariantProps<typeof badgeVariants>

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { RouterLink } from 'vue-router'
const props = defineProps<{
class?: HTMLAttributes['class']
@@ -13,14 +12,14 @@ const props = defineProps<{
<div
:class="
cn('rounded-lg border bg-card text-card-foreground shadow-sm transition-all', props.class, {
'relative outline outline-2 outline-offset-4 outline-transparent hover:bg-sky-950 hover:-translate-y-[.2rem] focus-within:outline-sky-900':
'relative outline outline-2 outline-offset-4 outline-transparent hover:-translate-y-[.2rem]':
props.link
})
"
>
<slot />
<RouterLink
<NuxtLink
v-if="props.link"
:to="props.link"
class="absolute inset-0 focus-visible:outline-none"

View File

@@ -8,7 +8,7 @@ const props = defineProps<{
</script>
<template>
<div :class="cn('p-6 pt-0 text-muted-foreground', props.class)">
<div :class="cn('p-6 pt-0', props.class)">
<slot />
</div>
</template>

View File

@@ -8,7 +8,7 @@ const props = defineProps<{
</script>
<template>
<p :class="cn('text-sm text-muted-foreground mt-1', props.class)">
<div :class="cn('text-sm text-muted-foreground', props.class)">
<slot />
</p>
</div>
</template>

View File

@@ -8,7 +8,7 @@ const props = defineProps<{
</script>
<template>
<div :class="cn('flex flex-col gap-y-1.5 px-6 pt-6 pb-3', props.class)">
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
<slot />
</div>
</template>

View File

@@ -8,7 +8,11 @@ const props = defineProps<{
</script>
<template>
<h3 :class="cn('text-2xl font-semibold leading-none tracking-tight', props.class)">
<h3
:class="
cn('text-2xl font-semibold leading-none tracking-tight', props.class)
"
>
<slot />
</h3>
</template>

View File

@@ -1,3 +1,5 @@
export { ComboboxPortal } from 'radix-vue'
export { default as Command } from './Command.vue'
export { default as CommandDialog } from './CommandDialog.vue'
export { default as CommandEmpty } from './CommandEmpty.vue'

Some files were not shown because too many files have changed in this diff Show More