Compare commits
30 Commits
feat/edito
...
feat/home
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7443769f00 | ||
|
|
692aaad868 | ||
|
|
371bb7e38a | ||
|
|
c4e70eb29c | ||
|
|
f8d2387b59 | ||
|
|
8c53616c79 | ||
|
|
8dff0763e9 | ||
|
|
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 */
|
/* 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 {
|
||||||
|
|||||||
@@ -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%);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
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">
|
<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>
|
||||||
|
|
||||||
|
|||||||
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>
|
<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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
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: {
|
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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
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 }
|
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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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[]) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
3
app/pages/my.vue
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtPage />
|
||||||
|
</template>
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
58
package.json
58
package.json
@@ -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
6752
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user