Compare commits
42 Commits
upgrade/nu
...
feat/nav
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
674a38c30d | ||
|
|
e7cef314a1 | ||
|
|
806d3a7908 | ||
|
|
6251499940 | ||
|
|
f3d5db76ac | ||
|
|
08620b5044 | ||
|
|
f89cc22e87 | ||
|
|
51c678d27e | ||
|
|
b9b891c61f | ||
|
|
df06228e68 | ||
|
|
44193aa018 | ||
|
|
73eceda96c | ||
|
|
e305b21f80 | ||
|
|
d2b66a42dd | ||
|
|
be32a018c6 | ||
|
|
a5a232bac7 | ||
|
|
20045b125e | ||
|
|
f1a3854d1d | ||
|
|
cebcb50903 | ||
|
|
396215bb91 | ||
|
|
7468374bb5 | ||
|
|
5a421cf07f | ||
|
|
25a9099a18 | ||
|
|
c208393f9f | ||
|
|
d75f3b5423 | ||
|
|
97ce5b3553 | ||
|
|
0a35f441e0 | ||
|
|
c8a345406c | ||
|
|
119968e7fc | ||
|
|
9b8f48ad8e | ||
|
|
2199359b96 | ||
|
|
422673cc4c | ||
|
|
6aad01c96f | ||
|
|
0c96466263 | ||
|
|
7697e818bf | ||
|
|
fd766b9f42 | ||
|
|
0e7b60235a | ||
|
|
300507bbed | ||
|
|
693f506610 | ||
|
|
f2076e413c | ||
|
|
6b02629b01 | ||
|
|
1d8b56c58e |
27
app/app.vue
27
app/app.vue
@@ -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 />
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -40,23 +40,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p, ul, ol {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
/* Normal link */
|
/* Normal link */
|
||||||
p a:not([class]) {
|
p a:not([class]) {
|
||||||
color: var(--color-sky-600);
|
color: var(--color-primary);
|
||||||
text-underline-offset: .25rem;
|
text-underline-offset: .25rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--color-sky-500);
|
color: var(--color-accent);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@variant dark {
|
|
||||||
color: var(--color-teal-500);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--color-teal-300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(0, 0%, 22.5%);
|
||||||
--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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ import { VisuallyHidden } from "radix-vue";
|
|||||||
const isDialogOpen = ref<boolean>(false);
|
const isDialogOpen = ref<boolean>(false);
|
||||||
const { resetSkeleton } = useCalendar();
|
const { resetSkeleton } = useCalendar();
|
||||||
|
|
||||||
|
// Watch the popover state
|
||||||
|
watch(isDialogOpen, (hasOpened, _o) => {
|
||||||
|
if (hasOpened) {
|
||||||
|
resetSkeleton()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Toggles the dialog
|
// Toggles the dialog
|
||||||
function toggleDialog() {
|
function toggleDialog() {
|
||||||
isDialogOpen.value = !isDialogOpen.value;
|
isDialogOpen.value = !isDialogOpen.value;
|
||||||
@@ -45,7 +52,7 @@ const breakpoints = useBreakpoints(
|
|||||||
class="max-md:translate-0 max-md:inset-0 max-md:w-full max-md:block"
|
class="max-md:translate-0 max-md:inset-0 max-md:w-full max-md:block"
|
||||||
:trap-focus="true"
|
:trap-focus="true"
|
||||||
@escape-key-down="handleClosing"
|
@escape-key-down="handleClosing"
|
||||||
@pointer-down-outside.prevent="handleClosing"
|
@pointer-down-outside="handleClosing"
|
||||||
>
|
>
|
||||||
<UiDialogTitle class="max-md:mb-8">
|
<UiDialogTitle class="max-md:mb-8">
|
||||||
{{ $t("entity.calendar.event.addSingle") }}
|
{{ $t("entity.calendar.event.addSingle") }}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function handleClosing() {
|
|||||||
@escape-key-down="handleClosing"
|
@escape-key-down="handleClosing"
|
||||||
@focus-outside="handleClosing"
|
@focus-outside="handleClosing"
|
||||||
@interact-outside="handleClosing"
|
@interact-outside="handleClosing"
|
||||||
@pointer-down-outside="(e) => e.preventDefault()"
|
@pointer-down-outside.prevent
|
||||||
>
|
>
|
||||||
<header class="pl-8 grid gap-y-2 max-md:mb-8">
|
<header class="pl-8 grid gap-y-2 max-md:mb-8">
|
||||||
<UiDialogTitle>
|
<UiDialogTitle>
|
||||||
|
|||||||
@@ -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') }}
|
||||||
|
|||||||
@@ -2,14 +2,17 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import type { RPGDate } from "@@/models/Date"
|
import type { RPGDate } from "@@/models/Date"
|
||||||
import type { CalendarEvent } from "@@/models/CalendarEvent"
|
import type { CalendarEvent } from "@@/models/CalendarEvent"
|
||||||
|
import { ja } from "zod/v4/locales";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
event: CalendarEvent
|
event: CalendarEvent
|
||||||
tileDate: RPGDate
|
tileDate: RPGDate
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { areDatesIdentical, revealEditEventModal, revealDeleteEventModal } = useCalendar()
|
const buttonRef = useTemplateRef<HTMLButtonElement>('buttonRef')
|
||||||
const { lastActiveEvent } = storeToRefs(useCalendar())
|
|
||||||
|
const { areDatesIdentical, revealEditEventModal, revealDeleteEventModal, resetSkeleton } = useCalendar()
|
||||||
|
const { lastActiveEvent, eventSkeleton, isDraggingEvent } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
const spansMultipleDays = computed(() => Boolean(props.event.startDate && props.event.endDate))
|
const spansMultipleDays = computed(() => Boolean(props.event.startDate && props.event.endDate))
|
||||||
const isStartEvent = computed(() => spansMultipleDays.value && areDatesIdentical(props.tileDate, props.event.startDate))
|
const isStartEvent = computed(() => spansMultipleDays.value && areDatesIdentical(props.tileDate, props.event.startDate))
|
||||||
@@ -49,13 +52,29 @@ onMounted(() => {
|
|||||||
handleDelete()
|
handleDelete()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Draggable logic
|
||||||
|
const isTileDragging = shallowRef(false)
|
||||||
|
|
||||||
|
function handleDragStart() {
|
||||||
|
isTileDragging.value = true
|
||||||
|
isDraggingEvent.value = true
|
||||||
|
eventSkeleton.value = structuredClone(toRaw(props.event))
|
||||||
|
handleClosePopover()
|
||||||
|
}
|
||||||
|
function handleDragEnd() {
|
||||||
|
isTileDragging.value = false
|
||||||
|
isDraggingEvent.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiPopover v-model:open="isPopoverDetailsOpen">
|
<UiPopover v-model:open="isPopoverDetailsOpen">
|
||||||
<UiPopoverTrigger as-child>
|
<UiPopoverTrigger as-child>
|
||||||
<button
|
<button
|
||||||
class="event-button text-2xs md:text-xs px-[5px] py-[5px] md:px-2 md:py-1 block w-full text-left rounded-sm transition-colors outline-offset-1 cursor-pointer"
|
ref="buttonRef"
|
||||||
|
class="event-button text-2xs md:text-xs px-[5px] py-[5px] md:px-2 md:py-1 block w-full text-left rounded-sm transition-opacity outline-offset-1 cursor-pointer"
|
||||||
|
:draggable="event.endDate ? 'false' : 'true'"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
`element-${event.category?.color}`,
|
`element-${event.category?.color}`,
|
||||||
@@ -63,11 +82,17 @@ onMounted(() => {
|
|||||||
'is-hidden': event.hidden,
|
'is-hidden': event.hidden,
|
||||||
'rounded-r-none': isStartEvent,
|
'rounded-r-none': isStartEvent,
|
||||||
'rounded-l-none': isEndEvent,
|
'rounded-l-none': isEndEvent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'opacity-60': isTileDragging,
|
||||||
|
'opacity-100': !isTileDragging
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@dblclick="handleDoubleClick"
|
@dblclick="handleDoubleClick"
|
||||||
@keydown.delete="handleDelete"
|
@keydown.delete="handleDelete"
|
||||||
|
@dragstart="handleDragStart"
|
||||||
|
@dragend="handleDragEnd"
|
||||||
>
|
>
|
||||||
<div class="line-clamp-2 [overflow-wrap:anywhere] hyphens-auto">
|
<div class="line-clamp-2 [overflow-wrap:anywhere] hyphens-auto">
|
||||||
{{ eventTitle }}
|
{{ eventTitle }}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ onMounted(() => {
|
|||||||
type FormTabs = "global" | "months" | "today"
|
type FormTabs = "global" | "months" | "today"
|
||||||
const activeTab = ref<FormTabs>("global")
|
const activeTab = ref<FormTabs>("global")
|
||||||
|
|
||||||
|
const newCalendarName = shallowRef<HTMLInputElement>()
|
||||||
|
useFocus(newCalendarName, { initialValue: true })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* === Current date ===
|
* === Current date ===
|
||||||
*/
|
*/
|
||||||
@@ -103,13 +106,14 @@ function handleFormCancel() {
|
|||||||
</UiTabsList>
|
</UiTabsList>
|
||||||
<UiTabsContent value="global" class="grid gap-4">
|
<UiTabsContent value="global" class="grid gap-4">
|
||||||
<input
|
<input
|
||||||
|
ref="newCalendarName"
|
||||||
id="new-calendar-name"
|
id="new-calendar-name"
|
||||||
v-model="calendarSkeleton.name"
|
v-model="calendarSkeleton.name"
|
||||||
type="text"
|
type="text"
|
||||||
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"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|||||||
@@ -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') }}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ onMounted(() => {
|
|||||||
type FormTabs = "global" | "months" | "today"
|
type FormTabs = "global" | "months" | "today"
|
||||||
const activeTab = ref<FormTabs>("global")
|
const activeTab = ref<FormTabs>("global")
|
||||||
|
|
||||||
|
const newCalendarName = shallowRef<HTMLInputElement>()
|
||||||
|
useFocus(newCalendarName, { initialValue: true })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* === Form Validation ===
|
* === Form Validation ===
|
||||||
*/
|
*/
|
||||||
@@ -85,13 +88,14 @@ function handleFormCancel() {
|
|||||||
</UiTabsList>
|
</UiTabsList>
|
||||||
<UiTabsContent value="global" class="grid gap-4">
|
<UiTabsContent value="global" class="grid gap-4">
|
||||||
<input
|
<input
|
||||||
|
ref="newCalendarName"
|
||||||
id="new-calendar-name"
|
id="new-calendar-name"
|
||||||
v-model="calendarSkeleton.name"
|
v-model="calendarSkeleton.name"
|
||||||
type="text"
|
type="text"
|
||||||
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"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -195,10 +195,9 @@ function handleEntitySwitch() {
|
|||||||
|
|
||||||
// Key combos to deploy modal
|
// Key combos to deploy modal
|
||||||
const keys = useMagicKeys()
|
const keys = useMagicKeys()
|
||||||
|
const controlPeriod = computed(() => keys.control_period?.value)
|
||||||
|
|
||||||
whenever(keys.control_period, () => {
|
whenever(controlPeriod, openUiDialog)
|
||||||
openUiDialog()
|
|
||||||
})
|
|
||||||
|
|
||||||
const searchResultsRef = ref<HTMLElement | null>(null)
|
const searchResultsRef = ref<HTMLElement | null>(null)
|
||||||
const { y: searchResultsY } = useScroll(searchResultsRef)
|
const { y: searchResultsY } = useScroll(searchResultsRef)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { storeToRefs } from "pinia"
|
|||||||
import { computed, ref, type ComputedRef } from "vue"
|
import { computed, ref, type ComputedRef } from "vue"
|
||||||
|
|
||||||
import CalendarEventButton from "../../event/Event.vue"
|
import CalendarEventButton from "../../event/Event.vue"
|
||||||
|
import { useToast } from "@/components/ui/toast"
|
||||||
|
import type { APIError } from "@@/models/Errors"
|
||||||
|
import { ToastLifetime } from "@/components/ui/toast/use-toast"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
date: RPGDate
|
date: RPGDate
|
||||||
@@ -16,11 +19,14 @@ const emit = defineEmits<{
|
|||||||
"on-open-create-dialog": [date: RPGDate]
|
"on-open-create-dialog": [date: RPGDate]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const { toast } = useToast()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const calendarTile = ref()
|
const calendarTile = ref()
|
||||||
const calendarEventsList = ref()
|
const calendarEventsList = ref()
|
||||||
|
|
||||||
const { defaultDate, selectDate, areDatesIdentical, getFormattedDateTitle } = useCalendar()
|
const { defaultDate, selectDate, areDatesIdentical, getFormattedDateTitle, updateEventFromSkeleton, resetSkeleton } = useCalendar()
|
||||||
const { selectedDate, currentEvents, isReadOnly } = storeToRefs(useCalendar())
|
const { selectedDate, currentEvents, isReadOnly, eventSkeleton, isDraggingEvent } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
const breakpoints = useBreakpoints(
|
const breakpoints = useBreakpoints(
|
||||||
breakpointsTailwind
|
breakpointsTailwind
|
||||||
@@ -60,9 +66,17 @@ const { top: tileListTop } = useElementBounding(calendarEventsList)
|
|||||||
const numberOfEventsToFit: ComputedRef<number> = computed(() => {
|
const numberOfEventsToFit: ComputedRef<number> = computed(() => {
|
||||||
if (!eventsForTheDay.value.length) return 0
|
if (!eventsForTheDay.value.length) return 0
|
||||||
|
|
||||||
return Math.trunc((tileHeight.value - (tileListTop.value - tileTop.value)) / 40)
|
let offset = 0
|
||||||
|
|
||||||
|
if (isDraggingEvent.value && !tileNotHovered.value && !skeletonInsideCurrentDay.value) {
|
||||||
|
offset = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.trunc((tileHeight.value - (tileListTop.value - tileTop.value)) / 40) - offset
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const skeletonInsideCurrentDay = computed(() => eventsForTheDay.value.find((e) => e.id === eventSkeleton.value.id))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events that can fit in the tile's space
|
* Events that can fit in the tile's space
|
||||||
*/
|
*/
|
||||||
@@ -76,16 +90,54 @@ const eventsToDisplay: ComputedRef<CalendarEvent[]> = computed<CalendarEvent[]>(
|
|||||||
* Used if not all events can be seen in the tile's space
|
* Used if not all events can be seen in the tile's space
|
||||||
*/
|
*/
|
||||||
const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsForTheDay.value.length - eventsToDisplay.value.length)
|
const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsForTheDay.value.length - eventsToDisplay.value.length)
|
||||||
|
|
||||||
|
// Handle drop events
|
||||||
|
const { isOutside: tileNotHovered } = useMouseInElement(calendarTile)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
async function handleTileDrop() {
|
||||||
|
if (isLoading.value) return
|
||||||
|
if (skeletonInsideCurrentDay.value) return
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
eventSkeleton.value.startDate = props.date
|
||||||
|
|
||||||
|
const { error } = await tryCatch(updateEventFromSkeleton())
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const apiError = (error as any).data as APIError
|
||||||
|
|
||||||
|
apiError.data.errors.forEach((error) => {
|
||||||
|
toast({
|
||||||
|
title: t("entity.calendar.event.editErrors.toastTitle"),
|
||||||
|
variant: "destructive",
|
||||||
|
description: t(`entity.calendar.event.editErrors.${error.path[1]}_${error.code}`),
|
||||||
|
duration: ToastLifetime.MEDIUM,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
isLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = false
|
||||||
|
resetSkeleton()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="calendarTile"
|
ref="calendarTile"
|
||||||
class="tile relative p-1 md:p-2 transition-colors"
|
class="tile relative p-1 md:p-2 md:pt-1 transition-all"
|
||||||
:class="{
|
:class="{
|
||||||
'text-slate-300 dark:text-slate-500': props.faded,
|
'text-slate-300 dark:text-slate-500': props.faded,
|
||||||
'text-slate-500 dark:text-slate-300': !props.faded
|
'text-slate-500 dark:text-slate-300': !props.faded
|
||||||
}"
|
}"
|
||||||
|
@drop.prevent="handleTileDrop"
|
||||||
|
@dragenter.prevent
|
||||||
|
@dragover.prevent
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="relative z-10 group block w-full text-2xs md:text-xs text-center cursor-pointer"
|
class="relative z-10 group block w-full text-2xs md:text-xs text-center cursor-pointer"
|
||||||
@@ -93,10 +145,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 }}
|
||||||
@@ -107,13 +159,25 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
|||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<ul
|
<ul
|
||||||
ref="calendarEventsList"
|
ref="calendarEventsList"
|
||||||
class="absolute top-9 md:top-12 bottom-1 md:bottom-2 inset-x-2 grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity"
|
class="absolute top-8 md:top-10 bottom-1 md:bottom-2 inset-x-2 grid auto-rows-min gap-1 z-10 pointer-events-none transition-opacity"
|
||||||
:class="{
|
:class="{
|
||||||
'opacity-40': props.faded && !isSelectedDate
|
'opacity-40': props.faded && !isSelectedDate
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<TransitionGroup name="event">
|
<li
|
||||||
<li v-for="event in eventsToDisplay" :key="event.id" class="grid w-full pointer-events-auto">
|
v-if="!tileNotHovered && isDraggingEvent && !skeletonInsideCurrentDay"
|
||||||
|
:key="eventSkeleton.id"
|
||||||
|
class="opacity-60"
|
||||||
|
>
|
||||||
|
<CalendarEventButton :event="eventSkeleton" :tile-date="date" />
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<TransitionGroup :name="!isDraggingEvent ? 'event' : ''">
|
||||||
|
<li
|
||||||
|
v-for="(event, i) in eventsToDisplay"
|
||||||
|
:key="event.id"
|
||||||
|
class="grid w-full pointer-events-auto"
|
||||||
|
>
|
||||||
<CalendarEventButton :event :tile-date="date" />
|
<CalendarEventButton :event :tile-date="date" />
|
||||||
</li>
|
</li>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
@@ -124,7 +188,12 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
|||||||
<button
|
<button
|
||||||
class="text-2xs md:text-xs px-[5px] py-[5px] md:px-2 md:py-1 block w-full text-left font-bold rounded-sm whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer transition-colors hover:text-foreground hover:bg-foreground/10"
|
class="text-2xs md:text-xs px-[5px] py-[5px] md:px-2 md:py-1 block w-full text-left font-bold rounded-sm whitespace-nowrap overflow-hidden text-ellipsis cursor-pointer transition-colors hover:text-foreground hover:bg-foreground/10"
|
||||||
>
|
>
|
||||||
{{ eventsNotDisplayed }} autre{{ eventsNotDisplayed > 1 ? 's' : '' }}
|
<template v-if="eventsNotDisplayed === 1">
|
||||||
|
{{ $t('entity.calendar.event.oneOtherEvent') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t('entity.calendar.event.multipleOtherEvents', { count: eventsNotDisplayed }) }}
|
||||||
|
</template>
|
||||||
</button>
|
</button>
|
||||||
</UiPopoverTrigger>
|
</UiPopoverTrigger>
|
||||||
<UiPopoverContent
|
<UiPopoverContent
|
||||||
@@ -158,6 +227,7 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
|||||||
|
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<LazyCalendarDialogCreateEvent v-if="!isReadOnly && breakpoints.lg.value" :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
|
<LazyCalendarDialogCreateEvent v-if="!isReadOnly && breakpoints.lg.value" :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-else-if="!isReadOnly && !breakpoints.lg.value"
|
v-else-if="!isReadOnly && !breakpoints.lg.value"
|
||||||
class="absolute inset-0 w-full h-full cursor-default z-0"
|
class="absolute inset-0 w-full h-full cursor-default z-0"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
39
app/components/global/Navbar.vue
Normal file
39
app/components/global/Navbar.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<header role="banner" class="sticky top-0 border-b-1 border-border py-4 px-2 z-50 backdrop-blur">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="md:flex-1 flex justify-start" />
|
||||||
|
|
||||||
|
<nav class="md:flex-1">
|
||||||
|
<menu class="text-sm flex items-center justify-center gap-4">
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/" class="block p-1 font-medium text-foreground/80 dark:text-foreground/70 underline-offset-4 focus-visible:underline hover:underline dark:hover:text-foreground hover:text-foreground transition-colors" active-class="text-primary dark:text-primary">
|
||||||
|
{{ $t('breadcrumbs.home') }}
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/explore" class="block p-1 font-medium text-foreground/80 dark:text-foreground/70 underline-offset-4 focus-visible:underline hover:underline dark:hover:text-foreground hover:text-foreground transition-colors" active-class="text-primary dark:text-primary">
|
||||||
|
{{ $t('pages.explore.menuLabel') }}
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/about" class="block p-1 font-medium text-foreground/80 dark:text-foreground/70 underline-offset-4 focus-visible:underline hover:underline dark:hover:text-foreground hover:text-foreground transition-colors" active-class="text-primary dark:text-primary">
|
||||||
|
{{ $t('pages.about.menuLabel') }}
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="md:flex-1 flex justify-end">
|
||||||
|
<UserDashboardLink size="xs" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
header {
|
||||||
|
background-color: color-mix(in srgb, var(--color-background) 75%, transparent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
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>
|
<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"
|
||||||
|
|||||||
343
app/components/global/input/Editor.vue
Normal file
343
app/components/global/input/Editor.vue
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useEditor, EditorContent, type Content } from '@tiptap/vue-3'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { PhArrowUDownRight, PhArrowUUpLeft, PhListBullets, PhListNumbers, PhQuotes, PhTextB, PhTextItalic, PhTextStrikethrough, PhTextTSlash } from '@phosphor-icons/vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
disableInlines?: boolean
|
||||||
|
disableBlocks?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const model = defineModel<any>()
|
||||||
|
|
||||||
|
const editor = useEditor({
|
||||||
|
content: model.value as Content,
|
||||||
|
extensions: [
|
||||||
|
StarterKit
|
||||||
|
],
|
||||||
|
|
||||||
|
onUpdate: ({ editor }) => {
|
||||||
|
model.value = editor.getHTML()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function clearFormatting() {
|
||||||
|
editor.value?.chain().focus().unsetAllMarks().run()
|
||||||
|
editor.value?.chain().focus().clearNodes().run()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="editor">
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div class="flex gap-1.5">
|
||||||
|
<!-- Inline styles -->
|
||||||
|
<div class="flex" v-if="!props.disableInlines">
|
||||||
|
<UiTooltipProvider :delay-duration="250">
|
||||||
|
<UiTooltip>
|
||||||
|
<UiTooltipTrigger as-child>
|
||||||
|
<UiButton
|
||||||
|
@click.prevent="editor.chain().focus().toggleBold().run()"
|
||||||
|
:disabled="!editor.can().chain().focus().toggleBold().run()"
|
||||||
|
:variant="editor.isActive('bold') ? 'default' : 'secondary'"
|
||||||
|
class="size-7 rounded-e-none"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<PhTextB weight="bold" />
|
||||||
|
</UiButton>
|
||||||
|
</UiTooltipTrigger>
|
||||||
|
<UiTooltipContent>
|
||||||
|
<p>
|
||||||
|
{{ $t('editor.bold') }}
|
||||||
|
</p>
|
||||||
|
</UiTooltipContent>
|
||||||
|
</UiTooltip>
|
||||||
|
</UiTooltipProvider>
|
||||||
|
|
||||||
|
<UiTooltipProvider :delay-duration="250">
|
||||||
|
<UiTooltip>
|
||||||
|
<UiTooltipTrigger as-child>
|
||||||
|
<UiButton
|
||||||
|
@click.prevent="editor.chain().focus().toggleItalic().run()"
|
||||||
|
:disabled="!editor.can().chain().focus().toggleItalic().run()"
|
||||||
|
:variant="editor.isActive('italic') ? 'default' : 'secondary'"
|
||||||
|
class="size-7 rounded-none"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<PhTextItalic weight="bold" />
|
||||||
|
</UiButton>
|
||||||
|
</UiTooltipTrigger>
|
||||||
|
<UiTooltipContent>
|
||||||
|
<p>
|
||||||
|
{{ $t('editor.italic') }}
|
||||||
|
</p>
|
||||||
|
</UiTooltipContent>
|
||||||
|
</UiTooltip>
|
||||||
|
</UiTooltipProvider>
|
||||||
|
|
||||||
|
<UiTooltipProvider :delay-duration="250">
|
||||||
|
<UiTooltip>
|
||||||
|
<UiTooltipTrigger as-child>
|
||||||
|
<UiButton
|
||||||
|
@click.prevent="editor.chain().focus().toggleStrike().run()"
|
||||||
|
:disabled="!editor.can().chain().focus().toggleStrike().run()"
|
||||||
|
:variant="editor.isActive('strike') ? 'default' : 'secondary'"
|
||||||
|
class="size-7 rounded-s-none"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<PhTextStrikethrough weight="bold" />
|
||||||
|
</UiButton>
|
||||||
|
</UiTooltipTrigger>
|
||||||
|
<UiTooltipContent>
|
||||||
|
<p>
|
||||||
|
{{ $t('editor.strike') }}
|
||||||
|
</p>
|
||||||
|
</UiTooltipContent>
|
||||||
|
</UiTooltip>
|
||||||
|
</UiTooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Block styles -->
|
||||||
|
<div class="flex" v-if="!props.disableBlocks">
|
||||||
|
<UiTooltipProvider :delay-duration="250">
|
||||||
|
<UiTooltip>
|
||||||
|
<UiTooltipTrigger as-child>
|
||||||
|
<UiButton
|
||||||
|
@click.prevent="editor.chain().focus().toggleBulletList().run()"
|
||||||
|
:variant="editor.isActive('bulletList') ? 'default' : 'secondary'"
|
||||||
|
class="size-7 rounded-e-none"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<PhListBullets weight="bold" />
|
||||||
|
</UiButton>
|
||||||
|
</UiTooltipTrigger>
|
||||||
|
<UiTooltipContent>
|
||||||
|
<p>
|
||||||
|
{{ $t('editor.ulist') }}
|
||||||
|
</p>
|
||||||
|
</UiTooltipContent>
|
||||||
|
</UiTooltip>
|
||||||
|
</UiTooltipProvider>
|
||||||
|
|
||||||
|
<UiTooltipProvider :delay-duration="250">
|
||||||
|
<UiTooltip>
|
||||||
|
<UiTooltipTrigger as-child>
|
||||||
|
<UiButton
|
||||||
|
@click.prevent="editor.chain().focus().toggleOrderedList().run()"
|
||||||
|
:variant="editor.isActive('orderedList') ? 'default' : 'secondary'"
|
||||||
|
class="size-7 rounded-none"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<PhListNumbers weight="bold" />
|
||||||
|
</UiButton>
|
||||||
|
</UiTooltipTrigger>
|
||||||
|
<UiTooltipContent>
|
||||||
|
<p>
|
||||||
|
{{ $t('editor.olist') }}
|
||||||
|
</p>
|
||||||
|
</UiTooltipContent>
|
||||||
|
</UiTooltip>
|
||||||
|
</UiTooltipProvider>
|
||||||
|
|
||||||
|
<UiTooltipProvider :delay-duration="250">
|
||||||
|
<UiTooltip>
|
||||||
|
<UiTooltipTrigger as-child>
|
||||||
|
<UiButton
|
||||||
|
@click.prevent="editor.chain().focus().toggleBlockquote().run()"
|
||||||
|
:variant="editor.isActive('blockquote') ? 'default' : 'secondary'"
|
||||||
|
class="size-7 rounded-s-none"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<PhQuotes weight="fill" />
|
||||||
|
</UiButton>
|
||||||
|
</UiTooltipTrigger>
|
||||||
|
<UiTooltipContent>
|
||||||
|
<p>
|
||||||
|
{{ $t('editor.quote') }}
|
||||||
|
</p>
|
||||||
|
</UiTooltipContent>
|
||||||
|
</UiTooltip>
|
||||||
|
</UiTooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
<UiTooltipProvider :delay-duration="250">
|
||||||
|
<UiTooltip>
|
||||||
|
<UiTooltipTrigger as-child>
|
||||||
|
<UiButton
|
||||||
|
@click.prevent="clearFormatting"
|
||||||
|
variant="secondary"
|
||||||
|
class="size-7 rounded-e-none"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<PhTextTSlash weight="bold" />
|
||||||
|
</UiButton>
|
||||||
|
</UiTooltipTrigger>
|
||||||
|
<UiTooltipContent>
|
||||||
|
<p>
|
||||||
|
{{ $t('editor.noFormat') }}
|
||||||
|
</p>
|
||||||
|
</UiTooltipContent>
|
||||||
|
</UiTooltip>
|
||||||
|
</UiTooltipProvider>
|
||||||
|
|
||||||
|
<UiTooltipProvider :delay-duration="250">
|
||||||
|
<UiTooltip>
|
||||||
|
<UiTooltipTrigger as-child>
|
||||||
|
<UiButton
|
||||||
|
@click.prevent="editor.chain().focus().undo().run()"
|
||||||
|
:disabled="!editor.can().chain().focus().undo().run()"
|
||||||
|
variant="secondary"
|
||||||
|
class="size-7 rounded-none"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<PhArrowUUpLeft weight="bold" />
|
||||||
|
</UiButton>
|
||||||
|
</UiTooltipTrigger>
|
||||||
|
<UiTooltipContent>
|
||||||
|
<p>
|
||||||
|
{{ $t('editor.undo') }}
|
||||||
|
</p>
|
||||||
|
</UiTooltipContent>
|
||||||
|
</UiTooltip>
|
||||||
|
</UiTooltipProvider>
|
||||||
|
|
||||||
|
<UiTooltipProvider :delay-duration="250">
|
||||||
|
<UiTooltip>
|
||||||
|
<UiTooltipTrigger as-child>
|
||||||
|
<UiButton
|
||||||
|
@click.prevent="editor.chain().focus().redo().run()"
|
||||||
|
:disabled="!editor.can().chain().focus().redo().run()"
|
||||||
|
variant="secondary"
|
||||||
|
class="size-7 rounded-s-none"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
<PhArrowUDownRight weight="bold" />
|
||||||
|
</UiButton>
|
||||||
|
</UiTooltipTrigger>
|
||||||
|
<UiTooltipContent>
|
||||||
|
<p>
|
||||||
|
{{ $t('editor.redo') }}
|
||||||
|
</p>
|
||||||
|
</UiTooltipContent>
|
||||||
|
</UiTooltip>
|
||||||
|
</UiTooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditorContent
|
||||||
|
:editor
|
||||||
|
class="overflow-y-auto md:text-sm border-b-1 border-border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.tiptap,
|
||||||
|
.content-editor {
|
||||||
|
/* List styles */
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
padding: 0 1rem;
|
||||||
|
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||||
|
|
||||||
|
li p {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Heading styles */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
text-wrap: pretty;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
margin-top: 3.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code and preformatted text styles */
|
||||||
|
code {
|
||||||
|
background-color: var(--purple-light);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
color: var(--black);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.25em 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: var(--black);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: var(--white);
|
||||||
|
font-family: 'JetBrainsMono', monospace;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 3px solid var(--gray-3);
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--gray-2);
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Basic editor styles */
|
||||||
|
.tiptap {
|
||||||
|
padding-block: 0.75rem;
|
||||||
|
min-height: 7rem;
|
||||||
|
max-height: 12rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
18
app/components/global/sidebar/Footer.vue
Normal file
18
app/components/global/sidebar/Footer.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { PhInfo } from '@phosphor-icons/vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex justify-between items-center gap-2">
|
||||||
|
<div class="grow">
|
||||||
|
<UserCTA />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiButton size="icon" class="size-8 hover:bg-secondary hover:text-secondary-foreground" variant="ghost" as-child>
|
||||||
|
<NuxtLink to="/about">
|
||||||
|
<PhInfo size="18" />
|
||||||
|
</NuxtLink>
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
50
app/components/global/sidebar/Menu.vue
Normal file
50
app/components/global/sidebar/Menu.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { PanelsLeftBottom } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<menu class="min-w-32 pt-16 flex flex-col gap-0.5">
|
||||||
|
<li class="w-full">
|
||||||
|
<UiButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 gap-1.5 hover:bg-secondary w-full justify-start"
|
||||||
|
as-child
|
||||||
|
>
|
||||||
|
<NuxtLink to="/my" active-class="active-link" exact-active-class="exact-link">
|
||||||
|
<PanelsLeftBottom :size="18" />
|
||||||
|
|
||||||
|
<span class="text-[.9em]">
|
||||||
|
{{ $t('ui.sidebarMenu.projects') }}
|
||||||
|
</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</UiButton>
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.active-link {
|
||||||
|
color: var(--color-secondary-foreground);
|
||||||
|
background-color: var(--color-secondary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary) 33%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.exact-link {
|
||||||
|
color: var(--color-primary-foreground);
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary) 66%, transparent);
|
||||||
|
cursor: initial;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary) 66%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .exact-link {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,190 +1,32 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PhCompass, PhGlobeHemisphereEast, PhHurricane, PhInfo, PhX } from "@phosphor-icons/vue"
|
import { PanelsLeftBottom } from "lucide-vue-next"
|
||||||
import type { SidebarMenuActionType, SidebarMenuIcon } from "./SidebarProps";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { breakpointsTailwind } from "@vueuse/core"
|
import { PhInfo } from "@phosphor-icons/vue";
|
||||||
|
|
||||||
const { revealAdvancedSearch } = useCalendar()
|
const { isSidebarOpened } = storeToRefs(useUiStore())
|
||||||
const { toggleSidebar } = useUiStore()
|
|
||||||
const { currentMenu, isSidebarOpened } = storeToRefs(useUiStore())
|
|
||||||
|
|
||||||
function handleMenuItemAction(actionType: SidebarMenuActionType) {
|
|
||||||
if (actionType === "event-search") {
|
|
||||||
revealAdvancedSearch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeMenuItemIcon(iconString: SidebarMenuIcon) {
|
|
||||||
switch (iconString) {
|
|
||||||
case "universe":
|
|
||||||
return PhHurricane
|
|
||||||
case "world":
|
|
||||||
return PhGlobeHemisphereEast
|
|
||||||
default:
|
|
||||||
return PhCompass
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const breakpoints = useBreakpoints(
|
|
||||||
breakpointsTailwind
|
|
||||||
)
|
|
||||||
|
|
||||||
// const sidebarRef = ref(null)
|
|
||||||
|
|
||||||
// onClickOutside(sidebarRef, () => {
|
|
||||||
// isSidebarOpened.value = false
|
|
||||||
// })
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav
|
<nav
|
||||||
ref="sidebarRef"
|
ref="sidebarRef"
|
||||||
:class="cn(
|
:class="cn(
|
||||||
['md:relative md:isolate w-16 py-6 grid gap-4 grid-rows-[1fr_auto] justify-center md:transition-none'], // Base appearance
|
['max-md:hidden w-fit relative isolate p-2 grid gap-4 grid-rows-[1fr_auto] bg-background justify-center transition-colors'], // Base appearance
|
||||||
['after:opacity-50 after:contrast-125 dark:after:opacity-75 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:-translate-x-40': !isSidebarOpened,
|
|
||||||
'max-md:-translate-x-0 shadow-navbar-dark dark:bg-slate-950': isSidebarOpened
|
|
||||||
}
|
|
||||||
)"
|
)"
|
||||||
>
|
>
|
||||||
<menu class="flex flex-col gap-4 max-md:items-center">
|
<SidebarMenu />
|
||||||
<li class="mb-12 mt-4 max-md:self-start">
|
|
||||||
<UiButton
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
class="md:hidden size-9 border-background/30"
|
|
||||||
@click="toggleSidebar"
|
|
||||||
>
|
|
||||||
<PhX size="19" />
|
|
||||||
</UiButton>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="max-md:self-start">
|
<SidebarFooter />
|
||||||
<UiTooltipProvider :delay-duration="50" :disabled="!breakpoints.md.value">
|
|
||||||
<UiTooltip>
|
|
||||||
<UiTooltipTrigger as-child>
|
|
||||||
<UiButton
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="rounded-full max-md:hidden"
|
|
||||||
as-child
|
|
||||||
>
|
|
||||||
<RouterLink to="/explore">
|
|
||||||
<PhCompass size="24" weight="fill" />
|
|
||||||
</RouterLink>
|
|
||||||
</UiButton>
|
|
||||||
<RouterLink
|
|
||||||
to="/explore"
|
|
||||||
class="md:hidden flex items-center gap-[.6ch] underline-offset-4 hover:underline"
|
|
||||||
>
|
|
||||||
<PhCompass size="22" weight="fill" />
|
|
||||||
|
|
||||||
<span class="text-[.9em]">
|
|
||||||
{{ $t('pages.explore.menuLabel') }}
|
|
||||||
</span>
|
|
||||||
</RouterLink>
|
|
||||||
</UiTooltipTrigger>
|
|
||||||
<UiTooltipContent :side="'right'" :side-offset="6">
|
|
||||||
<p>
|
|
||||||
{{ $t('pages.explore.menuLabel') }}
|
|
||||||
</p>
|
|
||||||
</UiTooltipContent>
|
|
||||||
</UiTooltip>
|
|
||||||
</UiTooltipProvider>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="max-md:self-start">
|
|
||||||
<UiTooltipProvider :delay-duration="50" :disabled="!breakpoints.md.value">
|
|
||||||
<UiTooltip>
|
|
||||||
<UiTooltipTrigger as-child>
|
|
||||||
<UiButton
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="rounded-full max-md:hidden"
|
|
||||||
as-child
|
|
||||||
>
|
|
||||||
<RouterLink to="/about">
|
|
||||||
<PhInfo size="24" weight="fill" />
|
|
||||||
</RouterLink>
|
|
||||||
</UiButton>
|
|
||||||
<RouterLink
|
|
||||||
to="/about"
|
|
||||||
class="md:hidden flex items-center gap-[.6ch] underline-offset-4 hover:underline"
|
|
||||||
>
|
|
||||||
<PhInfo size="22" weight="fill" />
|
|
||||||
|
|
||||||
<span class="text-[.9em]">
|
|
||||||
{{ $t('pages.about.menuLabel') }}
|
|
||||||
</span>
|
|
||||||
</RouterLink>
|
|
||||||
</UiTooltipTrigger>
|
|
||||||
<UiTooltipContent :side="'right'" :side-offset="6">
|
|
||||||
<p>
|
|
||||||
{{ $t('pages.about.menuLabel') }}
|
|
||||||
</p>
|
|
||||||
</UiTooltipContent>
|
|
||||||
</UiTooltip>
|
|
||||||
</UiTooltipProvider>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<ClientOnly>
|
|
||||||
<li v-for="(item, i) in currentMenu" :key="i">
|
|
||||||
<UiTooltipProvider :delay-duration="50">
|
|
||||||
<UiTooltip>
|
|
||||||
<UiTooltipTrigger as-child>
|
|
||||||
<UiButton
|
|
||||||
v-if="item.to"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="rounded-full"
|
|
||||||
as-child
|
|
||||||
>
|
|
||||||
<RouterLink :to="item.to">
|
|
||||||
<component :is="computeMenuItemIcon(item.phIcon)" size="24" :weight="item.phIconWeight || 'fill'" />
|
|
||||||
</RouterLink>
|
|
||||||
</UiButton>
|
|
||||||
<UiButton
|
|
||||||
v-if="item.action"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="rounded-full"
|
|
||||||
@click="handleMenuItemAction(item.action!)"
|
|
||||||
>
|
|
||||||
<component :is="computeMenuItemIcon(item.phIcon)" size="24" :weight="item.phIconWeight || 'fill'" />
|
|
||||||
</UiButton>
|
|
||||||
</UiTooltipTrigger>
|
|
||||||
<UiTooltipContent :side="'right'" :side-offset="6">
|
|
||||||
<p>{{ item.tooltip }}</p>
|
|
||||||
</UiTooltipContent>
|
|
||||||
</UiTooltip>
|
|
||||||
</UiTooltipProvider>
|
|
||||||
</li>
|
|
||||||
</ClientOnly>
|
|
||||||
</menu>
|
|
||||||
|
|
||||||
<UserCTA />
|
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<UiSheet v-model:open="isSidebarOpened">
|
||||||
nav {
|
<UiSheetContent side="left" class="p-2 grid gap-4 grid-rows-[auto_1fr]">
|
||||||
&::after {
|
<UiSheetHeader />
|
||||||
display: block;
|
|
||||||
content: '';
|
<SidebarMenu />
|
||||||
position: absolute;
|
<SidebarFooter />
|
||||||
bottom: 0;
|
|
||||||
inset-inline: 0;
|
</UiSheetContent>
|
||||||
height: 25rem;
|
</UiSheet>
|
||||||
max-height: 100%;
|
</template>
|
||||||
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>
|
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ const { toggleSidebar } = useUiStore()
|
|||||||
<UiButton
|
<UiButton
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
class="size-7 border-border hover:bg-secondary hover:text-secondary-foreground"
|
||||||
@click="toggleSidebar"
|
@click="toggleSidebar"
|
||||||
>
|
>
|
||||||
<PhList size="19" weight="light" />
|
<PhList size="14" weight="light" />
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
|
|
||||||
import { PhCheckCircle, PhUser, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle } from "@phosphor-icons/vue"
|
import { PhCheckCircle, PhUser, PhLaptop, PhMoon, PhPalette, PhSignIn, PhSignOut, PhSun, PhTranslate, PhUserCircle, PhInfo } from "@phosphor-icons/vue"
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -44,32 +44,42 @@ async function handleLogout() {
|
|||||||
console.log(error.message)
|
console.log(error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AvailableRoutes = "/my" | "/my/settings"
|
|
||||||
|
|
||||||
function pushRoute(to: AvailableRoutes) {
|
|
||||||
router.push({ path: to })
|
|
||||||
|
|
||||||
closeMenu()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<UiDropdownMenu v-model:open="menuOpened">
|
<UiDropdownMenu v-model:open="menuOpened">
|
||||||
<UiDropdownMenuTrigger>
|
<UiDropdownMenuTrigger as-child>
|
||||||
<UiAvatar v-if="user" id="user-avatar" class="ring-[.2rem] ring-indigo-700 dark:ring-neutral-900 cursor-pointer">
|
<UiButton
|
||||||
<UiAvatarImage
|
variant="ghost"
|
||||||
:src="userMeta?.avatar_url"
|
class="w-full justify-start p-2 h-fit hover:text-secondary-foreground hover:bg-secondary"
|
||||||
:alt="userMeta?.full_name"
|
:class="cn({ 'bg-secondary': menuOpened })"
|
||||||
referrerpolicy="no-referrer"
|
>
|
||||||
/>
|
<TransitionGroup name="fade-group" appear>
|
||||||
<UiAvatarFallback>
|
<div
|
||||||
{{ $t('ui.sidebarMenu.avatarFallback') }}
|
v-if="user"
|
||||||
</UiAvatarFallback>
|
class="flex gap-2 items-center"
|
||||||
</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">
|
<UiAvatar
|
||||||
<PhUserCircle size="24" />
|
id="user-avatar"
|
||||||
|
size="sm"
|
||||||
|
class="rounded-sm ring-[.2rem] ring-primary transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
<UiAvatarImage
|
||||||
|
:src="userMeta?.avatar_url"
|
||||||
|
:alt="userMeta?.full_name"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
<UiAvatarFallback>
|
||||||
|
{{ $t('ui.sidebarMenu.avatarFallback') }}
|
||||||
|
</UiAvatarFallback>
|
||||||
|
</UiAvatar>
|
||||||
|
|
||||||
|
<div class="text-xs">
|
||||||
|
{{ userMeta?.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</UiDropdownMenuTrigger>
|
</UiDropdownMenuTrigger>
|
||||||
|
|
||||||
@@ -78,15 +88,6 @@ function pushRoute(to: AvailableRoutes) {
|
|||||||
<p class="p-2 text-[.7em] opacity-75">
|
<p class="p-2 text-[.7em] opacity-75">
|
||||||
{{ $t('ui.greeting', { user: user?.email }) }}
|
{{ $t('ui.greeting', { user: user?.email }) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<UiDropdownMenuItem class="flex gap-[.5ch] items-center rounded-none" @click="pushRoute('/my')">
|
|
||||||
<PhUser size="20" weight="fill" />
|
|
||||||
<span>
|
|
||||||
{{ $t('ui.sidebarMenu.profile') }}
|
|
||||||
</span>
|
|
||||||
</UiDropdownMenuItem>
|
|
||||||
|
|
||||||
<UiDropdownMenuSeparator />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|||||||
43
app/components/global/user/DashboardLink.vue
Normal file
43
app/components/global/user/DashboardLink.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { PrimitiveProps } from 'radix-vue'
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import type { ButtonVariants } from '@/components/ui/button'
|
||||||
|
|
||||||
|
const { auth } = useSupabaseClient()
|
||||||
|
const user = useSupabaseUser()
|
||||||
|
const profileUrl: string = `${useRequestURL().origin}/my/`
|
||||||
|
|
||||||
|
interface Props extends PrimitiveProps {
|
||||||
|
size?: ButtonVariants["size"]
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
async function handleGoogleLogin() {
|
||||||
|
const { error } = await auth.signInWithOAuth({
|
||||||
|
provider: "google",
|
||||||
|
options: {
|
||||||
|
queryParams: {
|
||||||
|
access_type: "offline",
|
||||||
|
prompt: "consent"
|
||||||
|
},
|
||||||
|
redirectTo: profileUrl
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.log(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UiButton v-if="user" :size as-child>
|
||||||
|
<NuxtLink to="/my">
|
||||||
|
Dashboard
|
||||||
|
</NuxtLink>
|
||||||
|
</UiButton>
|
||||||
|
<UiButton v-else @click="handleGoogleLogin" :size>
|
||||||
|
Log in
|
||||||
|
</UiButton>
|
||||||
|
</template>
|
||||||
@@ -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,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ export const avatarVariant = cva(
|
|||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
sm: "h-10 w-10 text-xs",
|
sm: "size-6 text-xs",
|
||||||
base: "h-16 w-16 text-2xl",
|
base: "size-14 text-2xl",
|
||||||
lg: "h-32 w-32 text-5xl",
|
lg: "size-24 text-5xl",
|
||||||
},
|
},
|
||||||
shape: {
|
shape: {
|
||||||
circle: "rounded-full",
|
circle: "rounded-full",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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/40",
|
||||||
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: {
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 dark:hover:text-primary px-3 py-2 text-sm ring-offset-background transition-colors placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
18
app/components/ui/sheet/Sheet.vue
Normal file
18
app/components/ui/sheet/Sheet.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
|
||||||
|
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<DialogRootProps>()
|
||||||
|
const emits = defineEmits<DialogRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogRoot
|
||||||
|
data-slot="sheet"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogRoot>
|
||||||
|
</template>
|
||||||
15
app/components/ui/sheet/SheetClose.vue
Normal file
15
app/components/ui/sheet/SheetClose.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogCloseProps } from "reka-ui"
|
||||||
|
import { DialogClose } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<DialogCloseProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogClose
|
||||||
|
data-slot="sheet-close"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogClose>
|
||||||
|
</template>
|
||||||
53
app/components/ui/sheet/SheetContent.vue
Normal file
53
app/components/ui/sheet/SheetContent.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
DialogContent,
|
||||||
|
DialogPortal,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import SheetOverlay from "./SheetOverlay.vue"
|
||||||
|
|
||||||
|
interface SheetContentProps extends DialogContentProps {
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
side?: "top" | "right" | "bottom" | "left"
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SheetContentProps>(), {
|
||||||
|
side: "right",
|
||||||
|
})
|
||||||
|
const emits = defineEmits<DialogContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class", "side")
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<DialogContent
|
||||||
|
data-slot="sheet-content"
|
||||||
|
:class="cn(
|
||||||
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||||
|
side === 'right'
|
||||||
|
&& 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full border-l max-w-sm',
|
||||||
|
side === 'left'
|
||||||
|
&& 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-full border-r max-w-sm',
|
||||||
|
side === 'top'
|
||||||
|
&& 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
|
||||||
|
side === 'bottom'
|
||||||
|
&& 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
|
||||||
|
props.class)"
|
||||||
|
v-bind="{ ...forwarded, ...$attrs }"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogContent>
|
||||||
|
</DialogPortal>
|
||||||
|
</template>
|
||||||
21
app/components/ui/sheet/SheetDescription.vue
Normal file
21
app/components/ui/sheet/SheetDescription.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogDescriptionProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { DialogDescription } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogDescription
|
||||||
|
data-slot="sheet-description"
|
||||||
|
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogDescription>
|
||||||
|
</template>
|
||||||
16
app/components/ui/sheet/SheetFooter.vue
Normal file
16
app/components/ui/sheet/SheetFooter.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="sheet-footer"
|
||||||
|
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
33
app/components/ui/sheet/SheetHeader.vue
Normal file
33
app/components/ui/sheet/SheetHeader.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
DialogClose
|
||||||
|
} from "reka-ui"
|
||||||
|
import { PhX } from "@phosphor-icons/vue";
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="sheet-header"
|
||||||
|
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<DialogClose
|
||||||
|
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 disabled:pointer-events-none"
|
||||||
|
as-child
|
||||||
|
>
|
||||||
|
<UiButton
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
class="size-7 border-border bg-background hover:bg-secondary hover:text-secondary-foreground"
|
||||||
|
>
|
||||||
|
<PhX size="14" />
|
||||||
|
<span class="sr-only">{{ $t('ui.actions.close') }}</span>
|
||||||
|
</UiButton>
|
||||||
|
</DialogClose>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
21
app/components/ui/sheet/SheetOverlay.vue
Normal file
21
app/components/ui/sheet/SheetOverlay.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogOverlayProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { DialogOverlay } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogOverlay
|
||||||
|
data-slot="sheet-overlay"
|
||||||
|
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/70', props.class)"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogOverlay>
|
||||||
|
</template>
|
||||||
21
app/components/ui/sheet/SheetTitle.vue
Normal file
21
app/components/ui/sheet/SheetTitle.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogTitleProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { DialogTitle } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogTitle
|
||||||
|
data-slot="sheet-title"
|
||||||
|
:class="cn('text-foreground font-semibold', props.class)"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogTitle>
|
||||||
|
</template>
|
||||||
15
app/components/ui/sheet/SheetTrigger.vue
Normal file
15
app/components/ui/sheet/SheetTrigger.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogTriggerProps } from "reka-ui"
|
||||||
|
import { DialogTrigger } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<DialogTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogTrigger
|
||||||
|
data-slot="sheet-trigger"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogTrigger>
|
||||||
|
</template>
|
||||||
8
app/components/ui/sheet/index.ts
Normal file
8
app/components/ui/sheet/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export { default as Sheet } from "./Sheet.vue"
|
||||||
|
export { default as SheetClose } from "./SheetClose.vue"
|
||||||
|
export { default as SheetContent } from "./SheetContent.vue"
|
||||||
|
export { default as SheetDescription } from "./SheetDescription.vue"
|
||||||
|
export { default as SheetFooter } from "./SheetFooter.vue"
|
||||||
|
export { default as SheetHeader } from "./SheetHeader.vue"
|
||||||
|
export { default as SheetTitle } from "./SheetTitle.vue"
|
||||||
|
export { default as SheetTrigger } from "./SheetTrigger.vue"
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ onMounted(() => {
|
|||||||
worldSkeleton.value = { ...defaultWorld }
|
worldSkeleton.value = { ...defaultWorld }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const newWorldName = shallowRef<HTMLInputElement>()
|
||||||
|
useFocus(newWorldName, { initialValue: true })
|
||||||
|
|
||||||
const isLoading = ref<boolean>(false)
|
const isLoading = ref<boolean>(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,22 +64,20 @@ function handleFormCancel() {
|
|||||||
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
|
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
|
||||||
<div class="grid gap-4">
|
<div class="grid gap-4">
|
||||||
<input
|
<input
|
||||||
|
ref="newWorldName"
|
||||||
id="new-world-name"
|
id="new-world-name"
|
||||||
v-model="worldSkeleton.name"
|
v-model="worldSkeleton.name"
|
||||||
type="text"
|
type="text"
|
||||||
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">
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ onMounted(() => {
|
|||||||
worldSkeleton.value = { ...props.world } as World
|
worldSkeleton.value = { ...props.world } as World
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const newWorldName = shallowRef<HTMLInputElement>()
|
||||||
|
useFocus(newWorldName, { initialValue: true })
|
||||||
|
|
||||||
const isLoading = ref<boolean>(false)
|
const isLoading = ref<boolean>(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,6 +41,8 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log(error.message)
|
console.log(error.message)
|
||||||
|
isLoading.value = false
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@@ -73,22 +78,20 @@ function handleFormCancel() {
|
|||||||
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
|
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
|
||||||
<div class="grid gap-4">
|
<div class="grid gap-4">
|
||||||
<input
|
<input
|
||||||
|
ref="newWorldName"
|
||||||
id="new-world-name"
|
id="new-world-name"
|
||||||
v-model="worldSkeleton.name"
|
v-model="worldSkeleton.name"
|
||||||
type="text"
|
type="text"
|
||||||
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">
|
||||||
|
|||||||
@@ -60,38 +60,32 @@ 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 w-full grid place-items-center transition-colors">
|
||||||
<Sidebar />
|
<Head>
|
||||||
|
<Title>{{ $t(titleKey) }}</Title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
<div class="wrapper shadow-body-light dark:shadow-body-dark transition-all">
|
<div class="grid text-center justify-items-center opacity-80">
|
||||||
<div class="h-full w-full grid place-items-center">
|
<PhImageBroken v-if="statusCode === 404" size="100" class="opacity-60" />
|
||||||
<Head>
|
<PhLinkBreak v-else-if="statusCode === 500" size="100" class="opacity-60" />
|
||||||
<Title>{{ $t(titleKey) }}</Title>
|
<PhBugBeetle v-else size="100" class="opacity-60" />
|
||||||
</Head>
|
|
||||||
|
|
||||||
<div class="grid text-center justify-items-center opacity-80">
|
<Heading level="h0">
|
||||||
<PhImageBroken v-if="statusCode === 404" size="100" class="opacity-60" />
|
{{ $t(titleKey) }}
|
||||||
<PhLinkBreak v-else-if="statusCode === 500" size="100" class="opacity-60" />
|
</Heading>
|
||||||
<PhBugBeetle v-else size="100" class="opacity-60" />
|
|
||||||
|
|
||||||
<Heading level="h0">
|
<div class="mt-6 md:text-lg">
|
||||||
{{ $t(titleKey) }}
|
<p>{{ $t(descriptionKey) }}</p>
|
||||||
</Heading>
|
<p>{{ $t(subDescriptionKey) }}</p>
|
||||||
|
|
||||||
<div class="mt-6 md:text-lg">
|
|
||||||
<p>{{ $t(descriptionKey) }}</p>
|
|
||||||
<p>{{ $t(subDescriptionKey) }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UiButton variant="default" class="mt-6 gap-2" as-child>
|
|
||||||
<RouterLink to="/">
|
|
||||||
<PhArrowBendDoubleUpLeft size="24" />
|
|
||||||
|
|
||||||
{{ $t('ui.backToHome') }}
|
|
||||||
</RouterLink>
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<UiButton variant="default" class="mt-6 gap-2" as-child>
|
||||||
|
<RouterLink to="/">
|
||||||
|
<PhArrowBendDoubleUpLeft size="24" />
|
||||||
|
|
||||||
|
{{ $t('ui.backToHome') }}
|
||||||
|
</RouterLink>
|
||||||
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
13
app/layouts/default.vue
Normal file
13
app/layouts/default.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="h-full grid md:grid-cols-[auto_1fr] bg-background transition-colors after:absolute after:transition-colors"
|
||||||
|
>
|
||||||
|
<Sidebar />
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="wrapper max-h-screen transition-all overflow-y-auto"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
36
app/layouts/public.vue
Normal file
36
app/layouts/public.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="wrapper max-h-screen h-full transition-all overflow-y-auto grid grid-rows-[auto_1fr]"
|
||||||
|
>
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<div class="inner-content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.inner-content {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
inset-inline: 0;
|
||||||
|
top: 0;
|
||||||
|
display: block;
|
||||||
|
content: '';
|
||||||
|
height: .1rem;
|
||||||
|
max-width: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 0%,
|
||||||
|
transparent 10%,
|
||||||
|
color-mix(in srgb, var(--color-primary) 50%, transparent) 25%,
|
||||||
|
color-mix(in srgb, var(--color-primary) 50%, transparent) 75%,
|
||||||
|
transparent 90%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { type ClassValue, clsx } from "clsx"
|
import type { ClassValue } from "clsx"
|
||||||
|
import { clsx } from "clsx"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
|||||||
@@ -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,19 @@ 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">
|
<Heading level="h1">
|
||||||
<div class="flex items-center gap-3">
|
{{ $t("pages.about.title") }}
|
||||||
<div class="md:hidden">
|
</Heading>
|
||||||
<SidebarToggle />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Heading level="h1">
|
<div v-if="status === 'pending'" class="grid place-items-center">
|
||||||
{{ $t("pages.about.title") }}
|
<PhCircleNotch size="50" class="opacity-33 animate-spin"/>
|
||||||
</Heading>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ContentRenderer v-if="page" :value="page" class="content" />
|
|
||||||
</Spacing>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<LazyContentRenderer v-else-if="status === 'success' && page" :value="page" class="content max-w-4xl" />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -17,29 +18,26 @@ const isLoading = computed(() => calendarStatus.value === "pending")
|
|||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Spacing size="lg">
|
<Spacing size="lg">
|
||||||
<div class="flex items-center gap-3">
|
<Heading level="h1">
|
||||||
<div class="md:hidden">
|
{{ $t("pages.explore.title") }}
|
||||||
<SidebarToggle />
|
</Heading>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Heading level="h1">
|
|
||||||
{{ $t("pages.explore.title") }}
|
|
||||||
</Heading>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Spacing size="lg">
|
<Spacing size="lg">
|
||||||
<Heading level="h2">
|
<Heading level="h2">
|
||||||
{{ $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>
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ useHead({
|
|||||||
})
|
})
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ["reset-menu"]
|
layout: "public"
|
||||||
})
|
})
|
||||||
</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 +17,17 @@ 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-4xl mx-auto">
|
||||||
|
<h1 class="text-3xl md:text-5xl font-bold md:font-semibold">
|
||||||
|
{{ $t('pages.home.h1') }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-sm">
|
||||||
|
{{ $t('pages.home.tagline') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<UserDashboardLink />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
3
app/pages/my.vue
Normal file
3
app/pages/my.vue
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtPage />
|
||||||
|
</template>
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export const useCalendar = defineStore("calendar", () => {
|
|||||||
return Number(params.month)
|
return Number(params.month)
|
||||||
})
|
})
|
||||||
const currentMonthData = computed<CalendarMonth>(() => {
|
const currentMonthData = computed<CalendarMonth>(() => {
|
||||||
return sortedMonths.value[currentMonth.value]
|
return sortedMonths.value[currentMonth.value]!
|
||||||
})
|
})
|
||||||
// Gets the label from currentMonth index
|
// Gets the label from currentMonth index
|
||||||
const currentMonthName = computed<string>(() => getMonthName(currentMonth.value))
|
const currentMonthName = computed<string>(() => getMonthName(currentMonth.value))
|
||||||
@@ -605,7 +605,7 @@ export const useCalendar = defineStore("calendar", () => {
|
|||||||
// Else, we need to accelerate and decelerate
|
// Else, we need to accelerate and decelerate
|
||||||
else {
|
else {
|
||||||
// First, get all the remaining days in the current month
|
// First, get all the remaining days in the current month
|
||||||
const currentMonth = sortedMonths.value[datePivot.month]
|
const currentMonth = sortedMonths.value[datePivot.month]!
|
||||||
dateAcc.day = currentMonth.days - datePivot.day
|
dateAcc.day = currentMonth.days - datePivot.day
|
||||||
if (direction === "future") {
|
if (direction === "future") {
|
||||||
datePivot.month = getNextViewMonth(datePivot.month)
|
datePivot.month = getNextViewMonth(datePivot.month)
|
||||||
@@ -756,7 +756,7 @@ export const useCalendar = defineStore("calendar", () => {
|
|||||||
|
|
||||||
// Loop over all event once to convert the structure to a usable one
|
// Loop over all event once to convert the structure to a usable one
|
||||||
for (let i = 0; i < allEvents.value.length; i++) {
|
for (let i = 0; i < allEvents.value.length; i++) {
|
||||||
const e: CalendarEvent = allEvents.value[i]
|
const e: CalendarEvent = allEvents.value[i]!
|
||||||
|
|
||||||
// Estimate distance from pivot
|
// Estimate distance from pivot
|
||||||
const startDateDays: number = convertDateToDays(e.startDate)
|
const startDateDays: number = convertDateToDays(e.startDate)
|
||||||
@@ -830,6 +830,7 @@ export const useCalendar = defineStore("calendar", () => {
|
|||||||
const isCreatingEvent = ref<boolean>(false)
|
const isCreatingEvent = ref<boolean>(false)
|
||||||
const isUpdatingEvent = ref<boolean>(false)
|
const isUpdatingEvent = ref<boolean>(false)
|
||||||
const isDeletingEvent = ref<boolean>(false)
|
const isDeletingEvent = ref<boolean>(false)
|
||||||
|
const isDraggingEvent = ref<boolean>(false)
|
||||||
const operationInProgress = computed<boolean>(() => isCreatingEvent.value || isUpdatingEvent.value || isDeletingEvent.value)
|
const operationInProgress = computed<boolean>(() => isCreatingEvent.value || isUpdatingEvent.value || isDeletingEvent.value)
|
||||||
let abortController: AbortController | null = null
|
let abortController: AbortController | null = null
|
||||||
|
|
||||||
@@ -984,6 +985,7 @@ export const useCalendar = defineStore("calendar", () => {
|
|||||||
isCreatingEvent,
|
isCreatingEvent,
|
||||||
isUpdatingEvent,
|
isUpdatingEvent,
|
||||||
isDeletingEvent,
|
isDeletingEvent,
|
||||||
|
isDraggingEvent,
|
||||||
operationInProgress,
|
operationInProgress,
|
||||||
eventSkeleton,
|
eventSkeleton,
|
||||||
resetSkeleton,
|
resetSkeleton,
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://shadcn-vue.com/schema.json",
|
"$schema": "https://shadcn-vue.com/schema.json",
|
||||||
"style": "default",
|
"style": "new-york",
|
||||||
"typescript": true,
|
"typescript": true,
|
||||||
"tsConfigPath": ".nuxt/tsconfig.json",
|
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.js",
|
"config": "tailwind.config.js",
|
||||||
"css": "assets/main.css",
|
"css": "app/assets/main.css",
|
||||||
"baseColor": "slate",
|
"baseColor": "slate",
|
||||||
"cssVariables": true
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
},
|
},
|
||||||
"framework": "nuxt",
|
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils"
|
"composables": "@/composables",
|
||||||
}
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,7 @@ export default defineI18nConfig(() => ({
|
|||||||
save: "Save",
|
save: "Save",
|
||||||
delete: "Delete",
|
delete: "Delete",
|
||||||
edit: "Edit",
|
edit: "Edit",
|
||||||
|
close: "Close"
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
label: "Color",
|
label: "Color",
|
||||||
@@ -69,12 +70,14 @@ export default defineI18nConfig(() => ({
|
|||||||
backToHome: "Back to home",
|
backToHome: "Back to home",
|
||||||
sidebarMenu: {
|
sidebarMenu: {
|
||||||
profile: "Profile",
|
profile: "Profile",
|
||||||
|
projects: "Projects",
|
||||||
appearance: "Appearance",
|
appearance: "Appearance",
|
||||||
language: "Language",
|
language: "Language",
|
||||||
account: "Account",
|
account: "Account",
|
||||||
login: "Log in",
|
login: "Log in",
|
||||||
logout: "Log out",
|
logout: "Log out",
|
||||||
avatarFallback: "Profile",
|
avatarFallback: "Profile",
|
||||||
|
about: "About",
|
||||||
},
|
},
|
||||||
dark: "Dark",
|
dark: "Dark",
|
||||||
light: "Light",
|
light: "Light",
|
||||||
@@ -86,6 +89,17 @@ export default defineI18nConfig(() => ({
|
|||||||
createdAt: "Created {createdAt}",
|
createdAt: "Created {createdAt}",
|
||||||
updatedAt: "Last updated {updatedAt}",
|
updatedAt: "Last updated {updatedAt}",
|
||||||
},
|
},
|
||||||
|
editor: {
|
||||||
|
bold: "Bold",
|
||||||
|
italic: "Italic",
|
||||||
|
strike: "Strikethrough",
|
||||||
|
ulist: "Bullet list",
|
||||||
|
olist: "Numbered list",
|
||||||
|
quote: "Quote",
|
||||||
|
noFormat: "Remove formatting",
|
||||||
|
undo: "Undo",
|
||||||
|
redo: "Redo"
|
||||||
|
},
|
||||||
entity: {
|
entity: {
|
||||||
category: {
|
category: {
|
||||||
nameSingular: "Category",
|
nameSingular: "Category",
|
||||||
@@ -127,7 +141,7 @@ export default defineI18nConfig(() => ({
|
|||||||
world: {
|
world: {
|
||||||
nameSingular: "World",
|
nameSingular: "World",
|
||||||
namePlural: "Worlds",
|
namePlural: "Worlds",
|
||||||
backToMy: "Back to my universe",
|
backToMy: "Back to projects",
|
||||||
backToSingle: "Back to {world}",
|
backToSingle: "Back to {world}",
|
||||||
addSingle: "Add a world",
|
addSingle: "Add a world",
|
||||||
addSingleFirst: "Add your first world !",
|
addSingleFirst: "Add your first world !",
|
||||||
@@ -158,7 +172,7 @@ export default defineI18nConfig(() => ({
|
|||||||
nameSingular: "Calendar",
|
nameSingular: "Calendar",
|
||||||
namePlural: "Calendars",
|
namePlural: "Calendars",
|
||||||
namePublicSingular: "Public Calendar",
|
namePublicSingular: "Public Calendar",
|
||||||
namePublicPlural: "Public Calendars",
|
namePublicPlural: "Calendars",
|
||||||
addSingle: "Add a calendar",
|
addSingle: "Add a calendar",
|
||||||
addSingleFirst: "Add your first calendar !",
|
addSingleFirst: "Add your first calendar !",
|
||||||
notFound: "Calendar not found",
|
notFound: "Calendar not found",
|
||||||
@@ -206,11 +220,15 @@ 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",
|
||||||
hiddenTooltip: "This event is visible only to game masters.",
|
hiddenTooltip: "This event is visible only to game masters.",
|
||||||
addLocation: "Add a place",
|
addLocation: "Add a place",
|
||||||
|
oneOtherEvent: "1 other",
|
||||||
|
multipleOtherEvents: "{count} others",
|
||||||
prevPage: "Previous page with events",
|
prevPage: "Previous page with events",
|
||||||
nextPage: "Next page with events",
|
nextPage: "Next page with events",
|
||||||
outOfBoundsTitle: "No next or previous events were found",
|
outOfBoundsTitle: "No next or previous events were found",
|
||||||
@@ -241,6 +259,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: {
|
||||||
@@ -311,13 +332,17 @@ export default defineI18nConfig(() => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
|
home: {
|
||||||
|
h1: "A home for your creativity",
|
||||||
|
tagline: "Visualize your fantasy or sci-fi worlds with our interactive calendars"
|
||||||
|
},
|
||||||
explore: {
|
explore: {
|
||||||
menuLabel: "Explore",
|
menuLabel: "Explore",
|
||||||
title: "Explore worlds",
|
title: "Explore worlds",
|
||||||
},
|
},
|
||||||
profile: {
|
profile: {
|
||||||
title: "{user} — My universe",
|
title: "{user} — Projects",
|
||||||
metaTitle: "My universe",
|
metaTitle: "Projects",
|
||||||
},
|
},
|
||||||
about: {
|
about: {
|
||||||
title: "About this app",
|
title: "About this app",
|
||||||
@@ -328,7 +353,7 @@ export default defineI18nConfig(() => ({
|
|||||||
breadcrumbs: {
|
breadcrumbs: {
|
||||||
home: "Home",
|
home: "Home",
|
||||||
explore: "Explore",
|
explore: "Explore",
|
||||||
profile: "Universe",
|
profile: "Projects",
|
||||||
world: "World",
|
world: "World",
|
||||||
calendar: "Calendar"
|
calendar: "Calendar"
|
||||||
}
|
}
|
||||||
@@ -362,6 +387,7 @@ export default defineI18nConfig(() => ({
|
|||||||
save: "Sauvegarder",
|
save: "Sauvegarder",
|
||||||
delete: "Supprimer",
|
delete: "Supprimer",
|
||||||
edit: "Modifier",
|
edit: "Modifier",
|
||||||
|
close: "Fermer"
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
label: "Couleur",
|
label: "Couleur",
|
||||||
@@ -399,12 +425,14 @@ export default defineI18nConfig(() => ({
|
|||||||
backToHome: "Retourner à l'accueil",
|
backToHome: "Retourner à l'accueil",
|
||||||
sidebarMenu: {
|
sidebarMenu: {
|
||||||
profile: "Profil",
|
profile: "Profil",
|
||||||
|
projects: "Projets",
|
||||||
appearance: "Apparence",
|
appearance: "Apparence",
|
||||||
language: "Langue",
|
language: "Langue",
|
||||||
account: "Compte",
|
account: "Compte",
|
||||||
login: "Connexion",
|
login: "Connexion",
|
||||||
logout: "Déconnexion",
|
logout: "Déconnexion",
|
||||||
avatarFallback: "Profil",
|
avatarFallback: "Profil",
|
||||||
|
about: "À propos",
|
||||||
},
|
},
|
||||||
dark: "Sombre",
|
dark: "Sombre",
|
||||||
light: "Clair",
|
light: "Clair",
|
||||||
@@ -416,6 +444,17 @@ export default defineI18nConfig(() => ({
|
|||||||
createdAt: "Créé le {createdAt}",
|
createdAt: "Créé le {createdAt}",
|
||||||
updatedAt: "Dernière modification le {updatedAt}",
|
updatedAt: "Dernière modification le {updatedAt}",
|
||||||
},
|
},
|
||||||
|
editor: {
|
||||||
|
bold: "Gras",
|
||||||
|
italic: "Italique",
|
||||||
|
strike: "Barré",
|
||||||
|
ulist: "Liste à puces",
|
||||||
|
olist: "Liste numerotée",
|
||||||
|
quote: "Citation",
|
||||||
|
noFormat: "Effacer le formattage",
|
||||||
|
undo: "Annuler",
|
||||||
|
redo: "Rétablir"
|
||||||
|
},
|
||||||
entity: {
|
entity: {
|
||||||
category: {
|
category: {
|
||||||
nameSingular: "Catégorie",
|
nameSingular: "Catégorie",
|
||||||
@@ -457,7 +496,7 @@ export default defineI18nConfig(() => ({
|
|||||||
world: {
|
world: {
|
||||||
nameSingular: "Monde",
|
nameSingular: "Monde",
|
||||||
namePlural: "Mondes",
|
namePlural: "Mondes",
|
||||||
backToMy: "Retourner à mon univers",
|
backToMy: "Retourner à Projets",
|
||||||
backToSingle: "Retourner sur {world}",
|
backToSingle: "Retourner sur {world}",
|
||||||
addSingle: "Ajouter un monde",
|
addSingle: "Ajouter un monde",
|
||||||
addSingleFirst: "Ajouter votre premier monde !",
|
addSingleFirst: "Ajouter votre premier monde !",
|
||||||
@@ -536,11 +575,15 @@ 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é",
|
||||||
hiddenTooltip: "Cet évènement est uniquement visible pour les maîtres du jeu.",
|
hiddenTooltip: "Cet évènement est uniquement visible pour les maîtres du jeu.",
|
||||||
addLocation: "Ajouter un endroit",
|
addLocation: "Ajouter un endroit",
|
||||||
|
oneOtherEvent: "1 autre",
|
||||||
|
multipleOtherEvents: "{count} autres",
|
||||||
prevPage: "Précédente page à évènements",
|
prevPage: "Précédente page à évènements",
|
||||||
nextPage: "Prochaine page à évènements",
|
nextPage: "Prochaine page à évènements",
|
||||||
outOfBoundsTitle: "Aucun évènement suivant ou précédent trouvé",
|
outOfBoundsTitle: "Aucun évènement suivant ou précédent trouvé",
|
||||||
@@ -571,6 +614,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: {
|
||||||
@@ -645,13 +691,17 @@ export default defineI18nConfig(() => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
pages: {
|
pages: {
|
||||||
|
home: {
|
||||||
|
h1: "Un espace pour votre créativité",
|
||||||
|
tagline: "Visualisez vos mondes fantasy ou de science-fiction grâce à nos calendriers interactifs"
|
||||||
|
},
|
||||||
explore: {
|
explore: {
|
||||||
menuLabel: "Explorer",
|
menuLabel: "Explorer",
|
||||||
title: "Explorer les mondes",
|
title: "Explorer les mondes",
|
||||||
},
|
},
|
||||||
profile: {
|
profile: {
|
||||||
title: "{user} — Mon univers",
|
title: "{user} — Projets",
|
||||||
metaTitle: "Mon univers",
|
metaTitle: "Projets",
|
||||||
},
|
},
|
||||||
about: {
|
about: {
|
||||||
title: "À propos",
|
title: "À propos",
|
||||||
@@ -662,7 +712,7 @@ export default defineI18nConfig(() => ({
|
|||||||
breadcrumbs: {
|
breadcrumbs: {
|
||||||
home: "Accueil",
|
home: "Accueil",
|
||||||
explore: "Explorer",
|
explore: "Explorer",
|
||||||
profile: "Univers",
|
profile: "Projets",
|
||||||
world: "Monde",
|
world: "Monde",
|
||||||
calendar: "Calendrier"
|
calendar: "Calendrier"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
})
|
})
|
||||||
|
|||||||
50
package.json
50
package.json
@@ -15,49 +15,55 @@
|
|||||||
"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.9.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.6",
|
||||||
"@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.12",
|
||||||
"@vueuse/core": "^13.5.0",
|
"@tiptap/pm": "^3.3.0",
|
||||||
"@vueuse/integrations": "^13.5.0",
|
"@tiptap/starter-kit": "^3.3.0",
|
||||||
"@vueuse/nuxt": "^13.5.0",
|
"@tiptap/vue-3": "^3.3.0",
|
||||||
|
"@vueuse/core": "^13.7.0",
|
||||||
|
"@vueuse/integrations": "^13.7.0",
|
||||||
|
"@vueuse/nuxt": "^13.7.0",
|
||||||
"better-sqlite3": "^12.2.0",
|
"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.541.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",
|
||||||
|
"reka-ui": "^2.4.1",
|
||||||
"shadcn-nuxt": "^2.2.0",
|
"shadcn-nuxt": "^2.2.0",
|
||||||
"sortablejs": "^1.15.6",
|
"sortablejs": "^1.15.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vue": "^3.5.18",
|
"tw-animate-css": "^1.3.7",
|
||||||
|
"vue": "^3.5.19",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"zod": "^4.0.10"
|
"zod": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxtjs/color-mode": "^3.5.2",
|
"@nuxtjs/color-mode": "^3.5.2",
|
||||||
"@nuxtjs/supabase": "^1.6.0",
|
"@nuxtjs/supabase": "^1.6.1",
|
||||||
"@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.40.0",
|
||||||
"@typescript-eslint/parser": "^8.38.0",
|
"@typescript-eslint/parser": "^8.40.0",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.34.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.34.3",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.12",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.9.2",
|
||||||
"wrangler": "^4.26.0"
|
"wrangler": "^4.32.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4724
pnpm-lock.yaml
generated
4724
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,
|
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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user