20 Commits

Author SHA1 Message Date
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
65 changed files with 2596 additions and 1520 deletions

View File

@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ConfigProvider } from "radix-vue" import { ConfigProvider } from "radix-vue"
import { cn } from "./lib/utils";
useHead({ useHead({
titleTemplate: (titleChunk) => { titleTemplate: (titleChunk) => {
@@ -27,33 +26,17 @@ useHead({
}) })
const useIdFunction = () => useId() const useIdFunction = () => useId()
const { isSidebarOpened } = storeToRefs(useUiStore())
</script> </script>
<template> <template>
<div class="h-screen"> <div class="h-screen">
<NuxtLoadingIndicator /> <NuxtLoadingIndicator />
<NuxtLayout> <ConfigProvider :use-id="useIdFunction">
<ConfigProvider :use-id="useIdFunction"> <NuxtLayout>
<div <NuxtPage />
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" </NuxtLayout>
:class="cn({ </ConfigProvider>
'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>
<ClientOnly> <ClientOnly>
<UiToaster /> <UiToaster />

View File

@@ -116,6 +116,11 @@
--color-event-white-background: var(--event-white-background); --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-button,
.event-popover, .event-popover,
.event-callout, .event-callout,

View File

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

View File

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

View File

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

View File

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

View File

@@ -51,7 +51,7 @@ function closeDeleteDialog() {
<UiDialogContent <UiDialogContent
:disable-outside-pointer-events="true" :disable-outside-pointer-events="true"
:trap-focus="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" @escape-key-down="closeDeleteDialog"
@focus-outside="closeDeleteDialog" @focus-outside="closeDeleteDialog"
@interact-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"> <div class="border-y-[1px] border-b-foreground/10 mr-4">
<form <form
ref="rowRef" 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" @submit.prevent="submitNew"
> >
<div v-if="currentMode === 'add'" class="col-span-1" /> <div v-if="currentMode === 'add'" class="col-span-1" />

View File

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

View File

@@ -59,7 +59,7 @@ watch(currentRPGDate, () => {
:collision-padding="60" :collision-padding="60"
:disable-outside-pointer-events="true" :disable-outside-pointer-events="true"
:trap-focus="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" @escape-key-down.prevent="handleClosing"
@pointer-down-outside.prevent="handleClosing" @pointer-down-outside.prevent="handleClosing"
> >

View File

@@ -27,7 +27,7 @@ function handleClosing(e: Event): void {
<UiAlertDialogContent <UiAlertDialogContent
:disable-outside-pointer-events="true" :disable-outside-pointer-events="true"
:trap-focus="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" @escape-key-down="handleClosing"
@focus-outside="handleClosing" @focus-outside="handleClosing"
@interact-outside="handleClosing" @interact-outside="handleClosing"

View File

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

View File

@@ -109,7 +109,7 @@ function handleFormCancel() {
name="new-calendar-name" name="new-calendar-name"
required required
:placeholder="$t('common.title')" :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" @input="handleNameChange"
> >

View File

@@ -66,7 +66,7 @@ function handleCancel() {
:placeholder="$t('entity.calendar.event.title')" :placeholder="$t('entity.calendar.event.title')"
:maxlength="120" :maxlength="120"
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,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"> <div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.title') }} {{ t('entity.calendar.event.patterns.title') }}
@@ -74,14 +74,11 @@ function handleCancel() {
</div> </div>
<div class="col-span-2 my-2 pl-8"> <div class="col-span-2 my-2 pl-8">
<textarea <InputEditor
id="new-event-description" disable-blocks
v-model="eventSkeleton.description" 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"> <div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.description') }} {{ t('entity.calendar.event.patterns.description') }}
</div> </div>
@@ -132,7 +129,7 @@ function handleCancel() {
:placeholder="$t('entity.calendar.event.addLocation')" :placeholder="$t('entity.calendar.event.addLocation')"
:maxlength="160" :maxlength="160"
pattern="([A-Za-zÀ-ÖØ-öø-ÿ0-9\s\&\-\~]+){3,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"> <div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.location') }} {{ t('entity.calendar.event.patterns.location') }}

View File

@@ -91,7 +91,7 @@ function handleFormCancel() {
name="new-calendar-name" name="new-calendar-name"
required required
:placeholder="$t('common.title')" :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" @input="handleNameChange"
> >

View File

@@ -82,7 +82,7 @@ function handleCancel() {
:placeholder="$t('entity.calendar.event.title')" :placeholder="$t('entity.calendar.event.title')"
:minlength="3" :minlength="3"
:maxlength="120" :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"> <div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.title') }} {{ t('entity.calendar.event.patterns.title') }}
@@ -92,14 +92,11 @@ function handleCancel() {
</div> </div>
<div class="col-span-2 my-2 ml-8"> <div class="col-span-2 my-2 ml-8">
<textarea <InputEditor
id="new-event-description" disable-blocks
v-model="eventSkeleton.description" 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"> <div class="mt-2 mb-1 text-xs opacity-50">
{{ t('entity.calendar.event.patterns.description') }} {{ t('entity.calendar.event.patterns.description') }}
</div> </div>
@@ -152,7 +149,7 @@ function handleCancel() {
:placeholder="$t('entity.calendar.event.addLocation')" :placeholder="$t('entity.calendar.event.addLocation')"
:minlength="3" :minlength="3"
:maxlength="160" :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"> <div class="mt-2 mb-1 text-xs opacity-50">

View File

@@ -37,7 +37,7 @@ const filteredCategories = computed(() =>
role="combobox" role="combobox"
:aria-expanded="isPopoverOpen" :aria-expanded="isPopoverOpen"
aria-controls="event-categories" 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"> <template v-if="!model.length">
{{ props.placeholder }} {{ props.placeholder }}
@@ -57,7 +57,7 @@ const filteredCategories = computed(() =>
</UiPopoverTrigger> </UiPopoverTrigger>
<UiPopoverContent <UiPopoverContent
id="event-categories" id="event-categories"
align="start" :align="'start'"
side="bottom" side="bottom"
:collision-padding="50" :collision-padding="50"
class="w-fit h-[33vh] p-0" class="w-fit h-[33vh] p-0"

View File

@@ -36,7 +36,7 @@ const filteredCategories = computed(() =>
role="combobox" role="combobox"
:aria-expanded="isPopoverOpen" :aria-expanded="isPopoverOpen"
aria-controls="event-category" 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"> <template v-if="!model">
{{ props.placeholder }} {{ props.placeholder }}

View File

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

View File

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

View File

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

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"> <div v-if="event.description" class="text-xs md:text-sm">
<hr class="my-3 md:my-2 border-white opacity-50" > <hr class="my-3 md:my-2 border-white opacity-50" >
<span class="opacity-75"> <div class="content-editor opacity-75" v-html="event.description" />
{{ event.description }}
</span>
</div> </div>
</button> </button>
</template> </template>

View File

@@ -81,7 +81,7 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
<template> <template>
<div <div
ref="calendarTile" ref="calendarTile"
class="tile relative p-1 md:p-2 transition-colors" class="tile relative p-1 md:p-2 transition-all"
:class="{ :class="{
'text-slate-300 dark:text-slate-500': props.faded, 'text-slate-300 dark:text-slate-500': props.faded,
'text-slate-500 dark:text-slate-300': !props.faded 'text-slate-500 dark:text-slate-300': !props.faded
@@ -93,10 +93,10 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
> >
<ClientOnly> <ClientOnly>
<span <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="{ :class="{
'text-white bg-slate-600 dark:bg-slate-800': isDefaultDate && !isSelectedDate, 'bg-secondary dark:bg-slate-700': isDefaultDate && !isSelectedDate,
'text-white bg-indigo-500': isSelectedDate 'bg-primary text-primary-foreground': isSelectedDate
}" }"
> >
{{ date.day }} {{ date.day }}

View File

@@ -2,6 +2,7 @@
import type { RPGDate } from "@@/models/Date" import type { RPGDate } from "@@/models/Date"
import { storeToRefs } from "pinia" import { storeToRefs } from "pinia"
import { computed, type ComputedRef } from "vue" import { computed, type ComputedRef } from "vue"
import { cn } from "~/lib/utils"
const { currentDate, defaultDate, selectDate, areDatesIdentical } = useCalendar() const { currentDate, defaultDate, selectDate, areDatesIdentical } = useCalendar()
const { selectedDate, currentEvents } = storeToRefs(useCalendar()) const { selectedDate, currentEvents } = storeToRefs(useCalendar())
@@ -38,14 +39,14 @@ const hasAtLeastOneEvent = computed<boolean>(() => {
<template> <template>
<button <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="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="{ :class="cn({
'text-slate-500 hover:border-indigo-300 hover:text-slate-900': !isDefaultDate && !isSelectedDate, 'text-foreground/75 hover:border-primary hover:text-foreground': !isDefaultDate && !isSelectedDate,
'font-semibold text-white bg-slate-600 dark:bg-slate-800 hover:border-indigo-400': isDefaultDate && !isSelectedDate, 'font-semibold text-foreground bg-secondary hover:border-primary/70': isDefaultDate && !isSelectedDate,
'font-semibold text-white bg-indigo-500 hover:bg-indigo-600 hover:border-indigo-500': isSelectedDate, 'font-semibold text-primary-foreground bg-primary hover:border-primary/70': isSelectedDate,
'after:bg-green-600': hasAtLeastOneEvent, 'after:bg-primary': hasAtLeastOneEvent,
'after:bg-slate-950': hasAtLeastOneEvent && isSelectedDate 'after:bg-primary-foreground': hasAtLeastOneEvent && isSelectedDate
}" })"
@click="selectDate(tileDate)" @click="selectDate(tileDate)"
> >
<span ref="tileRef" class="text-[.85em]">{{ dayNumber }}</span> <span ref="tileRef" class="text-[.85em]">{{ dayNumber }}</span>

View File

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

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> <template>
<UiSelect v-model="model"> <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 <UiSelectValue
:placeholder="$t('ui.colors.selectOne')" :placeholder="$t('ui.colors.selectOne')"
class="bgc" class="bgc"

View File

@@ -0,0 +1,244 @@
<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">
<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>
<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>
<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>
</div>
<!-- Block styles -->
<div class="flex" v-if="!props.disableBlocks">
<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>
<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>
<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>
</div>
<div class="flex">
<UiButton
@click.prevent="clearFormatting"
variant="secondary"
class="size-7 rounded-e-none"
size="icon"
>
<PhTextTSlash weight="bold" />
</UiButton>
<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>
<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>
</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

@@ -40,13 +40,13 @@ const breakpoints = useBreakpoints(
<nav <nav
ref="sidebarRef" ref="sidebarRef"
:class="cn( :class="cn(
['md:relative md:isolate w-16 py-6 grid gap-4 grid-rows-[1fr_auto] justify-center md:transition-none'], // Base appearance ['md:relative md:isolate w-16 py-6 grid gap-4 grid-rows-[1fr_auto] justify-center md:transition-colors'], // Base appearance
['after:opacity-50 after:contrast-125 dark:after:opacity-75 dark:after:contrast-175 after:-hue-rotate-60'], // After styling ['after:opacity-50 after:contrast-125 dark:after:opacity-75'], // 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 ['border-r-[1px] border-r-border dark:border-r-border'], // Colours
['max-md:justify-stretch max-md:px-4 max-md:py-4 max-md:absolute max-md:left-0 max-md:inset-0 max-md:z-50 max-md:w-40 max-md:max-w-full max-md:transition-all'], // Responsive behaviours ['max-md: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-40': !isSidebarOpened,
'max-md:-translate-x-0 shadow-navbar-dark dark:bg-slate-950': isSidebarOpened 'max-md:-translate-x-0': isSidebarOpened
} }
)" )"
> >
@@ -72,11 +72,11 @@ const breakpoints = useBreakpoints(
class="rounded-full max-md:hidden" class="rounded-full max-md:hidden"
as-child as-child
> >
<RouterLink to="/explore"> <NuxtLink to="/explore">
<PhCompass size="24" weight="fill" /> <PhCompass size="24" weight="fill" />
</RouterLink> </NuxtLink>
</UiButton> </UiButton>
<RouterLink <NuxtLink
to="/explore" to="/explore"
class="md:hidden flex items-center gap-[.6ch] underline-offset-4 hover:underline" class="md:hidden flex items-center gap-[.6ch] underline-offset-4 hover:underline"
> >
@@ -85,7 +85,7 @@ const breakpoints = useBreakpoints(
<span class="text-[.9em]"> <span class="text-[.9em]">
{{ $t('pages.explore.menuLabel') }} {{ $t('pages.explore.menuLabel') }}
</span> </span>
</RouterLink> </NuxtLink>
</UiTooltipTrigger> </UiTooltipTrigger>
<UiTooltipContent :side="'right'" :side-offset="6"> <UiTooltipContent :side="'right'" :side-offset="6">
<p> <p>
@@ -96,40 +96,6 @@ const breakpoints = useBreakpoints(
</UiTooltipProvider> </UiTooltipProvider>
</li> </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> <ClientOnly>
<li v-for="(item, i) in currentMenu" :key="i"> <li v-for="(item, i) in currentMenu" :key="i">
<UiTooltipProvider :delay-duration="50"> <UiTooltipProvider :delay-duration="50">
@@ -142,9 +108,9 @@ const breakpoints = useBreakpoints(
class="rounded-full" class="rounded-full"
as-child as-child
> >
<RouterLink :to="item.to"> <NuxtLink :to="item.to">
<component :is="computeMenuItemIcon(item.phIcon)" size="24" :weight="item.phIconWeight || 'fill'" /> <component :is="computeMenuItemIcon(item.phIcon)" size="24" :weight="item.phIconWeight || 'fill'" />
</RouterLink> </NuxtLink>
</UiButton> </UiButton>
<UiButton <UiButton
v-if="item.action" v-if="item.action"
@@ -168,23 +134,3 @@ const breakpoints = useBreakpoints(
<UserCTA /> <UserCTA />
</nav> </nav>
</template> </template>
<style lang="scss" scoped>
nav {
&::after {
display: block;
content: '';
position: absolute;
bottom: 0;
inset-inline: 0;
height: 25rem;
max-height: 100%;
background-image: url("/images/sidebar.png");
background-position: bottom;
background-size: cover;
background-repeat: no-repeat;
mask-image: linear-gradient(to top, black 25%, transparent 50%, transparent);
z-index: -1;
}
}
</style>

View File

@@ -57,20 +57,33 @@ function pushRoute(to: AvailableRoutes) {
<template> <template>
<ClientOnly> <ClientOnly>
<UiDropdownMenu v-model:open="menuOpened"> <UiDropdownMenu v-model:open="menuOpened">
<UiDropdownMenuTrigger> <UiDropdownMenuTrigger class="aspect-square">
<UiAvatar v-if="user" id="user-avatar" class="ring-[.2rem] ring-indigo-700 dark:ring-neutral-900 cursor-pointer"> <TransitionGroup name="fade-group" appear>
<UiAvatarImage <UiAvatar
:src="userMeta?.avatar_url" v-if="user"
:alt="userMeta?.full_name" id="user-avatar"
referrerpolicy="no-referrer" class="ring-[.2rem] ring-primary hover:bg-accent hover:ring-accent transition-all cursor-pointer"
/> :class="cn({ 'ring-accent bg-accent dark:bg-accent': menuOpened })"
<UiAvatarFallback> >
{{ $t('ui.sidebarMenu.avatarFallback') }} <UiAvatarImage
</UiAvatarFallback> :src="userMeta?.avatar_url"
</UiAvatar> :alt="userMeta?.full_name"
<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"> referrerpolicy="no-referrer"
<PhUserCircle size="24" /> />
</UiButton> <UiAvatarFallback>
{{ $t('ui.sidebarMenu.avatarFallback') }}
</UiAvatarFallback>
</UiAvatar>
<UiButton
v-else
variant="outline"
size="icon"
class="ring-[.2rem] ring-primary hover:bg-accent hover:ring-accent border-none dark:bg-background rounded-full bg-primary transition-all cursor-pointer"
:class="cn({ 'ring-accent bg-accent dark:bg-accent': menuOpened })"
>
<PhUserCircle size="24" />
</UiButton>
</TransitionGroup>
</UiDropdownMenuTrigger> </UiDropdownMenuTrigger>
<UiDropdownMenuContent class="w-72 p-0 pb-1" :align="'start'" :side="'top'" :side-offset="10" :align-offset="25" :collision-padding="40"> <UiDropdownMenuContent class="w-72 p-0 pb-1" :align="'start'" :side="'top'" :side-offset="10" :align-offset="25" :collision-padding="40">

View File

@@ -31,7 +31,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
v-bind="forwarded" v-bind="forwarded"
:class=" :class="
cn( cn(
'fixed left-1/2 top-1/2 z-50 grid w-[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, props.class,
) )
" "

View File

@@ -9,7 +9,7 @@ const props = defineProps<{
<template> <template>
<ol <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 /> <slot />
</ol> </ol>

View File

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

View File

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

View File

@@ -33,7 +33,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
v-bind="forwarded" v-bind="forwarded"
:class=" :class="
cn( cn(
'fixed left-1/2 top-1/2 z-50 grid w-[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 props.class
) )
" "

View File

@@ -32,7 +32,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
v-bind="forwarded" v-bind="forwarded"
:class=" :class="
cn( 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 props.class
) )
" "

View File

@@ -19,6 +19,6 @@ const delegatedProps = computed(() => {
<template> <template>
<DropdownMenuSeparator <DropdownMenuSeparator
v-bind="delegatedProps" 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> </template>

View File

@@ -25,7 +25,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
v-bind="forwarded" v-bind="forwarded"
:class=" :class="
cn( 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 props.class
) )
" "

View File

@@ -24,7 +24,7 @@ const forwardedProps = useForwardProps(delegatedProps)
v-bind="forwardedProps" v-bind="forwardedProps"
:class=" :class="
cn( 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 props.class
) )
" "

View File

@@ -37,7 +37,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
v-bind="{ ...forwarded, ...$attrs }" v-bind="{ ...forwarded, ...$attrs }"
:class=" :class="
cn( 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 props.class
) )
" "

View File

@@ -39,7 +39,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
style="max-height: var(--radix-select-content-available-height)" style="max-height: var(--radix-select-content-available-height)"
:class=" :class="
cn( 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' && 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', '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 props.class

View File

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

View File

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

View File

@@ -67,16 +67,13 @@ function handleFormCancel() {
name="new-world-name" name="new-world-name"
required required
:placeholder="$t('common.title')" :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" @input="handleNameChange"
> >
<textarea <InputEditor
id="new-world-description" disable-blocks
v-model="worldSkeleton.description" 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"> <div class="-mx-1 grid gap-3">

View File

@@ -38,6 +38,8 @@ async function handleSubmit() {
if (error) { if (error) {
console.log(error.message) console.log(error.message)
isLoading.value = false
return
} }
toast({ toast({
@@ -79,16 +81,13 @@ function handleFormCancel() {
name="new-world-name" name="new-world-name"
required required
:placeholder="$t('common.title')" :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" @input="handleNameChange"
> >
<textarea <InputEditor
id="new-world-description" disable-blocks
v-model="worldSkeleton.description" 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"> <div class="-mx-1 grid gap-3">

View File

@@ -60,7 +60,7 @@ switch (statusCode) {
<template> <template>
<div class="h-screen"> <div class="h-screen">
<div class="h-full grid grid-cols-[auto_1fr] dark:bg-black transition-colors"> <div class="h-full grid grid-cols-[auto_1fr] transition-colors">
<Sidebar /> <Sidebar />
<div class="wrapper shadow-body-light dark:shadow-body-dark transition-all"> <div class="wrapper shadow-body-light dark:shadow-body-dark transition-all">

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

@@ -0,0 +1,23 @@
<script lang="ts" setup>
import { cn } from '~/lib/utils';
const { isSidebarOpened } = storeToRefs(useUiStore())
</script>
<template>
<div
class="h-full grid md:grid-cols-[auto_1fr] bg-background transition-colors after:absolute after:transition-colors"
:class="cn({
'max-md:after:bg-transparent': isSidebarOpened,
'has-sidebar max-md:after:bg-black/20 md:after:opacity-0 md:after:pointer-events-none': isSidebarOpened
})"
>
<Sidebar />
<div
class="wrapper max-h-screen transition-all overflow-y-auto"
>
<slot />
</div>
</div>
</template>

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

@@ -0,0 +1,7 @@
<template>
<div
class="wrapper max-h-screen h-full transition-all overflow-y-auto"
>
<slot />
</div>
</template>

View File

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

View File

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

View File

@@ -2,12 +2,13 @@
import type { Calendar } from "@@/models/CalendarConfig"; import type { Calendar } from "@@/models/CalendarConfig";
definePageMeta({ 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 { 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", () => {
const isLoading = computed(() => calendarStatus.value === "pending") return $fetch("/api/calendars/query", { query: { full: true }})
})
</script> </script>
<template> <template>
@@ -32,14 +33,17 @@ const isLoading = computed(() => calendarStatus.value === "pending")
{{ $t("entity.calendar.namePublicPlural") }} {{ $t("entity.calendar.namePublicPlural") }}
</Heading> </Heading>
<div v-if="isLoading" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2"> <div v-if="status === 'pending'" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
<LoadingCard /> <LazyCardLoading />
</div> </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"> <template v-else-if="status === 'success' && calendars?.data">
<CalendarPreviewCard :calendar="calendar" :gm-id="calendar.world?.gmId" /> <ul class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
</li> <li v-for="calendar in calendars.data" :key="calendar.shortId">
</ul> <LazyCalendarPreviewCard :calendar="calendar" :gm-id="calendar.world?.gmId" />
</li>
</ul>
</template>
</Spacing> </Spacing>
</Spacing> </Spacing>
</main> </main>

View File

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

View File

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

View File

@@ -9,10 +9,10 @@ definePageMeta({
middleware: ["auth-guard", "reset-menu"] 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 { 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 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 // Redirect user back home when they log out on the page
watch(user, (n) => { watch(user, (n) => {
@@ -33,49 +33,13 @@ function hideCreateDialog() {
/** Active world channel */ /** Active world channel */
let worldChannel: RealtimeChannel 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(() => { onMounted(() => {
worldChannel = supabase.channel("realtime-world-channel") worldChannel = supabase.channel("realtime-worlds-channel")
.on( .on(
"postgres_changes", "postgres_changes",
{ event: "*", schema: "public", table: "worlds" }, { event: "*", schema: "public", table: "worlds" },
async (payload) => { async (payload) => {
switch (payload.eventType) { refresh()
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
}
} }
) )
.subscribe() .subscribe()
@@ -136,21 +100,21 @@ function hideEditModal() {
</div> </div>
<div v-if="isLoading" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2"> <div v-if="isLoading" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
<LoadingCard /> <LazyCardLoading />
</div> </div>
<ul v-else-if="worlds?.data" class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2"> <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"> <li v-for="world in sortedWorlds" :key="world.id">
<WorldPreviewCard :world="world" @on-edit="() => deployEditModal(world)" @on-delete="() => deployDeleteModal(world)" /> <WorldPreviewCard :world="world" @on-edit="() => deployEditModal(world)" @on-delete="() => deployDeleteModal(world)" />
</li> </li>
<li class="xl:w-fit"> <li class="xl:w-fit">
<AddCard @on-click="() => isCreateWorldModalOpen = true"> <LazyCardAdd @on-click="() => isCreateWorldModalOpen = true">
<template v-if="worlds?.data?.length > 0"> <template v-if="worlds?.data?.length > 0">
{{ $t('entity.world.addSingle') }} {{ $t('entity.world.addSingle') }}
</template> </template>
<template v-else> <template v-else>
{{ $t('entity.world.addSingleFirst') }} {{ $t('entity.world.addSingleFirst') }}
</template> </template>
</AddCard> </LazyCardAdd>
</li> </li>
</ul> </ul>
</Spacing> </Spacing>

View File

@@ -8,13 +8,6 @@ const supabase = useSupabaseClient()
const route = useRoute() const route = useRoute()
const id = route.params.id 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() const user = useSupabaseUser()
// Redirect user back home when they log out on the page // Redirect user back home when they log out on the page
@@ -24,105 +17,45 @@ watch(user, (n) => {
} }
}) })
// Set custom menu definePageMeta({
// This should be reserved for actions, not for breadcrumbs middleware: ["auth-guard"]
// })
// const { setCurrentMenu } = useUiStore()
// setCurrentMenu([ const { data: world, status, refresh } = await useAsyncData<{ data: World }>('user-world', () => {
// { return $fetch("/api/worlds/query", { query: { id }})
// tooltip: t("entity.world.backToMy"), })
// to: "/my", const worldGmId = world.value?.data.gmId
// phIcon: "universe",
// highlight: true 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 === * === Subscriptions ===
*/ */
/** Active calendar channel */
let calendarChannel: RealtimeChannel let calendarChannel: RealtimeChannel
/** Active world channel */
let worldChannel: RealtimeChannel let worldChannel: RealtimeChannel
/** Handles calendar insertion realtime events */ // Register channels
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)
})
onMounted(() => { onMounted(() => {
worldChannel = supabase.channel("realtime-world-channel") worldChannel = supabase.channel("realtime-world-channel")
.on( .on(
"postgres_changes", "postgres_changes",
{ event: "*", schema: "public", table: "worlds" }, { event: "*", schema: "public", table: "worlds" },
async (payload) => { async () => refresh()
switch (payload.eventType) { )
case "UPDATE": .subscribe()
if (!world.value?.data) return calendarChannel = supabase.channel("realtime-calendar-channel")
.on(
world.value.data = (await $fetch<{ data: World }>("/api/worlds/query", { query: { id, full: true } })).data "postgres_changes",
break { event: "*", schema: "public", table: "calendars" },
async () => refreshCalendars()
default:
console.log("Unknown event has been triggered. This should not happen unless Supabase added one somehow.")
console.log(payload)
break
}
}
) )
.subscribe() .subscribe()
}) })
// Unsubscribe from realtime
onUnmounted(() => { onUnmounted(() => {
// Unsubscribe from realtime supabase.removeChannel(calendarChannel)
supabase.removeChannel(worldChannel) supabase.removeChannel(worldChannel)
}) })
@@ -169,11 +102,60 @@ function hideEditModal() {
<Title>{{ $t("entity.world.namePlural") }}</Title> <Title>{{ $t("entity.world.namePlural") }}</Title>
</Head> </Head>
<Heading level="h1"> <header class="mb-8">
{{ $t('entity.isLoading') }} <Spacing size="lg">
</Heading> <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>
<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> <Head>
<Title>{{ world.data.name }}</Title> <Title>{{ world.data.name }}</Title>
</Head> </Head>
@@ -208,74 +190,75 @@ function hideEditModal() {
</UiTooltipProvider> </UiTooltipProvider>
</div> </div>
<p>{{ world.data.description }}</p> <div class="content-editor" v-html="world.data.description" />
</div> </div>
</Spacing> </Spacing>
</header> </header>
</template>
<section> <template v-if="world?.data">
<Spacing size="lg"> <LazyWorldDialogEdit :world="world.data" :modal-state="isEditWorldModalOpen" @on-close="hideEditModal" />
<div class="flex items-center gap-3"> <LazyCalendarDialogCreate :world="world.data" :modal-state="isCreateCalendarModalOpen" @on-close="hideCreateDialog" />
<Heading level="h2"> <LazyCalendarDialogUpdate :world="world.data" :calendar="markedCalendar" :modal-state="isUpdateCalendarModalOpen" @on-close="hideUpdateDialog" />
{{ $t('entity.calendar.namePlural') }} <LazyCalendarDialogDelete :calendar="markedCalendar" :modal-state="isDeleteCalendarModalOpen" @on-close="hideDeleteCalendarModal"/>
</Heading> </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> </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"> <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"> <li v-for="calendar in sortedCalendars" :key="calendar.id">
<CalendarPreviewCard <LazyCalendarPreviewCard
:calendar="calendar" :calendar="calendar"
:gm-id="world.data.gmId" :gm-id="worldGmId"
show-actions show-actions
@on-edit="() => deployUpdateDialog(calendar)" @on-edit="() => deployUpdateDialog(calendar)"
@on-delete="() => deployDeleteCalendarModal(calendar)" /> @on-delete="() => deployDeleteCalendarModal(calendar)" />
</li> </li>
<li class="xl:w-fit"> <li class="xl:w-fit">
<AddCard @on-click="() => isCreateCalendarModalOpen = true"> <LazyCardAdd @on-click="() => isCreateCalendarModalOpen = true">
<template v-if="sortedCalendars.length > 0"> <template v-if="sortedCalendars.length > 0">
{{ $t('entity.calendar.addSingle') }} {{ $t('entity.calendar.addSingle') }}
</template> </template>
<template v-else> <template v-else>
{{ $t('entity.calendar.addSingleFirst') }} {{ $t('entity.calendar.addSingleFirst') }}
</template> </template>
</AddCard> </LazyCardAdd>
</li> </li>
</ul> </ul>
</Spacing> </template>
</section> </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>
</main> </main>
</template> </template>

View File

@@ -14,4 +14,4 @@
"components": "@/components", "components": "@/components",
"utils": "@/lib/utils" "utils": "@/lib/utils"
} }
} }

View File

@@ -206,6 +206,8 @@ export default defineI18nConfig(() => ({
title: "Event title", title: "Event title",
isStart: "Start", isStart: "Start",
isEnd: "End", isEnd: "End",
gotoStart: "Go to the start of this event",
gotoEnd: "Go to the end of this event",
isHidden: "Hidden", isHidden: "Hidden",
isPublic: "Public", isPublic: "Public",
hiddenLabel: "Hide this event", hiddenLabel: "Hide this event",
@@ -241,6 +243,9 @@ export default defineI18nConfig(() => ({
title: "The event \"{event}\" has been successfuly deleted.", title: "The event \"{event}\" has been successfuly deleted.",
}, },
}, },
error: {
cannotFindAny: "We couldn't fetch your calendars…"
},
actionDialog: { actionDialog: {
tabs: { tabs: {
general: { general: {
@@ -536,6 +541,8 @@ export default defineI18nConfig(() => ({
title: "Titre de l'évènement", title: "Titre de l'évènement",
isStart: "Début", isStart: "Début",
isEnd: "Fin", isEnd: "Fin",
gotoStart: "Aller au début de l'évènement",
gotoEnd: "Aller à la fin de l'évènement",
isHidden: "Privé", isHidden: "Privé",
isPublic: "Public", isPublic: "Public",
hiddenLabel: "Rendre l'évènement privé", hiddenLabel: "Rendre l'évènement privé",
@@ -571,6 +578,9 @@ export default defineI18nConfig(() => ({
title: "L'évènement \"{event}\" a été supprimé avec succès.", title: "L'évènement \"{event}\" a été supprimé avec succès.",
}, },
}, },
error: {
cannotFindAny: "Nous n'avons pas pu récupérer vos calendriers…"
},
actionDialog: { actionDialog: {
tabs: { tabs: {
general: { general: {

View File

@@ -12,8 +12,9 @@ export default defineNuxtConfig({
"@nuxt/eslint", "@nuxt/eslint",
"shadcn-nuxt", "shadcn-nuxt",
"@nuxtjs/i18n", "@nuxtjs/i18n",
"@nuxthub/core",
"@nuxt/content", "@nuxt/content",
"@nuxthub/core",
"@nuxt/image",
], ],
css: ["@/assets/main.css"], css: ["@/assets/main.css"],
@@ -67,5 +68,5 @@ export default defineNuxtConfig({
}, },
eslint: {}, eslint: {},
compatibilityDate: "2025-04-16", compatibilityDate: "latest",
}) })

View File

@@ -15,21 +15,25 @@
"dependencies": { "dependencies": {
"@intlify/message-compiler": "^11.1.11", "@intlify/message-compiler": "^11.1.11",
"@nuxt/content": "3.6.3", "@nuxt/content": "3.6.3",
"@nuxt/eslint": "^1.7.1", "@nuxt/eslint": "^1.8.0",
"@nuxt/image": "1.11.0",
"@nuxthub/core": "^0.9.0", "@nuxthub/core": "^0.9.0",
"@nuxtjs/i18n": "^10.0.3", "@nuxtjs/i18n": "^10.0.3",
"@phosphor-icons/vue": "^2.2.1", "@phosphor-icons/vue": "^2.2.1",
"@pinia/nuxt": "^0.11.2", "@pinia/nuxt": "^0.11.2",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@vueuse/core": "^13.5.0", "@tiptap/pm": "^3.0.9",
"@vueuse/integrations": "^13.5.0", "@tiptap/starter-kit": "^3.0.9",
"@vueuse/nuxt": "^13.5.0", "@tiptap/vue-3": "^3.0.9",
"@vueuse/core": "^13.6.0",
"@vueuse/integrations": "^13.6.0",
"@vueuse/nuxt": "^13.6.0",
"better-sqlite3": "^12.2.0", "better-sqlite3": "^12.2.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-vue-next": "^0.526.0", "lucide-vue-next": "^0.537.0",
"luxon": "^3.7.1", "luxon": "^3.7.1",
"nuxt": "^4.0.1", "nuxt": "^4.0.3",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"radix-vue": "^1.9.17", "radix-vue": "^1.9.17",
"shadcn-nuxt": "^2.2.0", "shadcn-nuxt": "^2.2.0",
@@ -38,26 +42,26 @@
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-router": "^4.5.1", "vue-router": "^4.5.1",
"zod": "^4.0.10" "zod": "^4.0.15"
}, },
"devDependencies": { "devDependencies": {
"@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/supabase": "^1.6.0", "@nuxtjs/supabase": "^1.6.0",
"@nuxtjs/tailwindcss": "^6.14.0", "@nuxtjs/tailwindcss": "^6.14.0",
"@stylistic/eslint-plugin-js": "^4.4.1", "@stylistic/eslint-plugin-js": "^4.4.1",
"@types/luxon": "^3.6.2", "@types/luxon": "^3.7.1",
"@typescript-eslint/eslint-plugin": "^8.38.0", "@typescript-eslint/eslint-plugin": "^8.39.0",
"@typescript-eslint/parser": "^8.38.0", "@typescript-eslint/parser": "^8.39.0",
"eslint": "^9.32.0", "eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.3", "eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-vue": "^10.3.0", "eslint-plugin-vue": "^10.4.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"sass": "^1.89.2", "sass": "^1.90.0",
"supabase": "^2.31.8", "supabase": "^2.33.9",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"typescript": "^5.8.3", "typescript": "^5.9.2",
"wrangler": "^4.26.0" "wrangler": "^4.28.1"
} }
} }

2896
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, state,
createdAt:created_at, createdAt:created_at,
updatedAt:updated_at, updatedAt:updated_at,
calendars ( gmId:gm_id
createdAt:created_at,
updatedAt:updated_at,
months:calendar_months(*),
eventNb:calendar_events(count)
)
` `
let output let output

View File

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