30 Commits

Author SHA1 Message Date
Alexis
7443769f00 Updated package 2025-10-06 21:37:31 +02:00
Alexis
692aaad868 Updated package 2025-09-12 19:52:32 +02:00
Alexis
371bb7e38a Updated package 2025-09-03 21:14:07 +02:00
Alexis
c4e70eb29c Changed quick navigation (arrows will be for days) 2025-08-24 19:53:43 +02:00
Alexis
f8d2387b59 Added quick navigation for calendar pages 2025-08-24 19:44:27 +02:00
Alexis
8c53616c79 Added arrow navigation on calendar page 2025-08-24 15:02:42 +02:00
Alexis
8dff0763e9 Changed calendar menu subnav 2025-08-24 13:47:03 +02:00
Alexis
9c7b503ca8 Merge branch 'feat/nav' into dev 2025-08-24 11:49:21 +02:00
Alexis
674a38c30d Fixed search shortcut 2025-08-24 11:38:10 +02:00
Alexis
e7cef314a1 Fixed untranslated string 2025-08-24 11:26:11 +02:00
Alexis
806d3a7908 Updated package 2025-08-23 22:45:35 +02:00
Alexis
6251499940 Added sheet for sidebar mobile 2025-08-15 15:37:35 +02:00
Alexis
f3d5db76ac Removed sidebar toggle on public pages 2025-08-12 11:18:47 +02:00
Alexis
08620b5044 Made content body smaller 2025-08-12 11:16:52 +02:00
Alexis
f89cc22e87 Fixed quick create dialog not resetting skeleton 2025-08-12 11:09:28 +02:00
Alexis
51c678d27e Fixed dashboard link hitbox 2025-08-12 10:44:56 +02:00
Alexis
b9b891c61f Fixed nav colors in dark mode 2025-08-12 10:43:49 +02:00
Alexis
df06228e68 Fixed transition render issue 2025-08-12 10:37:37 +02:00
Alexis
44193aa018 Added about link to CTA 2025-08-12 10:34:23 +02:00
Alexis
73eceda96c Changed secondary button color 2025-08-12 10:18:07 +02:00
Alexis
e305b21f80 Changed projects page handle 2025-08-12 10:11:28 +02:00
Alexis
d2b66a42dd Fixed dark mode select hover 2025-08-12 10:07:12 +02:00
Alexis
be32a018c6 Added input focus on entity forms 2025-08-11 20:23:25 +02:00
Alexis
a5a232bac7 Added navbar on public pages 2025-08-11 20:00:04 +02:00
Alexis
20045b125e Changed explore page h2 2025-08-09 21:06:42 +02:00
Alexis
f1a3854d1d Merge branch 'feat/drag-events' into dev 2025-08-09 18:56:57 +02:00
Alexis
cebcb50903 Added tooltip on rich text 2025-08-09 18:55:55 +02:00
Alexis
396215bb91 Fixed some UI issues with drag state 2025-08-09 16:40:50 +02:00
Alexis
7468374bb5 Added update logic for single day events 2025-08-09 16:24:59 +02:00
Alexis
5a421cf07f Merge branch 'feat/editor' into dev 2025-08-08 10:49:14 +02:00
50 changed files with 4276 additions and 4093 deletions

View File

@@ -40,23 +40,19 @@
} }
} }
p, ul, ol {
font-size: var(--text-sm);
}
/* Normal link */ /* Normal link */
p a:not([class]) { p a:not([class]) {
color: var(--color-sky-600); color: var(--color-primary);
text-underline-offset: .25rem; text-underline-offset: .25rem;
&:hover { &:hover {
color: var(--color-sky-500); color: var(--color-accent);
text-decoration: underline; text-decoration: underline;
} }
@variant dark {
color: var(--color-teal-500);
&:hover {
color: var(--color-teal-300);
}
}
} }
ul { ul {

View File

@@ -56,7 +56,7 @@
--primary: rgb(101, 230, 166); --primary: rgb(101, 230, 166);
--primary-foreground: hsl(180 0 4.9%); --primary-foreground: hsl(180 0 4.9%);
--secondary: hsl(222.2 47.4% 11.2%); --secondary: hsl(0, 0%, 22.5%);
--secondary-foreground: hsl(210 40% 98%); --secondary-foreground: hsl(210 40% 98%);
--accent: hsl(11, 87%, 69%); --accent: hsl(11, 87%, 69%);

View File

@@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useCalendar } from "@/stores/CalendarStore" import { useCalendar } from "@/stores/CalendarStore"
import { computed, type Component, type ComputedRef } from "vue"
// import { PhMagnifyingGlass } from '@phosphor-icons/vue' // import { PhMagnifyingGlass } from '@phosphor-icons/vue'
import MonthlyLayout from "./state/monthly/Layout.vue" import MonthlyLayout from "./state/monthly/Layout.vue"
@@ -8,8 +7,10 @@ import CenturyLayout from "./state/centennially/Layout.vue"
import DecadeLayout from "./state/decennially/Layout.vue" import DecadeLayout from "./state/decennially/Layout.vue"
import YearLayout from "./state/yearly/Layout.vue" import YearLayout from "./state/yearly/Layout.vue"
const { currentConfig, jumpToDate, selectedDate } = useCalendar() import { onKeyDown } from "@vueuse/core"
const { isReadOnly } = storeToRefs(useCalendar())
const { currentConfig, jumpToDate, selectedDate, toPastFar, toPastNear, toFutureNear, toFutureFar, toggleCreatingEventModal } = useCalendar()
const { isReadOnly, calendarState } = storeToRefs(useCalendar())
const currentViewComponent: ComputedRef<Component> = computed<Component>(() => { const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
switch (currentConfig.viewType) { switch (currentConfig.viewType) {
@@ -31,6 +32,45 @@ const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
onMounted(() => { onMounted(() => {
jumpToDate(selectedDate) jumpToDate(selectedDate)
}) })
/**
* Keyboard quick controls
*/
// Key combos to navigate
const {
home, pageUp,
end, pageDown,
} = useMagicKeys()
watch(home!, (k) => {
if (calendarState.value === 'active' && k) {
toPastFar()
}
})
watch(pageUp!, (k) => {
if (calendarState.value === 'active' && k) {
toPastNear()
}
})
watch(pageDown!, (k) => {
if (calendarState.value === 'active' && k) {
toFutureNear()
}
})
watch(end!, (k) => {
if (calendarState.value === 'active' && k) {
toFutureFar()
}
})
// Key combo to create an event
onKeyDown("+", (e) => {
if (calendarState.value === 'active') {
e.preventDefault()
toggleCreatingEventModal()
}
})
</script> </script>
<template> <template>

View File

@@ -3,13 +3,8 @@ import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { PhPlus } from "@phosphor-icons/vue"; import { PhPlus } from "@phosphor-icons/vue";
import { VisuallyHidden } from "radix-vue"; import { VisuallyHidden } from "radix-vue";
const isDialogOpen = ref<boolean>(false); const { isCreatingEventModalOpen } = storeToRefs(useCalendar());
const { resetSkeleton } = useCalendar(); const { resetSkeleton, toggleCreatingEventModal } = useCalendar();
// Toggles the dialog
function toggleDialog() {
isDialogOpen.value = !isDialogOpen.value;
};
/** /**
* Prevents the modal from closing if's still loading * Prevents the modal from closing if's still loading
@@ -29,7 +24,7 @@ const breakpoints = useBreakpoints(
<UiButton <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" 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'" :size="breakpoints.md.value ? 'default' : 'icon'"
@click="toggleDialog" @click="toggleCreatingEventModal"
> >
<PhPlus :size="breakpoints.md.value ? 18 : 24" weight="bold" /> <PhPlus :size="breakpoints.md.value ? 18 : 24" weight="bold" />
@@ -40,12 +35,12 @@ const breakpoints = useBreakpoints(
</Transition> </Transition>
</ClientOnly> </ClientOnly>
<UiDialog v-model:open="isDialogOpen"> <UiDialog v-model:open="isCreatingEventModalOpen">
<UiDialogContent <UiDialogContent
class="max-md:translate-0 max-md:inset-0 max-md:w-full max-md:block" class="max-md:translate-0 max-md:inset-0 max-md:w-full max-md:block"
:trap-focus="true" :trap-focus="true"
@escape-key-down="handleClosing" @escape-key-down="handleClosing"
@pointer-down-outside.prevent="handleClosing" @pointer-down-outside="handleClosing"
> >
<UiDialogTitle class="max-md:mb-8"> <UiDialogTitle class="max-md:mb-8">
{{ $t("entity.calendar.event.addSingle") }} {{ $t("entity.calendar.event.addSingle") }}
@@ -57,7 +52,7 @@ const breakpoints = useBreakpoints(
</UiDialogDescription> </UiDialogDescription>
</VisuallyHidden> </VisuallyHidden>
<CalendarFormCreateEvent @event-created="toggleDialog" /> <CalendarFormCreateEvent @event-created="toggleCreatingEventModal" />
</UiDialogContent> </UiDialogContent>
</UiDialog> </UiDialog>
</template> </template>

View File

@@ -29,7 +29,7 @@ function handleClosing() {
@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.prevent
> >
<header class="pl-8 grid gap-y-2 max-md:mb-8"> <header class="pl-8 grid gap-y-2 max-md:mb-8">
<UiDialogTitle> <UiDialogTitle>

View File

@@ -2,14 +2,17 @@
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 { ja } from "zod/v4/locales";
const props = defineProps<{ const props = defineProps<{
event: CalendarEvent event: CalendarEvent
tileDate: RPGDate tileDate: RPGDate
}>() }>()
const { areDatesIdentical, revealEditEventModal, revealDeleteEventModal } = useCalendar() const buttonRef = useTemplateRef<HTMLButtonElement>('buttonRef')
const { lastActiveEvent } = storeToRefs(useCalendar())
const { areDatesIdentical, revealEditEventModal, revealDeleteEventModal, resetSkeleton } = useCalendar()
const { lastActiveEvent, eventSkeleton, isDraggingEvent } = storeToRefs(useCalendar())
const spansMultipleDays = computed(() => Boolean(props.event.startDate && props.event.endDate)) const spansMultipleDays = computed(() => Boolean(props.event.startDate && props.event.endDate))
const isStartEvent = computed(() => spansMultipleDays.value && areDatesIdentical(props.tileDate, props.event.startDate)) const isStartEvent = computed(() => spansMultipleDays.value && areDatesIdentical(props.tileDate, props.event.startDate))
@@ -49,13 +52,29 @@ onMounted(() => {
handleDelete() handleDelete()
}) })
}) })
// Draggable logic
const isTileDragging = shallowRef(false)
function handleDragStart() {
isTileDragging.value = true
isDraggingEvent.value = true
eventSkeleton.value = structuredClone(toRaw(props.event))
handleClosePopover()
}
function handleDragEnd() {
isTileDragging.value = false
isDraggingEvent.value = false
}
</script> </script>
<template> <template>
<UiPopover v-model:open="isPopoverDetailsOpen"> <UiPopover v-model:open="isPopoverDetailsOpen">
<UiPopoverTrigger as-child> <UiPopoverTrigger as-child>
<button <button
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" ref="buttonRef"
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-opacity outline-offset-1 cursor-pointer"
:draggable="event.endDate ? 'false' : 'true'"
:class=" :class="
cn( cn(
`element-${event.category?.color}`, `element-${event.category?.color}`,
@@ -63,11 +82,17 @@ onMounted(() => {
'is-hidden': event.hidden, 'is-hidden': event.hidden,
'rounded-r-none': isStartEvent, 'rounded-r-none': isStartEvent,
'rounded-l-none': isEndEvent, 'rounded-l-none': isEndEvent,
},
{
'opacity-60': isTileDragging,
'opacity-100': !isTileDragging
} }
) )
" "
@dblclick="handleDoubleClick" @dblclick="handleDoubleClick"
@keydown.delete="handleDelete" @keydown.delete="handleDelete"
@dragstart="handleDragStart"
@dragend="handleDragEnd"
> >
<div class="line-clamp-2 [overflow-wrap:anywhere] hyphens-auto"> <div class="line-clamp-2 [overflow-wrap:anywhere] hyphens-auto">
{{ eventTitle }} {{ eventTitle }}

View File

@@ -16,6 +16,9 @@ onMounted(() => {
type FormTabs = "global" | "months" | "today" type FormTabs = "global" | "months" | "today"
const activeTab = ref<FormTabs>("global") const activeTab = ref<FormTabs>("global")
const newCalendarName = shallowRef<HTMLInputElement>()
useFocus(newCalendarName, { initialValue: true })
/** /**
* === Current date === * === Current date ===
*/ */
@@ -103,6 +106,7 @@ function handleFormCancel() {
</UiTabsList> </UiTabsList>
<UiTabsContent value="global" class="grid gap-4"> <UiTabsContent value="global" class="grid gap-4">
<input <input
ref="newCalendarName"
id="new-calendar-name" id="new-calendar-name"
v-model="calendarSkeleton.name" v-model="calendarSkeleton.name"
type="text" type="text"

View File

@@ -17,6 +17,9 @@ onMounted(() => {
type FormTabs = "global" | "months" | "today" type FormTabs = "global" | "months" | "today"
const activeTab = ref<FormTabs>("global") const activeTab = ref<FormTabs>("global")
const newCalendarName = shallowRef<HTMLInputElement>()
useFocus(newCalendarName, { initialValue: true })
/** /**
* === Form Validation === * === Form Validation ===
*/ */
@@ -85,6 +88,7 @@ function handleFormCancel() {
</UiTabsList> </UiTabsList>
<UiTabsContent value="global" class="grid gap-4"> <UiTabsContent value="global" class="grid gap-4">
<input <input
ref="newCalendarName"
id="new-calendar-name" id="new-calendar-name"
v-model="calendarSkeleton.name" v-model="calendarSkeleton.name"
type="text" type="text"

View File

@@ -12,7 +12,7 @@ interface DirectionLabels {
futureFar: string futureFar: string
} }
const { currentConfig, decrementViewMonth, incrementViewMonth, decrementViewYear, incrementViewYear } = const { currentConfig, toPastFar, toPastNear, toFutureNear, toFutureFar } =
useCalendar() useCalendar()
const activeDirectionLabels: ComputedRef<DirectionLabels> = computed(() => { const activeDirectionLabels: ComputedRef<DirectionLabels> = computed(() => {
@@ -51,101 +51,17 @@ const activeDirectionLabels: ComputedRef<DirectionLabels> = computed(() => {
} }
} }
}) })
function toPastFar(): void {
switch (currentConfig.viewType) {
case "month":
decrementViewYear()
break
case "year":
decrementViewYear(10)
break
case "decade":
decrementViewYear(100)
break
case "century":
default:
decrementViewYear(1000)
break
}
}
function toPastNear(): void {
switch (currentConfig.viewType) {
case "month":
decrementViewMonth()
break
case "year":
decrementViewYear()
break
case "decade":
decrementViewYear(10)
break
case "century":
default:
decrementViewYear(100)
break
}
}
function toFutureNear(): void {
switch (currentConfig.viewType) {
case "month":
incrementViewMonth()
break
case "year":
incrementViewYear()
break
case "decade":
incrementViewYear(10)
break
case "century":
default:
incrementViewYear(100)
break
}
}
function toFutureFar(): void {
switch (currentConfig.viewType) {
case "month":
incrementViewYear()
break
case "year":
incrementViewYear(10)
break
case "decade":
incrementViewYear(100)
break
case "century":
default:
incrementViewYear(1000)
break
}
}
</script> </script>
<template> <template>
<div class="flex gap-2"> <div class="flex gap-1">
<div class="grid items-center w-40 px-3 md:px-4 py-2 bg-background border-border border-x-[1px] border-t-[1px] rounded-t-sm text-sm max-md:text-xs transition-colors"> <div class="grid items-center w-40 px-1.5 md:px-3 py-1 bg-background border-border border-x-[1px] border-t-[1px] rounded-t-sm">
<ClientOnly> <ClientOnly>
<span>{{ currentDate.currentDateTitle }}</span> <span class="text-xs font-medium">{{ currentDate.currentDateTitle }}</span>
<template #fallback> <template #fallback>
<span class="inline-block"> <span class="inline-block">
<UiSkeleton class="h-[19px] w-full rounded-sm" /> <UiSkeleton class="h-[18px] w-full rounded-sm" />
</span> </span>
</template> </template>
</ClientOnly> </ClientOnly>
@@ -157,10 +73,10 @@ function toFutureFar(): void {
<UiButton <UiButton
variant="outline" variant="outline"
size="icon" size="icon"
class="rounded-t-sm rounded-b-none border-b-0" class="size-8 rounded-t-sm rounded-b-none border-b-0"
@click="toPastFar()" @click="toPastFar()"
> >
<PhCaretDoubleLeft size="18" /> <PhCaretDoubleLeft size="14" />
</UiButton> </UiButton>
</UiTooltipTrigger> </UiTooltipTrigger>
<UiTooltipContent> <UiTooltipContent>
@@ -176,10 +92,10 @@ function toFutureFar(): void {
<UiButton <UiButton
variant="outline" variant="outline"
size="icon" size="icon"
class="rounded-t-sm rounded-b-none border-b-0" class="size-8 rounded-t-sm rounded-b-none border-b-0"
@click="toPastNear()" @click="toPastNear()"
> >
<PhCaretLeft size="18" /> <PhCaretLeft size="14" />
</UiButton> </UiButton>
</UiTooltipTrigger> </UiTooltipTrigger>
<UiTooltipContent> <UiTooltipContent>
@@ -195,10 +111,10 @@ function toFutureFar(): void {
<UiButton <UiButton
variant="outline" variant="outline"
size="icon" size="icon"
class="rounded-t-sm rounded-b-none border-b-0" class="size-8 rounded-t-sm rounded-b-none border-b-0"
@click="toFutureNear()" @click="toFutureNear()"
> >
<PhCaretRight size="18" /> <PhCaretRight size="14" />
</UiButton> </UiButton>
</UiTooltipTrigger> </UiTooltipTrigger>
<UiTooltipContent> <UiTooltipContent>
@@ -214,10 +130,10 @@ function toFutureFar(): void {
<UiButton <UiButton
variant="outline" variant="outline"
size="icon" size="icon"
class="rounded-t-sm rounded-b-none border-b-0" class="size-8 rounded-t-sm rounded-b-none border-b-0"
@click="toFutureFar()" @click="toFutureFar()"
> >
<PhCaretDoubleRight size="18" /> <PhCaretDoubleRight size="14" />
</UiButton> </UiButton>
</UiTooltipTrigger> </UiTooltipTrigger>
<UiTooltipContent> <UiTooltipContent>

View File

@@ -195,10 +195,9 @@ function handleEntitySwitch() {
// Key combos to deploy modal // Key combos to deploy modal
const keys = useMagicKeys() const keys = useMagicKeys()
const controlPeriod = computed(() => keys.control_period?.value)
whenever(keys.control_period, () => { whenever(controlPeriod, openUiDialog)
openUiDialog()
})
const searchResultsRef = ref<HTMLElement | null>(null) const searchResultsRef = ref<HTMLElement | null>(null)
const { y: searchResultsY } = useScroll(searchResultsRef) const { y: searchResultsY } = useScroll(searchResultsRef)

View File

@@ -6,6 +6,9 @@ import { storeToRefs } from "pinia"
import { computed, ref, type ComputedRef } from "vue" import { computed, ref, type ComputedRef } from "vue"
import CalendarEventButton from "../../event/Event.vue" import CalendarEventButton from "../../event/Event.vue"
import { useToast } from "@/components/ui/toast"
import type { APIError } from "@@/models/Errors"
import { ToastLifetime } from "@/components/ui/toast/use-toast"
const props = defineProps<{ const props = defineProps<{
date: RPGDate date: RPGDate
@@ -16,11 +19,14 @@ const emit = defineEmits<{
"on-open-create-dialog": [date: RPGDate] "on-open-create-dialog": [date: RPGDate]
}>() }>()
const { toast } = useToast()
const { t } = useI18n()
const calendarTile = ref() const calendarTile = ref()
const calendarEventsList = ref() const calendarEventsList = ref()
const { defaultDate, selectDate, areDatesIdentical, getFormattedDateTitle } = useCalendar() const { defaultDate, selectDate, areDatesIdentical, getFormattedDateTitle, updateEventFromSkeleton, resetSkeleton } = useCalendar()
const { selectedDate, currentEvents, isReadOnly } = storeToRefs(useCalendar()) const { selectedDate, currentEvents, isReadOnly, eventSkeleton, isDraggingEvent } = storeToRefs(useCalendar())
const breakpoints = useBreakpoints( const breakpoints = useBreakpoints(
breakpointsTailwind breakpointsTailwind
@@ -60,9 +66,17 @@ const { top: tileListTop } = useElementBounding(calendarEventsList)
const numberOfEventsToFit: ComputedRef<number> = computed(() => { const numberOfEventsToFit: ComputedRef<number> = computed(() => {
if (!eventsForTheDay.value.length) return 0 if (!eventsForTheDay.value.length) return 0
return Math.trunc((tileHeight.value - (tileListTop.value - tileTop.value)) / 40) let offset = 0
if (isDraggingEvent.value && !tileNotHovered.value && !skeletonInsideCurrentDay.value) {
offset = 1
}
return Math.trunc((tileHeight.value - (tileListTop.value - tileTop.value)) / 40) - offset
}) })
const skeletonInsideCurrentDay = computed(() => eventsForTheDay.value.find((e) => e.id === eventSkeleton.value.id))
/** /**
* Events that can fit in the tile's space * Events that can fit in the tile's space
*/ */
@@ -76,16 +90,54 @@ const eventsToDisplay: ComputedRef<CalendarEvent[]> = computed<CalendarEvent[]>(
* Used if not all events can be seen in the tile's space * Used if not all events can be seen in the tile's space
*/ */
const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsForTheDay.value.length - eventsToDisplay.value.length) const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsForTheDay.value.length - eventsToDisplay.value.length)
// Handle drop events
const { isOutside: tileNotHovered } = useMouseInElement(calendarTile)
const isLoading = ref(false)
async function handleTileDrop() {
if (isLoading.value) return
if (skeletonInsideCurrentDay.value) return
isLoading.value = true
eventSkeleton.value.startDate = props.date
const { error } = await tryCatch(updateEventFromSkeleton())
if (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const apiError = (error as any).data as APIError
apiError.data.errors.forEach((error) => {
toast({
title: t("entity.calendar.event.editErrors.toastTitle"),
variant: "destructive",
description: t(`entity.calendar.event.editErrors.${error.path[1]}_${error.code}`),
duration: ToastLifetime.MEDIUM,
})
})
isLoading.value = false
return
}
isLoading.value = false
resetSkeleton()
}
</script> </script>
<template> <template>
<div <div
ref="calendarTile" ref="calendarTile"
class="tile relative p-1 md:p-2 transition-all" class="tile relative p-1 md:p-2 md:pt-1 transition-all"
: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
}" }"
@drop.prevent="handleTileDrop"
@dragenter.prevent
@dragover.prevent
> >
<button <button
class="relative z-10 group block w-full text-2xs md:text-xs text-center cursor-pointer" class="relative z-10 group block w-full text-2xs md:text-xs text-center cursor-pointer"
@@ -107,13 +159,25 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
<ClientOnly> <ClientOnly>
<ul <ul
ref="calendarEventsList" ref="calendarEventsList"
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="absolute top-8 md:top-10 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
}" }"
> >
<TransitionGroup name="event"> <li
<li v-for="event in eventsToDisplay" :key="event.id" class="grid w-full pointer-events-auto"> v-if="!tileNotHovered && isDraggingEvent && !skeletonInsideCurrentDay"
:key="eventSkeleton.id"
class="opacity-60"
>
<CalendarEventButton :event="eventSkeleton" :tile-date="date" />
</li>
<TransitionGroup :name="!isDraggingEvent ? 'event' : ''">
<li
v-for="(event, i) in eventsToDisplay"
:key="event.id"
class="grid w-full pointer-events-auto"
>
<CalendarEventButton :event :tile-date="date" /> <CalendarEventButton :event :tile-date="date" />
</li> </li>
</TransitionGroup> </TransitionGroup>
@@ -124,7 +188,12 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
<button <button
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" 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' : '' }} <template v-if="eventsNotDisplayed === 1">
{{ $t('entity.calendar.event.oneOtherEvent') }}
</template>
<template v-else>
{{ $t('entity.calendar.event.multipleOtherEvents', { count: eventsNotDisplayed }) }}
</template>
</button> </button>
</UiPopoverTrigger> </UiPopoverTrigger>
<UiPopoverContent <UiPopoverContent
@@ -158,6 +227,7 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
<ClientOnly> <ClientOnly>
<LazyCalendarDialogCreateEvent v-if="!isReadOnly && breakpoints.lg.value" :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 <button
v-else-if="!isReadOnly && !breakpoints.lg.value" v-else-if="!isReadOnly && !breakpoints.lg.value"
class="absolute inset-0 w-full h-full cursor-default z-0" class="absolute inset-0 w-full h-full cursor-default z-0"

View File

@@ -0,0 +1,39 @@
<template>
<header role="banner" class="sticky top-0 border-b-1 border-border py-4 px-2 z-50 backdrop-blur">
<div class="max-w-4xl mx-auto">
<div class="flex items-center justify-between">
<div class="md:flex-1 flex justify-start" />
<nav class="md:flex-1">
<menu class="text-sm flex items-center justify-center gap-4">
<li>
<NuxtLink to="/" class="block p-1 font-medium text-foreground/80 dark:text-foreground/70 underline-offset-4 focus-visible:underline hover:underline dark:hover:text-foreground hover:text-foreground transition-colors" active-class="text-primary dark:text-primary">
{{ $t('breadcrumbs.home') }}
</NuxtLink>
</li>
<li>
<NuxtLink to="/explore" class="block p-1 font-medium text-foreground/80 dark:text-foreground/70 underline-offset-4 focus-visible:underline hover:underline dark:hover:text-foreground hover:text-foreground transition-colors" active-class="text-primary dark:text-primary">
{{ $t('pages.explore.menuLabel') }}
</NuxtLink>
</li>
<li>
<NuxtLink to="/about" class="block p-1 font-medium text-foreground/80 dark:text-foreground/70 underline-offset-4 focus-visible:underline hover:underline dark:hover:text-foreground hover:text-foreground transition-colors" active-class="text-primary dark:text-primary">
{{ $t('pages.about.menuLabel') }}
</NuxtLink>
</li>
</menu>
</nav>
<div class="md:flex-1 flex justify-end">
<UserDashboardLink size="xs" />
</div>
</div>
</div>
</header>
</template>
<style lang="scss" scoped>
header {
background-color: color-mix(in srgb, var(--color-background) 75%, transparent);
}
</style>

View File

@@ -33,6 +33,9 @@ function clearFormatting() {
<div class="flex gap-1.5"> <div class="flex gap-1.5">
<!-- Inline styles --> <!-- Inline styles -->
<div class="flex" v-if="!props.disableInlines"> <div class="flex" v-if="!props.disableInlines">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton <UiButton
@click.prevent="editor.chain().focus().toggleBold().run()" @click.prevent="editor.chain().focus().toggleBold().run()"
:disabled="!editor.can().chain().focus().toggleBold().run()" :disabled="!editor.can().chain().focus().toggleBold().run()"
@@ -42,7 +45,18 @@ function clearFormatting() {
> >
<PhTextB weight="bold" /> <PhTextB weight="bold" />
</UiButton> </UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.bold') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton <UiButton
@click.prevent="editor.chain().focus().toggleItalic().run()" @click.prevent="editor.chain().focus().toggleItalic().run()"
:disabled="!editor.can().chain().focus().toggleItalic().run()" :disabled="!editor.can().chain().focus().toggleItalic().run()"
@@ -52,7 +66,18 @@ function clearFormatting() {
> >
<PhTextItalic weight="bold" /> <PhTextItalic weight="bold" />
</UiButton> </UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.italic') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton <UiButton
@click.prevent="editor.chain().focus().toggleStrike().run()" @click.prevent="editor.chain().focus().toggleStrike().run()"
:disabled="!editor.can().chain().focus().toggleStrike().run()" :disabled="!editor.can().chain().focus().toggleStrike().run()"
@@ -62,10 +87,21 @@ function clearFormatting() {
> >
<PhTextStrikethrough weight="bold" /> <PhTextStrikethrough weight="bold" />
</UiButton> </UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.strike') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div> </div>
<!-- Block styles --> <!-- Block styles -->
<div class="flex" v-if="!props.disableBlocks"> <div class="flex" v-if="!props.disableBlocks">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton <UiButton
@click.prevent="editor.chain().focus().toggleBulletList().run()" @click.prevent="editor.chain().focus().toggleBulletList().run()"
:variant="editor.isActive('bulletList') ? 'default' : 'secondary'" :variant="editor.isActive('bulletList') ? 'default' : 'secondary'"
@@ -74,7 +110,18 @@ function clearFormatting() {
> >
<PhListBullets weight="bold" /> <PhListBullets weight="bold" />
</UiButton> </UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.ulist') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton <UiButton
@click.prevent="editor.chain().focus().toggleOrderedList().run()" @click.prevent="editor.chain().focus().toggleOrderedList().run()"
:variant="editor.isActive('orderedList') ? 'default' : 'secondary'" :variant="editor.isActive('orderedList') ? 'default' : 'secondary'"
@@ -83,7 +130,18 @@ function clearFormatting() {
> >
<PhListNumbers weight="bold" /> <PhListNumbers weight="bold" />
</UiButton> </UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.olist') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton <UiButton
@click.prevent="editor.chain().focus().toggleBlockquote().run()" @click.prevent="editor.chain().focus().toggleBlockquote().run()"
:variant="editor.isActive('blockquote') ? 'default' : 'secondary'" :variant="editor.isActive('blockquote') ? 'default' : 'secondary'"
@@ -92,9 +150,20 @@ function clearFormatting() {
> >
<PhQuotes weight="fill" /> <PhQuotes weight="fill" />
</UiButton> </UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.quote') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div> </div>
<div class="flex"> <div class="flex">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton <UiButton
@click.prevent="clearFormatting" @click.prevent="clearFormatting"
variant="secondary" variant="secondary"
@@ -103,7 +172,18 @@ function clearFormatting() {
> >
<PhTextTSlash weight="bold" /> <PhTextTSlash weight="bold" />
</UiButton> </UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.noFormat') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton <UiButton
@click.prevent="editor.chain().focus().undo().run()" @click.prevent="editor.chain().focus().undo().run()"
:disabled="!editor.can().chain().focus().undo().run()" :disabled="!editor.can().chain().focus().undo().run()"
@@ -113,7 +193,18 @@ function clearFormatting() {
> >
<PhArrowUUpLeft weight="bold" /> <PhArrowUUpLeft weight="bold" />
</UiButton> </UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.undo') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton <UiButton
@click.prevent="editor.chain().focus().redo().run()" @click.prevent="editor.chain().focus().redo().run()"
:disabled="!editor.can().chain().focus().redo().run()" :disabled="!editor.can().chain().focus().redo().run()"
@@ -123,6 +214,14 @@ function clearFormatting() {
> >
<PhArrowUDownRight weight="bold" /> <PhArrowUDownRight weight="bold" />
</UiButton> </UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.redo') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import { PhInfo } from '@phosphor-icons/vue';
</script>
<template>
<div class="flex justify-between items-center gap-2">
<div class="grow">
<UserCTA />
</div>
<UiButton size="icon" class="size-8 hover:bg-secondary hover:text-secondary-foreground" variant="ghost" as-child>
<NuxtLink to="/about">
<PhInfo size="18" />
</NuxtLink>
</UiButton>
</div>
</template>

View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import { PanelsLeftBottom } from 'lucide-vue-next';
</script>
<template>
<menu class="min-w-32 pt-16 flex flex-col gap-0.5">
<li class="w-full">
<UiButton
variant="ghost"
size="sm"
class="h-8 gap-1.5 hover:bg-secondary w-full justify-start"
as-child
>
<NuxtLink to="/my" active-class="active-link" exact-active-class="exact-link">
<PanelsLeftBottom :size="18" />
<span class="text-[.9em]">
{{ $t('ui.sidebarMenu.projects') }}
</span>
</NuxtLink>
</UiButton>
</li>
</menu>
</template>
<style lang="scss" scoped>
.active-link {
color: var(--color-secondary-foreground);
background-color: var(--color-secondary);
&:hover {
background-color: color-mix(in srgb, var(--color-primary) 33%, transparent);
}
}
.exact-link {
color: var(--color-primary-foreground);
background-color: color-mix(in srgb, var(--color-primary) 66%, transparent);
cursor: initial;
&:hover {
background-color: color-mix(in srgb, var(--color-primary) 66%, transparent);
}
}
.dark .exact-link {
background-color: var(--color-primary);
}
</style>

View File

@@ -1,136 +1,32 @@
<script lang="ts" setup> <script lang="ts" setup>
import { PhCompass, PhGlobeHemisphereEast, PhHurricane, PhInfo, PhX } from "@phosphor-icons/vue" import { PanelsLeftBottom } from "lucide-vue-next"
import type { SidebarMenuActionType, SidebarMenuIcon } from "./SidebarProps";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { breakpointsTailwind } from "@vueuse/core" import { PhInfo } from "@phosphor-icons/vue";
const { revealAdvancedSearch } = useCalendar() const { isSidebarOpened } = storeToRefs(useUiStore())
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> </script>
<template> <template>
<nav <nav
ref="sidebarRef" ref="sidebarRef"
:class="cn( :class="cn(
['md:relative md:isolate w-16 py-6 grid gap-4 grid-rows-[1fr_auto] justify-center md:transition-colors'], // Base appearance ['max-md:hidden w-fit relative isolate p-2 grid gap-4 grid-rows-[1fr_auto] bg-background justify-center transition-colors'], // Base appearance
['after:opacity-50 after:contrast-125 dark:after:opacity-75'], // After styling ['after:opacity-50 after:contrast-125 dark:after:opacity-75'], // After styling
['border-r-[1px] border-r-border dark:border-r-border'], // Colours ['border-r-[1px] border-r-border dark:border-r-border'], // 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': isSidebarOpened
}
)" )"
> >
<menu class="flex flex-col gap-4 max-md:items-center"> <SidebarMenu />
<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"> <SidebarFooter />
<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
>
<NuxtLink to="/explore">
<PhCompass size="24" weight="fill" />
</NuxtLink>
</UiButton>
<NuxtLink
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>
</NuxtLink>
</UiTooltipTrigger>
<UiTooltipContent :side="'right'" :side-offset="6">
<p>
{{ $t('pages.explore.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
>
<NuxtLink :to="item.to">
<component :is="computeMenuItemIcon(item.phIcon)" size="24" :weight="item.phIconWeight || 'fill'" />
</NuxtLink>
</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> </nav>
<UiSheet v-model:open="isSidebarOpened">
<UiSheetContent side="left" class="p-2 grid gap-4 grid-rows-[auto_1fr]">
<UiSheetHeader />
<SidebarMenu />
<SidebarFooter />
</UiSheetContent>
</UiSheet>
</template> </template>

View File

@@ -8,8 +8,9 @@ const { toggleSidebar } = useUiStore()
<UiButton <UiButton
size="icon" size="icon"
variant="outline" variant="outline"
class="size-7 border-border hover:bg-secondary hover:text-secondary-foreground"
@click="toggleSidebar" @click="toggleSidebar"
> >
<PhList size="19" weight="light" /> <PhList size="14" weight="light" />
</UiButton> </UiButton>
</template> </template>

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
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, PhInfo } from "@phosphor-icons/vue"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const router = useRouter() const router = useRouter()
@@ -44,26 +44,26 @@ async function handleLogout() {
console.log(error.message) console.log(error.message)
} }
} }
type AvailableRoutes = "/my" | "/my/settings"
function pushRoute(to: AvailableRoutes) {
router.push({ path: to })
closeMenu()
}
</script> </script>
<template> <template>
<ClientOnly> <ClientOnly>
<UiDropdownMenu v-model:open="menuOpened"> <UiDropdownMenu v-model:open="menuOpened">
<UiDropdownMenuTrigger class="aspect-square"> <UiDropdownMenuTrigger as-child>
<UiButton
variant="ghost"
class="w-full justify-start p-2 h-fit hover:text-secondary-foreground hover:bg-secondary"
:class="cn({ 'bg-secondary': menuOpened })"
>
<TransitionGroup name="fade-group" appear> <TransitionGroup name="fade-group" appear>
<UiAvatar <div
v-if="user" v-if="user"
class="flex gap-2 items-center"
>
<UiAvatar
id="user-avatar" id="user-avatar"
class="ring-[.2rem] ring-primary hover:bg-accent hover:ring-accent transition-all cursor-pointer" size="sm"
:class="cn({ 'ring-accent bg-accent dark:bg-accent': menuOpened })" class="rounded-sm ring-[.2rem] ring-primary transition-all cursor-pointer"
> >
<UiAvatarImage <UiAvatarImage
:src="userMeta?.avatar_url" :src="userMeta?.avatar_url"
@@ -74,16 +74,13 @@ function pushRoute(to: AvailableRoutes) {
{{ $t('ui.sidebarMenu.avatarFallback') }} {{ $t('ui.sidebarMenu.avatarFallback') }}
</UiAvatarFallback> </UiAvatarFallback>
</UiAvatar> </UiAvatar>
<UiButton
v-else <div class="text-xs">
variant="outline" {{ userMeta?.name }}
size="icon" </div>
class="ring-[.2rem] ring-primary hover:bg-accent hover:ring-accent border-none dark:bg-background rounded-full bg-primary transition-all cursor-pointer" </div>
:class="cn({ 'ring-accent bg-accent dark:bg-accent': menuOpened })"
>
<PhUserCircle size="24" />
</UiButton>
</TransitionGroup> </TransitionGroup>
</UiButton>
</UiDropdownMenuTrigger> </UiDropdownMenuTrigger>
<UiDropdownMenuContent class="w-72 p-0 pb-1" :align="'start'" :side="'top'" :side-offset="10" :align-offset="25" :collision-padding="40"> <UiDropdownMenuContent class="w-72 p-0 pb-1" :align="'start'" :side="'top'" :side-offset="10" :align-offset="25" :collision-padding="40">
@@ -91,15 +88,6 @@ function pushRoute(to: AvailableRoutes) {
<p class="p-2 text-[.7em] opacity-75"> <p class="p-2 text-[.7em] opacity-75">
{{ $t('ui.greeting', { user: user?.email }) }} {{ $t('ui.greeting', { user: user?.email }) }}
</p> </p>
<UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="pushRoute('/my')">
<PhUser size="20" weight="fill" />
<span>
{{ $t('ui.sidebarMenu.profile') }}
</span>
</UiDropdownMenuItem>
<UiDropdownMenuSeparator />
</template> </template>
<template v-else> <template v-else>

View File

@@ -0,0 +1,43 @@
<script lang="ts" setup>
import type { PrimitiveProps } from 'radix-vue'
import type { HTMLAttributes } from 'vue'
import type { ButtonVariants } from '@/components/ui/button'
const { auth } = useSupabaseClient()
const user = useSupabaseUser()
const profileUrl: string = `${useRequestURL().origin}/my/`
interface Props extends PrimitiveProps {
size?: ButtonVariants["size"]
}
defineProps<Props>()
async function handleGoogleLogin() {
const { error } = await auth.signInWithOAuth({
provider: "google",
options: {
queryParams: {
access_type: "offline",
prompt: "consent"
},
redirectTo: profileUrl
}
})
if (error) {
console.log(error.message)
}
}
</script>
<template>
<UiButton v-if="user" :size as-child>
<NuxtLink to="/my">
Dashboard
</NuxtLink>
</UiButton>
<UiButton v-else @click="handleGoogleLogin" :size>
Log in
</UiButton>
</template>

View File

@@ -9,9 +9,9 @@ export const avatarVariant = cva(
{ {
variants: { variants: {
size: { size: {
sm: "h-10 w-10 text-xs", sm: "size-6 text-xs",
base: "h-16 w-16 text-2xl", base: "size-14 text-2xl",
lg: "h-32 w-32 text-5xl", lg: "size-24 text-5xl",
}, },
shape: { shape: {
circle: "rounded-full", circle: "rounded-full",

View File

@@ -26,7 +26,7 @@ const props = withDefaults(defineProps<Props>(), {
<span <span
v-if="props.searchSlash" v-if="props.searchSlash"
class="h-4 p-1 ml-1 grid place-items-center text-[0.7em] leading-none font-semibold text-slate-500 bg-slate-100 border-slate-300 border-[1px] rounded-[3px] shadow-xs group-hover:text-slate-600 group-hover:bg-slate-200 group-hover:border-slate-400 transition-colors" class="h-4 p-1 ml-1 grid place-items-center text-[0.75em] leading-none font-semibold text-slate-500 bg-slate-100 border-slate-300 border-[1px] rounded-[3px] shadow-xs group-hover:text-slate-600 group-hover:bg-slate-200 group-hover:border-slate-400 transition-colors"
> >
<kbd class="-mt-[2px]">CTRL + :</kbd> <kbd class="-mt-[2px]">CTRL + :</kbd>
</span> </span>

View File

@@ -10,7 +10,7 @@ export const buttonVariants = cva(
default: "bg-primary text-primary-foreground hover:bg-primary/70", default: "bg-primary text-primary-foreground hover:bg-primary/70",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input hover:bg-primary hover:text-primary-foreground", outline: "border border-input hover:bg-primary hover:text-primary-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-primary/20", secondary: "bg-secondary text-secondary-foreground hover:bg-primary/40",
ghost: "hover:bg-primary/70 hover:text-primary-foreground", ghost: "hover:bg-primary/70 hover:text-primary-foreground",
link: "text-primary underline-offset-4 hover:underline" link: "text-primary underline-offset-4 hover:underline"
}, },

View File

@@ -20,7 +20,7 @@ const forwardedProps = useForwardProps(delegatedProps)
v-bind="forwardedProps" v-bind="forwardedProps"
:class=" :class="
cn( cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background hover:bg-secondary hover:text-primary-foreground px-3 py-2 text-sm ring-offset-background transition-colors placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50', 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background hover:bg-secondary hover:text-primary-foreground dark:hover:text-primary px-3 py-2 text-sm ring-offset-background transition-colors placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50',
props.class props.class
) )
" "

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DialogRoot
data-slot="sheet"
v-bind="forwarded"
>
<slot />
</DialogRoot>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DialogCloseProps } from "reka-ui"
import { DialogClose } from "reka-ui"
const props = defineProps<DialogCloseProps>()
</script>
<template>
<DialogClose
data-slot="sheet-close"
v-bind="props"
>
<slot />
</DialogClose>
</template>

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
DialogContent,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
import SheetOverlay from "./SheetOverlay.vue"
interface SheetContentProps extends DialogContentProps {
class?: HTMLAttributes["class"]
side?: "top" | "right" | "bottom" | "left"
}
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<SheetContentProps>(), {
side: "right",
})
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, "class", "side")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<SheetOverlay />
<DialogContent
data-slot="sheet-content"
:class="cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
side === 'right'
&& 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full border-l max-w-sm',
side === 'left'
&& 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-full border-r max-w-sm',
side === 'top'
&& 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
side === 'bottom'
&& 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
props.class)"
v-bind="{ ...forwarded, ...$attrs }"
>
<slot />
</DialogContent>
</DialogPortal>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogDescriptionProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogDescription } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogDescription
data-slot="sheet-description"
:class="cn('text-muted-foreground text-sm', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogDescription>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
data-slot="sheet-footer"
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)
"
>
<slot />
</div>
</template>

View File

@@ -0,0 +1,33 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import {
DialogClose
} from "reka-ui"
import { PhX } from "@phosphor-icons/vue";
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
data-slot="sheet-header"
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
>
<slot />
<DialogClose
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 disabled:pointer-events-none"
as-child
>
<UiButton
size="icon"
variant="outline"
class="size-7 border-border bg-background hover:bg-secondary hover:text-secondary-foreground"
>
<PhX size="14" />
<span class="sr-only">{{ $t('ui.actions.close') }}</span>
</UiButton>
</DialogClose>
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogOverlayProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogOverlay } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogOverlay
data-slot="sheet-overlay"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/70', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogOverlay>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import type { DialogTitleProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogTitle } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<DialogTitle
data-slot="sheet-title"
:class="cn('text-foreground font-semibold', props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogTitle>
</template>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DialogTriggerProps } from "reka-ui"
import { DialogTrigger } from "reka-ui"
const props = defineProps<DialogTriggerProps>()
</script>
<template>
<DialogTrigger
data-slot="sheet-trigger"
v-bind="props"
>
<slot />
</DialogTrigger>
</template>

View File

@@ -0,0 +1,8 @@
export { default as Sheet } from "./Sheet.vue"
export { default as SheetClose } from "./SheetClose.vue"
export { default as SheetContent } from "./SheetContent.vue"
export { default as SheetDescription } from "./SheetDescription.vue"
export { default as SheetFooter } from "./SheetFooter.vue"
export { default as SheetHeader } from "./SheetHeader.vue"
export { default as SheetTitle } from "./SheetTitle.vue"
export { default as SheetTrigger } from "./SheetTrigger.vue"

View File

@@ -11,6 +11,9 @@ onMounted(() => {
worldSkeleton.value = { ...defaultWorld } worldSkeleton.value = { ...defaultWorld }
}) })
const newWorldName = shallowRef<HTMLInputElement>()
useFocus(newWorldName, { initialValue: true })
const isLoading = ref<boolean>(false) const isLoading = ref<boolean>(false)
/** /**
@@ -61,6 +64,7 @@ function handleFormCancel() {
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit"> <form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
<div class="grid gap-4"> <div class="grid gap-4">
<input <input
ref="newWorldName"
id="new-world-name" id="new-world-name"
v-model="worldSkeleton.name" v-model="worldSkeleton.name"
type="text" type="text"

View File

@@ -19,6 +19,9 @@ onMounted(() => {
worldSkeleton.value = { ...props.world } as World worldSkeleton.value = { ...props.world } as World
}) })
const newWorldName = shallowRef<HTMLInputElement>()
useFocus(newWorldName, { initialValue: true })
const isLoading = ref<boolean>(false) const isLoading = ref<boolean>(false)
/** /**
@@ -75,6 +78,7 @@ function handleFormCancel() {
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit"> <form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
<div class="grid gap-4"> <div class="grid gap-4">
<input <input
ref="newWorldName"
id="new-world-name" id="new-world-name"
v-model="worldSkeleton.name" v-model="worldSkeleton.name"
type="text" type="text"

View File

@@ -60,11 +60,7 @@ switch (statusCode) {
<template> <template>
<div class="h-screen"> <div class="h-screen">
<div class="h-full grid grid-cols-[auto_1fr] transition-colors"> <div class="h-full w-full grid place-items-center transition-colors">
<Sidebar />
<div class="wrapper shadow-body-light dark:shadow-body-dark transition-all">
<div class="h-full w-full grid place-items-center">
<Head> <Head>
<Title>{{ $t(titleKey) }}</Title> <Title>{{ $t(titleKey) }}</Title>
</Head> </Head>
@@ -93,8 +89,6 @@ switch (statusCode) {
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,16 +1,6 @@
<script lang="ts" setup>
import { cn } from '~/lib/utils';
const { isSidebarOpened } = storeToRefs(useUiStore())
</script>
<template> <template>
<div <div
class="h-full grid md:grid-cols-[auto_1fr] bg-background transition-colors after:absolute after:transition-colors" class="h-full grid md:grid-cols-[auto_1fr] bg-background 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 />

View File

@@ -1,7 +1,36 @@
<template> <template>
<div <div
class="wrapper max-h-screen h-full transition-all overflow-y-auto" class="wrapper max-h-screen h-full transition-all overflow-y-auto grid grid-rows-[auto_1fr]"
> >
<Navbar />
<div class="inner-content">
<slot /> <slot />
</div> </div>
</div>
</template> </template>
<style lang="scss" scoped>
.inner-content {
position: relative;
&::before {
position: absolute;
inset-inline: 0;
top: 0;
display: block;
content: '';
height: .1rem;
max-width: 100%;
background: linear-gradient(
90deg,
transparent 0%,
transparent 10%,
color-mix(in srgb, var(--color-primary) 50%, transparent) 25%,
color-mix(in srgb, var(--color-primary) 50%, transparent) 75%,
transparent 90%,
transparent 100%
);
}
}
</style>

View File

@@ -1,4 +1,5 @@
import { type ClassValue, clsx } from "clsx" import type { ClassValue } from "clsx"
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {

View File

@@ -20,20 +20,14 @@ watch(locale, () => refresh())
</Head> </Head>
<main class="overflow-y-auto py-8 px-5 md:px-8 grid gap-4 grid-rows-[auto_1fr]"> <main class="overflow-y-auto py-8 px-5 md:px-8 grid gap-4 grid-rows-[auto_1fr]">
<div class="flex items-center gap-3">
<div class="md:hidden">
<SidebarToggle />
</div>
<Heading level="h1"> <Heading level="h1">
{{ $t("pages.about.title") }} {{ $t("pages.about.title") }}
</Heading> </Heading>
</div>
<div v-if="status === 'pending'" class="grid place-items-center"> <div v-if="status === 'pending'" class="grid place-items-center">
<PhCircleNotch size="50" class="opacity-33 animate-spin"/> <PhCircleNotch size="50" class="opacity-33 animate-spin"/>
</div> </div>
<LazyContentRenderer v-else-if="status === 'success' && page" :value="page" class="content max-w-5xl" /> <LazyContentRenderer v-else-if="status === 'success' && page" :value="page" class="content max-w-4xl" />
</main> </main>
</template> </template>

View File

@@ -18,15 +18,9 @@ const { data: calendars, status } = useLazyAsyncData<{ data: Calendar[] }>("expl
</Head> </Head>
<Spacing size="lg"> <Spacing size="lg">
<div class="flex items-center gap-3">
<div class="md:hidden">
<SidebarToggle />
</div>
<Heading level="h1"> <Heading level="h1">
{{ $t("pages.explore.title") }} {{ $t("pages.explore.title") }}
</Heading> </Heading>
</div>
<Spacing size="lg"> <Spacing size="lg">
<Heading level="h2"> <Heading level="h2">

View File

@@ -6,27 +6,6 @@ useHead({
definePageMeta({ definePageMeta({
layout: "public" layout: "public"
}) })
const { auth } = useSupabaseClient()
const user = useSupabaseUser()
const profileUrl: string = `${useRequestURL().origin}/my/`
async function handleGoogleLogin() {
const { error } = await auth.signInWithOAuth({
provider: "google",
options: {
queryParams: {
access_type: "offline",
prompt: "consent"
},
redirectTo: profileUrl
}
})
if (error) {
console.log(error.message)
}
}
</script> </script>
<template> <template>
@@ -39,20 +18,15 @@ async function handleGoogleLogin() {
</Head> </Head>
<div class="py-8 md:py-24 px-4"> <div class="py-8 md:py-24 px-4">
<div class="flow text-center max-w-2xl mx-auto"> <div class="flow text-center max-w-4xl mx-auto">
<h1 class="text-3xl md:text-5xl font-bold md:font-semibold">A home for your creativity</h1> <h1 class="text-3xl md:text-5xl font-bold md:font-semibold">
<p class="text-sm">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ipsam eos rem accusantium voluptates? Inventore dolorum reprehenderit quibusdam consequatur, totam iusto velit neque dolor dicta possimus cum, deleniti, saepe expedita culpa!</p> {{ $t('pages.home.h1') }}
</h1>
<p class="text-sm">
{{ $t('pages.home.tagline') }}
</p>
<TransitionGroup name="fade-group" appear> <UserDashboardLink />
<UiButton v-if="user" as-child>
<NuxtLink to="/my">
Dashboard
</NuxtLink>
</UiButton>
<UiButton v-else @click="handleGoogleLogin">
Log in
</UiButton>
</TransitionGroup>
</div> </div>
</div> </div>
</main> </main>

3
app/pages/my.vue Normal file
View File

@@ -0,0 +1,3 @@
<template>
<NuxtPage />
</template>

View File

@@ -10,6 +10,8 @@ import type { CalendarEvent } from "@@/models/CalendarEvent"
import type { CalendarMonth } from "@@/models/CalendarMonth" import type { CalendarMonth } from "@@/models/CalendarMonth"
import type { Category } from "@@/models/Category" import type { Category } from "@@/models/Category"
type CalendarState = "active" | "idle"
type CalendarViewType = "month" | "year" | "decade" | "century" type CalendarViewType = "month" | "year" | "decade" | "century"
type CalendarCurrentConfig = { type CalendarCurrentConfig = {
@@ -130,7 +132,7 @@ export const useCalendar = defineStore("calendar", () => {
return Number(params.month) return Number(params.month)
}) })
const currentMonthData = computed<CalendarMonth>(() => { const currentMonthData = computed<CalendarMonth>(() => {
return sortedMonths.value[currentMonth.value] return sortedMonths.value[currentMonth.value]!
}) })
// Gets the label from currentMonth index // Gets the label from currentMonth index
const currentMonthName = computed<string>(() => getMonthName(currentMonth.value)) const currentMonthName = computed<string>(() => getMonthName(currentMonth.value))
@@ -605,7 +607,7 @@ export const useCalendar = defineStore("calendar", () => {
// Else, we need to accelerate and decelerate // Else, we need to accelerate and decelerate
else { else {
// First, get all the remaining days in the current month // First, get all the remaining days in the current month
const currentMonth = sortedMonths.value[datePivot.month] const currentMonth = sortedMonths.value[datePivot.month]!
dateAcc.day = currentMonth.days - datePivot.day dateAcc.day = currentMonth.days - datePivot.day
if (direction === "future") { if (direction === "future") {
datePivot.month = getNextViewMonth(datePivot.month) datePivot.month = getNextViewMonth(datePivot.month)
@@ -668,6 +670,90 @@ export const useCalendar = defineStore("calendar", () => {
currentEvents.value = computeCurrentEvents() currentEvents.value = computeCurrentEvents()
}, { deep: true, immediate: true }) }, { deep: true, immediate: true })
function toPastFar(): void {
switch (currentConfig.value.viewType) {
case "month":
decrementViewYear()
break
case "year":
decrementViewYear(10)
break
case "decade":
decrementViewYear(100)
break
case "century":
default:
decrementViewYear(1000)
break
}
}
function toPastNear(): void {
switch (currentConfig.value.viewType) {
case "month":
decrementViewMonth()
break
case "year":
decrementViewYear()
break
case "decade":
decrementViewYear(10)
break
case "century":
default:
decrementViewYear(100)
break
}
}
function toFutureNear(): void {
switch (currentConfig.value.viewType) {
case "month":
incrementViewMonth()
break
case "year":
incrementViewYear()
break
case "decade":
incrementViewYear(10)
break
case "century":
default:
incrementViewYear(100)
break
}
}
function toFutureFar(): void {
switch (currentConfig.value.viewType) {
case "month":
incrementViewYear()
break
case "year":
incrementViewYear(10)
break
case "decade":
incrementViewYear(100)
break
case "century":
default:
incrementViewYear(1000)
break
}
}
/** /**
* Determines if the event can appear in the front end * Determines if the event can appear in the front end
* *
@@ -756,7 +842,7 @@ export const useCalendar = defineStore("calendar", () => {
// Loop over all event once to convert the structure to a usable one // Loop over all event once to convert the structure to a usable one
for (let i = 0; i < allEvents.value.length; i++) { for (let i = 0; i < allEvents.value.length; i++) {
const e: CalendarEvent = allEvents.value[i] const e: CalendarEvent = allEvents.value[i]!
// Estimate distance from pivot // Estimate distance from pivot
const startDateDays: number = convertDateToDays(e.startDate) const startDateDays: number = convertDateToDays(e.startDate)
@@ -805,6 +891,22 @@ export const useCalendar = defineStore("calendar", () => {
} }
} }
/**
* State for event modal creation
*/
const isCreatingEventModalOpen = ref<boolean>(false)
// Watch the popover state
watch(isCreatingEventModalOpen, (hasOpened, _o) => {
if (hasOpened) {
resetSkeleton()
}
})
function toggleCreatingEventModal() {
isCreatingEventModalOpen.value = !isCreatingEventModalOpen.value
}
/** /**
* State for event modal edition * State for event modal edition
*/ */
@@ -830,6 +932,7 @@ export const useCalendar = defineStore("calendar", () => {
const isCreatingEvent = ref<boolean>(false) const isCreatingEvent = ref<boolean>(false)
const isUpdatingEvent = ref<boolean>(false) const isUpdatingEvent = ref<boolean>(false)
const isDeletingEvent = ref<boolean>(false) const isDeletingEvent = ref<boolean>(false)
const isDraggingEvent = ref<boolean>(false)
const operationInProgress = computed<boolean>(() => isCreatingEvent.value || isUpdatingEvent.value || isDeletingEvent.value) const operationInProgress = computed<boolean>(() => isCreatingEvent.value || isUpdatingEvent.value || isDeletingEvent.value)
let abortController: AbortController | null = null let abortController: AbortController | null = null
@@ -935,6 +1038,19 @@ export const useCalendar = defineStore("calendar", () => {
isCategoriesModalOpen.value = state isCategoriesModalOpen.value = state
} }
/**
* Handle the state of the active calendar for UI interactions
*/
const calendarState = ref<CalendarState>("active")
watch([isCategoriesModalOpen, isCreatingEventModalOpen, isDeleteEventModalOpen, isEditEventModalOpen, isAdvancedSearchOpen, isDraggingEvent], (values) => {
if (values.every(v => !v)) {
calendarState.value = 'active'
} else {
calendarState.value = 'idle'
}
})
return { return {
isReadOnly, isReadOnly,
setReadStatus, setReadStatus,
@@ -974,16 +1090,22 @@ export const useCalendar = defineStore("calendar", () => {
areDatesIdentical, areDatesIdentical,
compareDates, compareDates,
getRelativeString, getRelativeString,
toPastFar,
toPastNear,
toFutureNear,
toFutureFar,
baseEvents, baseEvents,
allEvents, allEvents,
categories, categories,
currentEvents, currentEvents,
calendarState,
getRelativeEventFromDate, getRelativeEventFromDate,
getRelativeEventFromEvent, getRelativeEventFromEvent,
cancelLatestRequest, cancelLatestRequest,
isCreatingEvent, isCreatingEvent,
isUpdatingEvent, isUpdatingEvent,
isDeletingEvent, isDeletingEvent,
isDraggingEvent,
operationInProgress, operationInProgress,
eventSkeleton, eventSkeleton,
resetSkeleton, resetSkeleton,
@@ -991,6 +1113,8 @@ export const useCalendar = defineStore("calendar", () => {
lastActiveEvent, lastActiveEvent,
updateEventFromSkeleton, updateEventFromSkeleton,
deleteEventFromSkeleton, deleteEventFromSkeleton,
isCreatingEventModalOpen,
toggleCreatingEventModal,
isEditEventModalOpen, isEditEventModalOpen,
revealEditEventModal, revealEditEventModal,
isDeleteEventModalOpen, isDeleteEventModalOpen,

View File

@@ -1,17 +1,20 @@
{ {
"$schema": "https://shadcn-vue.com/schema.json", "$schema": "https://shadcn-vue.com/schema.json",
"style": "default", "style": "new-york",
"typescript": true, "typescript": true,
"tsConfigPath": ".nuxt/tsconfig.json",
"tailwind": { "tailwind": {
"config": "tailwind.config.js", "config": "tailwind.config.js",
"css": "assets/main.css", "css": "app/assets/main.css",
"baseColor": "slate", "baseColor": "slate",
"cssVariables": true "cssVariables": true,
"prefix": ""
}, },
"framework": "nuxt",
"aliases": { "aliases": {
"components": "@/components", "components": "@/components",
"utils": "@/lib/utils" "composables": "@/composables",
} "utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib"
},
"iconLibrary": "lucide"
} }

View File

@@ -32,6 +32,7 @@ export default defineI18nConfig(() => ({
save: "Save", save: "Save",
delete: "Delete", delete: "Delete",
edit: "Edit", edit: "Edit",
close: "Close"
}, },
colors: { colors: {
label: "Color", label: "Color",
@@ -69,12 +70,14 @@ export default defineI18nConfig(() => ({
backToHome: "Back to home", backToHome: "Back to home",
sidebarMenu: { sidebarMenu: {
profile: "Profile", profile: "Profile",
projects: "Projects",
appearance: "Appearance", appearance: "Appearance",
language: "Language", language: "Language",
account: "Account", account: "Account",
login: "Log in", login: "Log in",
logout: "Log out", logout: "Log out",
avatarFallback: "Profile", avatarFallback: "Profile",
about: "About",
}, },
dark: "Dark", dark: "Dark",
light: "Light", light: "Light",
@@ -86,6 +89,17 @@ export default defineI18nConfig(() => ({
createdAt: "Created {createdAt}", createdAt: "Created {createdAt}",
updatedAt: "Last updated {updatedAt}", updatedAt: "Last updated {updatedAt}",
}, },
editor: {
bold: "Bold",
italic: "Italic",
strike: "Strikethrough",
ulist: "Bullet list",
olist: "Numbered list",
quote: "Quote",
noFormat: "Remove formatting",
undo: "Undo",
redo: "Redo"
},
entity: { entity: {
category: { category: {
nameSingular: "Category", nameSingular: "Category",
@@ -127,7 +141,7 @@ export default defineI18nConfig(() => ({
world: { world: {
nameSingular: "World", nameSingular: "World",
namePlural: "Worlds", namePlural: "Worlds",
backToMy: "Back to my universe", backToMy: "Back to projects",
backToSingle: "Back to {world}", backToSingle: "Back to {world}",
addSingle: "Add a world", addSingle: "Add a world",
addSingleFirst: "Add your first world !", addSingleFirst: "Add your first world !",
@@ -158,7 +172,7 @@ export default defineI18nConfig(() => ({
nameSingular: "Calendar", nameSingular: "Calendar",
namePlural: "Calendars", namePlural: "Calendars",
namePublicSingular: "Public Calendar", namePublicSingular: "Public Calendar",
namePublicPlural: "Public Calendars", namePublicPlural: "Calendars",
addSingle: "Add a calendar", addSingle: "Add a calendar",
addSingleFirst: "Add your first calendar !", addSingleFirst: "Add your first calendar !",
notFound: "Calendar not found", notFound: "Calendar not found",
@@ -213,6 +227,8 @@ export default defineI18nConfig(() => ({
hiddenLabel: "Hide this event", hiddenLabel: "Hide this event",
hiddenTooltip: "This event is visible only to game masters.", hiddenTooltip: "This event is visible only to game masters.",
addLocation: "Add a place", addLocation: "Add a place",
oneOtherEvent: "1 other",
multipleOtherEvents: "{count} others",
prevPage: "Previous page with events", prevPage: "Previous page with events",
nextPage: "Next page with events", nextPage: "Next page with events",
outOfBoundsTitle: "No next or previous events were found", outOfBoundsTitle: "No next or previous events were found",
@@ -316,13 +332,17 @@ export default defineI18nConfig(() => ({
} }
}, },
pages: { pages: {
home: {
h1: "A home for your creativity",
tagline: "Visualize your fantasy or sci-fi worlds with our interactive calendars"
},
explore: { explore: {
menuLabel: "Explore", menuLabel: "Explore",
title: "Explore worlds", title: "Explore worlds",
}, },
profile: { profile: {
title: "{user} — My universe", title: "{user} — Projects",
metaTitle: "My universe", metaTitle: "Projects",
}, },
about: { about: {
title: "About this app", title: "About this app",
@@ -333,7 +353,7 @@ export default defineI18nConfig(() => ({
breadcrumbs: { breadcrumbs: {
home: "Home", home: "Home",
explore: "Explore", explore: "Explore",
profile: "Universe", profile: "Projects",
world: "World", world: "World",
calendar: "Calendar" calendar: "Calendar"
} }
@@ -367,6 +387,7 @@ export default defineI18nConfig(() => ({
save: "Sauvegarder", save: "Sauvegarder",
delete: "Supprimer", delete: "Supprimer",
edit: "Modifier", edit: "Modifier",
close: "Fermer"
}, },
colors: { colors: {
label: "Couleur", label: "Couleur",
@@ -404,12 +425,14 @@ export default defineI18nConfig(() => ({
backToHome: "Retourner à l'accueil", backToHome: "Retourner à l'accueil",
sidebarMenu: { sidebarMenu: {
profile: "Profil", profile: "Profil",
projects: "Projets",
appearance: "Apparence", appearance: "Apparence",
language: "Langue", language: "Langue",
account: "Compte", account: "Compte",
login: "Connexion", login: "Connexion",
logout: "Déconnexion", logout: "Déconnexion",
avatarFallback: "Profil", avatarFallback: "Profil",
about: "À propos",
}, },
dark: "Sombre", dark: "Sombre",
light: "Clair", light: "Clair",
@@ -421,6 +444,17 @@ export default defineI18nConfig(() => ({
createdAt: "Créé le {createdAt}", createdAt: "Créé le {createdAt}",
updatedAt: "Dernière modification le {updatedAt}", updatedAt: "Dernière modification le {updatedAt}",
}, },
editor: {
bold: "Gras",
italic: "Italique",
strike: "Barré",
ulist: "Liste à puces",
olist: "Liste numerotée",
quote: "Citation",
noFormat: "Effacer le formattage",
undo: "Annuler",
redo: "Rétablir"
},
entity: { entity: {
category: { category: {
nameSingular: "Catégorie", nameSingular: "Catégorie",
@@ -462,7 +496,7 @@ export default defineI18nConfig(() => ({
world: { world: {
nameSingular: "Monde", nameSingular: "Monde",
namePlural: "Mondes", namePlural: "Mondes",
backToMy: "Retourner à mon univers", backToMy: "Retourner à Projets",
backToSingle: "Retourner sur {world}", backToSingle: "Retourner sur {world}",
addSingle: "Ajouter un monde", addSingle: "Ajouter un monde",
addSingleFirst: "Ajouter votre premier monde !", addSingleFirst: "Ajouter votre premier monde !",
@@ -548,6 +582,8 @@ export default defineI18nConfig(() => ({
hiddenLabel: "Rendre l'évènement privé", hiddenLabel: "Rendre l'évènement privé",
hiddenTooltip: "Cet évènement est uniquement visible pour les maîtres du jeu.", hiddenTooltip: "Cet évènement est uniquement visible pour les maîtres du jeu.",
addLocation: "Ajouter un endroit", addLocation: "Ajouter un endroit",
oneOtherEvent: "1 autre",
multipleOtherEvents: "{count} autres",
prevPage: "Précédente page à évènements", prevPage: "Précédente page à évènements",
nextPage: "Prochaine page à évènements", nextPage: "Prochaine page à évènements",
outOfBoundsTitle: "Aucun évènement suivant ou précédent trouvé", outOfBoundsTitle: "Aucun évènement suivant ou précédent trouvé",
@@ -655,13 +691,17 @@ export default defineI18nConfig(() => ({
} }
}, },
pages: { pages: {
home: {
h1: "Un espace pour votre créativité",
tagline: "Visualisez vos mondes fantasy ou de science-fiction grâce à nos calendriers interactifs"
},
explore: { explore: {
menuLabel: "Explorer", menuLabel: "Explorer",
title: "Explorer les mondes", title: "Explorer les mondes",
}, },
profile: { profile: {
title: "{user} — Mon univers", title: "{user} — Projets",
metaTitle: "Mon univers", metaTitle: "Projets",
}, },
about: { about: {
title: "À propos", title: "À propos",
@@ -672,7 +712,7 @@ export default defineI18nConfig(() => ({
breadcrumbs: { breadcrumbs: {
home: "Accueil", home: "Accueil",
explore: "Explorer", explore: "Explorer",
profile: "Univers", profile: "Projets",
world: "Monde", world: "Monde",
calendar: "Calendrier" calendar: "Calendrier"
} }

View File

@@ -1,3 +1,4 @@
import { defineNuxtConfig } from 'nuxt/config'
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config

View File

@@ -13,55 +13,57 @@
"lint:prettier": "prettier . --check" "lint:prettier": "prettier . --check"
}, },
"dependencies": { "dependencies": {
"@intlify/message-compiler": "^11.1.11", "@intlify/message-compiler": "^11.1.12",
"@nuxt/content": "3.6.3", "@nuxt/content": "3.7.0",
"@nuxt/eslint": "^1.8.0", "@nuxt/eslint": "^1.9.0",
"@nuxt/image": "1.11.0", "@nuxt/image": "1.11.0",
"@nuxthub/core": "^0.9.0", "@nuxthub/core": "^0.9.0",
"@nuxtjs/i18n": "^10.0.3", "@nuxtjs/i18n": "^10.1.0",
"@phosphor-icons/vue": "^2.2.1", "@phosphor-icons/vue": "^2.2.1",
"@pinia/nuxt": "^0.11.2", "@pinia/nuxt": "^0.11.2",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.14",
"@tiptap/pm": "^3.0.9", "@tiptap/pm": "^3.6.5",
"@tiptap/starter-kit": "^3.0.9", "@tiptap/starter-kit": "^3.6.5",
"@tiptap/vue-3": "^3.0.9", "@tiptap/vue-3": "^3.6.5",
"@vueuse/core": "^13.6.0", "@vueuse/core": "^13.9.0",
"@vueuse/integrations": "^13.6.0", "@vueuse/integrations": "^13.9.0",
"@vueuse/nuxt": "^13.6.0", "@vueuse/nuxt": "^13.9.0",
"better-sqlite3": "^12.2.0", "better-sqlite3": "^12.4.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-vue-next": "^0.537.0", "lucide-vue-next": "^0.544.0",
"luxon": "^3.7.1", "luxon": "^3.7.2",
"nuxt": "^4.0.3", "nuxt": "^4.1.3",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"radix-vue": "^1.9.17", "radix-vue": "^1.9.17",
"shadcn-nuxt": "^2.2.0", "reka-ui": "^2.5.1",
"shadcn-nuxt": "^2.3.1",
"sortablejs": "^1.15.6", "sortablejs": "^1.15.6",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vue": "^3.5.18", "tw-animate-css": "^1.4.0",
"vue": "^3.5.22",
"vue-router": "^4.5.1", "vue-router": "^4.5.1",
"zod": "^4.0.15" "zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
"@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/supabase": "^1.6.0", "@nuxtjs/supabase": "^1.6.2",
"@nuxtjs/tailwindcss": "^6.14.0", "@nuxtjs/tailwindcss": "^6.14.0",
"@stylistic/eslint-plugin-js": "^4.4.1", "@stylistic/eslint-plugin-js": "^4.4.1",
"@types/luxon": "^3.7.1", "@types/luxon": "^3.7.1",
"@typescript-eslint/eslint-plugin": "^8.39.0", "@typescript-eslint/eslint-plugin": "^8.46.0",
"@typescript-eslint/parser": "^8.39.0", "@typescript-eslint/parser": "^8.46.0",
"eslint": "^9.32.0", "eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-vue": "^10.4.0", "eslint-plugin-vue": "^10.5.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"sass": "^1.90.0", "sass": "^1.93.2",
"supabase": "^2.33.9", "supabase": "^2.48.3",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.14",
"typescript": "^5.9.2", "typescript": "^5.9.3",
"wrangler": "^4.28.1" "wrangler": "^4.42.0"
} }
} }

6752
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff