79 Commits

Author SHA1 Message Date
Alexis
8290d02900 Updated nvm version 2025-07-27 15:14:28 +02:00
Alexis
b67f663f05 Updated legal text as well 2025-07-27 15:12:15 +02:00
Alexis
456135d679 Fixed better sqlite 3 not being installed 2025-07-27 15:04:26 +02:00
Alexis
7fdab8601f Migration to nuxt 4
Used codemods CLI and reworked most alias'd path that stopped working
2025-07-27 14:30:30 +02:00
Alexis
68b9a38f83 Updated other major packages 2025-07-26 23:36:10 +02:00
Alexis
77752e8164 Updated nuxt i18n to v10 2025-07-26 23:32:32 +02:00
Alexis
1d74d6f64b Updated zod to v4 2025-07-26 23:18:09 +02:00
Alexis
b0975ee293 Updated median and minor packages 2025-07-26 23:01:08 +02:00
Alexis
33f4f78ea7 Merge branch 'code/cleanup' into dev 2025-07-26 18:47:27 +02:00
Alexis
4a04728183 Fixed about page being auth guarded 2025-07-26 18:47:16 +02:00
Alexis
8cd20aa386 Removed vector from supabase
This resolves all the issues with unhealthy containers, idk why it still doesn't work in 2025
2025-07-17 22:02:37 +02:00
Alexis
8e325196e6 Updated packages 2025-05-13 11:22:27 +02:00
Alexis
47883d1577 More legal stuff changes 2025-05-13 11:05:33 +02:00
Alexis
4433f5f1b6 Fixed some content rules 2025-05-07 14:54:13 +02:00
Alexis
f132e6d1e0 Added container to about page 2025-05-07 14:34:46 +02:00
Alexis
245fb421b3 Fixed redundant part of digit regex 2025-05-07 14:31:35 +02:00
Alexis
46d7aa74f9 Removed useless assignement 2025-05-07 14:26:19 +02:00
Alexis
9e40ab0982 Changed constructor to cast 2025-05-07 14:16:14 +02:00
Alexis
63d5ba2224 Changed sort to toSorted 2025-05-07 14:12:21 +02:00
Alexis
114e235a93 Removed useless type inference 2025-05-07 14:10:51 +02:00
Alexis
f03d952183 Fixed aria combobox attributes 2025-05-07 14:03:49 +02:00
Alexis
45eefbe35d Removed a leftover comment 2025-05-07 13:57:32 +02:00
Alexis
18f332be54 Removed unused imports 2025-05-07 13:45:20 +02:00
Alexis
67b79000f6 Removed some commented code 2025-05-07 13:40:50 +02:00
Alexis
896bac5f83 Changed defineEmits to be more legible 2025-05-07 13:37:23 +02:00
Alexis
09cd25ce33 Removed duplicate character in regex 2025-05-07 13:18:01 +02:00
Alexis
1cf8510a7b First draft of legal page 2025-05-07 13:15:01 +02:00
Alexis
a83096a17b Added scannerwork to gitignore 2025-05-07 13:14:41 +02:00
Alexis
216617a8e6 Updated deps 2025-05-05 16:42:03 +02:00
Alexis
a668fa8d81 Hotfix for previous ui feature 2025-04-30 17:00:28 +02:00
AlexisNP
0fdb1508a4 Merge pull request #80 from AlexisNP/ui/event-visibility-checkbox
UI/event visibility checkbox
2025-04-30 14:05:10 +02:00
Alexis
d7447eb700 Changed toggle to checkbox 2025-04-30 14:02:02 +02:00
AlexisNP
c2712f7b86 Merge pull request #79 from AlexisNP/features/mobile-calendars
Features/mobile calendars
2025-04-29 16:19:14 +02:00
Alexis
6b01ce181d Added back breadcrumb 2025-04-29 16:16:45 +02:00
Alexis
acb5c0d92f Added middleware for closing sidebar on route change 2025-04-29 09:34:58 +02:00
Alexis
387597755d Added sidebar toggle 2025-04-29 09:28:49 +02:00
Alexis
a5f7e1ded9 Added first sidebar draft 2025-04-28 17:18:06 +02:00
Alexis
93a03ca4e2 Removed breadcrumbs on mobile 2025-04-28 12:27:10 +02:00
Alexis
a6fe4c7ea3 Categories appear on more options 2025-04-28 11:29:33 +02:00
Alexis
805972bf3c Fixed css color issue 2025-04-26 21:16:35 +02:00
Alexis
c071e9c33b Added some transitions 2025-04-26 21:01:52 +02:00
Alexis
b430ab2660 Changed head description 2025-04-26 20:32:31 +02:00
Alexis
a6d388f5da Added missing description to event creation dialog 2025-04-26 20:19:11 +02:00
Alexis
a03b7890e6 Made dialog go full screen on mobile 2025-04-25 21:30:40 +02:00
Alexis
b71e715cf5 Fixed call stack issue 2025-04-25 21:06:19 +02:00
Alexis
9ba09c2d2b Fixed black button for day tile 2025-04-25 17:56:56 +02:00
Alexis
b1ff2ec232 Changed today CTA on mobile 2025-04-25 17:53:40 +02:00
Alexis
093b4e7493 Fixed overflow issues on search 2025-04-25 17:36:54 +02:00
Alexis
7b0bb1e939 Made event creation dialog behaviour the same 2025-04-25 17:33:45 +02:00
Alexis
60bbb6f48f Fixed no contrast event buttons 2025-04-25 17:30:50 +02:00
Alexis
f502f80341 Updated list style 2025-04-25 17:25:01 +02:00
Alexis
e634cc8127 Fixed some inconsistencies 2025-04-25 16:56:09 +02:00
Alexis
ee29169f4f Added transitions for client only buttons 2025-04-25 15:38:37 +02:00
Alexis
2d2069cae6 Fixed hydration mismatches 2025-04-25 15:23:03 +02:00
Alexis
f7f12feaec Changed popover to modal on small screens 2025-04-25 14:58:47 +02:00
Alexis
5b8fedba7c Added space-x for alert dialog 2025-04-25 14:17:48 +02:00
Alexis
fa872e6914 Fixed collision padding for calendar events 2025-04-25 14:15:20 +02:00
Alexis
43dd12782b Reduced spacing on calendar menu 2025-04-23 21:59:45 +02:00
Alexis
f1f140f2bc Reduced some spacing on mobile 2025-04-23 21:41:32 +02:00
Alexis
707d4374ef Changed calendar layout for buttons 2025-04-23 16:21:51 +02:00
AlexisNP
048413286f Merge pull request #77 from AlexisNP/upgrade/nuxt
Upgrade/nuxt
2025-04-23 14:11:29 +02:00
Alexis
9d06040c5d Changed muted colour 2025-04-22 22:30:14 +02:00
Alexis
15bfc66087 Removed dark classes for calendar preview 2025-04-22 22:15:49 +02:00
Alexis
e6b2670438 Removed character string 2025-04-22 17:17:13 +02:00
Alexis
e2d2810db6 Event creation form closes if the date changes 2025-04-22 17:07:29 +02:00
Alexis
1827b9118f Changed chip colour for tags input 2025-04-22 15:36:58 +02:00
Alexis
086b997845 Fixed default breadcrumb look 2025-04-22 15:31:41 +02:00
Alexis
73f5e01244 Removed default lucide icons and updated some packages 2025-04-22 14:09:25 +02:00
Alexis
d02bfacd57 Made search bigger 2025-04-18 18:07:13 +02:00
Alexis
5370a9df06 Added color square to event creation forms 2025-04-18 18:03:55 +02:00
Alexis
5bd76495f4 Added color square to category filter 2025-04-18 17:59:49 +02:00
Alexis
7480447420 Standardized tilde imports for non modules 2025-04-18 17:44:21 +02:00
Alexis
034713961b Changed card color for calendars and worlds 2025-04-18 17:35:02 +02:00
Alexis
00224417be Cleaned up most vanilla try catches 2025-04-18 17:11:45 +02:00
Alexis
9b4556245e Fixed hardcoded world id for calendar creation 2025-04-17 22:34:52 +02:00
Alexis
ad8120bdd0 Removed useless lazy directives 2025-04-17 22:14:10 +02:00
Alexis
73bd79c3c5 Fixed hydration error 2025-04-17 21:47:25 +02:00
Alexis
8215791830 Changed h-w to size 2025-04-17 21:41:48 +02:00
AlexisNP
36456ce377 Merge pull request #76 from AlexisNP/upgrade/tailwind-4
Upgrade/tailwind 4
2025-04-17 19:58:02 +02:00
298 changed files with 7712 additions and 5045 deletions

2
.gitignore vendored
View File

@@ -26,3 +26,5 @@ logs
# Supabase # Supabase
.branches .branches
.temp .temp
.scannerwork/

2
.nvmrc
View File

@@ -1 +1 @@
20.12.0 24.4.0

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ConfigProvider } from "radix-vue" import { ConfigProvider } from "radix-vue"
import { cn } from "./lib/utils";
useHead({ useHead({
titleTemplate: (titleChunk) => { titleTemplate: (titleChunk) => {
@@ -26,6 +27,8 @@ useHead({
}) })
const useIdFunction = () => useId() const useIdFunction = () => useId()
const { isSidebarOpened } = storeToRefs(useUiStore())
</script> </script>
<template> <template>
@@ -34,10 +37,18 @@ const useIdFunction = () => useId()
<NuxtLayout> <NuxtLayout>
<ConfigProvider :use-id="useIdFunction"> <ConfigProvider :use-id="useIdFunction">
<div class="h-full grid grid-cols-[auto_1fr] dark:bg-black transition-colors"> <div
class="h-full grid md:grid-cols-[auto_1fr] dark:bg-black shadow-body-light dark:shadow-body-dark transition-colors after:absolute after:transition-colors"
:class="cn({
'max-md:after:bg-transparent': isSidebarOpened,
'has-sidebar max-md:after:bg-black/20 md:after:opacity-0 md:after:pointer-events-none': isSidebarOpened
})"
>
<Sidebar /> <Sidebar />
<div class="wrapper shadow-body-light dark:shadow-body-dark transition-all"> <div
class="wrapper max-h-screen transition-all overflow-y-auto"
>
<NuxtPage /> <NuxtPage />
</div> </div>
</div> </div>
@@ -54,4 +65,17 @@ const useIdFunction = () => useId()
.wrapper > * { .wrapper > * {
height: 100%; height: 100%;
} }
.has-sidebar {
position: relative;
isolation: isolate;
&::after {
content: '';
display: block;
position: fixed;
inset: 0;
z-index: 30;
}
}
</style> </style>

View File

@@ -118,7 +118,8 @@
.event-button, .event-button,
.event-popover, .event-popover,
.event-callout { .event-callout,
.card-color {
--fg-color: var(--color-foreground); --fg-color: var(--color-foreground);
--bg-color: var(--color-background); --bg-color: var(--color-background);
@@ -126,6 +127,13 @@
transition-timing-function: var(--default-transition-timing-function); transition-timing-function: var(--default-transition-timing-function);
} }
.event-callout hr,
.card-color hr {
transition-property: border-color;
transition-duration: var(--default-transition-duration);
transition-timing-function: var(--default-transition-timing-function);
}
.event-button { .event-button {
color: var(--fg-color); color: var(--fg-color);
background-color: var(--bg-color); background-color: var(--bg-color);
@@ -138,7 +146,7 @@
} }
.event-button.is-hidden { .event-button.is-hidden {
background-color: transparent; background-color: transparent;
color: var(--bg-color); color: var(--color-foreground);
border: 1px dashed var(--bg-color); border: 1px dashed var(--bg-color);
transition-property: background-color, color, border-color; transition-property: background-color, color, border-color;
} }
@@ -206,6 +214,23 @@
background-color: color-mix(in srgb, var(--bg-color) 50%, var(--color-background)); background-color: color-mix(in srgb, var(--bg-color) 50%, var(--color-background));
} }
.card-color {
border-color: color-mix(in srgb, var(--bg-color) 30%, var(--color-background));
background-color: color-mix(in srgb, var(--bg-color) 10%, var(--color-background));
hr {
border-color: color-mix(in srgb, var(--bg-color) 30%, var(--color-background))
}
}
.card-color:is(:hover, :focus-visible) {
border-color: color-mix(in srgb, var(--bg-color) 40%, var(--color-background));
background-color: color-mix(in srgb, var(--bg-color) 20%, var(--color-background));
hr {
border-color: color-mix(in srgb, var(--bg-color) 40%, var(--color-background))
}
}
.bgc { .bgc {
display: flex; display: flex;
align-items: center; align-items: center;

70
app/assets/_content.css Normal file
View File

@@ -0,0 +1,70 @@
.content {
> * + * {
margin-top: .5em;
@variant md {
margin-top: .75rem;
}
}
h1 {
font-size: var(--text-3xl);
font-weight: var(--font-weight-bold);
@variant md {
font-size: var(--text-4xl);
}
}
h2 {
font-size: var(--text-xl);
font-weight: var(--font-weight-bold);
@variant md {
font-size: var(--text-2xl);
}
}
h3 {
font-size: var(--text-lg);
font-weight: var(--font-weight-bold);
@variant md {
font-size: var(--text-xl);
}
}
h4 {
font-size: var(--text-md);
font-weight: var(--font-weight-bold);
@variant md {
font-size: var(--text-lg);
}
}
/* Normal link */
p a:not([class]) {
color: var(--color-sky-600);
text-underline-offset: .25rem;
&:hover {
color: var(--color-sky-500);
text-decoration: underline;
}
@variant dark {
color: var(--color-teal-500);
&:hover {
color: var(--color-teal-300);
}
}
}
ul {
list-style-type: square;
padding-inline-start: 1.5rem;
}
hr {
margin-block: 2rem;
}
}

View File

@@ -0,0 +1,4 @@
@theme inline {
--text-2xs: 0.7rem;
--text-2xs--line-height: 0.75rem;
}

View File

@@ -1,11 +1,14 @@
@import 'tailwindcss'; @import 'tailwindcss';
@config '../tailwind.config.js'; @config '../../tailwind.config.js';
@import "./theme.css" layer(theme); @import "./theme.css" layer(theme);
@import "./_typography.css" layer(theme);
@import "./_colors.css" layer(theme); @import "./_colors.css" layer(theme);
@import "./_colors.css" layer(utilities); @import "./_colors.css" layer(utilities);
@import "./_content.css";
/* /*
The default border color has changed to `currentcolor` in Tailwind CSS v4, The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still so we've added these compatibility styles to make sure everything still

View File

@@ -39,7 +39,7 @@
--background: hsl(180 0 4.9%); --background: hsl(180 0 4.9%);
--foreground: hsl(210 40% 98%); --foreground: hsl(210 40% 98%);
--muted: hsl(244 47% 20%); --muted: hsl(0, 0%, 12%);
--muted-foreground: hsl(215 20.2% 65.1%); --muted-foreground: hsl(215 20.2% 65.1%);
--popover: hsl(180 0 4.9%); --popover: hsl(180 0 4.9%);

View File

@@ -40,7 +40,7 @@ onMounted(() => {
<component :is="currentViewComponent" /> <component :is="currentViewComponent" />
<LazyCalendarSearch /> <CalendarSearch />
<LazyCalendarDialogCategories v-if="!isReadOnly" /> <LazyCalendarDialogCategories v-if="!isReadOnly" />
<LazyCalendarDialogUpdateEvent v-if="!isReadOnly" /> <LazyCalendarDialogUpdateEvent v-if="!isReadOnly" />
<LazyCalendarDialogDeleteEvent v-if="!isReadOnly" /> <LazyCalendarDialogDeleteEvent v-if="!isReadOnly" />

View File

@@ -9,9 +9,11 @@ const { isReadOnly } = storeToRefs(useCalendar())
<UiTooltipProvider :delay-duration="250"> <UiTooltipProvider :delay-duration="250">
<UiTooltip> <UiTooltip>
<UiTooltipTrigger as-child> <UiTooltipTrigger as-child>
<ClientOnly>
<UiButton v-if="!isReadOnly" variant="secondary" size="icon" @click="toggleCategoriesModal(true)"> <UiButton v-if="!isReadOnly" variant="secondary" size="icon" @click="toggleCategoriesModal(true)">
<PhTag size="20" weight="light" /> <PhTag size="20" weight="light" />
</UiButton> </UiButton>
</ClientOnly>
</UiTooltipTrigger> </UiTooltipTrigger>
<UiTooltipContent> <UiTooltipContent>
<p> <p>

View File

@@ -3,10 +3,15 @@ import { storeToRefs } from "pinia"
import { computed } from "vue" import { computed } from "vue"
import { PhMapPin } from "@phosphor-icons/vue" import { PhMapPin } from "@phosphor-icons/vue"
import { breakpointsTailwind } from "@vueuse/core"
const { defaultDate, getFormattedDateTitle, getRelativeString, getDifferenceInDays } = useCalendar() const { defaultDate, getFormattedDateTitle, getRelativeString, getDifferenceInDays } = useCalendar()
const { selectedDate } = storeToRefs(useCalendar()) const { selectedDate } = storeToRefs(useCalendar())
const breakpoints = useBreakpoints(
breakpointsTailwind
)
const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true)) const mainDateTitle = computed(() => getFormattedDateTitle(selectedDate.value, true))
// const mainDateTitle = computed(() => convertDateToDays(selectedDate.value)) // const mainDateTitle = computed(() => convertDateToDays(selectedDate.value))
@@ -16,19 +21,24 @@ const isToday = computed(() => getDifferenceInDays(defaultDate, selectedDate.val
<template> <template>
<ClientOnly> <ClientOnly>
<div class="flex gap-2 items-center"> <div class="grid md:flex md:gap-2 items-center">
<h1 class="text-2xl font-bold flex items-center gap-1"> <h1 class="text-lg md:text-2xl max-md:leading-tight font-bold flex items-center gap-1">
<PhMapPin size="26" weight="bold" /> {{ mainDateTitle }} <PhMapPin
:size="breakpoints.md.value ? 26 : 18"
class="max-md:hidden"
weight="light"
/>
{{ mainDateTitle }}
</h1> </h1>
<h2 v-if="!isToday" class="text-xl italic opacity-75"> <h2 v-if="!isToday" class="text-sm max-md:leading-tight md:text-xl italic opacity-75">
{{ dateDifference }} <span class="max-md:hidden"></span> {{ dateDifference }}
</h2> </h2>
</div> </div>
<template #fallback> <template #fallback>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<UiSkeleton class="h-8 w-64" /> <UiSkeleton class="h-8 w-64 max-md:max-w-full" />
<UiSkeleton class="h-6 w-28" /> <UiSkeleton class="max-md:hidden h-6 w-28" />
</div> </div>
</template> </template>
</ClientOnly> </ClientOnly>

View File

@@ -1,10 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { breakpointsTailwind } from "@vueuse/core"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useCalendar } from "@/stores/CalendarStore" import { useCalendar } from "@/stores/CalendarStore"
import { PhCalendarBlank, PhCheckCircle, PhGear } from "@phosphor-icons/vue" import { PhCalendarBlank, PhCheckCircle, PhDotsThreeVertical, PhGear, PhTag } from "@phosphor-icons/vue"
import { computed } from "vue"
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType } = useCalendar() const breakpoints = useBreakpoints(
breakpointsTailwind
)
const { currentConfig, viewTypeOptions, getViewTypeTitle, setViewType, toggleCategoriesModal } = useCalendar()
const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType)) const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
</script> </script>
@@ -12,15 +16,29 @@ const viewTypeTitle = computed(() => getViewTypeTitle(currentConfig.viewType))
<UiDropdownMenu> <UiDropdownMenu>
<UiDropdownMenuTrigger as-child> <UiDropdownMenuTrigger as-child>
<UiButton variant="secondary" size="icon"> <UiButton variant="secondary" size="icon">
<PhGear size="20" weight="fill" /> <ClientOnly>
<PhGear v-if="breakpoints.md.value" size="20" weight="fill" />
<PhDotsThreeVertical v-else size="26" weight="bold" />
</ClientOnly>
</UiButton> </UiButton>
</UiDropdownMenuTrigger> </UiDropdownMenuTrigger>
<UiDropdownMenuContent :side="'bottom'" :align="'start'" :side-offset="10" :align-offset="25" :collision-padding="40" class="text-right"> <UiDropdownMenuContent :side="'bottom'" :align="'start'" :side-offset="10" :align-offset="25" :collision-padding="20" class="text-right">
<UiDropdownMenuArrow /> <UiDropdownMenuArrow />
<UiDropdownMenuLabel> <UiDropdownMenuLabel>
{{ $t('entity.calendar.seeOptions') }} {{ $t('entity.calendar.seeOptions') }}
</UiDropdownMenuLabel> </UiDropdownMenuLabel>
<UiDropdownMenuItem
v-if="!breakpoints.md.value"
class="flex gap-[1ch] justify-end items-center"
@click="toggleCategoriesModal(true)"
>
{{ $t('entity.calendar.seeCategories') }}
<PhTag size="18" />
</UiDropdownMenuItem>
<UiDropdownMenuSub> <UiDropdownMenuSub>
<UiDropdownMenuSubTrigger arrow-direction="left" class="p-0 rounded-none"> <UiDropdownMenuSubTrigger arrow-direction="left" class="p-0 rounded-none">
<UiDropdownMenuItem class="flex gap-[1ch] justify-end items-center pointer-events-none"> <UiDropdownMenuItem class="flex gap-[1ch] justify-end items-center pointer-events-none">

View File

@@ -0,0 +1,103 @@
<script lang="ts" setup>
import { cn } from "@/lib/utils"
import { PhArchive, PhCalendarDots, PhFile, PhFileDashed, PhFilePlus, PhPencil, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue"
import { DateTime } from "luxon"
import type { Calendar } from "@@/models/CalendarConfig"
const props = defineProps<{
calendar: Calendar
gmId?: string
showActions?: boolean
}>()
const emit = defineEmits(["on-edit", "on-delete"])
const cardRef = ref(null)
const isCardHovered = useElementHover(cardRef)
const { focused: isCardFocused } = useFocusWithin(cardRef)
const { locale } = useI18n()
const createdAt = DateTime.fromISO(props.calendar.createdAt!).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value })
const updatedAt = computed<string>(() => props.calendar.updatedAt ? DateTime.fromISO(props.calendar.updatedAt).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value }) : "")
const user = useSupabaseUser()
const isOwner = computed(() => user.value && props.gmId && user.value.id === props.gmId)
const calendarLink = computed(() => isOwner.value ? `/my/calendars/${props.calendar.id}` : `/calendars/${props.calendar.shortId}`)
const hasActions = computed(() => isOwner.value && props.showActions)
</script>
<template>
<UiCard
ref="cardRef"
class="w-full h-full flex flex-col"
:link="calendarLink"
:class="cn(
calendar.color ? `group card-color element-${calendar.color}` : '',
)"
>
<UiCardHeader class="gap-4">
<UiCardTitle
class="text-xl"
:class="cn({
'pr-18': hasActions
})"
>
{{ calendar.name }}
</UiCardTitle>
<div v-if="calendar.state === 'published'" class="flex items-center gap-1 text-sm">
<PhFile size="20" weight="fill" />
<span>{{ $t('ui.contentState.published') }}</span>
</div>
<div v-if="calendar.state === 'draft'" class="flex items-center gap-1 text-sm">
<PhFileDashed size="20" weight="fill" />
<span>{{ $t('ui.contentState.draft') }}</span>
</div>
<div v-if="calendar.state === 'archived'" class="flex items-center gap-1 text-sm">
<PhArchive size="20" weight="fill" />
<span>{{ $t('ui.contentState.archived') }}</span>
</div>
</UiCardHeader>
<UiCardContent class="grow">
<p class="flex items-center gap-1">
<PhCalendarDots size="24" weight="fill" />
<span v-if="calendar.eventNb?.[0]!.count! > 1">{{ $t("entity.calendar.hasXEvents", { count: calendar.eventNb?.[0]!.count }) }}</span>
<span v-else>{{ $t("entity.calendar.hasXEvent", { count: calendar.eventNb?.[0]!.count }) }}</span>
</p>
<div
v-if="hasActions && (isCardHovered || isCardFocused)"
class="flex gap-2 absolute top-4 right-4 z-20"
>
<Transition name="fade" appear>
<UiButton size="icon" variant="outline" class="size-8 border-foreground/20 group-hover:border-foreground/30 hover:text-background hover:border-foreground hover:bg-foreground" @click="emit('on-edit')">
<PhPencil size="15" weight="fill" />
</UiButton>
</Transition>
<Transition name="fade" appear>
<UiButton size="icon" variant="outline" class="size-8 border-foreground/25 group-hover:border-foreground/40 hover:text-background hover:border-rose-500 hover:bg-rose-500" @click="emit('on-delete')">
<PhTrash size="15" weight="fill" />
</UiButton>
</Transition>
</div>
</UiCardContent>
<hr>
<UiCardFooter class="border-t-0">
<ul class="grid gap-1 text-sm">
<li class="flex gap-1 items-center">
<PhFilePlus size="18" />
<span>{{ $t('common.createdAt', { createdAt }) }}</span>
</li>
<li v-if="updatedAt" class="flex gap-1 items-center">
<PhPencilSimpleLine size="18" />
<span>{{ $t('common.updatedAt', { updatedAt }) }}</span>
</li>
</ul>
</UiCardFooter>
</UiCard>
</template>

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { PhMagnifyingGlass } from "@phosphor-icons/vue"
const { revealAdvancedSearch } = useCalendar()
const breakpoints = useBreakpoints(
breakpointsTailwind
)
</script>
<template>
<ClientOnly>
<Transition name="fade" appear>
<UiButton
:search-slash="breakpoints.lg.value"
:size="breakpoints.lg.value ? 'default' : 'icon'"
@click="revealAdvancedSearch()"
>
<PhMagnifyingGlass size="20" weight="light" />
<span v-if="breakpoints.lg.value">
{{ $t('entity.advancedSearch.title') }}
</span>
</UiButton>
</Transition>
</ClientOnly>
</template>

View File

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

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Category } from "~/models/Category"; import type { Category } from "@@/models/Category";
defineProps<{ defineProps<{
categories: Category[] categories: Category[]

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Category } from "~/models/Category"; import type { Category } from "@@/models/Category";
defineProps<{ defineProps<{
category: Category category: Category

View File

@@ -1,9 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Category } from "~/models/Category"; import type { Category } from "@@/models/Category";
import { ScrollAreaRoot, ScrollAreaViewport, ScrollAreaScrollbar, ScrollAreaThumb } from "radix-vue" import { ScrollAreaRoot, ScrollAreaViewport, ScrollAreaScrollbar, ScrollAreaThumb } from "radix-vue"
const { t } = useI18n()
const { categories } = defineProps<{ const { categories } = defineProps<{
categories: Category[] categories: Category[]
}>() }>()

View File

@@ -2,7 +2,7 @@
import { PhPlus } from "@phosphor-icons/vue" import { PhPlus } from "@phosphor-icons/vue"
import { useToast } from "~/components/ui/toast" import { useToast } from "~/components/ui/toast"
import { ToastLifetime } from "~/components/ui/toast/use-toast" import { ToastLifetime } from "~/components/ui/toast/use-toast"
import { cn } from "~/lib/utils" import { cn } from "@/lib/utils"
const { toast } = useToast() const { toast } = useToast()
const { t } = useI18n() const { t } = useI18n()

View File

@@ -2,8 +2,8 @@
import { PhCheck, PhTrash } from "@phosphor-icons/vue" import { PhCheck, PhTrash } from "@phosphor-icons/vue"
import { useToast } from "~/components/ui/toast" import { useToast } from "~/components/ui/toast"
import { ToastLifetime } from "~/components/ui/toast/use-toast" import { ToastLifetime } from "~/components/ui/toast/use-toast"
import { cn } from "~/lib/utils" import { cn } from "@/lib/utils"
import type { Category } from "~/models/Category" import type { Category } from "@@/models/Category"
const { toast } = useToast() const { toast } = useToast()
const { t } = useI18n() const { t } = useI18n()
@@ -13,7 +13,7 @@ const { category } = defineProps<{
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "on-delete-category", payload: Category): void "on-delete-category": [payload: Category]
}>() }>()
type RowMode = "edit" | "view" type RowMode = "edit" | "view"

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PhX } from "@phosphor-icons/vue"; import { PhX } from "@phosphor-icons/vue";
import type { World } from "~/models/World"; import type { World } from "@@/models/World";
defineProps<{ defineProps<{
world: World, world: World,
@@ -41,7 +41,7 @@ function handleClose() {
<PhX size="20" /> <PhX size="20" />
</UiButton> </UiButton>
<CalendarFormCreate @on-changed-name="onChangedName" @on-close="handleClose" /> <CalendarFormCreate v-if="world.id" :world-id="world.id" @on-changed-name="onChangedName" @on-close="handleClose" />
</UiAlertDialogContent> </UiAlertDialogContent>
</UiAlertDialog> </UiAlertDialog>
</template> </template>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from "~/models/Date"; import type { RPGDate } from "@@/models/Date";
const { eventSkeleton, operationInProgress } = storeToRefs(useCalendar()) const { eventSkeleton, operationInProgress, currentRPGDate } = storeToRefs(useCalendar())
const { resetSkeleton } = useCalendar() const { resetSkeleton } = useCalendar()
const popoverOpen = ref(false) const popoverOpen = ref(false)
@@ -39,6 +39,13 @@ function handleClosing() {
popoverOpen.value = false popoverOpen.value = false
resetSkeleton() resetSkeleton()
} }
// If the date changes, the popover should close
// Only for months for now (because it's the only view)
watch(currentRPGDate, () => {
if (!popoverOpen.value) return
handleClosing()
})
</script> </script>
<template> <template>

View File

@@ -2,7 +2,7 @@
import { PhCircleNotch } from "@phosphor-icons/vue"; import { PhCircleNotch } from "@phosphor-icons/vue";
import { useToast } from "~/components/ui/toast"; import { useToast } from "~/components/ui/toast";
import { ToastLifetime } from "~/components/ui/toast/use-toast"; import { ToastLifetime } from "~/components/ui/toast/use-toast";
import type { Calendar } from "~/models/CalendarConfig"; import type { Calendar } from "@@/models/CalendarConfig";
const { toast } = useToast() const { toast } = useToast()
const { t } = useI18n() const { t } = useI18n()
@@ -22,27 +22,26 @@ async function handleAction(): Promise<void> {
isLoading.value = true isLoading.value = true
try { const { error } = await tryCatch($fetch(`/api/calendars/${props.calendar.id}`, { method: "DELETE" }))
await $fetch(`/api/calendars/${props.calendar.id}`, { method: "DELETE" })
emit("on-close") if (error) {
toast({
title: error.message,
variant: "destructive"
})
isLoading.value = false
return
}
toast({ toast({
title: t("entity.calendar.deletedToast.title", { calendar: props.calendar.name }), title: t("entity.calendar.deletedToast.title", { calendar: props.calendar.name }),
variant: "success", variant: "success",
duration: ToastLifetime.SHORT duration: ToastLifetime.SHORT
}) })
} catch (err) {
console.log(err) emit("on-close")
if (err instanceof Error) {
toast({
title: err.message,
variant: "destructive"
})
}
} finally {
isLoading.value = false isLoading.value = false
} }
}
/** /**
* Prevents the modal from closing if's still loading * Prevents the modal from closing if's still loading
@@ -77,14 +76,6 @@ function handleClosing() {
</UiAlertDialogDescription> </UiAlertDialogDescription>
<form @submit.prevent="handleAction"> <form @submit.prevent="handleAction">
<div class="grid grid-cols-2 gap-y-4">
<div class="text-red-500 ml-8">
<span class="text-sm">
<!-- {{ formErrors.message }} -->
</span>
</div>
</div>
<footer class="flex gap-2 justify-between"> <footer class="flex gap-2 justify-between">
<UiButton type="button" size="sm" variant="outline" @click="handleClosing"> <UiButton type="button" size="sm" variant="outline" @click="handleClosing">
{{ $t('ui.action.back') }} {{ $t('ui.action.back') }}

View File

@@ -0,0 +1,63 @@
<script setup lang="ts">
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { PhPlus } from "@phosphor-icons/vue";
import { VisuallyHidden } from "radix-vue";
const isDialogOpen = ref<boolean>(false);
const { resetSkeleton } = useCalendar();
// Toggles the dialog
function toggleDialog() {
isDialogOpen.value = !isDialogOpen.value;
};
/**
* Prevents the modal from closing if's still loading
*/
function handleClosing() {
setTimeout(() => resetSkeleton(), 100)
}
const breakpoints = useBreakpoints(
breakpointsTailwind
)
</script>
<template>
<ClientOnly>
<Transition name="fade" appear>
<UiButton
class="max-md:fixed max-md:bottom-8 max-md:right-8 max-md:z-50 max-md:size-14 max-md:rounded-xl"
:size="breakpoints.md.value ? 'default' : 'icon'"
@click="toggleDialog"
>
<PhPlus :size="breakpoints.md.value ? 18 : 24" weight="bold" />
<strong v-if="breakpoints.md.value" class="font-semibold">
{{ $t("entity.calendar.event.newEvent") }}
</strong>
</UiButton>
</Transition>
</ClientOnly>
<UiDialog v-model:open="isDialogOpen">
<UiDialogContent
class="max-md:translate-0 max-md:inset-0 max-md:w-full max-md:block"
:trap-focus="true"
@escape-key-down="handleClosing"
@pointer-down-outside.prevent="handleClosing"
>
<UiDialogTitle class="max-md:mb-8">
{{ $t("entity.calendar.event.addSingle") }}
</UiDialogTitle>
<VisuallyHidden>
<UiDialogDescription>
{{ $t("entity.calendar.event.addSingleDescription") }}
</UiDialogDescription>
</VisuallyHidden>
<CalendarFormCreateEvent @event-created="toggleDialog" />
</UiDialogContent>
</UiDialog>
</template>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PhX } from "@phosphor-icons/vue"; import { PhX } from "@phosphor-icons/vue";
import type { Calendar } from "~/models/CalendarConfig"; import type { Calendar } from "@@/models/CalendarConfig";
import type { World } from "~/models/World"; import type { World } from "@@/models/World";
const props = defineProps<{ const props = defineProps<{
calendar: Calendar | null, calendar: Calendar | null,

View File

@@ -25,13 +25,13 @@ function handleClosing() {
<UiDialogContent <UiDialogContent
:disable-outside-pointer-events="true" :disable-outside-pointer-events="true"
:trap-focus="true" :trap-focus="true"
class="pl-3 min-w-96 border-border" class="pl-3 md:min-w-96 max-md:translate-0 max-md:inset-0 max-md:w-full max-md:block"
@escape-key-down="handleClosing" @escape-key-down="handleClosing"
@focus-outside="handleClosing" @focus-outside="handleClosing"
@interact-outside="handleClosing" @interact-outside="handleClosing"
@pointer-down-outside="(e) => e.preventDefault()" @pointer-down-outside="(e) => e.preventDefault()"
> >
<header class="pl-8 grid gap-y-2"> <header class="pl-8 grid gap-y-2 max-md:mb-8">
<UiDialogTitle> <UiDialogTitle>
{{ $t('entity.calendar.event.editDialog.title') }} {{ $t('entity.calendar.event.editDialog.title') }}
</UiDialogTitle> </UiDialogTitle>

View File

@@ -1,8 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import type { RPGDate } from "@/models/Date" import type { RPGDate } from "@@/models/Date"
import type { CalendarEvent } from "@/models/CalendarEvent" import type { CalendarEvent } from "@@/models/CalendarEvent"
import { useCalendar } from "@/stores/CalendarStore" import { useCalendar } from "@/stores/CalendarStore"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { import {
PhHourglassMedium, PhHourglassMedium,
@@ -13,6 +14,7 @@ import {
PhDotsThreeOutlineVertical, PhDotsThreeOutlineVertical,
PhEye PhEye
} from "@phosphor-icons/vue" } from "@phosphor-icons/vue"
import type { CollisionPadding } from "@@/models/Popover"
const { defaultDate, getFormattedDateTitle, jumpToDate, getRelativeString, revealEditEventModal, revealDeleteEventModal } = useCalendar() const { defaultDate, getFormattedDateTitle, jumpToDate, getRelativeString, revealEditEventModal, revealDeleteEventModal } = useCalendar()
const { lastActiveEvent, isReadOnly } = storeToRefs(useCalendar()) const { lastActiveEvent, isReadOnly } = storeToRefs(useCalendar())
@@ -24,6 +26,22 @@ const props = defineProps<{
isEndEvent?: boolean isEndEvent?: boolean
}>() }>()
const breakpoints = useBreakpoints(
breakpointsTailwind
)
const collisionPadLg: CollisionPadding = {
top: 50,
bottom: 50,
left: 50,
right: 50,
}
const collisionPad: CollisionPadding = {
top: 80,
bottom: 80,
left: 12.5,
right: 12.5,
}
// Ref for the popover // Ref for the popover
const eventDetails = ref<HTMLElement>() const eventDetails = ref<HTMLElement>()
@@ -62,12 +80,12 @@ function deployDeleteModal() {
<template> <template>
<UiPopoverContent <UiPopoverContent
class="w-96 event-popover" class="max-w-full w-96 event-popover"
:align="'center'" :align="'center'"
:align-offset="50" :align-offset="breakpoints.lg.value ? 50 : 25"
:side="'left'" :side-offset="8"
:collision-padding="60" :side="breakpoints.lg.value ? 'left' : 'top'"
:hide-when-detached="true" :collision-padding="breakpoints.lg.value ? collisionPadLg : collisionPad"
:class="cn( :class="cn(
event.category ? `element-${event.category.color}` : '', event.category ? `element-${event.category.color}` : '',
)" )"

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import type { RPGDate } from "@/models/Date" import type { RPGDate } from "@@/models/Date"
import type { CalendarEvent } from "~/models/CalendarEvent" import type { CalendarEvent } from "@@/models/CalendarEvent"
const props = defineProps<{ const props = defineProps<{
event: CalendarEvent event: CalendarEvent
@@ -55,7 +55,7 @@ onMounted(() => {
<UiPopover v-model:open="isPopoverDetailsOpen"> <UiPopover v-model:open="isPopoverDetailsOpen">
<UiPopoverTrigger as-child> <UiPopoverTrigger as-child>
<button <button
class="event-button text-xs px-2 py-1 block w-full text-left rounded-sm transition-colors outline-offset-1 cursor-pointer" class="event-button text-2xs md:text-xs px-[5px] py-[5px] md:px-2 md:py-1 block w-full text-left rounded-sm transition-colors outline-offset-1 cursor-pointer"
:class=" :class="
cn( cn(
`element-${event.category?.color}`, `element-${event.category?.color}`,
@@ -75,7 +75,7 @@ onMounted(() => {
</button> </button>
</UiPopoverTrigger> </UiPopoverTrigger>
<LazyCalendarEventDetails <CalendarEventDetails
:event :event
:spans-multiple-days :spans-multiple-days
:is-start-event :is-start-event

View File

@@ -1,8 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Calendar } from "~/models/CalendarConfig"; import type { Calendar } from "@@/models/CalendarConfig";
import { PhAlarm, PhCalendarDots, PhCircleNotch, PhWrench } from "@phosphor-icons/vue"; import { PhAlarm, PhCalendarDots, PhCircleNotch, PhWrench } from "@phosphor-icons/vue";
const defaultSkeleton: Calendar = { name: "", today: { day: 1, month: 0, year: 0 }, months: [], events: [], state: "draft", color: "white" } const props = defineProps<{
worldId: number
}>()
const defaultSkeleton: Calendar = { name: "", today: { day: 1, month: 0, year: 0 }, months: [], events: [], categories: [], state: "draft", color: "white" }
const calendarSkeleton = ref<Calendar>({ ...defaultSkeleton }) const calendarSkeleton = ref<Calendar>({ ...defaultSkeleton })
onMounted(() => { onMounted(() => {
@@ -38,26 +42,26 @@ const validSkeleton = computed(() => validSkeletonGeneral.value && validSkeleton
const isCreatingCalendar = ref<boolean>(false) const isCreatingCalendar = ref<boolean>(false)
async function handleSubmit() { async function handleSubmit() {
try {
isCreatingCalendar.value = true isCreatingCalendar.value = true
await $fetch("/api/calendars/create", { method: "POST", body: { ...calendarSkeleton.value, worldId: 1 } })
const { error } = await tryCatch($fetch("/api/calendars/create", { method: "POST", body: { ...calendarSkeleton.value, worldId: props.worldId } }))
if (error) {
console.log(error.message)
isCreatingCalendar.value = false
return
}
emit("on-close") emit("on-close")
} catch (err) {
console.log(err)
} finally {
isCreatingCalendar.value = false isCreatingCalendar.value = false
} }
}
/** /**
* === Watch for name changes to display above === * Watch for name changes to display above
*/ */
const emit = defineEmits<{ const emit = defineEmits<{
// eslint-disable-next-line no-unused-vars "on-changed-name": [calendarName: string]
(e: "on-changed-name", calendarName: string): void "on-close": []
// eslint-disable-next-line no-unused-vars
(e: "on-close"): void
}>() }>()
/** Hook to emit a debounced event for the changed skeleton name */ /** Hook to emit a debounced event for the changed skeleton name */

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from "~/models/Date"; import type { RPGDate } from "@@/models/Date";
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhTag } from "@phosphor-icons/vue" import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhTag } from "@phosphor-icons/vue"
const { t } = useI18n() const { t } = useI18n()
@@ -29,18 +29,17 @@ async function handleSubmit() {
isLoading.value = true isLoading.value = true
try { const { error } = await tryCatch(submitSkeleton())
await submitSkeleton()
if (error) {
formErrors.message = error.message
isLoading.value = false
return
}
emit("event-created") emit("event-created")
} catch (err) {
if (err instanceof Error) {
formErrors.message = err.message
}
} finally {
isLoading.value = false isLoading.value = false
} }
}
/** /**
* Click on the cancel button * Click on the cancel button
@@ -120,14 +119,6 @@ function handleCancel() {
</div> </div>
</div> </div>
<!-- <div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" :placeholder="$t('entity.category.addSecondaries')" />
</div>
</div> -->
<div class="col-span-2 mb-2"> <div class="col-span-2 mb-2">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<PhMapPinArea size="18" weight="fill" /> <PhMapPinArea size="18" weight="fill" />
@@ -156,14 +147,9 @@ function handleCancel() {
<PhEyeClosed v-else size="18" /> <PhEyeClosed v-else size="18" />
<div class="flex items-center gap-x-2"> <div class="flex items-center gap-x-2">
<UiSwitch id="new-event-visibility" v-model:checked="eventSkeleton.hidden" /> <UiCheckbox id="new-event-visibility" v-model:checked="eventSkeleton.hidden" />
<UiLabel for="new-event-visibility"> <UiLabel for="new-event-visibility">
<template v-if="!eventSkeleton.hidden"> {{ $t('entity.calendar.event.hiddenLabel') }}
{{ $t('entity.calendar.event.isPublic') }}
</template>
<template v-else>
{{ $t('entity.calendar.event.isHidden') }}
</template>
</UiLabel> </UiLabel>
</div> </div>
</div> </div>

View File

@@ -22,22 +22,21 @@ async function handleAction(): Promise<void> {
const categoryName = categorySkeleton.value?.name const categoryName = categorySkeleton.value?.name
try { const { error } = await tryCatch(deleteCategoryFromSkeleton())
await deleteCategoryFromSkeleton()
if (error) {
formErrors.message = error.message
isLoading.value = false
return
}
toast({ toast({
title: t("entity.category.deletedToast.title", { category: categoryName }), title: t("entity.category.deletedToast.title", { category: categoryName }),
variant: "success", variant: "success",
duration: ToastLifetime.MEDIUM duration: ToastLifetime.MEDIUM
}) })
} catch (err) {
if (err instanceof Error) {
formErrors.message = err.message
}
} finally {
isLoading.value = false isLoading.value = false
} }
}
/** /**
* Click on the cancel button * Click on the cancel button

View File

@@ -22,8 +22,13 @@ async function handleAction(): Promise<void> {
const eventTitle = eventSkeleton.value.title const eventTitle = eventSkeleton.value.title
try { const { error } = await tryCatch(deleteEventFromSkeleton())
await deleteEventFromSkeleton()
if (error) {
formErrors.message = error.message
isLoading.value = false
return
}
isDeleteEventModalOpen.value = false isDeleteEventModalOpen.value = false
resetSkeleton() resetSkeleton()
@@ -33,14 +38,8 @@ async function handleAction(): Promise<void> {
variant: "success", variant: "success",
duration: ToastLifetime.MEDIUM duration: ToastLifetime.MEDIUM
}) })
} catch (err) {
if (err instanceof Error) {
formErrors.message = err.message
}
} finally {
isLoading.value = false isLoading.value = false
} }
}
/** /**
* Click on the cancel button * Click on the cancel button

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Calendar } from "~/models/CalendarConfig"; import type { Calendar } from "@@/models/CalendarConfig";
import { PhCircleNotch, PhWrench } from "@phosphor-icons/vue"; import { PhCircleNotch, PhWrench } from "@phosphor-icons/vue";
import type { World } from "~/models/World"; import type { World } from "@@/models/World";
const props = defineProps<{ const props = defineProps<{
calendar: Calendar | null, calendar: Calendar | null,
@@ -34,26 +34,30 @@ const validSkeleton = computed(() => validSkeletonGeneral.value && validSkeleton
const isUpdatingCalendar = ref<boolean>(false) const isUpdatingCalendar = ref<boolean>(false)
async function handleSubmit() { async function handleSubmit() {
try {
isUpdatingCalendar.value = true isUpdatingCalendar.value = true
await $fetch(`/api/calendars/${calendarSkeleton.value.id}`, { method: "PATCH", body: { ...calendarSkeleton.value, worldId: props.world?.id } })
const fetchBody = { ...calendarSkeleton.value, worldId: props.world?.id }
const { error } = await tryCatch(
$fetch(`/api/calendars/${calendarSkeleton.value.id}`, { method: "PATCH", body: fetchBody })
)
if (error) {
console.log(error.message)
isUpdatingCalendar.value = false
return
}
emit("on-close") emit("on-close")
} catch (err) {
console.log(err)
} finally {
isUpdatingCalendar.value = false isUpdatingCalendar.value = false
} }
}
/** /**
* === Watch for name changes to display above === * Watch for name changes to display above
*/ */
const emit = defineEmits<{ const emit = defineEmits<{
// eslint-disable-next-line no-unused-vars "on-changed-name": [calendarName: string]
(e: "on-changed-name", calendarName: string): void "on-close": []
// eslint-disable-next-line no-unused-vars
(e: "on-close"): void
}>() }>()
/** Hook to emit a debounced event for the changed skeleton name */ /** Hook to emit a debounced event for the changed skeleton name */
@@ -78,20 +82,6 @@ function handleFormCancel() {
{{ $t('entity.calendar.actionDialog.tabs.general.title') }} {{ $t('entity.calendar.actionDialog.tabs.general.title') }}
</div> </div>
</UiTabsTrigger> </UiTabsTrigger>
<!-- <UiTabsTrigger value="months" class="font-bold">
<div class="flex items-center gap-1">
<PhCalendarDots size="18" weight="fill" />
{{ $t('entity.calendar.actionDialog.tabs.months.title') }}
</div>
</UiTabsTrigger>
<UiTabsTrigger value="today" class="font-bold">
<div class="flex items-center gap-1">
<PhAlarm size="18" weight="fill" />
{{ $t('entity.calendar.actionDialog.tabs.today.title') }}
</div>
</UiTabsTrigger> -->
</UiTabsList> </UiTabsList>
<UiTabsContent value="global" class="grid gap-4"> <UiTabsContent value="global" class="grid gap-4">
<input <input
@@ -121,12 +111,6 @@ function handleFormCancel() {
<InputColor id="new-calendar-color" v-model="calendarSkeleton.color" /> <InputColor id="new-calendar-color" v-model="calendarSkeleton.color" />
</div> </div>
</UiTabsContent> </UiTabsContent>
<!-- <UiTabsContent value="months">
<CalendarInputMonthList v-model:model-value="calendarSkeleton.months" />
</UiTabsContent>
<UiTabsContent value="today">
<CalendarInputTodaySelect v-model:model-value="calendarSkeleton.today" :available-months="calendarSkeleton.months"/>
</UiTabsContent> -->
</UiTabs> </UiTabs>
<footer class="flex justify-end gap-2 mt-6"> <footer class="flex justify-end gap-2 mt-6">

View File

@@ -2,7 +2,7 @@
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhPencilSimpleLine, PhTag } from "@phosphor-icons/vue" import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhPencilSimpleLine, PhTag } from "@phosphor-icons/vue"
import { useToast } from "~/components/ui/toast"; import { useToast } from "~/components/ui/toast";
import { ToastLifetime } from "~/components/ui/toast/use-toast"; import { ToastLifetime } from "~/components/ui/toast/use-toast";
import type { APIError } from "~/models/Errors"; import type { APIError } from "@@/models/Errors";
const emit = defineEmits(["event-updated"]) const emit = defineEmits(["event-updated"])
@@ -23,19 +23,11 @@ async function handleAction() {
isLoading.value = true isLoading.value = true
try { const { error } = await tryCatch(updateEventFromSkeleton())
await updateEventFromSkeleton()
emit("event-updated") if (error) {
toast({
title: t("entity.calendar.event.updatedToast.title", { event: eventSkeleton.value.title }),
variant: "success",
duration: ToastLifetime.SHORT
})
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const apiError = (err as any).data as APIError const apiError = (error as any).data as APIError
apiError.data.errors.forEach((error) => { apiError.data.errors.forEach((error) => {
toast({ toast({
@@ -45,10 +37,21 @@ async function handleAction() {
duration: ToastLifetime.MEDIUM, duration: ToastLifetime.MEDIUM,
}) })
}) })
} finally {
resetSkeleton()
isLoading.value = false isLoading.value = false
return
} }
emit("event-updated")
toast({
title: t("entity.calendar.event.updatedToast.title", { event: eventSkeleton.value.title }),
variant: "success",
duration: ToastLifetime.SHORT
})
isLoading.value = false
resetSkeleton()
} }
/** /**
@@ -136,16 +139,6 @@ function handleCancel() {
</div> </div>
</div> </div>
<!-- <div class="col-span-2">
<div class="flex items-center gap-4">
<PhTag size="18" weight="fill" />
<div class="w-1/2">
<CalendarInputEventCategories v-model="eventSkeleton.secondaryCategories" placeholder="Ajouter des catégories secondaires" />
</div>
</div>
</div> -->
<div class="col-span-2 mb-2"> <div class="col-span-2 mb-2">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<PhMapPinArea size="18" weight="fill" /> <PhMapPinArea size="18" weight="fill" />
@@ -175,14 +168,9 @@ function handleCancel() {
<PhEyeClosed v-else size="18" /> <PhEyeClosed v-else size="18" />
<div class="flex items-center gap-x-2"> <div class="flex items-center gap-x-2">
<UiSwitch id="new-event-visibility" v-model:checked="eventSkeleton.hidden" /> <UiCheckbox id="new-event-visibility" v-model:checked="eventSkeleton.hidden" />
<UiLabel for="new-event-visibility"> <UiLabel for="new-event-visibility">
<template v-if="!eventSkeleton.hidden"> {{ $t('entity.calendar.event.hiddenLabel') }}
{{ $t('entity.calendar.event.isPublic') }}
</template>
<template v-else>
{{ $t('entity.calendar.event.isHidden') }}
</template>
</UiLabel> </UiLabel>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Category } from "~/models/Category"; import type { Category } from "@@/models/Category";
import { PhCaretDown, PhCheck } from "@phosphor-icons/vue"; import { PhCaretDown, PhCheck } from "@phosphor-icons/vue";
@@ -35,6 +35,8 @@ const filteredCategories = computed(() =>
<UiButton <UiButton
variant="outline" variant="outline"
role="combobox" role="combobox"
:aria-expanded="isPopoverOpen"
aria-controls="event-categories"
class="relative w-full max-w-full h-fit justify-between" class="relative w-full max-w-full h-fit justify-between"
> >
<template v-if="!model.length"> <template v-if="!model.length">
@@ -50,16 +52,17 @@ const filteredCategories = computed(() =>
</ul> </ul>
</template> </template>
<PhCaretDown class="ml-2 h-4 w-4 shrink-0 opacity-50" /> <PhCaretDown class="ml-2 size-4 shrink-0 opacity-50" />
</UiButton> </UiButton>
</UiPopoverTrigger> </UiPopoverTrigger>
<UiPopoverContent <UiPopoverContent
id="event-categories"
align="start" align="start"
side="bottom" side="bottom"
:collision-padding="50" :collision-padding="50"
class="w-fit h-[33vh] p-0" class="w-fit h-[33vh] p-0"
> >
<UiCommand v-model="modelBuffer" v-model:searchTerm="searchTerm" :multiple="true"> <UiCommand v-model="modelBuffer" v-model:search-term="searchTerm" :multiple="true">
<UiCommandInput :placeholder="$t('entity.category.search')" /> <UiCommandInput :placeholder="$t('entity.category.search')" />
<UiCommandEmpty>{{ $t('entity.category.notFoundAny') }}</UiCommandEmpty> <UiCommandEmpty>{{ $t('entity.category.notFoundAny') }}</UiCommandEmpty>
<UiCommandList> <UiCommandList>

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PhCaretDown } from "@phosphor-icons/vue"; import { PhCaretDown } from "@phosphor-icons/vue";
import type { Category } from "~/models/Category"; import { cn } from "@/lib/utils";
import type { Category } from "@@/models/Category";
const isPopoverOpen = ref<boolean>(false) const isPopoverOpen = ref<boolean>(false)
@@ -33,25 +34,33 @@ const filteredCategories = computed(() =>
<UiButton <UiButton
variant="outline" variant="outline"
role="combobox" role="combobox"
:aria-expanded="isPopoverOpen"
aria-controls="event-category"
class="w-full max-w-full justify-between" class="w-full max-w-full justify-between"
> >
<template v-if="!model"> <template v-if="!model">
{{ props.placeholder }} {{ props.placeholder }}
</template> </template>
<template v-else> <template v-else>
{{ model.name }} <span
class="bgc"
:class="cn(`element-${model.color}`)"
>
{{ capitalize(model.name) }}
</span>
</template> </template>
<PhCaretDown class="ml-2 h-4 w-4 shrink-0 opacity-50" /> <PhCaretDown class="ml-2 size-4 shrink-0 opacity-50" />
</UiButton> </UiButton>
</UiPopoverTrigger> </UiPopoverTrigger>
<UiPopoverContent <UiPopoverContent
id="event-category"
align="start" align="start"
side="bottom" side="bottom"
:collision-padding="50" :collision-padding="50"
class="w-fit h-[33vh] p-0" class="w-fit h-[33vh] p-0"
> >
<UiCommand v-model="model" v-model:searchTerm="searchTerm"> <UiCommand v-model="model" v-model:search-term="searchTerm">
<UiCommandInput :placeholder="$t('entity.category.search')" /> <UiCommandInput :placeholder="$t('entity.category.search')" />
<UiCommandEmpty>{{ $t('entity.category.notFoundAny') }}</UiCommandEmpty> <UiCommandEmpty>{{ $t('entity.category.notFoundAny') }}</UiCommandEmpty>
<UiCommandList> <UiCommandList>
@@ -63,7 +72,12 @@ const filteredCategories = computed(() =>
class="cursor-pointer" class="cursor-pointer"
@select="handleCatSelect" @select="handleCatSelect"
> >
{{ category.name }} <span
class="bgc"
:class="cn(`element-${category.color}`)"
>
{{ capitalize(category.name) }}
</span>
</UiCommandItem> </UiCommandItem>
</UiCommandGroup> </UiCommandGroup>
</UiCommandList> </UiCommandList>

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from "~/lib/utils"; import { cn } from "@/lib/utils";
import { useSortable } from "@vueuse/integrations/useSortable"; import { useSortable } from "@vueuse/integrations/useSortable";
import type { CalendarMonth } from "~/models/CalendarMonth"; import type { CalendarMonth } from "@@/models/CalendarMonth";
import { PhList, PhPlus, PhTrash } from "@phosphor-icons/vue"; import { PhList, PhPlus, PhTrash } from "@phosphor-icons/vue";
@@ -91,7 +91,7 @@ useSortable(monthSortableList, model.value, { animation: 150, handle: ".handle"
/> />
</div> </div>
<div class="md:col-span-1"> <div class="md:col-span-1">
<UiButton size="icon" class="rounded-full h-8 w-8" :disabled="!validNewMonth" @click.prevent="addMonthToModel"> <UiButton size="icon" class="rounded-full size-8" :disabled="!validNewMonth" @click.prevent="addMonthToModel">
<PhPlus size="17"/> <PhPlus size="17"/>
</UiButton> </UiButton>
</div> </div>
@@ -103,7 +103,7 @@ useSortable(monthSortableList, model.value, { animation: 150, handle: ".handle"
<div v-if="model.length" class="hidden md:block col-span-1"> <div v-if="model.length" class="hidden md:block col-span-1">
<ul class="grid gap-y-4 justify-center"> <ul class="grid gap-y-4 justify-center">
<li v-for="(m, i) in model" :key="`num-${m.name}`"> <li v-for="(m, i) in model" :key="`num-${m.name}`">
<UiButton size="icon" variant="secondary" class="h-8 w-8 rounded-full"> <UiButton size="icon" variant="secondary" class="size-8 rounded-full">
<span class="font-bold text-sm">{{ i + 1 }}</span> <span class="font-bold text-sm">{{ i + 1 }}</span>
</UiButton> </UiButton>
</li> </li>
@@ -115,7 +115,7 @@ useSortable(monthSortableList, model.value, { animation: 150, handle: ".handle"
<template v-if="model.length"> <template v-if="model.length">
<li v-for="(m, i) in model" :key="m.name" class="grid md:grid-cols-12 gap-4 md:items-center text-slate-900 bg-slate-200 rounded-md"> <li v-for="(m, i) in model" :key="m.name" class="grid md:grid-cols-12 gap-4 md:items-center text-slate-900 bg-slate-200 rounded-md">
<div class="md:col-span-1 text-right duration-200 ease-out transition transform origin-top-right"> <div class="md:col-span-1 text-right duration-200 ease-out transition transform origin-top-right">
<UiButton type="button" variant="ghost" size="icon" class="handle rounded-full h-8 w-8"> <UiButton type="button" variant="ghost" size="icon" class="handle rounded-full size-8">
<PhList size="17" /> <PhList size="17" />
</UiButton> </UiButton>
</div> </div>
@@ -145,7 +145,7 @@ useSortable(monthSortableList, model.value, { animation: 150, handle: ".handle"
<UiTooltipProvider> <UiTooltipProvider>
<UiTooltip> <UiTooltip>
<UiTooltipTrigger as-child> <UiTooltipTrigger as-child>
<UiButton type="button" variant="ghost" size="icon" class="rounded-full h-8 w-8" @click="removeMonthFromModel(i)"> <UiButton type="button" variant="ghost" size="icon" class="rounded-full size-8" @click="removeMonthFromModel(i)">
<PhTrash size="17" /> <PhTrash size="17" />
</UiButton> </UiButton>
</UiTooltipTrigger> </UiTooltipTrigger>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from "~/models/Date"; import type { RPGDate } from "@@/models/Date";
import { import {
PhXCircle PhXCircle
@@ -135,15 +135,6 @@ function handleValueReset() {
<UiInput v-model="model.year" type="number" /> <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" > <hr class="col-span-2" >
<div class="col-span-2"> <div class="col-span-2">

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { CalendarMonth } from "~/models/CalendarMonth"; import type { CalendarMonth } from "@@/models/CalendarMonth";
import type { RPGDate } from "~/models/Date"; import type { RPGDate } from "@@/models/Date";
import { PhCalendarBlank } from "@phosphor-icons/vue"; import { PhCalendarBlank } from "@phosphor-icons/vue";

View File

@@ -0,0 +1,46 @@
<script lang="ts" setup>
import { useCalendar } from "@/stores/CalendarStore"
import { breakpointsTailwind } from "@vueuse/core"
const breakpoints = useBreakpoints(
breakpointsTailwind
)
const { isReadOnly, defaultDate } = storeToRefs(useCalendar())
</script>
<template>
<header class="md:mt-2 grid gap-3 md:gap-4 border-border border-b-[1px] transition-colors">
<div class="px-5 md:px-8 flex items-center justify-between gap-2">
<menu class="flex items-center md:gap-2">
<li>
<LazyCalendarDialogQuickCreateEvent v-if="!isReadOnly" />
</li>
<li class="max-md:hidden">
<LazyCalendarMenuToday v-if="defaultDate" />
</li>
<li class="ml-2 md:ml-4">
<CalendarCurrentDate />
</li>
</menu>
<menu class="flex items-center gap-2">
<li>
<CalendarSearchCTA />
</li>
<ClientOnly>
<li v-if="breakpoints.md.value">
<CalendarCategoriesCTA />
</li>
</ClientOnly>
<li>
<CalendarOptionsCTA />
</li>
</menu>
</div>
<div class="ml-4 md:ml-8">
<CalendarMenuSubnav />
</div>
</header>
</template>

View File

@@ -139,7 +139,7 @@ function toFutureFar(): void {
<template> <template>
<div class="flex gap-2"> <div class="flex gap-2">
<div class="grid items-end w-40 px-4 py-2 bg-white dark:bg-black border-border border-x-[1px] border-t-[1px] rounded-t-sm text-sm transition-colors"> <div class="grid items-center w-40 px-3 md:px-4 py-2 bg-white dark:bg-black border-border border-x-[1px] border-t-[1px] rounded-t-sm text-sm max-md:text-xs transition-colors">
<ClientOnly> <ClientOnly>
<span>{{ currentDate.currentDateTitle }}</span> <span>{{ currentDate.currentDateTitle }}</span>

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue" import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { PhCalendar } from "@phosphor-icons/vue"
const { defaultDate, jumpToDefaultDate, getFormattedDateTitle, currentDate } = useCalendar() const { defaultDate, jumpToDefaultDate, getFormattedDateTitle, currentDate } = useCalendar()
@@ -8,15 +9,29 @@ const defaultDateFormatted: string = getFormattedDateTitle(defaultDate, true)
const buttonDisabledState: ComputedRef<boolean> = computed<boolean>(() => { const buttonDisabledState: ComputedRef<boolean> = computed<boolean>(() => {
return currentDate.currentMonth === defaultDate.month && currentDate.currentYear === defaultDate.year return currentDate.currentMonth === defaultDate.month && currentDate.currentYear === defaultDate.year
}) })
const breakpoints = useBreakpoints(
breakpointsTailwind
)
</script> </script>
<template> <template>
<UiTooltipProvider :delay-duration="250"> <UiTooltipProvider :delay-duration="250">
<UiTooltip> <UiTooltip>
<UiTooltipTrigger as-child> <UiTooltipTrigger as-child>
<UiButton size="sm" variant="secondary" :disabled="buttonDisabledState" @click="jumpToDefaultDate"> <ClientOnly>
<UiButton
:size="breakpoints.md.value ? 'default' : 'icon'"
variant="secondary"
:disabled="buttonDisabledState" @click="jumpToDefaultDate"
>
<PhCalendar v-if="!breakpoints.md.value" size="20" weight="fill" />
<span v-if="breakpoints.md.value">
{{ $t('entity.calendar.date.today') }} {{ $t('entity.calendar.date.today') }}
</span>
</UiButton> </UiButton>
</ClientOnly>
</UiTooltipTrigger> </UiTooltipTrigger>
<UiTooltipContent> <UiTooltipContent>
<p>{{ defaultDateFormatted }}</p> <p>{{ defaultDateFormatted }}</p>

View File

@@ -2,13 +2,13 @@
import { import {
isCharacter, isCharacter,
type Character, type Character,
} from "@/models/Characters" } from "@@/models/Characters"
import type { RPGDateOrder } from "@/models/Date" import type { RPGDateOrder } from "@@/models/Date"
import { import {
isCalendarEvent, isCalendarEvent,
type CalendarEvent, type CalendarEvent,
} from "~/models/CalendarEvent" } from "@@/models/CalendarEvent"
import { capitalize } from "@/utils/Strings" import { capitalize } from "~/utils/Strings"
import { useMagicKeys, useScroll, useStorage, whenever } from "@vueuse/core" import { useMagicKeys, useScroll, useStorage, whenever } from "@vueuse/core"
import { computed, ref, watch } from "vue" import { computed, ref, watch } from "vue"
import { searchUnifier, type SearchMode } from "../SearchMode" import { searchUnifier, type SearchMode } from "../SearchMode"
@@ -24,7 +24,8 @@ import {
} from "radix-vue" } from "radix-vue"
import SearchList from "./lists/SearchList.vue" import SearchList from "./lists/SearchList.vue"
import type { Category } from "~/models/Category" import type { Category } from "@@/models/Category"
import { cn } from "@/lib/utils"
const { isAdvancedSearchOpen, allEvents, categories } = storeToRefs(useCalendar()) const { isAdvancedSearchOpen, allEvents, categories } = storeToRefs(useCalendar())
const { characters } = storeToRefs(useCharacters()) const { characters } = storeToRefs(useCharacters())
@@ -87,7 +88,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
results = dataToFilter.filter((item) => { results = dataToFilter.filter((item) => {
// Filter calendar events // Filter calendar events
if (isCalendarEvent(item)) { if (isCalendarEvent(item)) {
const queryString = new String(searchQuery.value) const queryString = (searchQuery.value as string)
.replace(searchUnifier, "") .replace(searchUnifier, "")
.toLocaleLowerCase() .toLocaleLowerCase()
@@ -125,7 +126,7 @@ const searchResults = computed<(Character | CalendarEvent)[]>(() => {
// Filter characters // Filter characters
if (isCharacter(item)) { if (isCharacter(item)) {
const queryString = new String(searchQuery.value) const queryString = (searchQuery.value as string)
.replace(searchUnifier, "") .replace(searchUnifier, "")
.toLocaleLowerCase() .toLocaleLowerCase()
@@ -264,10 +265,10 @@ function handleCategoryUnselect(e: Category) {
<template> <template>
<UiDialog v-model:open="isAdvancedSearchOpen" @update:open="resetSearch()"> <UiDialog v-model:open="isAdvancedSearchOpen" @update:open="resetSearch()">
<UiDialogContent <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="flex flex-col flex-nowrap top-10 -translate-y-0 data-[state=closed]:slide-out-to-top-[5%] data-[state=open]:slide-in-from-top-[5%] max-md:rounded-none"
:class="{ :class="cn({
'bottom-16': searchResults.length > 0 'max-md:w-full bottom-0 md:bottom-10 top-0 md:top-10': searchResults.length > 0
}" })"
> >
<VisuallyHidden> <VisuallyHidden>
<UiDialogTitle> {{ $t('entity.advancedSearch.title') }} </UiDialogTitle> <UiDialogTitle> {{ $t('entity.advancedSearch.title') }} </UiDialogTitle>
@@ -278,7 +279,6 @@ function handleCategoryUnselect(e: Category) {
</UiDialogDescription> </UiDialogDescription>
</VisuallyHidden> </VisuallyHidden>
<!-- UiDialog header -->
<div id="searchForm" class="grid gap-3"> <div id="searchForm" class="grid gap-3">
<div class="relative w-full h-fit"> <div class="relative w-full h-fit">
<UiInput <UiInput
@@ -296,7 +296,7 @@ function handleCategoryUnselect(e: Category) {
</div> </div>
<div class="flex items-center justify-between gap-8"> <div class="flex items-center justify-between gap-8">
<div> <div class="hidden">
<UiToggleGroup <UiToggleGroup
v-model="selectedEntity" v-model="selectedEntity"
type="single" type="single"
@@ -306,18 +306,20 @@ function handleCategoryUnselect(e: Category) {
<UiToggleGroupItem value="events" aria-label="Uniquement les évènements"> <UiToggleGroupItem value="events" aria-label="Uniquement les évènements">
{{ $t('entity.calendar.event.namePlural') }} {{ $t('entity.calendar.event.namePlural') }}
</UiToggleGroupItem> </UiToggleGroupItem>
<!-- Not used for now -->
<!-- <UiToggleGroupItem value="characters" aria-label="Uniquement les personnages">
{{ $t('entity.character.namePlural') }}
</UiToggleGroupItem> -->
</UiToggleGroup> </UiToggleGroup>
</div> </div>
<div class="grow flex justify-end items-center gap-1"> <div class="grow flex justify-end items-center gap-1">
<UiTagsInput class="grow px-0 gap-y-1 w-80"> <UiTagsInput class="grow px-0 gap-y-1 md:w-80">
<div v-if="selectedCategories.length > 0" class="flex gap-2 flex-wrap items-center px-3"> <div v-if="selectedCategories.length > 0" class="flex gap-2 flex-wrap items-center px-3">
<UiTagsInputItem v-for="item in selectedCategories" :key="item.id" :value="item.name"> <UiTagsInputItem v-for="cat in selectedCategories" :key="cat.id" :value="cat.name">
<UiTagsInputItemText class="capitalize cursor-pointer" @click="handleCategoryUnselect(item)" /> <button
class="bgc px-2 capitalize cursor-pointer"
:class="cn(`element-${cat.color}`)"
@click="handleCategoryUnselect(cat)"
>
{{ capitalize(cat.name) }}
</button>
</UiTagsInputItem> </UiTagsInputItem>
</div> </div>
@@ -355,8 +357,13 @@ function handleCategoryUnselect(e: Category) {
:key="cat.name" :key="cat.name"
:value="cat" :value="cat"
@select.prevent="handleCategorySelect(cat)" @select.prevent="handleCategorySelect(cat)"
>
<span
class="bgc"
:class="cn(`element-${cat.color}`)"
> >
{{ capitalize(cat.name) }} {{ capitalize(cat.name) }}
</span>
</UiCommandItem> </UiCommandItem>
</UiCommandGroup> </UiCommandGroup>
</UiCommandList> </UiCommandList>
@@ -435,7 +442,7 @@ function handleCategoryUnselect(e: Category) {
as-child as-child
> >
<UiButton <UiButton
class="w-10 h-10 p-0" class="size-8 md:size-10 p-0"
:variant="item.value === currentPage ? 'default' : 'outline'" :variant="item.value === currentPage ? 'default' : 'outline'"
> >
{{ item.value }} {{ item.value }}

View File

@@ -1,10 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Character } from "@/models/Characters" import type { Character } from "@@/models/Characters"
import type { RPGDate } from "@/models/Date" import type { RPGDate } from "@@/models/Date"
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"
import { Button } from "@/components/ui/button" import { Button } from "~/components/ui/button"
import { PhArrowSquareOut, PhPlant, PhSkull } from "@phosphor-icons/vue" import { PhArrowSquareOut, PhPlant, PhSkull } from "@phosphor-icons/vue"
const props = defineProps<{ const props = defineProps<{
@@ -12,7 +12,7 @@ const props = defineProps<{
}>() }>()
defineEmits<{ defineEmits<{
(e: "query:date-jump", payload: RPGDate): void "query:date-jump": [payload: RPGDate]
}>() }>()
const { getFormattedDateTitle } = useCalendar() const { getFormattedDateTitle } = useCalendar()

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import type { RPGDate } from "@/models/Date" import type { RPGDate } from "@@/models/Date"
import type { CalendarEvent } from "@/models/CalendarEvent" import type { CalendarEvent } from "@@/models/CalendarEvent"
import { PhArrowSquareOut, PhHourglassMedium, PhAlarm, PhMapPinArea, PhEye } from "@phosphor-icons/vue" import { PhArrowSquareOut, PhHourglassMedium, PhAlarm, PhMapPinArea, PhEye } from "@phosphor-icons/vue"
@@ -10,7 +10,7 @@ const props = defineProps<{
}>() }>()
defineEmits<{ defineEmits<{
(e: "query:date-jump", payload: RPGDate): void "query:date-jump": [payload: RPGDate]
}>() }>()
const { getRelativeString, defaultDate, getFormattedDateTitle } = useCalendar() const { getRelativeString, defaultDate, getFormattedDateTitle } = useCalendar()
@@ -21,7 +21,7 @@ const dateDuration = computed<string | null>(() => props.event.endDate ? getRela
<template> <template>
<button <button
class="event-callout group relative block w-full text-left py-3 px-4 cursor-pointer transition-colors" class="event-callout group relative block w-full text-left py-2 px-3 md:py-3 md:px-4 cursor-pointer transition-colors"
:class="cn( :class="cn(
event.category ? `element-${event.category.color}` : '', event.category ? `element-${event.category.color}` : '',
{ {
@@ -30,7 +30,7 @@ const dateDuration = computed<string | null>(() => props.event.endDate ? getRela
@click="$emit('query:date-jump', event.startDate)" @click="$emit('query:date-jump', event.startDate)"
> >
<div class="flex gap-2 items-center mb-1"> <div class="flex gap-2 items-center mb-1">
<h2 class="font-bold text-lg underline-offset-4 group-hover:underline"> <h2 class="font-semibold md:font-bold md:text-lg underline-offset-4 group-hover:underline">
{{ event.title }} {{ event.title }}
</h2> </h2>
<div v-if="event.wiki"> <div v-if="event.wiki">
@@ -43,7 +43,7 @@ const dateDuration = computed<string | null>(() => props.event.endDate ? getRela
</div> </div>
</div> </div>
<div class="flex gap-2 items-center justify-between mb-1"> <div class="flex gap-2 items-center justify-between mb-2 md:mb-1">
<template v-if="!event.endDate"> <template v-if="!event.endDate">
<p class="font-semibold text-sm opacity-75"> <p class="font-semibold text-sm opacity-75">
{{ getFormattedDateTitle(event.startDate, true) }} {{ getFormattedDateTitle(event.startDate, true) }}
@@ -81,15 +81,15 @@ const dateDuration = computed<string | null>(() => props.event.endDate ? getRela
<div class="mb-1 flex gap-x-2 items-center"> <div class="mb-1 flex gap-x-2 items-center">
<template v-if="event.location"> <template v-if="event.location">
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1"> <p class="w-fit text-xs md:text-sm italic opacity-75 flex items-center gap-1">
<PhMapPinArea size="16" weight="fill" /> {{ event.location }} <PhMapPinArea size="16" weight="fill" /> {{ event.location }}
</p> </p>
</template> </template>
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1"> <p class="w-fit text-xs md:text-sm italic opacity-75 flex items-center gap-1">
<PhAlarm size="16" weight="fill" /> {{ dateDifference }} <PhAlarm size="16" weight="fill" /> {{ dateDifference }}
</p> </p>
<template v-if="dateDuration"> <template v-if="dateDuration">
<p class="w-fit text-sm italic opacity-75 flex items-center gap-1"> <p class="w-fit text-xs md:text-sm italic opacity-75 flex items-center gap-1">
<PhHourglassMedium size="16" weight="fill" /> {{ $t('entity.calendar.date.while', { duration: dateDuration } )}} <PhHourglassMedium size="16" weight="fill" /> {{ $t('entity.calendar.date.while', { duration: dateDuration } )}}
</p> </p>
</template> </template>
@@ -111,8 +111,8 @@ const dateDuration = computed<string | null>(() => props.event.endDate ? getRela
</ul> </ul>
</div> </div>
<div v-if="event.description" class="text-sm"> <div v-if="event.description" class="text-xs md:text-sm">
<hr class="my-2 border-white opacity-50" > <hr class="my-3 md:my-2 border-white opacity-50" >
<span class="opacity-75"> <span class="opacity-75">
{{ event.description }} {{ event.description }}
</span> </span>

View File

@@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { isCharacter, type Character } from "@/models/Characters" import { isCharacter, type Character } from "@@/models/Characters"
import type { RPGDate, RPGDateOrder } from "@/models/Date" import type { RPGDate, RPGDateOrder } from "@@/models/Date"
import { useCalendar } from "@/stores/CalendarStore" import { useCalendar } from "@/stores/CalendarStore"
import { computed } from "vue" import { computed } from "vue"
import { isCalendarEvent, type CalendarEvent } from "~/models/CalendarEvent" import { isCalendarEvent, type CalendarEvent } from "@@/models/CalendarEvent"
import type { SearchMode } from "../../SearchMode" import type { SearchMode } from "../../SearchMode"
import CharacterCallout from "./CharacterCallout.vue" import CharacterCallout from "./CharacterCallout.vue"
@@ -60,7 +60,7 @@ const pagedResults = computed(() => sortedResults.value.slice(props.startAt, pro
</script> </script>
<template> <template>
<ul class="grid gap-4 pl-8 pr-12"> <ul class="grid gap-3 md:gap-4 pl-4 pr-6 md:pl-8 md:pr-12">
<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">
<EventCallout v-if="isCalendarEvent(r)" :event="r" @query:date-jump="handleJumpToDate" /> <EventCallout v-if="isCalendarEvent(r)" :event="r" @query:date-jump="handleJumpToDate" />

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from "@/models/Date" import type { RPGDate } from "@@/models/Date"
import type { CalendarEvent } from "@/models/CalendarEvent" import type { CalendarEvent } from "@@/models/CalendarEvent"
import { useElementBounding } from "@vueuse/core" import { breakpointsTailwind, useElementBounding } from "@vueuse/core"
import { storeToRefs } from "pinia" import { storeToRefs } from "pinia"
import { computed, ref, type ComputedRef } from "vue" import { computed, ref, type ComputedRef } from "vue"
@@ -12,12 +12,20 @@ const props = defineProps<{
faded?: boolean faded?: boolean
}>() }>()
const emit = defineEmits<{
"on-open-create-dialog": [date: RPGDate]
}>()
const calendarTile = ref() const calendarTile = ref()
const calendarEventsList = ref() const calendarEventsList = ref()
const { defaultDate, selectDate, areDatesIdentical } = useCalendar() const { defaultDate, selectDate, areDatesIdentical, getFormattedDateTitle } = useCalendar()
const { selectedDate, currentEvents, isReadOnly } = storeToRefs(useCalendar()) const { selectedDate, currentEvents, isReadOnly } = storeToRefs(useCalendar())
const breakpoints = useBreakpoints(
breakpointsTailwind
)
/** /**
* All events with a startDate / endDate that starts or ends on the tile * All events with a startDate / endDate that starts or ends on the tile
*/ */
@@ -73,19 +81,19 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
<template> <template>
<div <div
ref="calendarTile" ref="calendarTile"
class="tile relative text-xs p-2" class="tile relative p-1 md:p-2 transition-colors"
:class="{ :class="{
'text-slate-300 dark:text-slate-500': props.faded, 'text-slate-300 dark:text-slate-500': props.faded,
'text-slate-500 dark:text-slate-300': !props.faded 'text-slate-500 dark:text-slate-300': !props.faded
}" }"
> >
<button <button
class="relative z-10 group block w-full text-center cursor-pointer" class="relative z-10 group block w-full text-2xs md:text-xs text-center cursor-pointer"
@click="selectDate(date)" @click="selectDate(date)"
> >
<ClientOnly> <ClientOnly>
<span <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-indigo-300 dark:group-hover:border-indigo-700" class="inline-flex size-7 md:size-8 aspect-square items-center justify-center rounded-full border-1 md:border-2 border-transparent font-bold transition-colors group-hover:border-indigo-300 dark:group-hover:border-indigo-700"
:class="{ :class="{
'text-white bg-slate-600 dark:bg-slate-800': isDefaultDate && !isSelectedDate, 'text-white bg-slate-600 dark:bg-slate-800': isDefaultDate && !isSelectedDate,
'text-white bg-indigo-500': isSelectedDate 'text-white bg-indigo-500': isSelectedDate
@@ -99,7 +107,7 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
<ClientOnly> <ClientOnly>
<ul <ul
ref="calendarEventsList" 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="absolute top-9 md:top-12 bottom-1 md:bottom-2 inset-x-2 grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity"
:class="{ :class="{
'opacity-40': props.faded && !isSelectedDate 'opacity-40': props.faded && !isSelectedDate
}" }"
@@ -114,17 +122,23 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
<UiPopover> <UiPopover>
<UiPopoverTrigger as-child> <UiPopoverTrigger as-child>
<button <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" class="text-2xs md:text-xs px-[5px] py-[5px] md:px-2 md:py-1 block w-full text-left font-bold rounded-sm whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer transition-colors hover:text-foreground hover:bg-foreground/10"
> >
{{ eventsNotDisplayed }} autre{{ eventsNotDisplayed > 1 ? 's' : '' }} {{ eventsNotDisplayed }} autre{{ eventsNotDisplayed > 1 ? 's' : '' }}
</button> </button>
</UiPopoverTrigger> </UiPopoverTrigger>
<UiPopoverContent class="w-80" :align="'center'" :side="'right'"> <UiPopoverContent
<div class="text-center mb-4"> class="w-80"
<span :align="'center'"
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" :align-offset="breakpoints.lg.value ? 50 : 25"
:collision-padding="breakpoints.lg.value ? 50 : 25"
:side="breakpoints.lg.value ? 'left' : 'bottom'"
> >
{{ date.day }} <div class="text-center mb-2">
<span
class="text-lg font-semibold rounded-full"
>
{{ getFormattedDateTitle(date, true) }}
</span> </span>
</div> </div>
<ul class="grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity"> <ul class="grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity">
@@ -143,14 +157,17 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
</ClientOnly> </ClientOnly>
<ClientOnly> <ClientOnly>
<LazyCalendarDialogCreateEvent v-if="!isReadOnly" :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" /> <LazyCalendarDialogCreateEvent v-if="!isReadOnly && breakpoints.lg.value" :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
<button
v-else-if="!isReadOnly && !breakpoints.lg.value"
class="absolute inset-0 w-full h-full cursor-default z-0"
@click="emit('on-open-create-dialog', props.date)"
/>
</ClientOnly> </ClientOnly>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
// .tile are the direct children of the grid
// We need to get, between each element, a 1px solid line to separate them, for columns and rows
.tile { .tile {
border-right-width: 1px; border-right-width: 1px;
border-bottom-width: 1px; border-bottom-width: 1px;

View File

@@ -0,0 +1,100 @@
<script lang="ts" setup>
import { useCalendar } from "@/stores/CalendarStore"
import { useThrottleFn } from "@vueuse/core"
import type { RPGDate } from "@@/models/Date"
import { VisuallyHidden } from "radix-vue"
const { currentDate, decrementViewMonth, incrementViewMonth, resetSkeleton } = useCalendar()
const { currentMonthData, operationInProgress, eventSkeleton } = storeToRefs(useCalendar())
function handleWheel(e: WheelEvent) {
// Prevent scrolling
e.preventDefault()
const isMovingUp = e.deltaY < 0
if (isMovingUp) {
moveCalendarLeft()
} else {
moveCalendarRight()
}
}
const moveCalendarLeft = useThrottleFn(() => {
decrementViewMonth()
}, 100)
const moveCalendarRight = useThrottleFn(() => {
incrementViewMonth()
}, 100)
const isDialogOpen = ref<boolean>(false);
/**
* Opens event creation's popover
*/
function handleDialogOpen(date: RPGDate) {
// If another operation is in progress, whether it's another create popup or a modal, don't bother opening it
if (operationInProgress.value) {
isDialogOpen.value = false
return
}
resetSkeleton()
isDialogOpen.value = true
// Set skeleton initial startDate if it's known
if (date) {
eventSkeleton.value.startDate = date
}
}
function toggleDialog() {
isDialogOpen.value = !isDialogOpen.value;
};
/**
* Prevents the modal from closing if's still loading
*/
function handleClosing() {
setTimeout(() => resetSkeleton(), 100)
}
</script>
<template>
<div class="grid grid-cols-5 md:grid-cols-7 lg:grid-cols-10" @wheel="handleWheel">
<template v-if="currentMonthData">
<CalendarStateMonthlyDayTile
v-for="day in currentMonthData?.days"
:key="`layout-month-grid-${day}`"
:date="{
day: day,
month: currentDate.currentMonth,
year: currentDate.currentYear
}"
@on-open-create-dialog="handleDialogOpen"
/>
<UiDialog v-model:open="isDialogOpen">
<UiDialogContent
class="max-md:translate-0 max-md:inset-0 max-md:w-full max-md:block"
:trap-focus="true"
@escape-key-down="handleClosing"
@pointer-down-outside.prevent="null"
>
<UiDialogTitle class="max-md:mb-8">
{{ $t("entity.calendar.event.addSingle") }}
</UiDialogTitle>
<VisuallyHidden>
<UiDialogDescription>
{{ $t("entity.calendar.event.addSingleDescription") }}
</UiDialogDescription>
</VisuallyHidden>
<CalendarFormCreateEvent @event-created="toggleDialog" />
</UiDialogContent>
</UiDialog>
</template>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RPGDate } from "@/models/Date" import type { RPGDate } from "@@/models/Date"
import { storeToRefs } from "pinia" import { storeToRefs } from "pinia"
import { computed, type ComputedRef } from "vue" import { computed, type ComputedRef } from "vue"

View File

@@ -24,7 +24,7 @@ const moveCalendarRight = useThrottleFn(() => {
</script> </script>
<template> <template>
<div class="container mt-[10vh] mb-auto" @wheel="handleWheel"> <div class="container mx-auto mt-[10vh] mb-auto" @wheel="handleWheel">
<div class="grid grid-cols-5 gap-x-8 gap-y-16"> <div class="grid grid-cols-5 gap-x-8 gap-y-16">
<CalendarStateYearlyMonthTile <CalendarStateYearlyMonthTile
v-for="month in months" v-for="month in months"

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { CalendarMonth } from "~/models/CalendarMonth"; import type { CalendarMonth } from "@@/models/CalendarMonth";
defineProps<{ defineProps<{
month: CalendarMonth month: CalendarMonth

View File

@@ -0,0 +1,66 @@
<script setup lang="ts">
import { PhCaretRight } from "@phosphor-icons/vue";
const route = useRoute()
export type BreadcrumbItem = {
label?: string;
translateKey?: string;
to?: string;
}
defineProps<{
items?: BreadcrumbItem[];
}>()
</script>
<template>
<div class="flex gap-3 items-center">
<div class="md:hidden">
<SidebarToggle />
</div>
<UiBreadcrumb>
<UiBreadcrumbList>
<UiBreadcrumbItem v-if="route.path !== '/my'">
<UiBreadcrumbLink as-child>
<NuxtLink to="/my">
{{ $t("breadcrumbs.profile") }}
</NuxtLink>
</UiBreadcrumbLink>
</UiBreadcrumbItem>
<UiBreadcrumbItem v-else>
<UiBreadcrumbPage>
{{ $t("breadcrumbs.profile") }}
</UiBreadcrumbPage>
</UiBreadcrumbItem>
<template v-for="(item, index) in items" :key="index">
<UiBreadcrumbSeparator>
<PhCaretRight />
</UiBreadcrumbSeparator>
<UiBreadcrumbItem>
<UiBreadcrumbLink v-if="item.to" as-child>
<NuxtLink :to="item.to">
<template v-if="item.label">
{{ item.label }}
</template>
<template v-else>
{{ $t(`breadcrumbs.${item.translateKey}`) }}
</template>
</NuxtLink>
</UiBreadcrumbLink>
<UiBreadcrumbPage v-else>
<template v-if="item.label">
{{ item.label }}
</template>
<template v-else>
{{ $t(`breadcrumbs.${item.translateKey}`) }}
</template>
</UiBreadcrumbPage>
</UiBreadcrumbItem>
</template>
</UiBreadcrumbList>
</UiBreadcrumb>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from "~/lib/utils"; import { cn } from "@/lib/utils";
interface SpacingProps { interface SpacingProps {
size?: "xs" | "sm" | "md" | "lg" | "xlg" | "2xl" | "3xl" size?: "xs" | "sm" | "md" | "lg" | "xlg" | "2xl" | "3xl"

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { type RPGColor, rpgColors } from "~/models/Color"; import { type RPGColor, rpgColors } from "@@/models/Color";
const { id, theme = "normal", position = "popper" } = defineProps<{ const { id, theme = "normal", position = "popper" } = defineProps<{
id: string id: string

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { contentStates, type ContentState } from "~/models/Entity"; import { contentStates, type ContentState } from "@@/models/Entity";
defineProps<{ defineProps<{
id: string id: string

View File

@@ -0,0 +1,190 @@
<script lang="ts" setup>
import { PhCompass, PhGlobeHemisphereEast, PhHurricane, PhInfo, PhX } from "@phosphor-icons/vue"
import type { SidebarMenuActionType, SidebarMenuIcon } from "./SidebarProps";
import { cn } from "@/lib/utils";
import { breakpointsTailwind } from "@vueuse/core"
const { revealAdvancedSearch } = useCalendar()
const { toggleSidebar } = useUiStore()
const { currentMenu, isSidebarOpened } = storeToRefs(useUiStore())
function handleMenuItemAction(actionType: SidebarMenuActionType) {
if (actionType === "event-search") {
revealAdvancedSearch()
}
}
function computeMenuItemIcon(iconString: SidebarMenuIcon) {
switch (iconString) {
case "universe":
return PhHurricane
case "world":
return PhGlobeHemisphereEast
default:
return PhCompass
}
}
const breakpoints = useBreakpoints(
breakpointsTailwind
)
// const sidebarRef = ref(null)
// onClickOutside(sidebarRef, () => {
// isSidebarOpened.value = false
// })
</script>
<template>
<nav
ref="sidebarRef"
:class="cn(
['md:relative md:isolate w-16 py-6 grid gap-4 grid-rows-[1fr_auto] justify-center md:transition-none'], // Base appearance
['after:opacity-50 after:contrast-125 dark:after:opacity-75 dark:after:contrast-175 after:-hue-rotate-60'], // After styling
['border-r-[1px] bg-indigo-700 dark:bg-black text-white border-r-indigo-700 dark:border-r-indigo-950 shadow-navbar-light dark:shadow-navbar-dark'], // Colours
['max-md:justify-stretch max-md:px-4 max-md:py-4 max-md:absolute max-md:left-0 max-md:inset-0 max-md:z-50 max-md:w-40 max-md:max-w-full max-md:transition-all'], // Responsive behaviours
{
'max-md:-translate-x-40': !isSidebarOpened,
'max-md:-translate-x-0 shadow-navbar-dark dark:bg-slate-950': isSidebarOpened
}
)"
>
<menu class="flex flex-col gap-4 max-md:items-center">
<li class="mb-12 mt-4 max-md:self-start">
<UiButton
variant="outline"
size="icon"
class="md:hidden size-9 border-background/30"
@click="toggleSidebar"
>
<PhX size="19" />
</UiButton>
</li>
<li class="max-md:self-start">
<UiTooltipProvider :delay-duration="50" :disabled="!breakpoints.md.value">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
variant="ghost"
size="icon"
class="rounded-full max-md:hidden"
as-child
>
<RouterLink to="/explore">
<PhCompass size="24" weight="fill" />
</RouterLink>
</UiButton>
<RouterLink
to="/explore"
class="md:hidden flex items-center gap-[.6ch] underline-offset-4 hover:underline"
>
<PhCompass size="22" weight="fill" />
<span class="text-[.9em]">
{{ $t('pages.explore.menuLabel') }}
</span>
</RouterLink>
</UiTooltipTrigger>
<UiTooltipContent :side="'right'" :side-offset="6">
<p>
{{ $t('pages.explore.menuLabel') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</li>
<li class="max-md:self-start">
<UiTooltipProvider :delay-duration="50" :disabled="!breakpoints.md.value">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
variant="ghost"
size="icon"
class="rounded-full max-md:hidden"
as-child
>
<RouterLink to="/about">
<PhInfo size="24" weight="fill" />
</RouterLink>
</UiButton>
<RouterLink
to="/about"
class="md:hidden flex items-center gap-[.6ch] underline-offset-4 hover:underline"
>
<PhInfo size="22" weight="fill" />
<span class="text-[.9em]">
{{ $t('pages.about.menuLabel') }}
</span>
</RouterLink>
</UiTooltipTrigger>
<UiTooltipContent :side="'right'" :side-offset="6">
<p>
{{ $t('pages.about.menuLabel') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</li>
<ClientOnly>
<li v-for="(item, i) in currentMenu" :key="i">
<UiTooltipProvider :delay-duration="50">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
v-if="item.to"
variant="ghost"
size="icon"
class="rounded-full"
as-child
>
<RouterLink :to="item.to">
<component :is="computeMenuItemIcon(item.phIcon)" size="24" :weight="item.phIconWeight || 'fill'" />
</RouterLink>
</UiButton>
<UiButton
v-if="item.action"
variant="ghost"
size="icon"
class="rounded-full"
@click="handleMenuItemAction(item.action!)"
>
<component :is="computeMenuItemIcon(item.phIcon)" size="24" :weight="item.phIconWeight || 'fill'" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent :side="'right'" :side-offset="6">
<p>{{ item.tooltip }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</li>
</ClientOnly>
</menu>
<UserCTA />
</nav>
</template>
<style lang="scss" scoped>
nav {
&::after {
display: block;
content: '';
position: absolute;
bottom: 0;
inset-inline: 0;
height: 25rem;
max-height: 100%;
background-image: url("/images/sidebar.png");
background-position: bottom;
background-size: cover;
background-repeat: no-repeat;
mask-image: linear-gradient(to top, black 25%, transparent 50%, transparent);
z-index: -1;
}
}
</style>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
import { PhList } from "@phosphor-icons/vue"
const { toggleSidebar } = useUiStore()
</script>
<template>
<UiButton
size="icon"
variant="outline"
@click="toggleSidebar"
>
<PhList size="19" weight="light" />
</UiButton>
</template>

View File

@@ -2,7 +2,7 @@
import { computed } from "vue" import { computed } from "vue"
import { PhCheckCircle, PhUser, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle } from "@phosphor-icons/vue" import { PhCheckCircle, PhUser, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle } from "@phosphor-icons/vue"
import { cn } from "~/lib/utils"; import { cn } from "@/lib/utils";
const router = useRouter() const router = useRouter()
@@ -21,8 +21,7 @@ function closeMenu() {
watch(user, closeMenu) watch(user, closeMenu)
async function handleGoogleLogin() { async function handleGoogleLogin() {
try { const { error } = await auth.signInWithOAuth({
auth.signInWithOAuth({
provider: "google", provider: "google",
options: { options: {
queryParams: { queryParams: {
@@ -32,18 +31,17 @@ async function handleGoogleLogin() {
redirectTo: profileUrl redirectTo: profileUrl
} }
}) })
} catch (err) {
console.log(err) if (error) {
console.log(error.message)
} }
} }
async function handleLogout() { async function handleLogout() {
try {
const { error } = await auth.signOut() const { error } = await auth.signOut()
if (error) throw error if (error) {
} catch (err) { console.log(error.message)
console.log(err)
} }
} }
@@ -186,12 +184,6 @@ function pushRoute(to: AvailableRoutes) {
<UiDropdownMenuSeparator /> <UiDropdownMenuSeparator />
<template v-if="user"> <template v-if="user">
<!-- <UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="pushRoute('/my/settings')">
<PhGear size="20" weight="fill" />
<span>
{{ $t('ui.sidebarMenu.account') }}
</span>
</UiDropdownMenuItem> -->
<UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="handleLogout"> <UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="handleLogout">
<PhSignOut size="20" weight="fill" /> <PhSignOut size="20" weight="fill" />
<span> <span>

View File

@@ -2,7 +2,7 @@
import { type HTMLAttributes, computed } from "vue" import { type HTMLAttributes, computed } from "vue"
import { AlertDialogAction, type AlertDialogActionProps } from "radix-vue" import { AlertDialogAction, type AlertDialogActionProps } from "radix-vue"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button/index" import { buttonVariants } from "~/components/ui/button/index"
const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes["class"] }>() const props = defineProps<AlertDialogActionProps & { class?: HTMLAttributes["class"] }>()

View File

@@ -2,7 +2,7 @@
import { type HTMLAttributes, computed } from "vue" import { type HTMLAttributes, computed } from "vue"
import { AlertDialogCancel, type AlertDialogCancelProps } from "radix-vue" import { AlertDialogCancel, type AlertDialogCancelProps } from "radix-vue"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button/index" import { buttonVariants } from "~/components/ui/button/index"
const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes["class"] }>() const props = defineProps<AlertDialogCancelProps & { class?: HTMLAttributes["class"] }>()

View File

@@ -31,7 +31,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-3xl -translate-x-1/2 -translate-y-1/2 gap-4 border border-indigo-200 bg-background dark:bg-slate-950 dark:border-indigo-950 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-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% 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-3xl -translate-x-1/2 -translate-y-1/2 gap-4 border border-indigo-200 bg-background dark:bg-slate-950 dark:border-indigo-950 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-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% data-[state=open]:slide-in-from-top-48% rounded-lg',
props.class, props.class,
) )
" "

View File

@@ -0,0 +1,9 @@
export { default as AlertDialog } from "@/components/ui/alert-dialog/AlertDialog.vue"
export { default as AlertDialogTrigger } from "@/components/ui/alert-dialog/AlertDialogTrigger.vue"
export { default as AlertDialogContent } from "@/components/ui/alert-dialog/AlertDialogContent.vue"
export { default as AlertDialogHeader } from "@/components/ui/alert-dialog/AlertDialogHeader.vue"
export { default as AlertDialogTitle } from "@/components/ui/alert-dialog/AlertDialogTitle.vue"
export { default as AlertDialogDescription } from "@/components/ui/alert-dialog/AlertDialogDescription.vue"
export { default as AlertDialogFooter } from "@/components/ui/alert-dialog/AlertDialogFooter.vue"
export { default as AlertDialogAction } from "@/components/ui/alert-dialog/AlertDialogAction.vue"
export { default as AlertDialogCancel } from "@/components/ui/alert-dialog/AlertDialogCancel.vue"

View File

@@ -1,8 +1,8 @@
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority"
export { default as Avatar } from "./Avatar.vue" export { default as Avatar } from "@/components/ui/avatar/Avatar.vue"
export { default as AvatarImage } from "./AvatarImage.vue" export { default as AvatarImage } from "@/components/ui/avatar/AvatarImage.vue"
export { default as AvatarFallback } from "./AvatarFallback.vue" export { default as AvatarFallback } from "@/components/ui/avatar/AvatarFallback.vue"
export const avatarVariant = cva( export const avatarVariant = cva(
"inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden", "inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden",

View File

@@ -1,6 +1,6 @@
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority"
export { default as Badge } from "./Badge.vue" export { default as Badge } from "@/components/ui/badge/Badge.vue"
export const badgeVariants = cva( export const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2", "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { HTMLAttributes } from "vue" import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { MoreHorizontal } from "lucide-vue-next" import { PhArrowsHorizontal } from "@phosphor-icons/vue";
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes["class"] class?: HTMLAttributes["class"]
@@ -15,7 +15,7 @@ const props = defineProps<{
:class="cn('flex h-9 w-9 items-center justify-center', props.class)" :class="cn('flex h-9 w-9 items-center justify-center', props.class)"
> >
<slot> <slot>
<MoreHorizontal class="h-4 w-4" /> <PhArrowsHorizontal class="size-4" />
</slot> </slot>
<span class="sr-only">More</span> <span class="sr-only">More</span>
</span> </span>

View File

@@ -13,7 +13,7 @@ const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes
<Primitive <Primitive
:as="as" :as="as"
:as-child="asChild" :as-child="asChild"
:class="cn('transition-colors hover:text-foreground', props.class)" :class="cn('transition-colors rounded-[1px] hover:text-primary focus-visible:text-primary underline-offset-4 hover:underline focus-visible:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-4', props.class)"
> >
<slot /> <slot />
</Primitive> </Primitive>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { HTMLAttributes } from "vue" import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { ChevronRight } from "lucide-vue-next" import { PhCaretRight } from "@phosphor-icons/vue";
const props = defineProps<{ const props = defineProps<{
class?: HTMLAttributes["class"] class?: HTMLAttributes["class"]
@@ -15,7 +15,7 @@ const props = defineProps<{
:class="cn('[&>svg]:w-3 [&>svg]:h-3', props.class)" :class="cn('[&>svg]:w-3 [&>svg]:h-3', props.class)"
> >
<slot> <slot>
<ChevronRight /> <PhCaretRight />
</slot> </slot>
</li> </li>
</template> </template>

View File

@@ -0,0 +1,7 @@
export { default as Breadcrumb } from "@/components/ui/breadcrumb/Breadcrumb.vue"
export { default as BreadcrumbEllipsis } from "@/components/ui/breadcrumb/BreadcrumbEllipsis.vue"
export { default as BreadcrumbItem } from "@/components/ui/breadcrumb/BreadcrumbItem.vue"
export { default as BreadcrumbLink } from "@/components/ui/breadcrumb/BreadcrumbLink.vue"
export { default as BreadcrumbList } from "@/components/ui/breadcrumb/BreadcrumbList.vue"
export { default as BreadcrumbPage } from "@/components/ui/breadcrumb/BreadcrumbPage.vue"
export { default as BreadcrumbSeparator } from "@/components/ui/breadcrumb/BreadcrumbSeparator.vue"

View File

@@ -1,6 +1,6 @@
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority"
export { default as Button } from "./Button.vue" export { default as Button } from "@/components/ui/button/Button.vue"
export const buttonVariants = cva( export const buttonVariants = cva(
"inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-30", "inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-30",

View File

@@ -15,21 +15,22 @@ const emit = defineEmits(["on-click"])
<div <div
:class=" :class="
cn('rounded-lg border bg-card text-card-foreground shadow-xs transition-all isolate', props.class, { cn('rounded-lg border bg-card text-card-foreground shadow-xs transition-all isolate', props.class, {
'relative outline outline-2 outline-offset-4 outline-transparent hover:-translate-y-[.2rem]': 'relative hover:-translate-y-[.2rem]':
props.link || props.hasClick props.link || props.hasClick
}) })
" "
> >
<slot />
<NuxtLink <NuxtLink
v-if="props.link" v-if="props.link"
:to="props.link" :to="props.link"
class="absolute inset-0 z-10 focus-visible:outline-hidden" class="rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 absolute inset-0 z-10 cursor-pointer"
/> />
<button <button
v-if="props.hasClick" v-if="props.hasClick"
class="absolute inset-0 z-10 focus-visible:outline-hidden" class="rounded-lg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 absolute inset-0 z-10 cursor-pointer"
@click="emit('on-click')" /> @click="emit('on-click')"
/>
<slot />
</div> </div>
</template> </template>

View File

@@ -8,7 +8,7 @@ const props = defineProps<{
</script> </script>
<template> <template>
<div :class="cn('flex items-center p-6 border-t-2 border-t-slate-900', props.class)"> <div :class="cn('flex items-center p-6 border-t-2 border-t-border', props.class)">
<slot /> <slot />
</div> </div>
</template> </template>

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