Compare commits
23 Commits
feat/edito
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c7b503ca8 | ||
|
|
674a38c30d | ||
|
|
e7cef314a1 | ||
|
|
806d3a7908 | ||
|
|
6251499940 | ||
|
|
f3d5db76ac | ||
|
|
08620b5044 | ||
|
|
f89cc22e87 | ||
|
|
51c678d27e | ||
|
|
b9b891c61f | ||
|
|
df06228e68 | ||
|
|
44193aa018 | ||
|
|
73eceda96c | ||
|
|
e305b21f80 | ||
|
|
d2b66a42dd | ||
|
|
be32a018c6 | ||
|
|
a5a232bac7 | ||
|
|
20045b125e | ||
|
|
f1a3854d1d | ||
|
|
cebcb50903 | ||
|
|
396215bb91 | ||
|
|
7468374bb5 | ||
|
|
5a421cf07f |
@@ -40,23 +40,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
p, ul, ol {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
/* Normal link */
|
||||
p a:not([class]) {
|
||||
color: var(--color-sky-600);
|
||||
color: var(--color-primary);
|
||||
text-underline-offset: .25rem;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-sky-500);
|
||||
color: var(--color-accent);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@variant dark {
|
||||
color: var(--color-teal-500);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-teal-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
--primary: rgb(101, 230, 166);
|
||||
--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%);
|
||||
|
||||
--accent: hsl(11, 87%, 69%);
|
||||
|
||||
@@ -6,6 +6,13 @@ import { VisuallyHidden } from "radix-vue";
|
||||
const isDialogOpen = ref<boolean>(false);
|
||||
const { resetSkeleton } = useCalendar();
|
||||
|
||||
// Watch the popover state
|
||||
watch(isDialogOpen, (hasOpened, _o) => {
|
||||
if (hasOpened) {
|
||||
resetSkeleton()
|
||||
}
|
||||
})
|
||||
|
||||
// Toggles the dialog
|
||||
function toggleDialog() {
|
||||
isDialogOpen.value = !isDialogOpen.value;
|
||||
@@ -45,7 +52,7 @@ const breakpoints = useBreakpoints(
|
||||
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"
|
||||
@pointer-down-outside="handleClosing"
|
||||
>
|
||||
<UiDialogTitle class="max-md:mb-8">
|
||||
{{ $t("entity.calendar.event.addSingle") }}
|
||||
|
||||
@@ -29,7 +29,7 @@ function handleClosing() {
|
||||
@escape-key-down="handleClosing"
|
||||
@focus-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">
|
||||
<UiDialogTitle>
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { RPGDate } from "@@/models/Date"
|
||||
import type { CalendarEvent } from "@@/models/CalendarEvent"
|
||||
import { ja } from "zod/v4/locales";
|
||||
|
||||
const props = defineProps<{
|
||||
event: CalendarEvent
|
||||
tileDate: RPGDate
|
||||
}>()
|
||||
|
||||
const { areDatesIdentical, revealEditEventModal, revealDeleteEventModal } = useCalendar()
|
||||
const { lastActiveEvent } = storeToRefs(useCalendar())
|
||||
const buttonRef = useTemplateRef<HTMLButtonElement>('buttonRef')
|
||||
|
||||
const { areDatesIdentical, revealEditEventModal, revealDeleteEventModal, resetSkeleton } = useCalendar()
|
||||
const { lastActiveEvent, eventSkeleton, isDraggingEvent } = storeToRefs(useCalendar())
|
||||
|
||||
const spansMultipleDays = computed(() => Boolean(props.event.startDate && props.event.endDate))
|
||||
const isStartEvent = computed(() => spansMultipleDays.value && areDatesIdentical(props.tileDate, props.event.startDate))
|
||||
@@ -49,13 +52,29 @@ onMounted(() => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
<UiPopover v-model:open="isPopoverDetailsOpen">
|
||||
<UiPopoverTrigger as-child>
|
||||
<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="
|
||||
cn(
|
||||
`element-${event.category?.color}`,
|
||||
@@ -63,11 +82,17 @@ onMounted(() => {
|
||||
'is-hidden': event.hidden,
|
||||
'rounded-r-none': isStartEvent,
|
||||
'rounded-l-none': isEndEvent,
|
||||
},
|
||||
{
|
||||
'opacity-60': isTileDragging,
|
||||
'opacity-100': !isTileDragging
|
||||
}
|
||||
)
|
||||
"
|
||||
@dblclick="handleDoubleClick"
|
||||
@keydown.delete="handleDelete"
|
||||
@dragstart="handleDragStart"
|
||||
@dragend="handleDragEnd"
|
||||
>
|
||||
<div class="line-clamp-2 [overflow-wrap:anywhere] hyphens-auto">
|
||||
{{ eventTitle }}
|
||||
|
||||
@@ -16,6 +16,9 @@ onMounted(() => {
|
||||
type FormTabs = "global" | "months" | "today"
|
||||
const activeTab = ref<FormTabs>("global")
|
||||
|
||||
const newCalendarName = shallowRef<HTMLInputElement>()
|
||||
useFocus(newCalendarName, { initialValue: true })
|
||||
|
||||
/**
|
||||
* === Current date ===
|
||||
*/
|
||||
@@ -103,6 +106,7 @@ function handleFormCancel() {
|
||||
</UiTabsList>
|
||||
<UiTabsContent value="global" class="grid gap-4">
|
||||
<input
|
||||
ref="newCalendarName"
|
||||
id="new-calendar-name"
|
||||
v-model="calendarSkeleton.name"
|
||||
type="text"
|
||||
|
||||
@@ -17,6 +17,9 @@ onMounted(() => {
|
||||
type FormTabs = "global" | "months" | "today"
|
||||
const activeTab = ref<FormTabs>("global")
|
||||
|
||||
const newCalendarName = shallowRef<HTMLInputElement>()
|
||||
useFocus(newCalendarName, { initialValue: true })
|
||||
|
||||
/**
|
||||
* === Form Validation ===
|
||||
*/
|
||||
@@ -85,6 +88,7 @@ function handleFormCancel() {
|
||||
</UiTabsList>
|
||||
<UiTabsContent value="global" class="grid gap-4">
|
||||
<input
|
||||
ref="newCalendarName"
|
||||
id="new-calendar-name"
|
||||
v-model="calendarSkeleton.name"
|
||||
type="text"
|
||||
|
||||
@@ -195,10 +195,9 @@ function handleEntitySwitch() {
|
||||
|
||||
// Key combos to deploy modal
|
||||
const keys = useMagicKeys()
|
||||
const controlPeriod = computed(() => keys.control_period?.value)
|
||||
|
||||
whenever(keys.control_period, () => {
|
||||
openUiDialog()
|
||||
})
|
||||
whenever(controlPeriod, openUiDialog)
|
||||
|
||||
const searchResultsRef = ref<HTMLElement | null>(null)
|
||||
const { y: searchResultsY } = useScroll(searchResultsRef)
|
||||
|
||||
@@ -6,6 +6,9 @@ import { storeToRefs } from "pinia"
|
||||
import { computed, ref, type ComputedRef } from "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<{
|
||||
date: RPGDate
|
||||
@@ -16,11 +19,14 @@ const emit = defineEmits<{
|
||||
"on-open-create-dialog": [date: RPGDate]
|
||||
}>()
|
||||
|
||||
const { toast } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const calendarTile = ref()
|
||||
const calendarEventsList = ref()
|
||||
|
||||
const { defaultDate, selectDate, areDatesIdentical, getFormattedDateTitle } = useCalendar()
|
||||
const { selectedDate, currentEvents, isReadOnly } = storeToRefs(useCalendar())
|
||||
const { defaultDate, selectDate, areDatesIdentical, getFormattedDateTitle, updateEventFromSkeleton, resetSkeleton } = useCalendar()
|
||||
const { selectedDate, currentEvents, isReadOnly, eventSkeleton, isDraggingEvent } = storeToRefs(useCalendar())
|
||||
|
||||
const breakpoints = useBreakpoints(
|
||||
breakpointsTailwind
|
||||
@@ -60,9 +66,17 @@ const { top: tileListTop } = useElementBounding(calendarEventsList)
|
||||
const numberOfEventsToFit: ComputedRef<number> = computed(() => {
|
||||
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
|
||||
*/
|
||||
@@ -76,16 +90,54 @@ const eventsToDisplay: ComputedRef<CalendarEvent[]> = computed<CalendarEvent[]>(
|
||||
* 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)
|
||||
|
||||
// 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>
|
||||
|
||||
<template>
|
||||
<div
|
||||
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="{
|
||||
'text-slate-300 dark:text-slate-500': props.faded,
|
||||
'text-slate-500 dark:text-slate-300': !props.faded
|
||||
}"
|
||||
@drop.prevent="handleTileDrop"
|
||||
@dragenter.prevent
|
||||
@dragover.prevent
|
||||
>
|
||||
<button
|
||||
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>
|
||||
<ul
|
||||
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="{
|
||||
'opacity-40': props.faded && !isSelectedDate
|
||||
}"
|
||||
>
|
||||
<TransitionGroup name="event">
|
||||
<li v-for="event in eventsToDisplay" :key="event.id" class="grid w-full pointer-events-auto">
|
||||
<li
|
||||
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" />
|
||||
</li>
|
||||
</TransitionGroup>
|
||||
@@ -124,7 +188,12 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
||||
<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"
|
||||
>
|
||||
{{ 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>
|
||||
</UiPopoverTrigger>
|
||||
<UiPopoverContent
|
||||
@@ -158,6 +227,7 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
||||
|
||||
<ClientOnly>
|
||||
<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"
|
||||
|
||||
39
app/components/global/Navbar.vue
Normal file
39
app/components/global/Navbar.vue
Normal 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>
|
||||
@@ -33,6 +33,9 @@ function clearFormatting() {
|
||||
<div class="flex gap-1.5">
|
||||
<!-- Inline styles -->
|
||||
<div class="flex" v-if="!props.disableInlines">
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
@click.prevent="editor.chain().focus().toggleBold().run()"
|
||||
:disabled="!editor.can().chain().focus().toggleBold().run()"
|
||||
@@ -42,7 +45,18 @@ function clearFormatting() {
|
||||
>
|
||||
<PhTextB weight="bold" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>
|
||||
{{ $t('editor.bold') }}
|
||||
</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
@click.prevent="editor.chain().focus().toggleItalic().run()"
|
||||
:disabled="!editor.can().chain().focus().toggleItalic().run()"
|
||||
@@ -52,7 +66,18 @@ function clearFormatting() {
|
||||
>
|
||||
<PhTextItalic weight="bold" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>
|
||||
{{ $t('editor.italic') }}
|
||||
</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
@click.prevent="editor.chain().focus().toggleStrike().run()"
|
||||
:disabled="!editor.can().chain().focus().toggleStrike().run()"
|
||||
@@ -62,10 +87,21 @@ function clearFormatting() {
|
||||
>
|
||||
<PhTextStrikethrough weight="bold" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>
|
||||
{{ $t('editor.strike') }}
|
||||
</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
|
||||
<!-- Block styles -->
|
||||
<div class="flex" v-if="!props.disableBlocks">
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
@click.prevent="editor.chain().focus().toggleBulletList().run()"
|
||||
:variant="editor.isActive('bulletList') ? 'default' : 'secondary'"
|
||||
@@ -74,7 +110,18 @@ function clearFormatting() {
|
||||
>
|
||||
<PhListBullets weight="bold" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>
|
||||
{{ $t('editor.ulist') }}
|
||||
</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
@click.prevent="editor.chain().focus().toggleOrderedList().run()"
|
||||
:variant="editor.isActive('orderedList') ? 'default' : 'secondary'"
|
||||
@@ -83,7 +130,18 @@ function clearFormatting() {
|
||||
>
|
||||
<PhListNumbers weight="bold" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>
|
||||
{{ $t('editor.olist') }}
|
||||
</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
@click.prevent="editor.chain().focus().toggleBlockquote().run()"
|
||||
:variant="editor.isActive('blockquote') ? 'default' : 'secondary'"
|
||||
@@ -92,9 +150,20 @@ function clearFormatting() {
|
||||
>
|
||||
<PhQuotes weight="fill" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>
|
||||
{{ $t('editor.quote') }}
|
||||
</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
@click.prevent="clearFormatting"
|
||||
variant="secondary"
|
||||
@@ -103,7 +172,18 @@ function clearFormatting() {
|
||||
>
|
||||
<PhTextTSlash weight="bold" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>
|
||||
{{ $t('editor.noFormat') }}
|
||||
</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
@click.prevent="editor.chain().focus().undo().run()"
|
||||
:disabled="!editor.can().chain().focus().undo().run()"
|
||||
@@ -113,7 +193,18 @@ function clearFormatting() {
|
||||
>
|
||||
<PhArrowUUpLeft weight="bold" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>
|
||||
{{ $t('editor.undo') }}
|
||||
</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
|
||||
<UiTooltipProvider :delay-duration="250">
|
||||
<UiTooltip>
|
||||
<UiTooltipTrigger as-child>
|
||||
<UiButton
|
||||
@click.prevent="editor.chain().focus().redo().run()"
|
||||
:disabled="!editor.can().chain().focus().redo().run()"
|
||||
@@ -123,6 +214,14 @@ function clearFormatting() {
|
||||
>
|
||||
<PhArrowUDownRight weight="bold" />
|
||||
</UiButton>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent>
|
||||
<p>
|
||||
{{ $t('editor.redo') }}
|
||||
</p>
|
||||
</UiTooltipContent>
|
||||
</UiTooltip>
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
18
app/components/global/sidebar/Footer.vue
Normal file
18
app/components/global/sidebar/Footer.vue
Normal 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>
|
||||
|
||||
50
app/components/global/sidebar/Menu.vue
Normal file
50
app/components/global/sidebar/Menu.vue
Normal 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>
|
||||
@@ -1,136 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhCompass, PhGlobeHemisphereEast, PhHurricane, PhInfo, PhX } from "@phosphor-icons/vue"
|
||||
import type { SidebarMenuActionType, SidebarMenuIcon } from "./SidebarProps";
|
||||
import { PanelsLeftBottom } from "lucide-vue-next"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { breakpointsTailwind } from "@vueuse/core"
|
||||
import { PhInfo } from "@phosphor-icons/vue";
|
||||
|
||||
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
|
||||
// })
|
||||
const { isSidebarOpened } = storeToRefs(useUiStore())
|
||||
</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-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
|
||||
['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">
|
||||
<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>
|
||||
<SidebarMenu />
|
||||
|
||||
<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
|
||||
>
|
||||
<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 />
|
||||
<SidebarFooter />
|
||||
</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>
|
||||
|
||||
@@ -8,8 +8,9 @@ const { toggleSidebar } = useUiStore()
|
||||
<UiButton
|
||||
size="icon"
|
||||
variant="outline"
|
||||
class="size-7 border-border hover:bg-secondary hover:text-secondary-foreground"
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
<PhList size="19" weight="light" />
|
||||
<PhList size="14" weight="light" />
|
||||
</UiButton>
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
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";
|
||||
|
||||
const router = useRouter()
|
||||
@@ -44,26 +44,26 @@ async function handleLogout() {
|
||||
console.log(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
type AvailableRoutes = "/my" | "/my/settings"
|
||||
|
||||
function pushRoute(to: AvailableRoutes) {
|
||||
router.push({ path: to })
|
||||
|
||||
closeMenu()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<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>
|
||||
<UiAvatar
|
||||
<div
|
||||
v-if="user"
|
||||
class="flex gap-2 items-center"
|
||||
>
|
||||
<UiAvatar
|
||||
id="user-avatar"
|
||||
class="ring-[.2rem] ring-primary hover:bg-accent hover:ring-accent transition-all cursor-pointer"
|
||||
:class="cn({ 'ring-accent bg-accent dark:bg-accent': menuOpened })"
|
||||
size="sm"
|
||||
class="rounded-sm ring-[.2rem] ring-primary transition-all cursor-pointer"
|
||||
>
|
||||
<UiAvatarImage
|
||||
:src="userMeta?.avatar_url"
|
||||
@@ -74,16 +74,13 @@ function pushRoute(to: AvailableRoutes) {
|
||||
{{ $t('ui.sidebarMenu.avatarFallback') }}
|
||||
</UiAvatarFallback>
|
||||
</UiAvatar>
|
||||
<UiButton
|
||||
v-else
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="ring-[.2rem] ring-primary hover:bg-accent hover:ring-accent border-none dark:bg-background rounded-full bg-primary transition-all cursor-pointer"
|
||||
:class="cn({ 'ring-accent bg-accent dark:bg-accent': menuOpened })"
|
||||
>
|
||||
<PhUserCircle size="24" />
|
||||
</UiButton>
|
||||
|
||||
<div class="text-xs">
|
||||
{{ userMeta?.name }}
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
|
||||
<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">
|
||||
{{ $t('ui.greeting', { user: user?.email }) }}
|
||||
</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 v-else>
|
||||
|
||||
43
app/components/global/user/DashboardLink.vue
Normal file
43
app/components/global/user/DashboardLink.vue
Normal 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>
|
||||
@@ -9,9 +9,9 @@ export const avatarVariant = cva(
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
sm: "h-10 w-10 text-xs",
|
||||
base: "h-16 w-16 text-2xl",
|
||||
lg: "h-32 w-32 text-5xl",
|
||||
sm: "size-6 text-xs",
|
||||
base: "size-14 text-2xl",
|
||||
lg: "size-24 text-5xl",
|
||||
},
|
||||
shape: {
|
||||
circle: "rounded-full",
|
||||
|
||||
@@ -10,7 +10,7 @@ export const buttonVariants = cva(
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/70",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
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",
|
||||
link: "text-primary underline-offset-4 hover:underline"
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@ const forwardedProps = useForwardProps(delegatedProps)
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
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
|
||||
)
|
||||
"
|
||||
|
||||
18
app/components/ui/sheet/Sheet.vue
Normal file
18
app/components/ui/sheet/Sheet.vue
Normal 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>
|
||||
15
app/components/ui/sheet/SheetClose.vue
Normal file
15
app/components/ui/sheet/SheetClose.vue
Normal 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>
|
||||
53
app/components/ui/sheet/SheetContent.vue
Normal file
53
app/components/ui/sheet/SheetContent.vue
Normal 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>
|
||||
21
app/components/ui/sheet/SheetDescription.vue
Normal file
21
app/components/ui/sheet/SheetDescription.vue
Normal 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>
|
||||
16
app/components/ui/sheet/SheetFooter.vue
Normal file
16
app/components/ui/sheet/SheetFooter.vue
Normal 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>
|
||||
33
app/components/ui/sheet/SheetHeader.vue
Normal file
33
app/components/ui/sheet/SheetHeader.vue
Normal 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>
|
||||
21
app/components/ui/sheet/SheetOverlay.vue
Normal file
21
app/components/ui/sheet/SheetOverlay.vue
Normal 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>
|
||||
21
app/components/ui/sheet/SheetTitle.vue
Normal file
21
app/components/ui/sheet/SheetTitle.vue
Normal 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>
|
||||
15
app/components/ui/sheet/SheetTrigger.vue
Normal file
15
app/components/ui/sheet/SheetTrigger.vue
Normal 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>
|
||||
8
app/components/ui/sheet/index.ts
Normal file
8
app/components/ui/sheet/index.ts
Normal 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"
|
||||
@@ -11,6 +11,9 @@ onMounted(() => {
|
||||
worldSkeleton.value = { ...defaultWorld }
|
||||
})
|
||||
|
||||
const newWorldName = shallowRef<HTMLInputElement>()
|
||||
useFocus(newWorldName, { initialValue: true })
|
||||
|
||||
const isLoading = ref<boolean>(false)
|
||||
|
||||
/**
|
||||
@@ -61,6 +64,7 @@ function handleFormCancel() {
|
||||
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
|
||||
<div class="grid gap-4">
|
||||
<input
|
||||
ref="newWorldName"
|
||||
id="new-world-name"
|
||||
v-model="worldSkeleton.name"
|
||||
type="text"
|
||||
|
||||
@@ -19,6 +19,9 @@ onMounted(() => {
|
||||
worldSkeleton.value = { ...props.world } as World
|
||||
})
|
||||
|
||||
const newWorldName = shallowRef<HTMLInputElement>()
|
||||
useFocus(newWorldName, { initialValue: true })
|
||||
|
||||
const isLoading = ref<boolean>(false)
|
||||
|
||||
/**
|
||||
@@ -75,6 +78,7 @@ function handleFormCancel() {
|
||||
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
|
||||
<div class="grid gap-4">
|
||||
<input
|
||||
ref="newWorldName"
|
||||
id="new-world-name"
|
||||
v-model="worldSkeleton.name"
|
||||
type="text"
|
||||
|
||||
@@ -60,11 +60,7 @@ switch (statusCode) {
|
||||
|
||||
<template>
|
||||
<div class="h-screen">
|
||||
<div class="h-full grid grid-cols-[auto_1fr] 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">
|
||||
<div class="h-full w-full grid place-items-center transition-colors">
|
||||
<Head>
|
||||
<Title>{{ $t(titleKey) }}</Title>
|
||||
</Head>
|
||||
@@ -93,8 +89,6 @@ switch (statusCode) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '~/lib/utils';
|
||||
|
||||
const { isSidebarOpened } = storeToRefs(useUiStore())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
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 />
|
||||
|
||||
|
||||
@@ -1,7 +1,36 @@
|
||||
<template>
|
||||
<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 />
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import type { ClassValue } from "clsx"
|
||||
import { clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
|
||||
@@ -20,20 +20,14 @@ watch(locale, () => refresh())
|
||||
</Head>
|
||||
|
||||
<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">
|
||||
{{ $t("pages.about.title") }}
|
||||
</Heading>
|
||||
</div>
|
||||
|
||||
<div v-if="status === 'pending'" class="grid place-items-center">
|
||||
<PhCircleNotch size="50" class="opacity-33 animate-spin"/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -18,15 +18,9 @@ const { data: calendars, status } = useLazyAsyncData<{ data: Calendar[] }>("expl
|
||||
</Head>
|
||||
|
||||
<Spacing size="lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="md:hidden">
|
||||
<SidebarToggle />
|
||||
</div>
|
||||
|
||||
<Heading level="h1">
|
||||
{{ $t("pages.explore.title") }}
|
||||
</Heading>
|
||||
</div>
|
||||
|
||||
<Spacing size="lg">
|
||||
<Heading level="h2">
|
||||
|
||||
@@ -6,27 +6,6 @@ useHead({
|
||||
definePageMeta({
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -39,20 +18,15 @@ async function handleGoogleLogin() {
|
||||
</Head>
|
||||
|
||||
<div class="py-8 md:py-24 px-4">
|
||||
<div class="flow text-center max-w-2xl mx-auto">
|
||||
<h1 class="text-3xl md:text-5xl font-bold md:font-semibold">A home for your creativity</h1>
|
||||
<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>
|
||||
<div class="flow text-center max-w-4xl mx-auto">
|
||||
<h1 class="text-3xl md:text-5xl font-bold md:font-semibold">
|
||||
{{ $t('pages.home.h1') }}
|
||||
</h1>
|
||||
<p class="text-sm">
|
||||
{{ $t('pages.home.tagline') }}
|
||||
</p>
|
||||
|
||||
<TransitionGroup name="fade-group" appear>
|
||||
<UiButton v-if="user" as-child>
|
||||
<NuxtLink to="/my">
|
||||
Dashboard
|
||||
</NuxtLink>
|
||||
</UiButton>
|
||||
<UiButton v-else @click="handleGoogleLogin">
|
||||
Log in
|
||||
</UiButton>
|
||||
</TransitionGroup>
|
||||
<UserDashboardLink />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
3
app/pages/my.vue
Normal file
3
app/pages/my.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<NuxtPage />
|
||||
</template>
|
||||
@@ -130,7 +130,7 @@ export const useCalendar = defineStore("calendar", () => {
|
||||
return Number(params.month)
|
||||
})
|
||||
const currentMonthData = computed<CalendarMonth>(() => {
|
||||
return sortedMonths.value[currentMonth.value]
|
||||
return sortedMonths.value[currentMonth.value]!
|
||||
})
|
||||
// Gets the label from currentMonth index
|
||||
const currentMonthName = computed<string>(() => getMonthName(currentMonth.value))
|
||||
@@ -605,7 +605,7 @@ export const useCalendar = defineStore("calendar", () => {
|
||||
// Else, we need to accelerate and decelerate
|
||||
else {
|
||||
// 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
|
||||
if (direction === "future") {
|
||||
datePivot.month = getNextViewMonth(datePivot.month)
|
||||
@@ -756,7 +756,7 @@ export const useCalendar = defineStore("calendar", () => {
|
||||
|
||||
// Loop over all event once to convert the structure to a usable one
|
||||
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
|
||||
const startDateDays: number = convertDateToDays(e.startDate)
|
||||
@@ -830,6 +830,7 @@ export const useCalendar = defineStore("calendar", () => {
|
||||
const isCreatingEvent = ref<boolean>(false)
|
||||
const isUpdatingEvent = ref<boolean>(false)
|
||||
const isDeletingEvent = ref<boolean>(false)
|
||||
const isDraggingEvent = ref<boolean>(false)
|
||||
const operationInProgress = computed<boolean>(() => isCreatingEvent.value || isUpdatingEvent.value || isDeletingEvent.value)
|
||||
let abortController: AbortController | null = null
|
||||
|
||||
@@ -984,6 +985,7 @@ export const useCalendar = defineStore("calendar", () => {
|
||||
isCreatingEvent,
|
||||
isUpdatingEvent,
|
||||
isDeletingEvent,
|
||||
isDraggingEvent,
|
||||
operationInProgress,
|
||||
eventSkeleton,
|
||||
resetSkeleton,
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
{
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "default",
|
||||
"style": "new-york",
|
||||
"typescript": true,
|
||||
"tsConfigPath": ".nuxt/tsconfig.json",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "assets/main.css",
|
||||
"css": "app/assets/main.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"framework": "nuxt",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
"composables": "@/composables",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export default defineI18nConfig(() => ({
|
||||
save: "Save",
|
||||
delete: "Delete",
|
||||
edit: "Edit",
|
||||
close: "Close"
|
||||
},
|
||||
colors: {
|
||||
label: "Color",
|
||||
@@ -69,12 +70,14 @@ export default defineI18nConfig(() => ({
|
||||
backToHome: "Back to home",
|
||||
sidebarMenu: {
|
||||
profile: "Profile",
|
||||
projects: "Projects",
|
||||
appearance: "Appearance",
|
||||
language: "Language",
|
||||
account: "Account",
|
||||
login: "Log in",
|
||||
logout: "Log out",
|
||||
avatarFallback: "Profile",
|
||||
about: "About",
|
||||
},
|
||||
dark: "Dark",
|
||||
light: "Light",
|
||||
@@ -86,6 +89,17 @@ export default defineI18nConfig(() => ({
|
||||
createdAt: "Created {createdAt}",
|
||||
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: {
|
||||
category: {
|
||||
nameSingular: "Category",
|
||||
@@ -127,7 +141,7 @@ export default defineI18nConfig(() => ({
|
||||
world: {
|
||||
nameSingular: "World",
|
||||
namePlural: "Worlds",
|
||||
backToMy: "Back to my universe",
|
||||
backToMy: "Back to projects",
|
||||
backToSingle: "Back to {world}",
|
||||
addSingle: "Add a world",
|
||||
addSingleFirst: "Add your first world !",
|
||||
@@ -158,7 +172,7 @@ export default defineI18nConfig(() => ({
|
||||
nameSingular: "Calendar",
|
||||
namePlural: "Calendars",
|
||||
namePublicSingular: "Public Calendar",
|
||||
namePublicPlural: "Public Calendars",
|
||||
namePublicPlural: "Calendars",
|
||||
addSingle: "Add a calendar",
|
||||
addSingleFirst: "Add your first calendar !",
|
||||
notFound: "Calendar not found",
|
||||
@@ -213,6 +227,8 @@ export default defineI18nConfig(() => ({
|
||||
hiddenLabel: "Hide this event",
|
||||
hiddenTooltip: "This event is visible only to game masters.",
|
||||
addLocation: "Add a place",
|
||||
oneOtherEvent: "1 other",
|
||||
multipleOtherEvents: "{count} others",
|
||||
prevPage: "Previous page with events",
|
||||
nextPage: "Next page with events",
|
||||
outOfBoundsTitle: "No next or previous events were found",
|
||||
@@ -316,13 +332,17 @@ export default defineI18nConfig(() => ({
|
||||
}
|
||||
},
|
||||
pages: {
|
||||
home: {
|
||||
h1: "A home for your creativity",
|
||||
tagline: "Visualize your fantasy or sci-fi worlds with our interactive calendars"
|
||||
},
|
||||
explore: {
|
||||
menuLabel: "Explore",
|
||||
title: "Explore worlds",
|
||||
},
|
||||
profile: {
|
||||
title: "{user} — My universe",
|
||||
metaTitle: "My universe",
|
||||
title: "{user} — Projects",
|
||||
metaTitle: "Projects",
|
||||
},
|
||||
about: {
|
||||
title: "About this app",
|
||||
@@ -333,7 +353,7 @@ export default defineI18nConfig(() => ({
|
||||
breadcrumbs: {
|
||||
home: "Home",
|
||||
explore: "Explore",
|
||||
profile: "Universe",
|
||||
profile: "Projects",
|
||||
world: "World",
|
||||
calendar: "Calendar"
|
||||
}
|
||||
@@ -367,6 +387,7 @@ export default defineI18nConfig(() => ({
|
||||
save: "Sauvegarder",
|
||||
delete: "Supprimer",
|
||||
edit: "Modifier",
|
||||
close: "Fermer"
|
||||
},
|
||||
colors: {
|
||||
label: "Couleur",
|
||||
@@ -404,12 +425,14 @@ export default defineI18nConfig(() => ({
|
||||
backToHome: "Retourner à l'accueil",
|
||||
sidebarMenu: {
|
||||
profile: "Profil",
|
||||
projects: "Projets",
|
||||
appearance: "Apparence",
|
||||
language: "Langue",
|
||||
account: "Compte",
|
||||
login: "Connexion",
|
||||
logout: "Déconnexion",
|
||||
avatarFallback: "Profil",
|
||||
about: "À propos",
|
||||
},
|
||||
dark: "Sombre",
|
||||
light: "Clair",
|
||||
@@ -421,6 +444,17 @@ export default defineI18nConfig(() => ({
|
||||
createdAt: "Créé le {createdAt}",
|
||||
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: {
|
||||
category: {
|
||||
nameSingular: "Catégorie",
|
||||
@@ -462,7 +496,7 @@ export default defineI18nConfig(() => ({
|
||||
world: {
|
||||
nameSingular: "Monde",
|
||||
namePlural: "Mondes",
|
||||
backToMy: "Retourner à mon univers",
|
||||
backToMy: "Retourner à Projets",
|
||||
backToSingle: "Retourner sur {world}",
|
||||
addSingle: "Ajouter un monde",
|
||||
addSingleFirst: "Ajouter votre premier monde !",
|
||||
@@ -548,6 +582,8 @@ export default defineI18nConfig(() => ({
|
||||
hiddenLabel: "Rendre l'évènement privé",
|
||||
hiddenTooltip: "Cet évènement est uniquement visible pour les maîtres du jeu.",
|
||||
addLocation: "Ajouter un endroit",
|
||||
oneOtherEvent: "1 autre",
|
||||
multipleOtherEvents: "{count} autres",
|
||||
prevPage: "Précédente page à évènements",
|
||||
nextPage: "Prochaine page à évènements",
|
||||
outOfBoundsTitle: "Aucun évènement suivant ou précédent trouvé",
|
||||
@@ -655,13 +691,17 @@ export default defineI18nConfig(() => ({
|
||||
}
|
||||
},
|
||||
pages: {
|
||||
home: {
|
||||
h1: "Un espace pour votre créativité",
|
||||
tagline: "Visualisez vos mondes fantasy ou de science-fiction grâce à nos calendriers interactifs"
|
||||
},
|
||||
explore: {
|
||||
menuLabel: "Explorer",
|
||||
title: "Explorer les mondes",
|
||||
},
|
||||
profile: {
|
||||
title: "{user} — Mon univers",
|
||||
metaTitle: "Mon univers",
|
||||
title: "{user} — Projets",
|
||||
metaTitle: "Projets",
|
||||
},
|
||||
about: {
|
||||
title: "À propos",
|
||||
@@ -672,7 +712,7 @@ export default defineI18nConfig(() => ({
|
||||
breadcrumbs: {
|
||||
home: "Accueil",
|
||||
explore: "Explorer",
|
||||
profile: "Univers",
|
||||
profile: "Projets",
|
||||
world: "Monde",
|
||||
calendar: "Calendrier"
|
||||
}
|
||||
|
||||
40
package.json
40
package.json
@@ -15,53 +15,55 @@
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "^11.1.11",
|
||||
"@nuxt/content": "3.6.3",
|
||||
"@nuxt/eslint": "^1.8.0",
|
||||
"@nuxt/eslint": "^1.9.0",
|
||||
"@nuxt/image": "1.11.0",
|
||||
"@nuxthub/core": "^0.9.0",
|
||||
"@nuxtjs/i18n": "^10.0.3",
|
||||
"@nuxtjs/i18n": "^10.0.6",
|
||||
"@phosphor-icons/vue": "^2.2.1",
|
||||
"@pinia/nuxt": "^0.11.2",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tiptap/pm": "^3.0.9",
|
||||
"@tiptap/starter-kit": "^3.0.9",
|
||||
"@tiptap/vue-3": "^3.0.9",
|
||||
"@vueuse/core": "^13.6.0",
|
||||
"@vueuse/integrations": "^13.6.0",
|
||||
"@vueuse/nuxt": "^13.6.0",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@tiptap/pm": "^3.3.0",
|
||||
"@tiptap/starter-kit": "^3.3.0",
|
||||
"@tiptap/vue-3": "^3.3.0",
|
||||
"@vueuse/core": "^13.7.0",
|
||||
"@vueuse/integrations": "^13.7.0",
|
||||
"@vueuse/nuxt": "^13.7.0",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-vue-next": "^0.537.0",
|
||||
"lucide-vue-next": "^0.541.0",
|
||||
"luxon": "^3.7.1",
|
||||
"nuxt": "^4.0.3",
|
||||
"pinia": "^3.0.3",
|
||||
"radix-vue": "^1.9.17",
|
||||
"reka-ui": "^2.4.1",
|
||||
"shadcn-nuxt": "^2.2.0",
|
||||
"sortablejs": "^1.15.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vue": "^3.5.18",
|
||||
"tw-animate-css": "^1.3.7",
|
||||
"vue": "^3.5.19",
|
||||
"vue-router": "^4.5.1",
|
||||
"zod": "^4.0.15"
|
||||
"zod": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@nuxtjs/supabase": "^1.6.0",
|
||||
"@nuxtjs/supabase": "^1.6.1",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
"@stylistic/eslint-plugin-js": "^4.4.1",
|
||||
"@types/luxon": "^3.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.39.0",
|
||||
"@typescript-eslint/parser": "^8.39.0",
|
||||
"eslint": "^9.32.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.40.0",
|
||||
"@typescript-eslint/parser": "^8.40.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.90.0",
|
||||
"supabase": "^2.33.9",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"supabase": "^2.34.3",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"typescript": "^5.9.2",
|
||||
"wrangler": "^4.28.1"
|
||||
"wrangler": "^4.32.0"
|
||||
}
|
||||
}
|
||||
|
||||
3412
pnpm-lock.yaml
generated
3412
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user