50 Commits

Author SHA1 Message Date
Alexis
7443769f00 Updated package 2025-10-06 21:37:31 +02:00
Alexis
692aaad868 Updated package 2025-09-12 19:52:32 +02:00
Alexis
371bb7e38a Updated package 2025-09-03 21:14:07 +02:00
Alexis
c4e70eb29c Changed quick navigation (arrows will be for days) 2025-08-24 19:53:43 +02:00
Alexis
f8d2387b59 Added quick navigation for calendar pages 2025-08-24 19:44:27 +02:00
Alexis
8c53616c79 Added arrow navigation on calendar page 2025-08-24 15:02:42 +02:00
Alexis
8dff0763e9 Changed calendar menu subnav 2025-08-24 13:47:03 +02:00
Alexis
9c7b503ca8 Merge branch 'feat/nav' into dev 2025-08-24 11:49:21 +02:00
Alexis
674a38c30d Fixed search shortcut 2025-08-24 11:38:10 +02:00
Alexis
e7cef314a1 Fixed untranslated string 2025-08-24 11:26:11 +02:00
Alexis
806d3a7908 Updated package 2025-08-23 22:45:35 +02:00
Alexis
6251499940 Added sheet for sidebar mobile 2025-08-15 15:37:35 +02:00
Alexis
f3d5db76ac Removed sidebar toggle on public pages 2025-08-12 11:18:47 +02:00
Alexis
08620b5044 Made content body smaller 2025-08-12 11:16:52 +02:00
Alexis
f89cc22e87 Fixed quick create dialog not resetting skeleton 2025-08-12 11:09:28 +02:00
Alexis
51c678d27e Fixed dashboard link hitbox 2025-08-12 10:44:56 +02:00
Alexis
b9b891c61f Fixed nav colors in dark mode 2025-08-12 10:43:49 +02:00
Alexis
df06228e68 Fixed transition render issue 2025-08-12 10:37:37 +02:00
Alexis
44193aa018 Added about link to CTA 2025-08-12 10:34:23 +02:00
Alexis
73eceda96c Changed secondary button color 2025-08-12 10:18:07 +02:00
Alexis
e305b21f80 Changed projects page handle 2025-08-12 10:11:28 +02:00
Alexis
d2b66a42dd Fixed dark mode select hover 2025-08-12 10:07:12 +02:00
Alexis
be32a018c6 Added input focus on entity forms 2025-08-11 20:23:25 +02:00
Alexis
a5a232bac7 Added navbar on public pages 2025-08-11 20:00:04 +02:00
Alexis
20045b125e Changed explore page h2 2025-08-09 21:06:42 +02:00
Alexis
f1a3854d1d Merge branch 'feat/drag-events' into dev 2025-08-09 18:56:57 +02:00
Alexis
cebcb50903 Added tooltip on rich text 2025-08-09 18:55:55 +02:00
Alexis
396215bb91 Fixed some UI issues with drag state 2025-08-09 16:40:50 +02:00
Alexis
7468374bb5 Added update logic for single day events 2025-08-09 16:24:59 +02:00
Alexis
5a421cf07f Merge branch 'feat/editor' into dev 2025-08-08 10:49:14 +02:00
Alexis
25a9099a18 Added rich text to event description 2025-08-08 10:48:29 +02:00
Alexis
c208393f9f Bigger richtext window 2025-08-07 23:01:34 +02:00
Alexis
d75f3b5423 Added control options 2025-08-07 22:55:53 +02:00
Alexis
97ce5b3553 Added simple tiptap component 2025-08-07 22:37:49 +02:00
Alexis
0a35f441e0 Merge branch 'code/cleanup' into dev 2025-08-04 22:43:49 +02:00
Alexis
c8a345406c Updated packages and some other colours 2025-08-04 22:43:38 +02:00
Alexis
119968e7fc Fixed a bunch of color transition issues on theme switch 2025-08-04 21:48:57 +02:00
Alexis
9b8f48ad8e Fixed tooltip position on client side component 2025-08-04 16:45:46 +02:00
Alexis
2199359b96 Fixed badge color 2025-08-04 13:41:43 +02:00
Alexis
422673cc4c Updated color scheme 2025-08-04 12:31:28 +02:00
Alexis
6aad01c96f Updated color scheme and identity 2025-08-03 19:42:43 +02:00
Alexis
0c96466263 Updated data loading strategy on world page 2025-08-02 22:42:23 +02:00
Alexis
7697e818bf Fixed supabase channel conflicts 2025-08-01 17:18:06 +02:00
Alexis
fd766b9f42 Updated packages 2025-07-29 22:45:22 +02:00
Alexis
0e7b60235a Fixed log in animation 2025-07-29 22:30:14 +02:00
Alexis
300507bbed Made sidebar image smaller 2025-07-29 21:59:36 +02:00
Alexis
693f506610 Reworked data fetching for explore page 2025-07-27 18:57:31 +02:00
Alexis
f2076e413c Made about page more reactive 2025-07-27 18:17:13 +02:00
Alexis
6b02629b01 Fixed nuxt database warning 2025-07-27 17:31:21 +02:00
Alexis
1d8b56c58e Merge branch 'upgrade/nuxt-4' into dev 2025-07-27 15:14:33 +02:00
91 changed files with 5499 additions and 4240 deletions

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import { ConfigProvider } from "radix-vue"
import { cn } from "./lib/utils";
useHead({
titleTemplate: (titleChunk) => {
@@ -27,33 +26,17 @@ useHead({
})
const useIdFunction = () => useId()
const { isSidebarOpened } = storeToRefs(useUiStore())
</script>
<template>
<div class="h-screen">
<NuxtLoadingIndicator />
<NuxtLayout>
<ConfigProvider :use-id="useIdFunction">
<div
class="h-full grid md:grid-cols-[auto_1fr] dark:bg-black shadow-body-light dark:shadow-body-dark transition-colors after:absolute after:transition-colors"
:class="cn({
'max-md:after:bg-transparent': isSidebarOpened,
'has-sidebar max-md:after:bg-black/20 md:after:opacity-0 md:after:pointer-events-none': isSidebarOpened
})"
>
<Sidebar />
<div
class="wrapper max-h-screen transition-all overflow-y-auto"
>
<NuxtPage />
</div>
</div>
</ConfigProvider>
</NuxtLayout>
<ConfigProvider :use-id="useIdFunction">
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</ConfigProvider>
<ClientOnly>
<UiToaster />

View File

@@ -116,6 +116,11 @@
--color-event-white-background: var(--event-white-background);
}
::selection {
color: var(--color-background);
background: color-mix(in srgb, var(--color-primary), var(--color-foreground) 20%)
}
.event-button,
.event-popover,
.event-callout,

View File

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

View File

@@ -2,3 +2,9 @@
--text-2xs: 0.7rem;
--text-2xs--line-height: 0.75rem;
}
@layer utilities {
.flow > * + * {
margin-top: 1em;
}
}

View File

@@ -4,6 +4,7 @@
@import "./theme.css" layer(theme);
@import "./_typography.css" layer(theme);
@import "./_typography.css" layer(utilities);
@import "./_colors.css" layer(theme);
@import "./_colors.css" layer(utilities);
@@ -33,18 +34,30 @@ body {
}
.fade-enter-active,
.fade-leave-active {
.fade-leave-active,
.fade-group-enter-active,
.fade-group-leave-active {
transition: all .5s ease;
}
.fade-delay-enter-active,
.fade-delay-leave-active {
.fade-delay-leave-active,
.fade-group-delay-enter-active,
.fade-group-delay-leave-active {
transition: all .5s ease 1s;
}
.fade-enter-from,
.fade-leave-to,
.fade-delay-enter-from,
.fade-delay-leave-to {
.fade-delay-leave-to,
.fade-group-enter-from,
.fade-group-leave-to,
.fade-group-delay-enter-from,
.fade-group-delay-leave-to {
opacity: 0;
visibility: hidden;
}
.fade-group-leave-active {
position: absolute;
}

View File

@@ -7,6 +7,7 @@
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(222.2 84% 4.9%);
--popover-border: rgb(175, 224, 200);
--card: hsl(0 0% 100%);
--card-foreground: hsl(222.2 84% 4.9%);
@@ -14,16 +15,16 @@
--border: hsl(214.3 31.8% 91.4%);
--input: hsl(214.3 31.8% 91.4%);
--primary: hsl(245 58% 51%);
--primary-foreground: hsl(0 0% 100%);
--primary: rgb(101, 230, 166);
--primary-foreground: hsl(222.2 84% 4.9%);
--secondary: hsl(210 50% 95%);
--secondary-foreground: hsl(222.2 47.4% 11.2%);
--secondary-foreground: hsl(222.2 84% 4.9%);
--accent: hsl(210 40% 96.1%);
--accent-foreground: hsl(222.2 47.4% 11.2%);
--accent: hsl(11, 100%, 86%);
--accent-foreground: hsl(222.2 84% 4.9%);
--destructive: hsl(0 84.2% 60.2%);
--destructive: hsl(0, 100%, 63%);
--destructive-foreground: hsl(210 40% 98%);
--success: hsl(156 72% 67%);
@@ -42,25 +43,26 @@
--muted: hsl(0, 0%, 12%);
--muted-foreground: hsl(215 20.2% 65.1%);
--popover: hsl(180 0 4.9%);
--popover: hsl(165, 47%, 5%);
--popover-foreground: hsl(210 40% 98%);
--popover-border: hsl(165, 47%, 20%);
--card: hsl(180 0 4.9%);
--card-foreground: hsl(210 40% 98%);
--border: hsl(0, 0%, 16%);
--input: hsl(244 47% 20%);
--border: hsl(0, 0%, 22.5%);
--input: hsl(165, 47%, 20%);
--primary: hsl(245 58% 51%);
--primary-foreground: hsl(0 0% 100%);
--primary: rgb(101, 230, 166);
--primary-foreground: hsl(180 0 4.9%);
--secondary: hsl(244 47% 20%);
--secondary: hsl(0, 0%, 22.5%);
--secondary-foreground: hsl(210 40% 98%);
--accent: hsl(244 47% 20%);
--accent-foreground: hsl(210 40% 98%);
--accent: hsl(11, 87%, 69%);
--accent-foreground: hsl(222.2 84% 4.9%);
--destructive: hsl(0 62.8% 30.6%);
--destructive: hsl(0, 62%, 49%);
--destructive-foreground: hsl(210 40% 98%);
--success: hsl(142 72% 29%);
@@ -81,6 +83,7 @@
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-popover-border: var(--popover-border);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);

View File

@@ -1,6 +1,5 @@
<script lang="ts" setup>
import { useCalendar } from "@/stores/CalendarStore"
import { computed, type Component, type ComputedRef } from "vue"
// import { PhMagnifyingGlass } from '@phosphor-icons/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 YearLayout from "./state/yearly/Layout.vue"
const { currentConfig, jumpToDate, selectedDate } = useCalendar()
const { isReadOnly } = storeToRefs(useCalendar())
import { onKeyDown } from "@vueuse/core"
const { currentConfig, jumpToDate, selectedDate, toPastFar, toPastNear, toFutureNear, toFutureFar, toggleCreatingEventModal } = useCalendar()
const { isReadOnly, calendarState } = storeToRefs(useCalendar())
const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
switch (currentConfig.viewType) {
@@ -31,6 +32,45 @@ const currentViewComponent: ComputedRef<Component> = computed<Component>(() => {
onMounted(() => {
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>
<template>

View File

@@ -6,20 +6,20 @@ const { isReadOnly } = storeToRefs(useCalendar())
</script>
<template>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<ClientOnly>
<ClientOnly>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton v-if="!isReadOnly" variant="secondary" size="icon" @click="toggleCategoriesModal(true)">
<PhTag size="20" weight="light" />
</UiButton>
</ClientOnly>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('entity.calendar.seeCategories') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('entity.calendar.seeCategories') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</ClientOnly>
</template>

View File

@@ -51,7 +51,7 @@ function closeDeleteDialog() {
<UiDialogContent
:disable-outside-pointer-events="true"
:trap-focus="true"
class="min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
class="min-w-96"
@escape-key-down="closeDeleteDialog"
@focus-outside="closeDeleteDialog"
@interact-outside="closeDeleteDialog"

View File

@@ -86,7 +86,7 @@ async function submitNew() {
<div class="border-y-[1px] border-b-foreground/10 mr-4">
<form
ref="rowRef"
class="grid grid-cols-12 items-center gap-4 p-1 bg-transparent hover:bg-slate-50 dark:bg-transparent dark:hover:bg-slate-900"
class="grid grid-cols-12 items-center gap-4 p-1 bg-primary/0 hover:bg-primary/15"
@submit.prevent="submitNew"
>
<div v-if="currentMode === 'add'" class="col-span-1" />

View File

@@ -103,8 +103,8 @@ function handleQueryDelete() {
<form
class="grid grid-cols-12 items-center gap-4 p-1 border-b-[1px] border-b-foreground/10"
:class="cn(
{ 'bg-slate-100 hover:bg-slate-200 dark:bg-slate-900 dark:hover:bg-slate-800': currentMode === 'edit' },
{ 'bg-transparent hover:bg-slate-50 dark:bg-transparent dark:hover:bg-slate-900': currentMode !== 'edit' }
{ 'bg-primary/10 hover:bg-primary/20': currentMode === 'edit' },
{ 'bg-primary/0 hover:bg-primary/15': currentMode !== 'edit' }
)"
@submit.prevent="submitUpdate"
>
@@ -162,9 +162,9 @@ function handleQueryDelete() {
v-if="currentMode === 'edit' && categorySkeleton"
>
<UiButton
variant="secondary"
variant="default"
size="icon"
class="w-6 h-6 rounded-full bg-emerald-500 hover:bg-emerald-600 text-white"
class="w-6 h-6 rounded-full"
:title="$t('ui.actions.edit')"
@click="submitUpdate"
>
@@ -173,9 +173,9 @@ function handleQueryDelete() {
</li>
<li v-else-if="rowHovered">
<UiButton
variant="secondary"
variant="destructive"
size="icon"
class="w-6 h-6 rounded-full hover:bg-red-600 hover:text-white"
class="w-6 h-6 rounded-full"
:title="$t('ui.actions.delete')"
@click="handleQueryDelete"
>

View File

@@ -59,7 +59,7 @@ watch(currentRPGDate, () => {
:collision-padding="60"
:disable-outside-pointer-events="true"
:trap-focus="true"
class="pl-3 w-[30rem] max-w-full border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
class="pl-3 w-[30rem] max-w-full"
@escape-key-down.prevent="handleClosing"
@pointer-down-outside.prevent="handleClosing"
>

View File

@@ -27,7 +27,7 @@ function handleClosing(e: Event): void {
<UiAlertDialogContent
:disable-outside-pointer-events="true"
:trap-focus="true"
class="min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
class="min-w-96"
@escape-key-down="handleClosing"
@focus-outside="handleClosing"
@interact-outside="handleClosing"

View File

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

View File

@@ -29,7 +29,7 @@ function handleClosing() {
@escape-key-down="handleClosing"
@focus-outside="handleClosing"
@interact-outside="handleClosing"
@pointer-down-outside="(e) => e.preventDefault()"
@pointer-down-outside.prevent
>
<header class="pl-8 grid gap-y-2 max-md:mb-8">
<UiDialogTitle>

View File

@@ -160,8 +160,8 @@ function deployDeleteModal() {
<template v-if="event.description">
<hr class="border-border mt-2" >
<div class="max-h-48 overflow-y-auto mt-2 text-sm text-slate-600 dark:text-slate-300">
{{ event.description }}
<div class="max-h-48 overflow-y-auto mt-2 text-sm opacity-80">
<div class="content-editor" v-html="event.description" />
</div>
</template>
</div>
@@ -187,19 +187,19 @@ function deployDeleteModal() {
</menu>
<nav v-if="event.startDate && event.endDate" class="mt-2 flex gap-2">
<UiBadge class="hover:bg-indigo-400 dark:hover:bg-slate-300" as-child>
<UiBadge as-child>
<button
class="flex gap-1"
title="Naviguer au début"
class="flex gap-1 cursor-pointer"
:title="$t('entity.calendar.event.gotoStart')"
@click="handleJumpToDate(event.startDate!)"
>
<PhHourglassHigh size="16" weight="fill" /> {{ $t('entity.calendar.event.isStart') }}
</button>
</UiBadge>
<UiBadge class="hover:bg-indigo-400 dark:hover:bg-slate-300" as-child title="Naviguer à la fin">
<UiBadge as-child>
<button
class="flex gap-1"
title="Naviguer à la fin"
class="flex gap-1 cursor-pointer"
:title="$t('entity.calendar.event.gotoEnd')"
@click="handleJumpToDate(event.endDate!)"
>
<PhHourglassLow size="16" weight="fill" /> {{ $t('entity.calendar.event.isEnd') }}

View File

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

View File

@@ -16,6 +16,9 @@ onMounted(() => {
type FormTabs = "global" | "months" | "today"
const activeTab = ref<FormTabs>("global")
const newCalendarName = shallowRef<HTMLInputElement>()
useFocus(newCalendarName, { initialValue: true })
/**
* === Current date ===
*/
@@ -103,13 +106,14 @@ function handleFormCancel() {
</UiTabsList>
<UiTabsContent value="global" class="grid gap-4">
<input
ref="newCalendarName"
id="new-calendar-name"
v-model="calendarSkeleton.name"
type="text"
name="new-calendar-name"
required
:placeholder="$t('common.title')"
class="w-full py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600"
class="w-full py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-primary invalid:border-destructive"
@input="handleNameChange"
>

View File

@@ -66,7 +66,7 @@ function handleCancel() {
:placeholder="$t('entity.calendar.event.title')"
:maxlength="120"
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,120}"
class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600 invalid:border-red-500"
class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-primary invalid:border-destructive"
>
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.title') }}
@@ -74,14 +74,11 @@ function handleCancel() {
</div>
<div class="col-span-2 my-2 pl-8">
<textarea
id="new-event-description"
<InputEditor
disable-blocks
v-model="eventSkeleton.description"
name="new-event-description"
:placeholder="$t('entity.addDescription')"
:maxlength="1200"
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600 invalid:border-red-500"
/>
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.description') }}
</div>
@@ -132,7 +129,7 @@ function handleCancel() {
:placeholder="$t('entity.calendar.event.addLocation')"
:maxlength="160"
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,160}"
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600"
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-primary invalid:border-destructive"
>
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.location') }}

View File

@@ -17,6 +17,9 @@ onMounted(() => {
type FormTabs = "global" | "months" | "today"
const activeTab = ref<FormTabs>("global")
const newCalendarName = shallowRef<HTMLInputElement>()
useFocus(newCalendarName, { initialValue: true })
/**
* === Form Validation ===
*/
@@ -85,13 +88,14 @@ function handleFormCancel() {
</UiTabsList>
<UiTabsContent value="global" class="grid gap-4">
<input
ref="newCalendarName"
id="new-calendar-name"
v-model="calendarSkeleton.name"
type="text"
name="new-calendar-name"
required
:placeholder="$t('common.title')"
class="w-full py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600"
class="w-full py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-primary invalid:border-destructive"
@input="handleNameChange"
>

View File

@@ -82,7 +82,7 @@ function handleCancel() {
:placeholder="$t('entity.calendar.event.title')"
:minlength="3"
:maxlength="120"
class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600 invalid:border-red-500"
class="w-full -my-1 py-1 -mx-1 px-1 text-lg border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-primary invalid:border-destructive"
>
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.title') }}
@@ -92,14 +92,11 @@ function handleCancel() {
</div>
<div class="col-span-2 my-2 ml-8">
<textarea
id="new-event-description"
<InputEditor
disable-blocks
v-model="eventSkeleton.description"
name="new-event-description"
:placeholder="$t('entity.addDescription')"
:maxlength="1200"
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600 invalid:border-red-500"
/>
<div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.description') }}
</div>
@@ -152,7 +149,7 @@ function handleCancel() {
:placeholder="$t('entity.calendar.event.addLocation')"
:minlength="3"
:maxlength="160"
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600 invalid:border-red-500"
class="w-full -my-1 py-2 px-2 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-primary invalid:border-destructive"
>
<div class="mt-2 mb-1 text-xs opacity-50">

View File

@@ -37,7 +37,7 @@ const filteredCategories = computed(() =>
role="combobox"
:aria-expanded="isPopoverOpen"
aria-controls="event-categories"
class="relative w-full max-w-full h-fit justify-between"
class="relative w-full max-w-full h-fit justify-between hover:bg-secondary hover:text-primary-foreground"
>
<template v-if="!model.length">
{{ props.placeholder }}
@@ -57,7 +57,7 @@ const filteredCategories = computed(() =>
</UiPopoverTrigger>
<UiPopoverContent
id="event-categories"
align="start"
:align="'start'"
side="bottom"
:collision-padding="50"
class="w-fit h-[33vh] p-0"

View File

@@ -36,7 +36,7 @@ const filteredCategories = computed(() =>
role="combobox"
:aria-expanded="isPopoverOpen"
aria-controls="event-category"
class="w-full max-w-full justify-between"
class="w-full max-w-full justify-between hover:bg-secondary hover:text-primary-foreground"
>
<template v-if="!model">
{{ props.placeholder }}

View File

@@ -4,6 +4,7 @@ import type { RPGDate } from "@@/models/Date";
import {
PhXCircle
} from "@phosphor-icons/vue"
import { cn } from "~/lib/utils";
const model = defineModel<RPGDate | null>()
@@ -117,7 +118,6 @@ function handleValueReset() {
v-if="model"
align="start"
side="bottom"
class="border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
>
<div class="grid grid-cols-2 items-center gap-x-2 gap-y-3">
<UiSelect @update:model-value="handleMonthChange">
@@ -135,7 +135,7 @@ function handleValueReset() {
<UiInput v-model="model.year" type="number" />
<hr class="col-span-2" >
<hr class="col-span-2" />
<div class="col-span-2">
<div v-if="monthData" class="grid grid-cols-7 gap-1">
@@ -143,10 +143,10 @@ function handleValueReset() {
v-for="day in monthData.days"
:key="`popover-day-grid-${id}-${day}`"
class="aspect-square rounded-full text-[.8em] transition-colors"
:class="{
'hover:bg-indigo-200 dark:hover:bg-indigo-700': day !== model.day,
'bg-indigo-500 hover:bg-indigo-700 text-white': day === model.day
}"
:class="cn({
'hover:bg-accent hover:text-accent-foreground': day !== model.day,
'bg-primary text-primary-foreground hover:bg-accent hover:text-accent-foreground': day === model.day
})"
@click="setModelDay(day)"
>
{{ day }}

View File

@@ -12,7 +12,7 @@ interface DirectionLabels {
futureFar: string
}
const { currentConfig, decrementViewMonth, incrementViewMonth, decrementViewYear, incrementViewYear } =
const { currentConfig, toPastFar, toPastNear, toFutureNear, toFutureFar } =
useCalendar()
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>
<template>
<div class="flex gap-2">
<div class="grid items-center w-40 px-3 md:px-4 py-2 bg-white dark:bg-black border-border border-x-[1px] border-t-[1px] rounded-t-sm text-sm max-md:text-xs transition-colors">
<div class="flex gap-1">
<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>
<span>{{ currentDate.currentDateTitle }}</span>
<span class="text-xs font-medium">{{ currentDate.currentDateTitle }}</span>
<template #fallback>
<span class="inline-block">
<UiSkeleton class="h-[19px] w-full rounded-sm" />
<UiSkeleton class="h-[18px] w-full rounded-sm" />
</span>
</template>
</ClientOnly>
@@ -157,10 +73,10 @@ function toFutureFar(): void {
<UiButton
variant="outline"
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()"
>
<PhCaretDoubleLeft size="18" />
<PhCaretDoubleLeft size="14" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
@@ -176,10 +92,10 @@ function toFutureFar(): void {
<UiButton
variant="outline"
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()"
>
<PhCaretLeft size="18" />
<PhCaretLeft size="14" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
@@ -195,10 +111,10 @@ function toFutureFar(): void {
<UiButton
variant="outline"
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()"
>
<PhCaretRight size="18" />
<PhCaretRight size="14" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
@@ -214,10 +130,10 @@ function toFutureFar(): void {
<UiButton
variant="outline"
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()"
>
<PhCaretDoubleRight size="18" />
<PhCaretDoubleRight size="14" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>

View File

@@ -16,10 +16,10 @@ const breakpoints = useBreakpoints(
</script>
<template>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<ClientOnly>
<ClientOnly>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
:size="breakpoints.md.value ? 'default' : 'icon'"
variant="secondary"
@@ -31,11 +31,11 @@ const breakpoints = useBreakpoints(
{{ $t('entity.calendar.date.today') }}
</span>
</UiButton>
</ClientOnly>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ defaultDateFormatted }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</UiTooltipTrigger>
<UiTooltipContent>
<p>{{ defaultDateFormatted }}</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</ClientOnly>
</template>

View File

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

View File

@@ -113,9 +113,7 @@ const dateDuration = computed<string | null>(() => props.event.endDate ? getRela
<div v-if="event.description" class="text-xs md:text-sm">
<hr class="my-3 md:my-2 border-white opacity-50" >
<span class="opacity-75">
{{ event.description }}
</span>
<div class="content-editor opacity-75" v-html="event.description" />
</div>
</button>
</template>

View File

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

View File

@@ -2,6 +2,7 @@
import type { RPGDate } from "@@/models/Date"
import { storeToRefs } from "pinia"
import { computed, type ComputedRef } from "vue"
import { cn } from "~/lib/utils"
const { currentDate, defaultDate, selectDate, areDatesIdentical } = useCalendar()
const { selectedDate, currentEvents } = storeToRefs(useCalendar())
@@ -38,14 +39,14 @@ const hasAtLeastOneEvent = computed<boolean>(() => {
<template>
<button
class="relative grid place-items-center aspect-square rounded-full border-2 border-transparent transition-colors after:content-[''] after:absolute after:top-1 after:right-1 after:w-[.3rem] after:h-[.3rem] after:rounded-full after:transition-colors"
:class="{
'text-slate-500 hover:border-indigo-300 hover:text-slate-900': !isDefaultDate && !isSelectedDate,
'font-semibold text-white bg-slate-600 dark:bg-slate-800 hover:border-indigo-400': isDefaultDate && !isSelectedDate,
'font-semibold text-white bg-indigo-500 hover:bg-indigo-600 hover:border-indigo-500': isSelectedDate,
'after:bg-green-600': hasAtLeastOneEvent,
'after:bg-slate-950': hasAtLeastOneEvent && isSelectedDate
}"
class="relative grid place-items-center aspect-square rounded-full border-2 border-transparent transition-colors after:content-[''] after:absolute after:top-1 after:right-1 after:w-[.3rem] after:h-[.3rem] after:rounded-full after:transition-colors cursor-pointer"
:class="cn({
'text-foreground/75 hover:border-primary hover:text-foreground': !isDefaultDate && !isSelectedDate,
'font-semibold text-foreground bg-secondary hover:border-primary/70': isDefaultDate && !isSelectedDate,
'font-semibold text-primary-foreground bg-primary hover:border-primary/70': isSelectedDate,
'after:bg-primary': hasAtLeastOneEvent,
'after:bg-primary-foreground': hasAtLeastOneEvent && isSelectedDate
})"
@click="selectDate(tileDate)"
>
<span ref="tileRef" class="text-[.85em]">{{ dayNumber }}</span>

View File

@@ -11,7 +11,7 @@ defineProps<{
<div class="font-medium">
{{ month.name }}
</div>
<div class="grid grid-cols-7 gap-1">
<div class="grid grid-cols-7">
<CalendarStateYearlyDayTile
v-for="day in month.days"
:key="day"

View File

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

View File

@@ -0,0 +1,22 @@
<script lang="ts" setup>
import { PhCloudX } from '@phosphor-icons/vue';
</script>
<template>
<UiCard class="w-full h-full flex flex-col border-rose-200 bg-rose-50 dark:border-rose-900 dark:bg-rose-950">
<template v-if="$slots.title">
<UiCardHeader class="flex-row items-center gap-2">
<PhCloudX :size="32"/>
<UiCardTitle>
<slot name="title" />
</UiCardTitle>
</UiCardHeader>
</template>
<template v-if="$slots.content">
<UiCardContent class="grow grid gap-2">
<slot name="content" />
</UiCardContent>
</template>
</UiCard>
</template>

View File

@@ -13,7 +13,15 @@ const model = defineModel<RPGColor>({ default: "white" });
<template>
<UiSelect v-model="model">
<UiSelectTrigger :id :class="cn({ 'h-9': theme === 'subtle' })">
<UiSelectTrigger
:id
:class="cn(
['hover:bg-secondary hover:text-primary-foreground'],
{
'h-9': theme === 'subtle'
}
)"
>
<UiSelectValue
:placeholder="$t('ui.colors.selectOne')"
class="bgc"

View File

@@ -0,0 +1,343 @@
<script lang="ts" setup>
import { useEditor, EditorContent, type Content } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import { PhArrowUDownRight, PhArrowUUpLeft, PhListBullets, PhListNumbers, PhQuotes, PhTextB, PhTextItalic, PhTextStrikethrough, PhTextTSlash } from '@phosphor-icons/vue'
const props = defineProps<{
disableInlines?: boolean
disableBlocks?: boolean
}>()
const model = defineModel<any>()
const editor = useEditor({
content: model.value as Content,
extensions: [
StarterKit
],
onUpdate: ({ editor }) => {
model.value = editor.getHTML()
}
})
function clearFormatting() {
editor.value?.chain().focus().unsetAllMarks().run()
editor.value?.chain().focus().clearNodes().run()
}
</script>
<template>
<div v-if="editor">
<!-- Toolbar -->
<div class="flex gap-1.5">
<!-- Inline styles -->
<div class="flex" v-if="!props.disableInlines">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().toggleBold().run()"
:disabled="!editor.can().chain().focus().toggleBold().run()"
:variant="editor.isActive('bold') ? 'default' : 'secondary'"
class="size-7 rounded-e-none"
size="icon"
>
<PhTextB weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.bold') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().toggleItalic().run()"
:disabled="!editor.can().chain().focus().toggleItalic().run()"
:variant="editor.isActive('italic') ? 'default' : 'secondary'"
class="size-7 rounded-none"
size="icon"
>
<PhTextItalic weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.italic') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().toggleStrike().run()"
:disabled="!editor.can().chain().focus().toggleStrike().run()"
:variant="editor.isActive('strike') ? 'default' : 'secondary'"
class="size-7 rounded-s-none"
size="icon"
>
<PhTextStrikethrough weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.strike') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
<!-- Block styles -->
<div class="flex" v-if="!props.disableBlocks">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().toggleBulletList().run()"
:variant="editor.isActive('bulletList') ? 'default' : 'secondary'"
class="size-7 rounded-e-none"
size="icon"
>
<PhListBullets weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.ulist') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().toggleOrderedList().run()"
:variant="editor.isActive('orderedList') ? 'default' : 'secondary'"
class="size-7 rounded-none"
size="icon"
>
<PhListNumbers weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.olist') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().toggleBlockquote().run()"
:variant="editor.isActive('blockquote') ? 'default' : 'secondary'"
class="size-7 rounded-s-none"
size="icon"
>
<PhQuotes weight="fill" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.quote') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
<div class="flex">
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="clearFormatting"
variant="secondary"
class="size-7 rounded-e-none"
size="icon"
>
<PhTextTSlash weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.noFormat') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().undo().run()"
:disabled="!editor.can().chain().focus().undo().run()"
variant="secondary"
class="size-7 rounded-none"
size="icon"
>
<PhArrowUUpLeft weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.undo') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
<UiTooltipProvider :delay-duration="250">
<UiTooltip>
<UiTooltipTrigger as-child>
<UiButton
@click.prevent="editor.chain().focus().redo().run()"
:disabled="!editor.can().chain().focus().redo().run()"
variant="secondary"
class="size-7 rounded-s-none"
size="icon"
>
<PhArrowUDownRight weight="bold" />
</UiButton>
</UiTooltipTrigger>
<UiTooltipContent>
<p>
{{ $t('editor.redo') }}
</p>
</UiTooltipContent>
</UiTooltip>
</UiTooltipProvider>
</div>
</div>
<EditorContent
:editor
class="overflow-y-auto md:text-sm border-b-1 border-border"
/>
</div>
</template>
<style lang="scss">
.tiptap,
.content-editor {
/* List styles */
ul,
ol {
padding: 0 1rem;
margin: 1.25rem 1rem 1.25rem 0.4rem;
li p {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
/* Heading styles */
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
margin-top: 2.5rem;
text-wrap: pretty;
}
h1,
h2 {
margin-top: 3.5rem;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
h4,
h5,
h6 {
font-size: 1rem;
}
/* Code and preformatted text styles */
code {
background-color: var(--purple-light);
border-radius: 0.4rem;
color: var(--black);
font-size: 0.85rem;
padding: 0.25em 0.3em;
}
pre {
background: var(--black);
border-radius: 0.5rem;
color: var(--white);
font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
}
blockquote {
border-left: 3px solid var(--gray-3);
margin: 1.5rem 0;
padding-left: 1rem;
}
hr {
border: none;
border-top: 1px solid var(--gray-2);
margin: 2rem 0;
}
}
/* Basic editor styles */
.tiptap {
padding-block: 0.75rem;
min-height: 7rem;
max-height: 12rem;
overflow-y: auto;
:first-child {
margin-top: 0;
}
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { computed } from "vue"
import { PhCheckCircle, PhUser, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle } from "@phosphor-icons/vue"
import { PhCheckCircle, PhUser, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle, PhInfo } from "@phosphor-icons/vue"
import { cn } from "@/lib/utils";
const router = useRouter()
@@ -44,32 +44,42 @@ async function handleLogout() {
console.log(error.message)
}
}
type AvailableRoutes = "/my" | "/my/settings"
function pushRoute(to: AvailableRoutes) {
router.push({ path: to })
closeMenu()
}
</script>
<template>
<ClientOnly>
<UiDropdownMenu v-model:open="menuOpened">
<UiDropdownMenuTrigger>
<UiAvatar v-if="user" id="user-avatar" class="ring-[.2rem] ring-indigo-700 dark:ring-neutral-900 cursor-pointer">
<UiAvatarImage
:src="userMeta?.avatar_url"
:alt="userMeta?.full_name"
referrerpolicy="no-referrer"
/>
<UiAvatarFallback>
{{ $t('ui.sidebarMenu.avatarFallback') }}
</UiAvatarFallback>
</UiAvatar>
<UiButton v-else variant="outline" size="icon" class="rounded-full border-indigo-200 bg-indigo-700 dark:border-slate-300 dark:bg-neutral-950 dark:hover:bg-slate-50 dark:hover:text-slate-950 cursor-pointer">
<PhUserCircle size="24" />
<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>
<div
v-if="user"
class="flex gap-2 items-center"
>
<UiAvatar
id="user-avatar"
size="sm"
class="rounded-sm ring-[.2rem] ring-primary transition-all cursor-pointer"
>
<UiAvatarImage
:src="userMeta?.avatar_url"
:alt="userMeta?.full_name"
referrerpolicy="no-referrer"
/>
<UiAvatarFallback>
{{ $t('ui.sidebarMenu.avatarFallback') }}
</UiAvatarFallback>
</UiAvatar>
<div class="text-xs">
{{ userMeta?.name }}
</div>
</div>
</TransitionGroup>
</UiButton>
</UiDropdownMenuTrigger>
@@ -78,15 +88,6 @@ function pushRoute(to: AvailableRoutes) {
<p class="p-2 text-[.7em] opacity-75">
{{ $t('ui.greeting', { user: user?.email }) }}
</p>
<UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="pushRoute('/my')">
<PhUser size="20" weight="fill" />
<span>
{{ $t('ui.sidebarMenu.profile') }}
</span>
</UiDropdownMenuItem>
<UiDropdownMenuSeparator />
</template>
<template v-else>

View File

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

View File

@@ -31,7 +31,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-2rem)] max-w-3xl -translate-x-1/2 -translate-y-1/2 gap-4 border border-indigo-200 bg-background dark:bg-slate-950 dark:border-indigo-950 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% data-[state=open]:slide-in-from-top-48% rounded-lg',
'fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-2rem)] max-w-3xl -translate-x-1/2 -translate-y-1/2 gap-4 border border-popover-border bg-popover p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% data-[state=open]:slide-in-from-top-48% rounded-lg',
props.class,
)
"

View File

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

View File

@@ -9,7 +9,7 @@ const props = defineProps<{
<template>
<ol
:class="cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', props.class)"
:class="cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground transition-colors sm:gap-2.5', props.class)"
>
<slot />
</ol>

View File

@@ -12,7 +12,7 @@ const props = defineProps<{
role="link"
aria-disabled="true"
aria-current="page"
:class="cn('font-normal text-foreground', props.class)"
:class="cn('font-normal text-foreground transition-colors', props.class)"
>
<slot />
</span>

View File

@@ -26,7 +26,7 @@ const props = withDefaults(defineProps<Props>(), {
<span
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>
</span>

View File

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

View File

@@ -33,7 +33,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-2rem)] max-w-4xl -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background border-indigo-200 dark:bg-slate-950 dark:border-indigo-950 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% data-[state=open]:slide-in-from-top-48% rounded-lg',
'fixed left-1/2 top-1/2 z-50 grid w-[calc(100%-2rem)] max-w-4xl -translate-x-1/2 -translate-y-1/2 gap-4 border border-popover-border bg-popover p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-50% data-[state=closed]:slide-out-to-top-48% data-[state=open]:slide-in-from-left-50% data-[state=open]:slide-in-from-top-48% rounded-lg',
props.class
)
"

View File

@@ -32,7 +32,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
v-bind="forwarded"
:class="
cn(
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 transition-all',
props.class
)
"

View File

@@ -19,6 +19,6 @@ const delegatedProps = computed(() => {
<template>
<DropdownMenuSeparator
v-bind="delegatedProps"
:class="cn('-mx-1 my-1 h-px bg-muted', props.class)"
:class="cn('-mx-1 my-1 h-px bg-muted transition-colors', props.class)"
/>
</template>

View File

@@ -25,7 +25,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
v-bind="forwarded"
:class="
cn(
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 transition-all',
props.class
)
"

View File

@@ -24,7 +24,7 @@ const forwardedProps = useForwardProps(delegatedProps)
v-bind="forwardedProps"
:class="
cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent data-[state=open]:bg-accent',
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:bg-accent transition-colors',
props.class
)
"

View File

@@ -37,7 +37,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
v-bind="{ ...forwarded, ...$attrs }"
:class="
cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'z-50 w-72 rounded-md border border-popover-border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class
)
"

View File

@@ -39,7 +39,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
style="max-height: var(--radix-select-content-available-height)"
:class="
cn(
'select-content relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'select-content relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border border-popover-border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
props.class

View File

@@ -20,7 +20,7 @@ const forwardedProps = useForwardProps(delegatedProps)
v-bind="forwardedProps"
:class="
cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background dark:bg-slate-950 contrast-more:dark:bg-slate-900 px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 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
)
"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,7 +47,7 @@ const updatedAt = computed<string>(() => props.world.updatedAt ? DateTime.fromIS
</UiCardHeader>
<UiCardContent class="grow">
<p class="italic">{{ world.description }}</p>
<div class="md:text-sm" v-html="world.description"></div>
<div
v-if="isCardHovered || isCardFocused"

View File

@@ -11,6 +11,9 @@ onMounted(() => {
worldSkeleton.value = { ...defaultWorld }
})
const newWorldName = shallowRef<HTMLInputElement>()
useFocus(newWorldName, { initialValue: true })
const isLoading = ref<boolean>(false)
/**
@@ -61,22 +64,20 @@ function handleFormCancel() {
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
<div class="grid gap-4">
<input
ref="newWorldName"
id="new-world-name"
v-model="worldSkeleton.name"
type="text"
name="new-world-name"
required
:placeholder="$t('common.title')"
class="w-full -my-1 mb-4 py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600"
class="w-full -my-1 py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-primary invalid:border-destructive"
@input="handleNameChange"
>
<textarea
id="new-world-description"
<InputEditor
disable-blocks
v-model="worldSkeleton.description"
name="new-world-description"
:placeholder="$t('entity.addDescription')"
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600"
/>
<div class="-mx-1 grid gap-3">

View File

@@ -19,6 +19,9 @@ onMounted(() => {
worldSkeleton.value = { ...props.world } as World
})
const newWorldName = shallowRef<HTMLInputElement>()
useFocus(newWorldName, { initialValue: true })
const isLoading = ref<boolean>(false)
/**
@@ -38,6 +41,8 @@ async function handleSubmit() {
if (error) {
console.log(error.message)
isLoading.value = false
return
}
toast({
@@ -73,22 +78,20 @@ function handleFormCancel() {
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
<div class="grid gap-4">
<input
ref="newWorldName"
id="new-world-name"
v-model="worldSkeleton.name"
type="text"
name="new-world-name"
required
:placeholder="$t('common.title')"
class="w-full -my-1 py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600"
class="w-full -my-1 py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-primary invalid:border-destructive"
@input="handleNameChange"
>
<textarea
id="new-world-description"
<InputEditor
disable-blocks
v-model="worldSkeleton.description"
name="new-world-description"
:placeholder="$t('entity.addDescription')"
class="w-full -my-1 py-1 -mx-1 px-1 min-h-24 max-h-36 text-sm border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600"
/>
<div class="-mx-1 grid gap-3">

View File

@@ -60,38 +60,32 @@ switch (statusCode) {
<template>
<div class="h-screen">
<div class="h-full grid grid-cols-[auto_1fr] dark:bg-black transition-colors">
<Sidebar />
<div class="h-full w-full grid place-items-center transition-colors">
<Head>
<Title>{{ $t(titleKey) }}</Title>
</Head>
<div class="wrapper shadow-body-light dark:shadow-body-dark transition-all">
<div class="h-full w-full grid place-items-center">
<Head>
<Title>{{ $t(titleKey) }}</Title>
</Head>
<div class="grid text-center justify-items-center opacity-80">
<PhImageBroken v-if="statusCode === 404" size="100" class="opacity-60" />
<PhLinkBreak v-else-if="statusCode === 500" size="100" class="opacity-60" />
<PhBugBeetle v-else size="100" class="opacity-60" />
<div class="grid text-center justify-items-center opacity-80">
<PhImageBroken v-if="statusCode === 404" size="100" class="opacity-60" />
<PhLinkBreak v-else-if="statusCode === 500" size="100" class="opacity-60" />
<PhBugBeetle v-else size="100" class="opacity-60" />
<Heading level="h0">
{{ $t(titleKey) }}
</Heading>
<Heading level="h0">
{{ $t(titleKey) }}
</Heading>
<div class="mt-6 md:text-lg">
<p>{{ $t(descriptionKey) }}</p>
<p>{{ $t(subDescriptionKey) }}</p>
</div>
<UiButton variant="default" class="mt-6 gap-2" as-child>
<RouterLink to="/">
<PhArrowBendDoubleUpLeft size="24" />
{{ $t('ui.backToHome') }}
</RouterLink>
</UiButton>
</div>
<div class="mt-6 md:text-lg">
<p>{{ $t(descriptionKey) }}</p>
<p>{{ $t(subDescriptionKey) }}</p>
</div>
<UiButton variant="default" class="mt-6 gap-2" as-child>
<RouterLink to="/">
<PhArrowBendDoubleUpLeft size="24" />
{{ $t('ui.backToHome') }}
</RouterLink>
</UiButton>
</div>
</div>
</div>

13
app/layouts/default.vue Normal file
View File

@@ -0,0 +1,13 @@
<template>
<div
class="h-full grid md:grid-cols-[auto_1fr] bg-background transition-colors after:absolute after:transition-colors"
>
<Sidebar />
<div
class="wrapper max-h-screen transition-all overflow-y-auto"
>
<slot />
</div>
</div>
</template>

36
app/layouts/public.vue Normal file
View File

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

View File

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

View File

@@ -1,7 +1,13 @@
<script lang="ts" setup>
import { PhCircleNotch } from '@phosphor-icons/vue'
const { locale } = useI18n()
const { data: page, refresh } = await useAsyncData(`about-content-${locale}`, () => {
definePageMeta({
layout: "public"
})
const { data: page, refresh, status } = useLazyAsyncData(`about-content-${locale}`, () => {
return queryCollection("sections").path(`/sections/${locale.value}/legal`).first()
})
@@ -9,25 +15,19 @@ watch(locale, () => refresh())
</script>
<template>
<main class="py-8 px-5 md:px-8 overflow-y-auto">
<Head>
<Title>{{ $t("pages.about.title") }}</Title>
</Head>
<Head>
<Title>{{ $t("pages.about.title") }}</Title>
</Head>
<div class="container max-md:px-0">
<Spacing size="lg">
<div class="flex items-center gap-3">
<div class="md:hidden">
<SidebarToggle />
</div>
<main class="overflow-y-auto py-8 px-5 md:px-8 grid gap-4 grid-rows-[auto_1fr]">
<Heading level="h1">
{{ $t("pages.about.title") }}
</Heading>
<Heading level="h1">
{{ $t("pages.about.title") }}
</Heading>
</div>
<ContentRenderer v-if="page" :value="page" class="content" />
</Spacing>
<div v-if="status === 'pending'" class="grid place-items-center">
<PhCircleNotch size="50" class="opacity-33 animate-spin"/>
</div>
<LazyContentRenderer v-else-if="status === 'success' && page" :value="page" class="content max-w-4xl" />
</main>
</template>

View File

@@ -44,7 +44,7 @@ watch(isLoading, (n) => {
<p>
{{ $t('entity.calendar.isLoading') }}
</p>
<PhCircleNotch size="50" class="animate-spin"/>
<PhCircleNotch size="50" class="opacity-33 animate-spin"/>
</div>
</div>

View File

@@ -2,12 +2,13 @@
import type { Calendar } from "@@/models/CalendarConfig";
definePageMeta({
middleware: ["reset-menu"]
layout: "public"
})
const { data: availableCalendars, status: calendarStatus } = await useLazyFetch<{ data: Calendar[] }>("/api/calendars/query", { key: "explore-calendars", query: { full: true } })
const isLoading = computed(() => calendarStatus.value === "pending")
// const { data: availableCalendars, status: calendarStatus } = useLazyFetch<{ data: Calendar[] }>("/api/calendars/query", { key: "explore-calendars", query: { full: true } })
const { data: calendars, status } = useLazyAsyncData<{ data: Calendar[] }>("explore-calendars", () => {
return $fetch("/api/calendars/query", { query: { full: true }})
})
</script>
<template>
@@ -17,29 +18,26 @@ const isLoading = computed(() => calendarStatus.value === "pending")
</Head>
<Spacing size="lg">
<div class="flex items-center gap-3">
<div class="md:hidden">
<SidebarToggle />
</div>
<Heading level="h1">
{{ $t("pages.explore.title") }}
</Heading>
</div>
<Heading level="h1">
{{ $t("pages.explore.title") }}
</Heading>
<Spacing size="lg">
<Heading level="h2">
{{ $t("entity.calendar.namePublicPlural") }}
</Heading>
<div v-if="isLoading" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
<LoadingCard />
<div v-if="status === 'pending'" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
<LazyCardLoading />
</div>
<ul v-else-if="availableCalendars?.data" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
<li v-for="calendar in availableCalendars.data" :key="calendar.shortId">
<CalendarPreviewCard :calendar="calendar" :gm-id="calendar.world?.gmId" />
</li>
</ul>
<template v-else-if="status === 'success' && calendars?.data">
<ul class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
<li v-for="calendar in calendars.data" :key="calendar.shortId">
<LazyCalendarPreviewCard :calendar="calendar" :gm-id="calendar.world?.gmId" />
</li>
</ul>
</template>
</Spacing>
</Spacing>
</main>

View File

@@ -4,12 +4,12 @@ useHead({
})
definePageMeta({
middleware: ["reset-menu"]
layout: "public"
})
</script>
<template>
<main class="py-8 px-5 md:px-8">
<main class="h-full grid place-items-center py-8 px-5 md:px-8">
<Head>
<Title>
TTTools {{ $t('head.title') }}
@@ -17,6 +17,17 @@ definePageMeta({
<Meta name="description" :content="$t('head.description')" />
</Head>
<Heading level="h1">TTTools</Heading>
<div class="py-8 md:py-24 px-4">
<div class="flow text-center max-w-4xl mx-auto">
<h1 class="text-3xl md:text-5xl font-bold md:font-semibold">
{{ $t('pages.home.h1') }}
</h1>
<p class="text-sm">
{{ $t('pages.home.tagline') }}
</p>
<UserDashboardLink />
</div>
</div>
</main>
</template>

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

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

View File

@@ -51,7 +51,7 @@ watch([calendar], () => {
<p>
{{ $t('entity.calendar.isLoading') }}
</p>
<PhCircleNotch size="50" class="animate-spin"/>
<PhCircleNotch size="50" class="opacity-33 animate-spin"/>
</div>
</div>

View File

@@ -9,10 +9,10 @@ definePageMeta({
middleware: ["auth-guard", "reset-menu"]
})
const { data: worlds, status: worldStatus } = await useLazyFetch<{ data: World[] }>("/api/worlds/query", { query: { gmId: user?.value!.id }, key: "user-worlds", keepalive: true })
const sortedWorlds = computed(() => worlds.value?.data ? [...worlds.value.data].sort((a, b) => (a.id ?? 0) - (b.id ?? 0)) : [])
const { data: worlds, status: worldStatus, refresh } = await useLazyFetch<{ data: World[] }>("/api/worlds/query", { query: { gmId: user?.value!.id }, key: "user-worlds", keepalive: true })
const sortedWorlds = computed(() => worlds.value?.data ? worlds.value.data.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)) : [])
const isLoading = computed(() => worldStatus.value === "idle")
const isLoading = computed(() => worldStatus.value === "pending")
// Redirect user back home when they log out on the page
watch(user, (n) => {
@@ -33,49 +33,13 @@ function hideCreateDialog() {
/** Active world channel */
let worldChannel: RealtimeChannel
/** Handles world insertion realtime events */
function handleInsertedWorld(newWorld: WorldChannelPayload) {
if (!worlds.value?.data) return
newWorld.createdAt = newWorld.created_at;
newWorld.gmId = newWorld.gm_id;
worlds.value?.data.push(newWorld)
}
/** Handles world deletion realtime events */
function handleDeletedWorld(id: number) {
if (!worlds.value?.data) return
worlds.value.data.splice(worlds.value.data.findIndex(w => w.id === id), 1)
}
onMounted(() => {
worldChannel = supabase.channel("realtime-world-channel")
worldChannel = supabase.channel("realtime-worlds-channel")
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "worlds" },
async (payload) => {
switch (payload.eventType) {
case "INSERT":
handleInsertedWorld(payload.new as World)
break
case "DELETE":
handleDeletedWorld(payload.old.id)
break
case "UPDATE":
if (!worlds.value?.data) return
worlds.value.data = (await $fetch("/api/worlds/query", { query: { gmId: user?.value!.id } })).data as World[]
break
default:
console.log("Unknown event has been triggered. This should not happen unless Supabase added one somehow.")
console.log(payload)
break
}
refresh()
}
)
.subscribe()
@@ -136,21 +100,21 @@ function hideEditModal() {
</div>
<div v-if="isLoading" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
<LoadingCard />
<LazyCardLoading />
</div>
<ul v-else-if="worlds?.data" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
<li v-for="world in sortedWorlds" :key="world.id">
<WorldPreviewCard :world="world" @on-edit="() => deployEditModal(world)" @on-delete="() => deployDeleteModal(world)" />
</li>
<li class="xl:w-fit">
<AddCard @on-click="() => isCreateWorldModalOpen = true">
<LazyCardAdd @on-click="() => isCreateWorldModalOpen = true">
<template v-if="worlds?.data?.length > 0">
{{ $t('entity.world.addSingle') }}
</template>
<template v-else>
{{ $t('entity.world.addSingleFirst') }}
</template>
</AddCard>
</LazyCardAdd>
</li>
</ul>
</Spacing>

View File

@@ -8,13 +8,6 @@ const supabase = useSupabaseClient()
const route = useRoute()
const id = route.params.id
const { data: world, status } = await useFetch<{ data: World }>("/api/worlds/query", { query: { id, full: true } })
const sortedCalendars = computed(() => world.value?.data.calendars ? [...world.value.data.calendars].sort((a, b) => (a.id ?? 0) - (b.id ?? 0)) : [])
definePageMeta({
middleware: ["auth-guard"]
})
const user = useSupabaseUser()
// Redirect user back home when they log out on the page
@@ -24,105 +17,45 @@ watch(user, (n) => {
}
})
// Set custom menu
// This should be reserved for actions, not for breadcrumbs
//
// const { setCurrentMenu } = useUiStore()
definePageMeta({
middleware: ["auth-guard"]
})
// setCurrentMenu([
// {
// tooltip: t("entity.world.backToMy"),
// to: "/my",
// phIcon: "universe",
// highlight: true
// },
// ])
const { data: world, status, refresh } = await useAsyncData<{ data: World }>('user-world', () => {
return $fetch("/api/worlds/query", { query: { id }})
})
const worldGmId = world.value?.data.gmId
const { data: calendars, status: calendarStatus, refresh: refreshCalendars } = useLazyFetch<{ data: Calendar[] }>('/api/calendars/query', { query: { worldId: world.value?.data.id, full: true }})
const sortedCalendars = computed(() => calendars.value?.data ? calendars.value.data.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)) : [])
/**
* === Subscriptions ===
*/
/** Active calendar channel */
let calendarChannel: RealtimeChannel
/** Active world channel */
let worldChannel: RealtimeChannel
/** Handles calendar insertion realtime events */
function handleInsertedCalendar(newCalendar: CalendarChannelPayload) {
if (!world.value) return
newCalendar.createdAt = newCalendar.created_at
newCalendar.eventNb = [{ count: 0 }]
world.value.data.calendars?.push(newCalendar)
}
/** Handles calendar deletion realtime events */
function handleDeletedCalendar(id: number) {
if (!world.value) return
world.value.data.calendars?.splice(world.value.data.calendars.findIndex(c => c.id === id), 1)
}
onMounted(() => {
calendarChannel = supabase.channel("realtime-calendar-channel")
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "calendars" },
async (payload) => {
switch (payload.eventType) {
case "INSERT":
handleInsertedCalendar(payload.new as Calendar)
break
case "DELETE":
handleDeletedCalendar(payload.old.id)
break
// Maybe this case could be handled better than doing a separate API call
case "UPDATE":
if (!world.value?.data) return
world.value.data = (await $fetch<{ data: World }>("/api/worlds/query", { query: { id, full: true } })).data
break
default:
console.log("Unknown event has been triggered. This should not happen unless Supabase added one somehow.")
break
}
}
)
.subscribe()
})
onUnmounted(() => {
// Unsubscribe from realtime
supabase.removeChannel(calendarChannel)
})
// Register channels
onMounted(() => {
worldChannel = supabase.channel("realtime-world-channel")
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "worlds" },
async (payload) => {
switch (payload.eventType) {
case "UPDATE":
if (!world.value?.data) return
world.value.data = (await $fetch<{ data: World }>("/api/worlds/query", { query: { id, full: true } })).data
break
default:
console.log("Unknown event has been triggered. This should not happen unless Supabase added one somehow.")
console.log(payload)
break
}
}
async () => refresh()
)
.subscribe()
calendarChannel = supabase.channel("realtime-calendar-channel")
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "calendars" },
async () => refreshCalendars()
)
.subscribe()
})
// Unsubscribe from realtime
onUnmounted(() => {
// Unsubscribe from realtime
supabase.removeChannel(calendarChannel)
supabase.removeChannel(worldChannel)
})
@@ -169,11 +102,60 @@ function hideEditModal() {
<Title>{{ $t("entity.world.namePlural") }}</Title>
</Head>
<Heading level="h1">
{{ $t('entity.isLoading') }}
</Heading>
<header class="mb-8">
<Spacing size="lg">
<Breadcrumb
:items="[]"
/>
<div class="lg:w-1/2">
<div class="flex items-center gap-2 mb-2">
<div class="flex items-center gap-3">
<UiSkeleton class="h-10 w-32 max-md:max-w-full" />
</div>
<UiButton size="icon" class="rounded-full size-8" disabled>
<PhPencil size="17" weight="fill" />
</UiButton>
</div>
<div class="grid gap-1">
<UiSkeleton class="h-6 w-full max-md:max-w-full" />
<UiSkeleton class="h-6 w-8/12 max-md:max-w-full" />
<UiSkeleton class="h-6 w-7/12 max-md:max-w-full" />
</div>
</div>
</Spacing>
</header>
</template>
<template v-else-if="world?.data">
<template v-else-if="!world?.data && status === 'error'">
<div class="h-full w-full grid place-items-center">
<Head>
<Title>{{ $t("entity.world.notFound") }}</Title>
</Head>
<div class="grid justify-items-center opacity-80">
<PhGlobeHemisphereWest size="75" class="opacity-60" weight="fill" />
<Heading level="h1">
{{ $t("entity.world.notFound") }}
</Heading>
<p>
{{ $t('entity.world.notFoundDescription') }}
</p>
<UiButton variant="default" class="mt-4 gap-2" as-child>
<RouterLink to="/my">
<PhArrowBendDoubleUpLeft size="24" />
{{ $t('entity.world.backToList') }}
</RouterLink>
</UiButton>
</div>
</div>
</template>
<template v-else-if="world?.data && status === 'success'">
<Head>
<Title>{{ world.data.name }}</Title>
</Head>
@@ -208,74 +190,75 @@ function hideEditModal() {
</UiTooltipProvider>
</div>
<p>{{ world.data.description }}</p>
<div class="content-editor" v-html="world.data.description" />
</div>
</Spacing>
</header>
</template>
<section>
<Spacing size="lg">
<div class="flex items-center gap-3">
<Heading level="h2">
{{ $t('entity.calendar.namePlural') }}
</Heading>
<template v-if="world?.data">
<LazyWorldDialogEdit :world="world.data" :modal-state="isEditWorldModalOpen" @on-close="hideEditModal" />
<LazyCalendarDialogCreate :world="world.data" :modal-state="isCreateCalendarModalOpen" @on-close="hideCreateDialog" />
<LazyCalendarDialogUpdate :world="world.data" :calendar="markedCalendar" :modal-state="isUpdateCalendarModalOpen" @on-close="hideUpdateDialog" />
<LazyCalendarDialogDelete :calendar="markedCalendar" :modal-state="isDeleteCalendarModalOpen" @on-close="hideDeleteCalendarModal"/>
</template>
<section>
<Spacing size="lg">
<div class="flex items-center gap-3">
<Heading level="h2">
{{ $t('entity.calendar.namePlural') }}
</Heading>
</div>
<template v-if="calendarStatus === 'pending'">
<ul class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
<li>
<LazyCardLoading />
</li>
<li class="w-64 max-w-full">
<LazyCardLoading />
</li>
</ul>
</template>
<template v-else-if="!calendars?.data && calendarStatus === 'error'">
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
<LazyCardError>
<template #title>
{{ $t('error.default.title') }}
</template>
<template #content>
{{ $t('entity.calendar.error.cannotFindAny') }}
</template>
</LazyCardError>
</div>
</template>
<template v-else-if="calendars?.data && calendarStatus === 'success' && worldGmId">
<ul class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
<li v-for="calendar in sortedCalendars" :key="calendar.id">
<CalendarPreviewCard
<LazyCalendarPreviewCard
:calendar="calendar"
:gm-id="world.data.gmId"
:gm-id="worldGmId"
show-actions
@on-edit="() => deployUpdateDialog(calendar)"
@on-delete="() => deployDeleteCalendarModal(calendar)" />
</li>
<li class="xl:w-fit">
<AddCard @on-click="() => isCreateCalendarModalOpen = true">
<LazyCardAdd @on-click="() => isCreateCalendarModalOpen = true">
<template v-if="sortedCalendars.length > 0">
{{ $t('entity.calendar.addSingle') }}
</template>
<template v-else>
{{ $t('entity.calendar.addSingleFirst') }}
</template>
</AddCard>
</LazyCardAdd>
</li>
</ul>
</Spacing>
</section>
<WorldDialogEdit :world="world.data" :modal-state="isEditWorldModalOpen" @on-close="hideEditModal" />
<CalendarDialogCreate :world="world.data" :modal-state="isCreateCalendarModalOpen" @on-close="hideCreateDialog" />
<CalendarDialogUpdate :world="world.data" :calendar="markedCalendar" :modal-state="isUpdateCalendarModalOpen" @on-close="hideUpdateDialog" />
<CalendarDialogDelete :calendar="markedCalendar" :modal-state="isDeleteCalendarModalOpen" @on-close="hideDeleteCalendarModal"/>
</template>
<template v-else>
<div class="h-full w-full grid place-items-center">
<Head>
<Title>{{ $t("entity.world.notFound") }}</Title>
</Head>
<div class="grid justify-items-center opacity-80">
<PhGlobeHemisphereWest size="75" class="opacity-60" weight="fill" />
<Heading level="h1">
{{ $t("entity.world.notFound") }}
</Heading>
<p>
{{ $t('entity.world.notFoundDescription') }}
</p>
<UiButton variant="default" class="mt-4 gap-2" as-child>
<RouterLink to="/my">
<PhArrowBendDoubleUpLeft size="24" />
{{ $t('entity.world.backToList') }}
</RouterLink>
</UiButton>
</div>
</div>
</template>
</template>
</Spacing>
</section>
</main>
</template>

View File

@@ -10,6 +10,8 @@ import type { CalendarEvent } from "@@/models/CalendarEvent"
import type { CalendarMonth } from "@@/models/CalendarMonth"
import type { Category } from "@@/models/Category"
type CalendarState = "active" | "idle"
type CalendarViewType = "month" | "year" | "decade" | "century"
type CalendarCurrentConfig = {
@@ -130,7 +132,7 @@ export const useCalendar = defineStore("calendar", () => {
return Number(params.month)
})
const currentMonthData = computed<CalendarMonth>(() => {
return sortedMonths.value[currentMonth.value]
return sortedMonths.value[currentMonth.value]!
})
// Gets the label from currentMonth index
const currentMonthName = computed<string>(() => getMonthName(currentMonth.value))
@@ -605,7 +607,7 @@ export const useCalendar = defineStore("calendar", () => {
// Else, we need to accelerate and decelerate
else {
// First, get all the remaining days in the current month
const currentMonth = sortedMonths.value[datePivot.month]
const currentMonth = sortedMonths.value[datePivot.month]!
dateAcc.day = currentMonth.days - datePivot.day
if (direction === "future") {
datePivot.month = getNextViewMonth(datePivot.month)
@@ -668,6 +670,90 @@ export const useCalendar = defineStore("calendar", () => {
currentEvents.value = computeCurrentEvents()
}, { 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
*
@@ -756,7 +842,7 @@ export const useCalendar = defineStore("calendar", () => {
// Loop over all event once to convert the structure to a usable one
for (let i = 0; i < allEvents.value.length; i++) {
const e: CalendarEvent = allEvents.value[i]
const e: CalendarEvent = allEvents.value[i]!
// Estimate distance from pivot
const startDateDays: number = convertDateToDays(e.startDate)
@@ -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
*/
@@ -830,6 +932,7 @@ export const useCalendar = defineStore("calendar", () => {
const isCreatingEvent = ref<boolean>(false)
const isUpdatingEvent = ref<boolean>(false)
const isDeletingEvent = ref<boolean>(false)
const isDraggingEvent = ref<boolean>(false)
const operationInProgress = computed<boolean>(() => isCreatingEvent.value || isUpdatingEvent.value || isDeletingEvent.value)
let abortController: AbortController | null = null
@@ -935,6 +1038,19 @@ export const useCalendar = defineStore("calendar", () => {
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 {
isReadOnly,
setReadStatus,
@@ -974,16 +1090,22 @@ export const useCalendar = defineStore("calendar", () => {
areDatesIdentical,
compareDates,
getRelativeString,
toPastFar,
toPastNear,
toFutureNear,
toFutureFar,
baseEvents,
allEvents,
categories,
currentEvents,
calendarState,
getRelativeEventFromDate,
getRelativeEventFromEvent,
cancelLatestRequest,
isCreatingEvent,
isUpdatingEvent,
isDeletingEvent,
isDraggingEvent,
operationInProgress,
eventSkeleton,
resetSkeleton,
@@ -991,6 +1113,8 @@ export const useCalendar = defineStore("calendar", () => {
lastActiveEvent,
updateEventFromSkeleton,
deleteEventFromSkeleton,
isCreatingEventModalOpen,
toggleCreatingEventModal,
isEditEventModalOpen,
revealEditEventModal,
isDeleteEventModalOpen,

View File

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

View File

@@ -32,6 +32,7 @@ export default defineI18nConfig(() => ({
save: "Save",
delete: "Delete",
edit: "Edit",
close: "Close"
},
colors: {
label: "Color",
@@ -69,12 +70,14 @@ export default defineI18nConfig(() => ({
backToHome: "Back to home",
sidebarMenu: {
profile: "Profile",
projects: "Projects",
appearance: "Appearance",
language: "Language",
account: "Account",
login: "Log in",
logout: "Log out",
avatarFallback: "Profile",
about: "About",
},
dark: "Dark",
light: "Light",
@@ -86,6 +89,17 @@ export default defineI18nConfig(() => ({
createdAt: "Created {createdAt}",
updatedAt: "Last updated {updatedAt}",
},
editor: {
bold: "Bold",
italic: "Italic",
strike: "Strikethrough",
ulist: "Bullet list",
olist: "Numbered list",
quote: "Quote",
noFormat: "Remove formatting",
undo: "Undo",
redo: "Redo"
},
entity: {
category: {
nameSingular: "Category",
@@ -127,7 +141,7 @@ export default defineI18nConfig(() => ({
world: {
nameSingular: "World",
namePlural: "Worlds",
backToMy: "Back to my universe",
backToMy: "Back to projects",
backToSingle: "Back to {world}",
addSingle: "Add a world",
addSingleFirst: "Add your first world !",
@@ -158,7 +172,7 @@ export default defineI18nConfig(() => ({
nameSingular: "Calendar",
namePlural: "Calendars",
namePublicSingular: "Public Calendar",
namePublicPlural: "Public Calendars",
namePublicPlural: "Calendars",
addSingle: "Add a calendar",
addSingleFirst: "Add your first calendar !",
notFound: "Calendar not found",
@@ -206,11 +220,15 @@ export default defineI18nConfig(() => ({
title: "Event title",
isStart: "Start",
isEnd: "End",
gotoStart: "Go to the start of this event",
gotoEnd: "Go to the end of this event",
isHidden: "Hidden",
isPublic: "Public",
hiddenLabel: "Hide this event",
hiddenTooltip: "This event is visible only to game masters.",
addLocation: "Add a place",
oneOtherEvent: "1 other",
multipleOtherEvents: "{count} others",
prevPage: "Previous page with events",
nextPage: "Next page with events",
outOfBoundsTitle: "No next or previous events were found",
@@ -241,6 +259,9 @@ export default defineI18nConfig(() => ({
title: "The event \"{event}\" has been successfuly deleted.",
},
},
error: {
cannotFindAny: "We couldn't fetch your calendars…"
},
actionDialog: {
tabs: {
general: {
@@ -311,13 +332,17 @@ export default defineI18nConfig(() => ({
}
},
pages: {
home: {
h1: "A home for your creativity",
tagline: "Visualize your fantasy or sci-fi worlds with our interactive calendars"
},
explore: {
menuLabel: "Explore",
title: "Explore worlds",
},
profile: {
title: "{user} — My universe",
metaTitle: "My universe",
title: "{user} — Projects",
metaTitle: "Projects",
},
about: {
title: "About this app",
@@ -328,7 +353,7 @@ export default defineI18nConfig(() => ({
breadcrumbs: {
home: "Home",
explore: "Explore",
profile: "Universe",
profile: "Projects",
world: "World",
calendar: "Calendar"
}
@@ -362,6 +387,7 @@ export default defineI18nConfig(() => ({
save: "Sauvegarder",
delete: "Supprimer",
edit: "Modifier",
close: "Fermer"
},
colors: {
label: "Couleur",
@@ -399,12 +425,14 @@ export default defineI18nConfig(() => ({
backToHome: "Retourner à l'accueil",
sidebarMenu: {
profile: "Profil",
projects: "Projets",
appearance: "Apparence",
language: "Langue",
account: "Compte",
login: "Connexion",
logout: "Déconnexion",
avatarFallback: "Profil",
about: "À propos",
},
dark: "Sombre",
light: "Clair",
@@ -416,6 +444,17 @@ export default defineI18nConfig(() => ({
createdAt: "Créé le {createdAt}",
updatedAt: "Dernière modification le {updatedAt}",
},
editor: {
bold: "Gras",
italic: "Italique",
strike: "Barré",
ulist: "Liste à puces",
olist: "Liste numerotée",
quote: "Citation",
noFormat: "Effacer le formattage",
undo: "Annuler",
redo: "Rétablir"
},
entity: {
category: {
nameSingular: "Catégorie",
@@ -457,7 +496,7 @@ export default defineI18nConfig(() => ({
world: {
nameSingular: "Monde",
namePlural: "Mondes",
backToMy: "Retourner à mon univers",
backToMy: "Retourner à Projets",
backToSingle: "Retourner sur {world}",
addSingle: "Ajouter un monde",
addSingleFirst: "Ajouter votre premier monde !",
@@ -536,11 +575,15 @@ export default defineI18nConfig(() => ({
title: "Titre de l'évènement",
isStart: "Début",
isEnd: "Fin",
gotoStart: "Aller au début de l'évènement",
gotoEnd: "Aller à la fin de l'évènement",
isHidden: "Privé",
isPublic: "Public",
hiddenLabel: "Rendre l'évènement privé",
hiddenTooltip: "Cet évènement est uniquement visible pour les maîtres du jeu.",
addLocation: "Ajouter un endroit",
oneOtherEvent: "1 autre",
multipleOtherEvents: "{count} autres",
prevPage: "Précédente page à évènements",
nextPage: "Prochaine page à évènements",
outOfBoundsTitle: "Aucun évènement suivant ou précédent trouvé",
@@ -571,6 +614,9 @@ export default defineI18nConfig(() => ({
title: "L'évènement \"{event}\" a été supprimé avec succès.",
},
},
error: {
cannotFindAny: "Nous n'avons pas pu récupérer vos calendriers…"
},
actionDialog: {
tabs: {
general: {
@@ -645,13 +691,17 @@ export default defineI18nConfig(() => ({
}
},
pages: {
home: {
h1: "Un espace pour votre créativité",
tagline: "Visualisez vos mondes fantasy ou de science-fiction grâce à nos calendriers interactifs"
},
explore: {
menuLabel: "Explorer",
title: "Explorer les mondes",
},
profile: {
title: "{user} — Mon univers",
metaTitle: "Mon univers",
title: "{user} — Projets",
metaTitle: "Projets",
},
about: {
title: "À propos",
@@ -662,7 +712,7 @@ export default defineI18nConfig(() => ({
breadcrumbs: {
home: "Accueil",
explore: "Explorer",
profile: "Univers",
profile: "Projets",
world: "Monde",
calendar: "Calendrier"
}

View File

@@ -1,3 +1,4 @@
import { defineNuxtConfig } from 'nuxt/config'
import tailwindcss from "@tailwindcss/vite";
// https://nuxt.com/docs/api/configuration/nuxt-config
@@ -12,8 +13,9 @@ export default defineNuxtConfig({
"@nuxt/eslint",
"shadcn-nuxt",
"@nuxtjs/i18n",
"@nuxthub/core",
"@nuxt/content",
"@nuxthub/core",
"@nuxt/image",
],
css: ["@/assets/main.css"],
@@ -67,5 +69,5 @@ export default defineNuxtConfig({
},
eslint: {},
compatibilityDate: "2025-04-16",
compatibilityDate: "latest",
})

View File

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

7288
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -41,12 +41,7 @@ export default defineEventHandler(async (event) => {
state,
createdAt:created_at,
updatedAt:updated_at,
calendars (
createdAt:created_at,
updatedAt:updated_at,
months:calendar_months(*),
eventNb:calendar_events(count)
)
gmId:gm_id
`
let output

View File

@@ -43,7 +43,7 @@ create table public.worlds (
id bigint generated by default as identity primary key,
name text not null,
description text,
color app_colors default 'black',
color app_colors default 'emerald',
state world_state default 'draft',
created_at timestamptz default now(),
updated_at timestamptz,
@@ -69,7 +69,7 @@ create table public.calendars (
short_id text unique,
name text not null,
today json not null,
color app_colors default 'black',
color app_colors default 'emerald',
state calendar_state default 'draft',
created_at timestamptz default now(),
updated_at timestamptz,