Merge branch 'code/cleanup' into dev
This commit is contained in:
27
app/app.vue
27
app/app.vue
@@ -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 />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,3 +2,9 @@
|
||||
--text-2xs: 0.7rem;
|
||||
--text-2xs--line-height: 0.75rem;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.flow > * + * {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(222.2 47.4% 11.2%);
|
||||
--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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
@@ -109,7 +109,7 @@ function handleFormCancel() {
|
||||
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"
|
||||
>
|
||||
|
||||
|
||||
@@ -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') }}
|
||||
@@ -80,7 +80,7 @@ function handleCancel() {
|
||||
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"
|
||||
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-primary invalid:border-destructive"
|
||||
/>
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.description') }}
|
||||
@@ -132,7 +132,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') }}
|
||||
|
||||
@@ -91,7 +91,7 @@ function handleFormCancel() {
|
||||
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"
|
||||
>
|
||||
|
||||
|
||||
@@ -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') }}
|
||||
@@ -98,7 +98,7 @@ function handleCancel() {
|
||||
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"
|
||||
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-primary invalid:border-destructive"
|
||||
/>
|
||||
<div class="mt-2 mb-1 text-xs opacity-50">
|
||||
{{ t('entity.calendar.event.patterns.description') }}
|
||||
@@ -152,7 +152,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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -139,7 +139,7 @@ function toFutureFar(): void {
|
||||
|
||||
<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="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>
|
||||
<span>{{ currentDate.currentDateTitle }}</span>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -81,7 +81,7 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
||||
<template>
|
||||
<div
|
||||
ref="calendarTile"
|
||||
class="tile relative p-1 md:p-2 transition-colors"
|
||||
class="tile relative p-1 md:p-2 transition-all"
|
||||
:class="{
|
||||
'text-slate-300 dark:text-slate-500': props.faded,
|
||||
'text-slate-500 dark:text-slate-300': !props.faded
|
||||
@@ -93,10 +93,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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
22
app/components/global/card/Error.vue
Normal file
22
app/components/global/card/Error.vue
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -40,13 +40,13 @@ const breakpoints = useBreakpoints(
|
||||
<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
|
||||
['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'], // After styling
|
||||
['border-r-[1px] border-r-border dark:border-r-border'], // Colours
|
||||
['max-md:justify-stretch max-md:px-4 max-md:py-4 max-md:absolute max-md:left-0 max-md:inset-0 max-md:z-50 max-md:w-40 max-md:max-w-full max-md:transition-all'], // Responsive behaviours
|
||||
{
|
||||
'max-md:-translate-x-40': !isSidebarOpened,
|
||||
'max-md:-translate-x-0 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"
|
||||
as-child
|
||||
>
|
||||
<RouterLink to="/explore">
|
||||
<NuxtLink to="/explore">
|
||||
<PhCompass size="24" weight="fill" />
|
||||
</RouterLink>
|
||||
</NuxtLink>
|
||||
</UiButton>
|
||||
<RouterLink
|
||||
<NuxtLink
|
||||
to="/explore"
|
||||
class="md:hidden flex items-center gap-[.6ch] underline-offset-4 hover:underline"
|
||||
>
|
||||
@@ -85,7 +85,7 @@ const breakpoints = useBreakpoints(
|
||||
<span class="text-[.9em]">
|
||||
{{ $t('pages.explore.menuLabel') }}
|
||||
</span>
|
||||
</RouterLink>
|
||||
</NuxtLink>
|
||||
</UiTooltipTrigger>
|
||||
<UiTooltipContent :side="'right'" :side-offset="6">
|
||||
<p>
|
||||
@@ -96,40 +96,6 @@ const breakpoints = useBreakpoints(
|
||||
</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">
|
||||
@@ -142,9 +108,9 @@ const breakpoints = useBreakpoints(
|
||||
class="rounded-full"
|
||||
as-child
|
||||
>
|
||||
<RouterLink :to="item.to">
|
||||
<NuxtLink :to="item.to">
|
||||
<component :is="computeMenuItemIcon(item.phIcon)" size="24" :weight="item.phIconWeight || 'fill'" />
|
||||
</RouterLink>
|
||||
</NuxtLink>
|
||||
</UiButton>
|
||||
<UiButton
|
||||
v-if="item.action"
|
||||
@@ -168,23 +134,3 @@ const breakpoints = useBreakpoints(
|
||||
<UserCTA />
|
||||
</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>
|
||||
|
||||
@@ -57,20 +57,33 @@ function pushRoute(to: AvailableRoutes) {
|
||||
<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" />
|
||||
</UiButton>
|
||||
<UiDropdownMenuTrigger class="aspect-square">
|
||||
<TransitionGroup name="fade-group" appear>
|
||||
<UiAvatar
|
||||
v-if="user"
|
||||
id="user-avatar"
|
||||
class="ring-[.2rem] ring-primary hover:bg-accent hover:ring-accent transition-all cursor-pointer"
|
||||
:class="cn({ 'ring-accent bg-accent dark:bg-accent': menuOpened })"
|
||||
>
|
||||
<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="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>
|
||||
|
||||
<UiDropdownMenuContent class="w-72 p-0 pb-1" :align="'start'" :side="'top'" :side-offset="10" :align-offset="25" :collision-padding="40">
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
outline: "border border-input hover:bg-primary hover:text-primary-foreground",
|
||||
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"
|
||||
},
|
||||
size: {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
"
|
||||
|
||||
@@ -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
|
||||
)
|
||||
"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
)
|
||||
"
|
||||
|
||||
@@ -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
|
||||
)
|
||||
"
|
||||
|
||||
@@ -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
|
||||
)
|
||||
"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 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
|
||||
)
|
||||
"
|
||||
|
||||
@@ -67,7 +67,7 @@ function handleFormCancel() {
|
||||
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 mb-4 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"
|
||||
>
|
||||
|
||||
@@ -76,7 +76,7 @@ function handleFormCancel() {
|
||||
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"
|
||||
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-primary invalid:border-destructive"
|
||||
/>
|
||||
|
||||
<div class="-mx-1 grid gap-3">
|
||||
|
||||
@@ -38,6 +38,8 @@ async function handleSubmit() {
|
||||
|
||||
if (error) {
|
||||
console.log(error.message)
|
||||
isLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
toast({
|
||||
@@ -79,7 +81,7 @@ function handleFormCancel() {
|
||||
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"
|
||||
>
|
||||
|
||||
@@ -88,7 +90,7 @@ function handleFormCancel() {
|
||||
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"
|
||||
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-primary invalid:border-destructive"
|
||||
/>
|
||||
|
||||
<div class="-mx-1 grid gap-3">
|
||||
|
||||
@@ -60,7 +60,7 @@ switch (statusCode) {
|
||||
|
||||
<template>
|
||||
<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 />
|
||||
|
||||
<div class="wrapper shadow-body-light dark:shadow-body-dark transition-all">
|
||||
|
||||
23
app/layouts/default.vue
Normal file
23
app/layouts/default.vue
Normal 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
7
app/layouts/public.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="wrapper max-h-screen h-full transition-all overflow-y-auto"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -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,25 @@ 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]">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="md:hidden">
|
||||
<SidebarToggle />
|
||||
</div>
|
||||
|
||||
<Heading level="h1">
|
||||
{{ $t("pages.about.title") }}
|
||||
</Heading>
|
||||
</div>
|
||||
|
||||
<ContentRenderer v-if="page" :value="page" class="content" />
|
||||
</Spacing>
|
||||
<Heading level="h1">
|
||||
{{ $t("pages.about.title") }}
|
||||
</Heading>
|
||||
</div>
|
||||
|
||||
<div v-if="status === 'pending'" class="grid place-items-center">
|
||||
<PhCircleNotch size="50" class="opacity-33 animate-spin"/>
|
||||
</div>
|
||||
|
||||
<LazyContentRenderer v-else-if="status === 'success' && page" :value="page" class="content max-w-5xl" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -32,14 +33,17 @@ const isLoading = computed(() => calendarStatus.value === "pending")
|
||||
{{ $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>
|
||||
|
||||
@@ -4,12 +4,33 @@ useHead({
|
||||
})
|
||||
|
||||
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>
|
||||
|
||||
<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 +38,22 @@ 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-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>
|
||||
</template>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -212,70 +194,71 @@ function hideEditModal() {
|
||||
</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>
|
||||
|
||||
@@ -14,4 +14,4 @@
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +206,8 @@ 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",
|
||||
@@ -241,6 +243,9 @@ export default defineI18nConfig(() => ({
|
||||
title: "The event \"{event}\" has been successfuly deleted.",
|
||||
},
|
||||
},
|
||||
error: {
|
||||
cannotFindAny: "We couldn't fetch your calendars…"
|
||||
},
|
||||
actionDialog: {
|
||||
tabs: {
|
||||
general: {
|
||||
@@ -536,6 +541,8 @@ 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é",
|
||||
@@ -571,6 +578,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: {
|
||||
|
||||
@@ -12,8 +12,9 @@ export default defineNuxtConfig({
|
||||
"@nuxt/eslint",
|
||||
"shadcn-nuxt",
|
||||
"@nuxtjs/i18n",
|
||||
"@nuxthub/core",
|
||||
"@nuxt/content",
|
||||
"@nuxthub/core",
|
||||
"@nuxt/image",
|
||||
],
|
||||
|
||||
css: ["@/assets/main.css"],
|
||||
@@ -67,5 +68,5 @@ export default defineNuxtConfig({
|
||||
},
|
||||
|
||||
eslint: {},
|
||||
compatibilityDate: "2025-04-16",
|
||||
compatibilityDate: "latest",
|
||||
})
|
||||
|
||||
27
package.json
27
package.json
@@ -16,20 +16,21 @@
|
||||
"@intlify/message-compiler": "^11.1.11",
|
||||
"@nuxt/content": "3.6.3",
|
||||
"@nuxt/eslint": "^1.7.1",
|
||||
"@nuxt/image": "1.11.0",
|
||||
"@nuxthub/core": "^0.9.0",
|
||||
"@nuxtjs/i18n": "^10.0.3",
|
||||
"@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",
|
||||
"@vueuse/core": "^13.6.0",
|
||||
"@vueuse/integrations": "^13.6.0",
|
||||
"@vueuse/nuxt": "^13.6.0",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-vue-next": "^0.526.0",
|
||||
"lucide-vue-next": "^0.536.0",
|
||||
"luxon": "^3.7.1",
|
||||
"nuxt": "^4.0.1",
|
||||
"nuxt": "^4.0.2",
|
||||
"pinia": "^3.0.3",
|
||||
"radix-vue": "^1.9.17",
|
||||
"shadcn-nuxt": "^2.2.0",
|
||||
@@ -38,26 +39,26 @@
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vue": "^3.5.18",
|
||||
"vue-router": "^4.5.1",
|
||||
"zod": "^4.0.10"
|
||||
"zod": "^4.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@nuxtjs/supabase": "^1.6.0",
|
||||
"@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",
|
||||
"@types/luxon": "^3.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.39.0",
|
||||
"@typescript-eslint/parser": "^8.39.0",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.3",
|
||||
"eslint-plugin-vue": "^10.3.0",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.89.2",
|
||||
"supabase": "^2.31.8",
|
||||
"supabase": "^2.33.9",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.8.3",
|
||||
"wrangler": "^4.26.0"
|
||||
"typescript": "^5.9.2",
|
||||
"wrangler": "^4.27.0"
|
||||
}
|
||||
}
|
||||
|
||||
2176
pnpm-lock.yaml
generated
2176
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 |
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user