Merge branch 'code/cleanup' into dev
This commit is contained in:
63
components/calendar/PreviewCard.vue
Normal file
63
components/calendar/PreviewCard.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { PhCalendarDots, PhFilePlus, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue";
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
import type { Calendar } from "~/models/CalendarConfig";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
calendar: Calendar,
|
||||||
|
gmId?: string,
|
||||||
|
showActions?: boolean,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(["on-delete"])
|
||||||
|
|
||||||
|
const { locale } = useI18n();
|
||||||
|
|
||||||
|
const createdAt = DateTime.fromISO(props.calendar.createdAt!).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value });
|
||||||
|
const updatedAt = computed<string>(() => props.calendar.updatedAt ? DateTime.fromISO(props.calendar.updatedAt).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value }) : "");
|
||||||
|
|
||||||
|
const user = useSupabaseUser();
|
||||||
|
const isOwner = computed(() => user.value && props.gmId && user.value.id === props.gmId);
|
||||||
|
const calendarLink = computed(() => isOwner.value ? `/my/calendars/${props.calendar.id}` : `/calendars/${props.calendar.shortId}`);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UiCard
|
||||||
|
class="w-full h-full transition-all hover:bg-slate-50 dark:bg-gray-950 dark:hover:bg-indigo-950 dark:focus-within:outline-gray-900"
|
||||||
|
:link="calendarLink"
|
||||||
|
>
|
||||||
|
<UiCardHeader>
|
||||||
|
<UiCardTitle class="text-xl pr-12">{{ calendar.name }}</UiCardTitle>
|
||||||
|
</UiCardHeader>
|
||||||
|
|
||||||
|
<UiCardContent>
|
||||||
|
<p class="flex items-center gap-1">
|
||||||
|
<PhCalendarDots size="24" weight="fill" />
|
||||||
|
<span>{{ $t("entity.calendar.hasXEvents", { count: calendar.eventNb?.[0].count }) }}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
v-if="isOwner && showActions"
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
class="absolute top-2 right-2 z-20 hover:text-white hover:bg-rose-400 dark:hover:bg-rose-700"
|
||||||
|
@click="emit('on-delete')"
|
||||||
|
>
|
||||||
|
<PhTrash size="16" />
|
||||||
|
</UiButton>
|
||||||
|
</UiCardContent>
|
||||||
|
|
||||||
|
<UiCardFooter>
|
||||||
|
<ul class="grid gap-2">
|
||||||
|
<li class="flex gap-1 items-center">
|
||||||
|
<PhFilePlus size="20" />
|
||||||
|
<span>{{ $t('common.createdAt', { createdAt }) }}</span>
|
||||||
|
</li>
|
||||||
|
<li v-if="updatedAt" class="flex gap-1 items-center">
|
||||||
|
<PhPencilSimpleLine size="20" />
|
||||||
|
<span>{{ $t('common.updatedAt', { updatedAt }) }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</UiCardFooter>
|
||||||
|
</UiCard>
|
||||||
|
</template>
|
||||||
@@ -22,7 +22,7 @@ function handleClose() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiAlertDialog :open="modalState">
|
<UiAlertDialog :open="modalState">
|
||||||
<UiAlertDialogContent class="grid grid-rows-[auto_1fr_auto] items-start min-h-[66vh] max-w-4xl gap-6">
|
<UiAlertDialogContent class="grid grid-rows-[auto_1fr_auto] items-start min-h-[66vh] max-w-4xl gap-6" @close-auto-focus="(e) => e.preventDefault()">
|
||||||
<UiAlertDialogTitle>
|
<UiAlertDialogTitle>
|
||||||
<span class="text-2xl">
|
<span class="text-2xl">
|
||||||
<strong class="font-bold">{{ world.name }}</strong>
|
<strong class="font-bold">{{ world.name }}</strong>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PhCircleNotch } from "@phosphor-icons/vue";
|
import { PhCircleNotch } from "@phosphor-icons/vue";
|
||||||
import { useToast } from "~/components/ui/toast";
|
import { useToast } from "~/components/ui/toast";
|
||||||
|
import { ToastLifetime } from "~/components/ui/toast/use-toast";
|
||||||
import type { Calendar } from "~/models/CalendarConfig";
|
import type { Calendar } from "~/models/CalendarConfig";
|
||||||
|
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
@@ -28,7 +29,7 @@ async function handleAction(): Promise<void> {
|
|||||||
toast({
|
toast({
|
||||||
title: t("entity.calendar.deletedToast.title", { calendar: props.calendar.name }),
|
title: t("entity.calendar.deletedToast.title", { calendar: props.calendar.name }),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
duration: 3000
|
duration: ToastLifetime.SHORT
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
@@ -65,6 +66,7 @@ function handleClosing() {
|
|||||||
@focus-outside="handleClosing"
|
@focus-outside="handleClosing"
|
||||||
@interact-outside="handleClosing"
|
@interact-outside="handleClosing"
|
||||||
@pointer-down-outside="handleClosing"
|
@pointer-down-outside="handleClosing"
|
||||||
|
@close-auto-focus="(e) => e.preventDefault()"
|
||||||
>
|
>
|
||||||
<UiAlertDialogTitle>
|
<UiAlertDialogTitle>
|
||||||
{{ $t('entity.calendar.deleteDialog.title') }}
|
{{ $t('entity.calendar.deleteDialog.title') }}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PhCircleNotch } from "@phosphor-icons/vue";
|
import { PhCircleNotch } from "@phosphor-icons/vue";
|
||||||
import { useToast } from "~/components/ui/toast";
|
import { useToast } from "~/components/ui/toast";
|
||||||
|
import { ToastLifetime } from "~/components/ui/toast/use-toast";
|
||||||
|
|
||||||
const { resetSkeleton, deleteEventFromSkeleton, cancelLatestRequest } = useCalendar()
|
const { resetSkeleton, deleteEventFromSkeleton, cancelLatestRequest } = useCalendar()
|
||||||
const { isDeleteEventModalOpen, eventSkeleton } = storeToRefs(useCalendar())
|
const { isDeleteEventModalOpen, eventSkeleton } = storeToRefs(useCalendar())
|
||||||
@@ -30,7 +31,7 @@ async function handleAction(): Promise<void> {
|
|||||||
toast({
|
toast({
|
||||||
title: t("entity.calendar.event.deletedToast.title", { event: eventTitle }),
|
title: t("entity.calendar.event.deletedToast.title", { event: eventTitle }),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
duration: 3000
|
duration: ToastLifetime.MEDIUM
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhPencilSimpleLine, PhTag } from "@phosphor-icons/vue"
|
import { PhAlarm, PhCircleNotch, PhEye, PhEyeClosed, PhMapPinArea, PhPencilSimpleLine, PhTag } from "@phosphor-icons/vue"
|
||||||
import { useToast } from "~/components/ui/toast";
|
import { useToast } from "~/components/ui/toast";
|
||||||
|
import { ToastLifetime } from "~/components/ui/toast/use-toast";
|
||||||
import type { APIError } from "~/models/Errors";
|
import type { APIError } from "~/models/Errors";
|
||||||
|
|
||||||
const emit = defineEmits(["event-updated"])
|
const emit = defineEmits(["event-updated"])
|
||||||
@@ -30,7 +31,7 @@ async function handleAction() {
|
|||||||
toast({
|
toast({
|
||||||
title: t("entity.calendar.event.updatedToast.title", { event: eventSkeleton.value.title }),
|
title: t("entity.calendar.event.updatedToast.title", { event: eventSkeleton.value.title }),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
duration: 3000
|
duration: ToastLifetime.SHORT
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -41,7 +42,7 @@ async function handleAction() {
|
|||||||
title: t("entity.calendar.event.editErrors.toastTitle"),
|
title: t("entity.calendar.event.editErrors.toastTitle"),
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
description: t(`entity.calendar.event.editErrors.${error.path[1]}_${error.code}`),
|
description: t(`entity.calendar.event.editErrors.${error.path[1]}_${error.code}`),
|
||||||
duration: 2000,
|
duration: ToastLifetime.MEDIUM,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
21
components/global/AddCard.vue
Normal file
21
components/global/AddCard.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { PhPlusCircle } from "@phosphor-icons/vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["on-click"])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UiCard
|
||||||
|
class="h-full md:w-fit transition-all bg-transparent dark:hover:bg-gray-950 dark:focus-within:outline-gray-900 hover:-translate-y-0"
|
||||||
|
has-click
|
||||||
|
@on-click="emit('on-click')"
|
||||||
|
>
|
||||||
|
<UiCardHeader class="h-full justify-center items-center text-center gap-0">
|
||||||
|
<PhPlusCircle size="38" weight="fill" />
|
||||||
|
|
||||||
|
<UiCardTitle class="text-xl">
|
||||||
|
<slot />
|
||||||
|
</UiCardTitle>
|
||||||
|
</UiCardHeader>
|
||||||
|
</UiCard>
|
||||||
|
</template>
|
||||||
@@ -1,20 +1,112 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { type RPGColor, rpgColors } from "~/models/Color";
|
import { type RPGColor, rpgColors } from "~/models/Color";
|
||||||
|
|
||||||
const model = defineModel<RPGColor>()
|
const model = defineModel<RPGColor>({ default: "white" });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiSelect v-model="model">
|
<UiSelect v-model="model">
|
||||||
<UiSelectTrigger>
|
<UiSelectTrigger>
|
||||||
<UiSelectValue :placeholder="$t('ui.colors.selectOne')" />
|
<UiSelectValue
|
||||||
|
:placeholder="$t('ui.colors.selectOne')"
|
||||||
|
class="input-color"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
`input-color-${model}`,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
</UiSelectTrigger>
|
</UiSelectTrigger>
|
||||||
<UiSelectContent>
|
<UiSelectContent position="popper">
|
||||||
<UiSelectGroup>
|
<UiSelectGroup>
|
||||||
<UiSelectItem v-for="color in rpgColors" :key="color" :value="color">
|
<UiSelectItem
|
||||||
|
v-for="color in rpgColors"
|
||||||
|
:key="color"
|
||||||
|
:value="color"
|
||||||
|
class="input-color"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
`input-color-${color}`,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
{{ $t(`ui.colors.${color}`) }}
|
{{ $t(`ui.colors.${color}`) }}
|
||||||
</UiSelectItem>
|
</UiSelectItem>
|
||||||
</UiSelectGroup>
|
</UiSelectGroup>
|
||||||
</UiSelectContent>
|
</UiSelectContent>
|
||||||
</UiSelect>
|
</UiSelect>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.input-color {
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: .75rem;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
border-radius: .25rem;
|
||||||
|
background-color: red;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.input-color-red::before {
|
||||||
|
@apply bg-red-500;
|
||||||
|
}
|
||||||
|
&.input-color-orange::before {
|
||||||
|
@apply bg-orange-500;
|
||||||
|
}
|
||||||
|
&.input-color-amber::before {
|
||||||
|
@apply bg-amber-500;
|
||||||
|
}
|
||||||
|
&.input-color-yellow::before {
|
||||||
|
@apply bg-yellow-500;
|
||||||
|
}
|
||||||
|
&.input-color-lime::before {
|
||||||
|
@apply bg-lime-500;
|
||||||
|
}
|
||||||
|
&.input-color-green::before {
|
||||||
|
@apply bg-green-500;
|
||||||
|
}
|
||||||
|
&.input-color-emerald::before {
|
||||||
|
@apply bg-emerald-600;
|
||||||
|
}
|
||||||
|
&.input-color-teal::before {
|
||||||
|
@apply bg-teal-600;
|
||||||
|
}
|
||||||
|
&.input-color-cyan::before {
|
||||||
|
@apply bg-cyan-600;
|
||||||
|
}
|
||||||
|
&.input-color-sky::before {
|
||||||
|
@apply bg-sky-600;
|
||||||
|
}
|
||||||
|
&.input-color-blue::before {
|
||||||
|
@apply bg-blue-600;
|
||||||
|
}
|
||||||
|
&.input-color-indigo::before {
|
||||||
|
@apply bg-indigo-600;
|
||||||
|
}
|
||||||
|
&.input-color-violet::before {
|
||||||
|
@apply bg-violet-600;
|
||||||
|
}
|
||||||
|
&.input-color-purple::before {
|
||||||
|
@apply bg-purple-600;
|
||||||
|
}
|
||||||
|
&.input-color-fuchsia::before {
|
||||||
|
@apply bg-fuchsia-600;
|
||||||
|
}
|
||||||
|
&.input-color-pink::before {
|
||||||
|
@apply bg-pink-600;
|
||||||
|
}
|
||||||
|
&.input-color-rose::before {
|
||||||
|
@apply bg-rose-600;
|
||||||
|
}
|
||||||
|
&.input-color-black::before {
|
||||||
|
@apply bg-black dark:border-[1px] dark:border-slate-300;
|
||||||
|
}
|
||||||
|
&.input-color-white::before {
|
||||||
|
@apply bg-white border-[1px] border-slate-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function pushRoute(to: AvailableRoutes) {
|
|||||||
{{ $t('ui.sidebarMenu.avatarFallback') }}
|
{{ $t('ui.sidebarMenu.avatarFallback') }}
|
||||||
</UiAvatarFallback>
|
</UiAvatarFallback>
|
||||||
</UiAvatar>
|
</UiAvatar>
|
||||||
<UiButton v-else variant="outline" size="icon" class="rounded-full border-indigo-200 bg-indigo-700 dark:border-slate-300 dark:bg-neutral-900 dark:hover:bg-slate-50 dark:hover:text-slate-950 cursor-pointer">
|
<UiButton v-else variant="outline" size="icon" class="rounded-full border-indigo-200 bg-indigo-700 dark:border-slate-300 dark:bg-neutral-950 dark:hover:bg-slate-50 dark:hover:text-slate-950 cursor-pointer">
|
||||||
<PhUserCircle size="24" />
|
<PhUserCircle size="24" />
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</UiDropdownMenuTrigger>
|
</UiDropdownMenuTrigger>
|
||||||
|
|||||||
@@ -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-full max-w-3xl -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background 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-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
'fixed left-1/2 top-1/2 z-50 grid w-full 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-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||||
props.class,
|
props.class,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import { cn } from "@/lib/utils"
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
class?: HTMLAttributes["class"]
|
class?: HTMLAttributes["class"]
|
||||||
link?: string
|
link?: string
|
||||||
|
hasClick?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(["on-click"])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -13,7 +16,7 @@ const props = defineProps<{
|
|||||||
:class="
|
:class="
|
||||||
cn('rounded-lg border bg-card text-card-foreground shadow-sm transition-all isolate', props.class, {
|
cn('rounded-lg border bg-card text-card-foreground shadow-sm transition-all isolate', props.class, {
|
||||||
'relative outline outline-2 outline-offset-4 outline-transparent hover:-translate-y-[.2rem]':
|
'relative outline outline-2 outline-offset-4 outline-transparent hover:-translate-y-[.2rem]':
|
||||||
props.link
|
props.link || props.hasClick
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -24,5 +27,9 @@ const props = defineProps<{
|
|||||||
:to="props.link"
|
:to="props.link"
|
||||||
class="absolute inset-0 z-10 focus-visible:outline-none"
|
class="absolute inset-0 z-10 focus-visible:outline-none"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
v-if="props.hasClick"
|
||||||
|
class="absolute inset-0 z-10 focus-visible:outline-none"
|
||||||
|
@click="emit('on-click')" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const props = defineProps<{
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="cn('flex items-center p-6 pt-0', props.class)">
|
<div :class="cn('flex items-center p-6 border-t-2 border-t-slate-900', props.class)">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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 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-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm: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 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-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -36,9 +36,10 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
<SelectPortal>
|
<SelectPortal>
|
||||||
<SelectContent
|
<SelectContent
|
||||||
v-bind="{ ...forwarded, ...$attrs }"
|
v-bind="{ ...forwarded, ...$attrs }"
|
||||||
|
style="max-height: var(--radix-select-content-available-height)"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'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 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 px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
'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-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ import type { ToastProps } from "."
|
|||||||
const TOAST_LIMIT = 3
|
const TOAST_LIMIT = 3
|
||||||
const TOAST_REMOVE_DELAY = 1000000
|
const TOAST_REMOVE_DELAY = 1000000
|
||||||
|
|
||||||
|
export enum ToastLifetime {
|
||||||
|
SHORT = 2000,
|
||||||
|
MEDIUM = 3500,
|
||||||
|
LONG = 6000,
|
||||||
|
}
|
||||||
|
|
||||||
export type StringOrVNode =
|
export type StringOrVNode =
|
||||||
| string
|
| string
|
||||||
| VNode
|
| VNode
|
||||||
|
|||||||
74
components/world/PreviewCard.vue
Normal file
74
components/world/PreviewCard.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { PhFilePlus, PhPencil, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue";
|
||||||
|
import type { World } from "~/models/World";
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
world: World
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(["on-edit", "on-delete"]);
|
||||||
|
|
||||||
|
const { locale } = useI18n();
|
||||||
|
const createdAt = DateTime.fromISO(props.world.createdAt!).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value });
|
||||||
|
const updatedAt = computed<string>(() => props.world.updatedAt ? DateTime.fromISO(props.world.updatedAt).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value }) : "");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UiCard
|
||||||
|
class="w-full h-full transition-all"
|
||||||
|
:link="`/my/worlds/${world.id}`"
|
||||||
|
:class="{
|
||||||
|
'hover:bg-slate-50 dark:hover:bg-sky-950 dark:focus-within:outline-sky-900': !world.color,
|
||||||
|
'bg-red-100 dark:bg-red-950 border-red-200 hover:bg-red-50 dark:hover:bg-red-900 dark:border-red-900 dark:focus-within:outline-red-900': world.color === 'red',
|
||||||
|
'bg-orange-100 dark:bg-orange-950 border-orange-200 hover:bg-orange-50 dark:hover:bg-orange-900 dark:border-orange-900 dark:focus-within:outline-orange-900': world.color === 'orange',
|
||||||
|
'bg-amber-100 dark:bg-amber-950 border-amber-200 hover:bg-amber-50 dark:hover:bg-amber-900 dark:border-amber-900 dark:focus-within:outline-amber-900': world.color === 'amber',
|
||||||
|
'bg-yellow-100 dark:bg-yellow-950 border-yellow-200 hover:bg-yellow-50 dark:hover:bg-yellow-900 dark:border-yellow-900 dark:focus-within:outline-yellow-900': world.color === 'yellow',
|
||||||
|
'bg-lime-100 dark:bg-lime-950 border-lime-200 hover:bg-lime-50 dark:hover:bg-lime-900 dark:border-lime-900 dark:focus-within:outline-lime-900': world.color === 'lime',
|
||||||
|
' bg-green-100 dark:bg-green-950 border-green-200 hover:bg-green-50 dark:hover:bg-green-900 dark:border-green-900 dark:focus-within:outline-green-900': world.color === 'green',
|
||||||
|
'bg-emerald-100 dark:bg-emerald-950 border-emerald-200 hover:bg-emerald-50 dark:hover:bg-emerald-900 dark:border-emerald-900 dark:focus-within:outline-emerald-900': world.color === 'emerald',
|
||||||
|
'bg-teal-100 dark:bg-teal-950 border-teal-200 hover:bg-teal-50 dark:hover:bg-teal-900 dark:border-teal-900 dark:focus-within:outline-teal-900': world.color === 'teal',
|
||||||
|
'bg-cyan-100 dark:bg-cyan-950 border-cyan-200 hover:bg-cyan-50 dark:hover:bg-cyan-900 dark:border-cyan-900 dark:focus-within:outline-cyan-900': world.color === 'cyan',
|
||||||
|
'bg-sky-100 dark:bg-sky-950 border-sky-200 hover:bg-sky-50 dark:hover:bg-sky-900 dark:border-sky-900 dark:focus-within:outline-sky-900': world.color === 'sky',
|
||||||
|
'bg-blue-100 dark:bg-blue-950 border-blue-200 hover:bg-blue-50 dark:hover:bg-blue-900 dark:border-blue-900 dark:focus-within:outline-blue-900': world.color === 'blue',
|
||||||
|
'bg-indigo-100 dark:bg-indigo-950 border-indigo-200 hover:bg-indigo-50 dark:hover:bg-indigo-900 dark:border-indigo-900 dark:focus-within:outline-indigo-900': world.color === 'indigo',
|
||||||
|
'bg-violet-100 dark:bg-violet-950 border-violet-200 hover:bg-violet-50 dark:hover:bg-violet-900 dark:border-violet-900 dark:focus-within:outline-violet-900': world.color === 'violet',
|
||||||
|
'bg-purple-100 dark:bg-purple-950 border-purple-200 hover:bg-purple-50 dark:hover:bg-purple-900 dark:border-purple-900 dark:focus-within:outline-purple-900': world.color === 'purple',
|
||||||
|
'bg-fuchsia-100 dark:bg-fuchsia-950 border-fuchsia-200 hover:bg-fuchsia-50 dark:hover:bg-fuchsia-900 dark:border-fuchsia-900 dark:focus-within:outline-fuchsia-900': world.color === 'fuchsia',
|
||||||
|
'bg-pink-100 dark:bg-pink-950 border-pink-200 hover:bg-pink-50 dark:hover:bg-pink-900 dark:border-pink-900 dark:focus-within:outline-pink-900': world.color === 'pink',
|
||||||
|
'bg-pink-100 dark:bg-rose-950 border-rose-200 hover:bg-rose-50 dark:hover:bg-rose-900 dark:border-rose-900 dark:focus-within:outline-rose-900': world.color === 'rose',
|
||||||
|
'text-slate-100 bg-slate-900 border-slate-700 hover:bg-slate-700 dark:hover:bg-slate-800 dark:border-slate-900 dark:focus-within:outline-slate-900': world.color === 'black',
|
||||||
|
' hover:bg-slate-50 dark:hover:text-slate-900 dark:hover:bg-slate-300 dark:border-slate-500 dark:focus-within:outline-slate-100': world.color === 'white',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<UiCardHeader>
|
||||||
|
<UiCardTitle>{{ world.name }}</UiCardTitle>
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<p class="italic">{{ world.description }}</p>
|
||||||
|
|
||||||
|
<div class="flex gap-1 absolute top-4 right-4 z-20">
|
||||||
|
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-indigo-400 dark:hover:bg-indigo-700" @click="emit('on-edit')">
|
||||||
|
<PhPencil size="16" />
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-rose-400 dark:hover:bg-rose-700" @click="emit('on-delete')">
|
||||||
|
<PhTrash size="16" />
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</UiCardContent>
|
||||||
|
|
||||||
|
<UiCardFooter>
|
||||||
|
<ul class="grid gap-2">
|
||||||
|
<li class="flex gap-1 items-center">
|
||||||
|
<PhFilePlus size="20" />
|
||||||
|
<span>{{ $t('common.createdAt', { createdAt }) }}</span>
|
||||||
|
</li>
|
||||||
|
<li v-if="updatedAt" class="flex gap-1 items-center">
|
||||||
|
<PhPencilSimpleLine size="20" />
|
||||||
|
<span>{{ $t('common.updatedAt', { updatedAt }) }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</UiCardFooter>
|
||||||
|
</UiCard>
|
||||||
|
</template>
|
||||||
@@ -15,12 +15,13 @@ const emit = defineEmits(["on-close"])
|
|||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
emit("on-close")
|
emit("on-close")
|
||||||
|
worldSkeletonName.value = ""
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiAlertDialog :open="modalState">
|
<UiAlertDialog :open="modalState">
|
||||||
<UiAlertDialogContent class="gap-4">
|
<UiAlertDialogContent class="gap-4" @close-auto-focus="(e) => e.preventDefault()">
|
||||||
<header>
|
<header>
|
||||||
<UiAlertDialogTitle>
|
<UiAlertDialogTitle>
|
||||||
<span class="text-2xl">
|
<span class="text-2xl">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PhCircleNotch } from "@phosphor-icons/vue";
|
import { PhCircleNotch } from "@phosphor-icons/vue";
|
||||||
import { useToast } from "~/components/ui/toast";
|
import { useToast } from "~/components/ui/toast";
|
||||||
|
import { ToastLifetime } from "~/components/ui/toast/use-toast";
|
||||||
import type { World } from "~/models/World";
|
import type { World } from "~/models/World";
|
||||||
|
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
@@ -28,7 +29,7 @@ async function handleAction(): Promise<void> {
|
|||||||
toast({
|
toast({
|
||||||
title: t("entity.world.deletedToast.title", { world: props.world.name }),
|
title: t("entity.world.deletedToast.title", { world: props.world.name }),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
duration: 3000
|
duration: ToastLifetime.SHORT
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
@@ -65,6 +66,7 @@ function handleClosing() {
|
|||||||
@focus-outside="handleClosing"
|
@focus-outside="handleClosing"
|
||||||
@interact-outside="handleClosing"
|
@interact-outside="handleClosing"
|
||||||
@pointer-down-outside="handleClosing"
|
@pointer-down-outside="handleClosing"
|
||||||
|
@close-auto-focus="(e) => e.preventDefault()"
|
||||||
>
|
>
|
||||||
<UiAlertDialogTitle>
|
<UiAlertDialogTitle>
|
||||||
{{ $t('entity.world.deleteDialog.title') }}
|
{{ $t('entity.world.deleteDialog.title') }}
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ const emit = defineEmits(["on-close"])
|
|||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
emit("on-close")
|
emit("on-close")
|
||||||
|
worldSkeletonName.value = props.world?.name ?? ""
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiAlertDialog :open="modalState">
|
<UiAlertDialog :open="modalState">
|
||||||
<UiAlertDialogContent class="gap-4">
|
<UiAlertDialogContent class="gap-4" @close-auto-focus="(e) => e.preventDefault()">
|
||||||
<header>
|
<header>
|
||||||
<UiAlertDialogTitle>
|
<UiAlertDialogTitle>
|
||||||
<span class="text-2xl">
|
<span class="text-2xl">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { World } from "~/models/World";
|
|||||||
|
|
||||||
const user = useSupabaseUser()
|
const user = useSupabaseUser()
|
||||||
|
|
||||||
const defaultWorld: World = { name: "", description: "", gmId: user.value?.id }
|
const defaultWorld: World = { name: "", description: "", gmId: user.value?.id, color: "white", state: "draft" }
|
||||||
const worldSkeleton = ref<World>({ ...defaultWorld })
|
const worldSkeleton = ref<World>({ ...defaultWorld })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PhCircleNotch } from "@phosphor-icons/vue";
|
import { PhCircleNotch } from "@phosphor-icons/vue";
|
||||||
import { useToast } from "~/components/ui/toast";
|
import { useToast } from "~/components/ui/toast";
|
||||||
|
import { ToastLifetime } from "~/components/ui/toast/use-toast";
|
||||||
import type { World } from "~/models/World";
|
import type { World } from "~/models/World";
|
||||||
|
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
@@ -36,7 +37,7 @@ async function handleSubmit() {
|
|||||||
toast({
|
toast({
|
||||||
title: t("entity.world.updatedToast.title", { world: worldSkeleton.value.name }),
|
title: t("entity.world.updatedToast.title", { world: worldSkeleton.value.name }),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
duration: 3000
|
duration: ToastLifetime.SHORT
|
||||||
})
|
})
|
||||||
|
|
||||||
emit("on-close")
|
emit("on-close")
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ export default defineI18nConfig(() => ({
|
|||||||
displayMode: "Display mode"
|
displayMode: "Display mode"
|
||||||
},
|
},
|
||||||
common: {
|
common: {
|
||||||
title: "Title"
|
title: "Title",
|
||||||
|
createdAt: "Created {createdAt}",
|
||||||
|
updatedAt: "Last updated {updatedAt}",
|
||||||
},
|
},
|
||||||
entity: {
|
entity: {
|
||||||
category: {
|
category: {
|
||||||
@@ -81,6 +83,8 @@ export default defineI18nConfig(() => ({
|
|||||||
nameSingular: "World",
|
nameSingular: "World",
|
||||||
namePlural: "Worlds",
|
namePlural: "Worlds",
|
||||||
addSingle: "Add a world",
|
addSingle: "Add a world",
|
||||||
|
addSingleFirst: "Add your first world !",
|
||||||
|
editSingle: "Edit world",
|
||||||
notFound: "World not found",
|
notFound: "World not found",
|
||||||
notFoundDescription: "The link is not valid or the world has been deleted / archived.",
|
notFoundDescription: "The link is not valid or the world has been deleted / archived.",
|
||||||
backToList: "Go back to worlds",
|
backToList: "Go back to worlds",
|
||||||
@@ -109,12 +113,13 @@ export default defineI18nConfig(() => ({
|
|||||||
namePublicSingular: "Public Calendar",
|
namePublicSingular: "Public Calendar",
|
||||||
namePublicPlural: "Public Calendars",
|
namePublicPlural: "Public Calendars",
|
||||||
addSingle: "Add a calendar",
|
addSingle: "Add a calendar",
|
||||||
|
addSingleFirst: "Add your first calendar !",
|
||||||
notFound: "Calendar not found",
|
notFound: "Calendar not found",
|
||||||
notFoundDescription: "The link is not valid or the calendar has been deleted / archived.",
|
notFoundDescription: "The link is not valid or the calendar has been deleted / archived.",
|
||||||
notFoundForWorld: "No calendar for this world… yet !",
|
notFoundForWorld: "No calendar for this world… yet !",
|
||||||
backToList: "Go back to calendars",
|
backToList: "Go back to calendars",
|
||||||
isLoading: "Calendar is loading…",
|
isLoading: "Calendar is loading…",
|
||||||
hasXEvents: "This calendar has {count} public events",
|
hasXEvents: "{count} available events",
|
||||||
date: {
|
date: {
|
||||||
start: "Start date",
|
start: "Start date",
|
||||||
end: "End date",
|
end: "End date",
|
||||||
@@ -306,7 +311,9 @@ export default defineI18nConfig(() => ({
|
|||||||
displayMode: "Mode d'affichage",
|
displayMode: "Mode d'affichage",
|
||||||
},
|
},
|
||||||
common: {
|
common: {
|
||||||
title: "Titre"
|
title: "Titre",
|
||||||
|
createdAt: "Créé le {createdAt}",
|
||||||
|
updatedAt: "Dernière modification le {updatedAt}",
|
||||||
},
|
},
|
||||||
entity: {
|
entity: {
|
||||||
category: {
|
category: {
|
||||||
@@ -331,6 +338,8 @@ export default defineI18nConfig(() => ({
|
|||||||
nameSingular: "Monde",
|
nameSingular: "Monde",
|
||||||
namePlural: "Mondes",
|
namePlural: "Mondes",
|
||||||
addSingle: "Ajouter un monde",
|
addSingle: "Ajouter un monde",
|
||||||
|
addSingleFirst: "Ajouter votre premier monde !",
|
||||||
|
editSingle: "Modifier le monde",
|
||||||
notFound: "Aucun monde trouvé",
|
notFound: "Aucun monde trouvé",
|
||||||
notFoundDescription: "Le lien n'est pas valide ou le monde a été supprimé / archivé.",
|
notFoundDescription: "Le lien n'est pas valide ou le monde a été supprimé / archivé.",
|
||||||
backToList: "Retourner aux mondes",
|
backToList: "Retourner aux mondes",
|
||||||
@@ -354,17 +363,18 @@ export default defineI18nConfig(() => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
calendar: {
|
calendar: {
|
||||||
nameSingular: "Calendriers",
|
nameSingular: "Calendrier",
|
||||||
namePlural: "Calendrier",
|
namePlural: "Calendriers",
|
||||||
namePublicSingular: "Calendrier Public",
|
namePublicSingular: "Calendrier Public",
|
||||||
namePublicPlural: "Calendriers Publics",
|
namePublicPlural: "Calendriers Publics",
|
||||||
addSingle: "Ajouter un calendrier",
|
addSingle: "Ajouter un calendrier",
|
||||||
|
addSingleFirst: "Ajouter votre premier calendrier !",
|
||||||
notFound: "Aucun calendrier trouvé",
|
notFound: "Aucun calendrier trouvé",
|
||||||
notFoundDescription: "Le lien n'est pas valide ou le calendrier a été supprimé / archivé.",
|
notFoundDescription: "Le lien n'est pas valide ou le calendrier a été supprimé / archivé.",
|
||||||
notFoundForWorld: "Aucun calendrier pour ce monde… pour l'instant !",
|
notFoundForWorld: "Aucun calendrier pour ce monde… pour l'instant !",
|
||||||
backToList: "Retourner aux calendriers",
|
backToList: "Retourner aux calendriers",
|
||||||
isLoading: "Chargement du calendrier…",
|
isLoading: "Chargement du calendrier…",
|
||||||
hasXEvents: "Ce calendrier contient {count} évènements publiques",
|
hasXEvents: "{count} évènements disponibles",
|
||||||
date: {
|
date: {
|
||||||
start: "Date de début",
|
start: "Date de début",
|
||||||
end: "Date de fin",
|
end: "Date de fin",
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export interface Calendar extends CalendarConfig {
|
|||||||
state: CalendarState
|
state: CalendarState
|
||||||
color?: string
|
color?: string
|
||||||
world?: World
|
world?: World
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const postCalendarSchema = z.object({
|
export const postCalendarSchema = z.object({
|
||||||
|
|||||||
@@ -8,16 +8,18 @@ export interface World {
|
|||||||
id?: number
|
id?: number
|
||||||
name: string
|
name: string
|
||||||
description?: string
|
description?: string
|
||||||
color?: RPGColor,
|
color?: RPGColor
|
||||||
calendars?: Calendar[],
|
calendars?: Calendar[]
|
||||||
gmId?: string
|
gmId?: string
|
||||||
state?: WorldState
|
state?: WorldState
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const postWorldSchema = z.object({
|
export const postWorldSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.string().optional().nullable(),
|
description: z.string().optional().nullable(),
|
||||||
color: rpgColorSchema,
|
color: rpgColorSchema.default("white"),
|
||||||
gmId: z.string().optional().nullable(),
|
gmId: z.string().optional().nullable(),
|
||||||
state: z.string().optional().nullable().default("draft"),
|
state: z.string().optional().nullable().default("draft"),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-vue-next": "^0.475.0",
|
"lucide-vue-next": "^0.475.0",
|
||||||
|
"luxon": "^3.5.0",
|
||||||
"nuxt": "^3.15.4",
|
"nuxt": "^3.15.4",
|
||||||
"pinia": "^3.0.1",
|
"pinia": "^3.0.1",
|
||||||
"radix-vue": "^1.9.16",
|
"radix-vue": "^1.9.16",
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
"@nuxtjs/supabase": "^1.4.6",
|
"@nuxtjs/supabase": "^1.4.6",
|
||||||
"@nuxtjs/tailwindcss": "^6.13.1",
|
"@nuxtjs/tailwindcss": "^6.13.1",
|
||||||
"@stylistic/eslint-plugin-js": "^3.1.0",
|
"@stylistic/eslint-plugin-js": "^3.1.0",
|
||||||
|
"@types/luxon": "^3.4.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.25.0",
|
"@typescript-eslint/eslint-plugin": "^8.25.0",
|
||||||
"@typescript-eslint/parser": "^8.25.0",
|
"@typescript-eslint/parser": "^8.25.0",
|
||||||
"eslint": "^9.21.0",
|
"eslint": "^9.21.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Calendar } from "~/models/CalendarConfig";
|
import type { Calendar } from "~/models/CalendarConfig";
|
||||||
|
|
||||||
const { data: availableCalendars } = await useLazyFetch<{ data: Calendar[] }>("/api/calendars/query", { key: "explore-calendars" })
|
const { data: availableCalendars } = await useLazyFetch<{ data: Calendar[] }>("/api/calendars/query", { key: "explore-calendars", query: { full: true } })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -22,20 +22,7 @@ const { data: availableCalendars } = await useLazyFetch<{ data: Calendar[] }>("/
|
|||||||
|
|
||||||
<ul v-if="availableCalendars?.data" class="grid md:grid-cols-3 gap-2">
|
<ul v-if="availableCalendars?.data" class="grid md:grid-cols-3 gap-2">
|
||||||
<li v-for="calendar in availableCalendars.data" :key="calendar.shortId">
|
<li v-for="calendar in availableCalendars.data" :key="calendar.shortId">
|
||||||
<UiCard
|
<CalendarPreviewCard :calendar="calendar" :gm-id="calendar.world?.gmId" />
|
||||||
class="w-full transition-all hover:bg-slate-50 dark:bg-gray-950 dark:hover:bg-indigo-950 dark:focus-within:outline-gray-900"
|
|
||||||
:link="`/calendars/${calendar.shortId}`"
|
|
||||||
>
|
|
||||||
<UiCardHeader>
|
|
||||||
<UiCardTitle class="text-xl pr-12">{{ calendar.name }}</UiCardTitle>
|
|
||||||
</UiCardHeader>
|
|
||||||
|
|
||||||
<UiCardContent>
|
|
||||||
<p>
|
|
||||||
{{ $t("entity.calendar.hasXEvents", { count: calendar.eventNb?.[0].count }) }}
|
|
||||||
</p>
|
|
||||||
</UiCardContent>
|
|
||||||
</UiCard>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Spacing>
|
</Spacing>
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PhPencil, PhPlus, PhTrash } from "@phosphor-icons/vue";
|
|
||||||
import type { RealtimeChannel } from "@supabase/supabase-js"
|
import type { RealtimeChannel } from "@supabase/supabase-js"
|
||||||
import type { World } from "~/models/World";
|
import type { World } from "~/models/World";
|
||||||
|
|
||||||
const supabase = useSupabaseClient()
|
const supabase = useSupabaseClient()
|
||||||
|
const user = useSupabaseUser()
|
||||||
const { data: worlds } = await useLazyFetch<{ data: World[] }>("/api/worlds/query")
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ["auth-guard"]
|
middleware: ["auth-guard"]
|
||||||
})
|
})
|
||||||
|
|
||||||
const user = useSupabaseUser()
|
const { data: worlds } = await useLazyFetch<{ data: World[] }>("/api/worlds/query", { query: { gmId: user?.value!.id } })
|
||||||
|
const sortedWorlds = computed(() => worlds.value?.data ? [...worlds.value.data].sort((a, b) => (a.id ?? 0) - (b.id ?? 0)) : [])
|
||||||
|
|
||||||
// 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) => {
|
||||||
@@ -48,7 +47,7 @@ function handleDeletedWorld(id: number) {
|
|||||||
if (!worlds.value?.data) return
|
if (!worlds.value?.data) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
worlds.value.data.splice(worlds.value.data.findIndex(w => w.id === id))
|
worlds.value.data.splice(worlds.value.data.findIndex(w => w.id === id), 1)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
}
|
}
|
||||||
@@ -72,7 +71,7 @@ onMounted(() => {
|
|||||||
case "UPDATE":
|
case "UPDATE":
|
||||||
if (!worlds.value?.data) return
|
if (!worlds.value?.data) return
|
||||||
|
|
||||||
worlds.value.data = (await $fetch("/api/worlds/query")).data as World[]
|
worlds.value.data = (await $fetch("/api/worlds/query", { query: { gmId: user?.value!.id } })).data as World[]
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -129,68 +128,21 @@ function hideEditModal() {
|
|||||||
<Heading level="h2">
|
<Heading level="h2">
|
||||||
{{ $t('entity.world.namePlural') }}
|
{{ $t('entity.world.namePlural') }}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<UiTooltipProvider :delay-duration="250">
|
|
||||||
<UiTooltip>
|
|
||||||
<UiTooltipTrigger as-child>
|
|
||||||
<UiButton size="icon" class="rounded-full h-8 w-8" @click="() => isCreateWorldModalOpen = true">
|
|
||||||
<PhPlus size="17"/>
|
|
||||||
</UiButton>
|
|
||||||
</UiTooltipTrigger>
|
|
||||||
<UiTooltipContent :side-offset="10">
|
|
||||||
<p>
|
|
||||||
{{ $t('entity.world.addSingle') }}
|
|
||||||
</p>
|
|
||||||
</UiTooltipContent>
|
|
||||||
</UiTooltip>
|
|
||||||
</UiTooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-if="worlds?.data" class="grid lg:grid-cols-3 gap-2">
|
<ul v-if="worlds?.data" class="grid lg:grid-cols-3 gap-2">
|
||||||
<li v-for="world in worlds.data" :key="world.id">
|
<li v-for="world in sortedWorlds" :key="world.id">
|
||||||
<UiCard
|
<WorldPreviewCard :world="world" @on-edit="() => deployEditModal(world)" @on-delete="() => deployDeleteModal(world)" />
|
||||||
class="w-full transition-all"
|
</li>
|
||||||
:link="`/my/worlds/${world.id}`"
|
<li class="md:w-fit">
|
||||||
:class="{
|
<AddCard @on-click="() => isCreateWorldModalOpen = true">
|
||||||
'hover:bg-slate-50 dark:hover:bg-sky-950 dark:focus-within:outline-sky-900': !world.color,
|
<template v-if="worlds?.data?.length > 0">
|
||||||
'bg-red-100 dark:bg-red-950 border-red-200 hover:bg-red-50 dark:hover:bg-red-900 dark:border-red-900 dark:focus-within:outline-red-900': world.color === 'red',
|
{{ $t('entity.world.addSingle') }}
|
||||||
'bg-orange-100 dark:bg-orange-950 border-orange-200 hover:bg-orange-50 dark:hover:bg-orange-900 dark:border-orange-900 dark:focus-within:outline-orange-900': world.color === 'orange',
|
</template>
|
||||||
'bg-amber-100 dark:bg-amber-950 border-amber-200 hover:bg-amber-50 dark:hover:bg-amber-900 dark:border-amber-900 dark:focus-within:outline-amber-900': world.color === 'amber',
|
<template v-else>
|
||||||
'bg-yellow-100 dark:bg-yellow-950 border-yellow-200 hover:bg-yellow-50 dark:hover:bg-yellow-900 dark:border-yellow-900 dark:focus-within:outline-yellow-900': world.color === 'yellow',
|
{{ $t('entity.world.addSingleFirst') }}
|
||||||
'bg-lime-100 dark:bg-lime-950 border-lime-200 hover:bg-lime-50 dark:hover:bg-lime-900 dark:border-lime-900 dark:focus-within:outline-lime-900': world.color === 'lime',
|
</template>
|
||||||
' bg-green-100 dark:bg-green-950 border-green-200 hover:bg-green-50 dark:hover:bg-green-900 dark:border-green-900 dark:focus-within:outline-green-900': world.color === 'green',
|
</AddCard>
|
||||||
'bg-emerald-100 dark:bg-emerald-950 border-emerald-200 hover:bg-emerald-50 dark:hover:bg-emerald-900 dark:border-emerald-900 dark:focus-within:outline-emerald-900': world.color === 'emerald',
|
|
||||||
'bg-teal-100 dark:bg-teal-950 border-teal-200 hover:bg-teal-50 dark:hover:bg-teal-900 dark:border-teal-900 dark:focus-within:outline-teal-900': world.color === 'teal',
|
|
||||||
'bg-cyan-100 dark:bg-cyan-950 border-cyan-200 hover:bg-cyan-50 dark:hover:bg-cyan-900 dark:border-cyan-900 dark:focus-within:outline-cyan-900': world.color === 'cyan',
|
|
||||||
'bg-sky-100 dark:bg-sky-950 border-sky-200 hover:bg-sky-50 dark:hover:bg-sky-900 dark:border-sky-900 dark:focus-within:outline-sky-900': world.color === 'sky',
|
|
||||||
'bg-blue-100 dark:bg-blue-950 border-blue-200 hover:bg-blue-50 dark:hover:bg-blue-900 dark:border-blue-900 dark:focus-within:outline-blue-900': world.color === 'blue',
|
|
||||||
'bg-indigo-100 dark:bg-indigo-950 border-indigo-200 hover:bg-indigo-50 dark:hover:bg-indigo-900 dark:border-indigo-900 dark:focus-within:outline-indigo-900': world.color === 'indigo',
|
|
||||||
'bg-violet-100 dark:bg-violet-950 border-violet-200 hover:bg-violet-50 dark:hover:bg-violet-900 dark:border-violet-900 dark:focus-within:outline-violet-900': world.color === 'violet',
|
|
||||||
'bg-purple-100 dark:bg-purple-950 border-purple-200 hover:bg-purple-50 dark:hover:bg-purple-900 dark:border-purple-900 dark:focus-within:outline-purple-900': world.color === 'purple',
|
|
||||||
'bg-fuchsia-100 dark:bg-fuchsia-950 border-fuchsia-200 hover:bg-fuchsia-50 dark:hover:bg-fuchsia-900 dark:border-fuchsia-900 dark:focus-within:outline-fuchsia-900': world.color === 'fuchsia',
|
|
||||||
'bg-pink-100 dark:bg-pink-950 border-pink-200 hover:bg-pink-50 dark:hover:bg-pink-900 dark:border-pink-900 dark:focus-within:outline-pink-900': world.color === 'pink',
|
|
||||||
'bg-pink-100 dark:bg-rose-950 border-rose-200 hover:bg-rose-50 dark:hover:bg-rose-900 dark:border-rose-900 dark:focus-within:outline-rose-900': world.color === 'rose',
|
|
||||||
'text-slate-100 bg-slate-900 border-slate-700 hover:bg-slate-700 dark:hover:bg-slate-800 dark:border-slate-900 dark:focus-within:outline-slate-900': world.color === 'black',
|
|
||||||
' hover:bg-slate-50 dark:hover:text-slate-900 dark:hover:bg-slate-300 dark:border-slate-500 dark:focus-within:outline-slate-100': world.color === 'white',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<UiCardHeader>
|
|
||||||
<UiCardTitle>{{ world.name }}</UiCardTitle>
|
|
||||||
</UiCardHeader>
|
|
||||||
<UiCardContent>
|
|
||||||
<p class="italic">{{ world.description }}</p>
|
|
||||||
|
|
||||||
<div class="flex gap-1 absolute top-4 right-4 z-20">
|
|
||||||
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-indigo-400 dark:hover:bg-indigo-700" @click="deployEditModal(world)">
|
|
||||||
<PhPencil size="16" />
|
|
||||||
</UiButton>
|
|
||||||
|
|
||||||
<UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-rose-400 dark:hover:bg-rose-700" @click="deployDeleteModal(world)">
|
|
||||||
<PhTrash size="16" />
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
|
||||||
</UiCardContent>
|
|
||||||
</UiCard>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Spacing>
|
</Spacing>
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
import type { RealtimeChannel } from "@supabase/supabase-js"
|
import type { RealtimeChannel } from "@supabase/supabase-js"
|
||||||
import type { World } from "~/models/World";
|
import type { World } from "~/models/World";
|
||||||
import type { Calendar } from "~/models/CalendarConfig";
|
import type { Calendar } from "~/models/CalendarConfig";
|
||||||
import { PhArrowBendDoubleUpLeft, PhGlobeHemisphereWest, PhPlus, PhTrash } from "@phosphor-icons/vue";
|
import { PhArrowBendDoubleUpLeft, PhGlobeHemisphereWest, PhPencil } from "@phosphor-icons/vue";
|
||||||
|
|
||||||
const supabase = useSupabaseClient()
|
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 { 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({
|
definePageMeta({
|
||||||
middleware: ["auth-guard"]
|
middleware: ["auth-guard"]
|
||||||
@@ -30,11 +31,13 @@ function hideCreateDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* === Calendar subscriptions ===
|
* === Subscriptions ===
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Active calendar channel */
|
/** Active calendar channel */
|
||||||
let calendarChannel: RealtimeChannel
|
let calendarChannel: RealtimeChannel
|
||||||
|
/** Active world channel */
|
||||||
|
let worldChannel: RealtimeChannel
|
||||||
|
|
||||||
/** Handles calendar insertion realtime events */
|
/** Handles calendar insertion realtime events */
|
||||||
function handleInsertedCalendar(newCalendar: Calendar) {
|
function handleInsertedCalendar(newCalendar: Calendar) {
|
||||||
@@ -52,7 +55,7 @@ function handleDeletedCalendar(id: number) {
|
|||||||
if (!world.value) return
|
if (!world.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
world.value.data.calendars?.splice(world.value.data.calendars.findIndex(c => c.id === id))
|
world.value.data.calendars?.splice(world.value.data.calendars.findIndex(c => c.id === id), 1)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
}
|
}
|
||||||
@@ -81,24 +84,59 @@ onMounted(() => {
|
|||||||
)
|
)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// Unsubscribe from realtime
|
// Unsubscribe from realtime
|
||||||
supabase.removeChannel(calendarChannel)
|
supabase.removeChannel(calendarChannel)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
worldChannel = supabase.channel("realtime-world-channel")
|
||||||
|
.on(
|
||||||
|
"postgres_changes",
|
||||||
|
{ event: "*", schema: "public", table: "worlds" },
|
||||||
|
async (payload) => {
|
||||||
|
switch (payload.eventType) {
|
||||||
|
case "UPDATE":
|
||||||
|
if (!world.value?.data) return
|
||||||
|
|
||||||
|
world.value.data = (await $fetch<{ data: World }>("/api/worlds/query", { query: { id, full: true } })).data
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log("Unknown event has been triggered. This should not happen unless Supabase added one somehow.")
|
||||||
|
console.log(payload)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.subscribe()
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
// Unsubscribe from realtime
|
||||||
|
supabase.removeChannel(worldChannel)
|
||||||
|
})
|
||||||
|
|
||||||
const markedCalendar = ref<Calendar | null>(null)
|
const markedCalendar = ref<Calendar | null>(null)
|
||||||
const isDeleteCalendarModalOpen = ref<boolean>(false)
|
const isDeleteCalendarModalOpen = ref<boolean>(false)
|
||||||
|
const isEditWorldModalOpen = ref<boolean>(false)
|
||||||
|
|
||||||
function deployDeleteModal(calendar: Calendar) {
|
function deployDeleteCalendarModal(calendar: Calendar) {
|
||||||
isDeleteCalendarModalOpen.value = true
|
isDeleteCalendarModalOpen.value = true
|
||||||
markedCalendar.value = calendar
|
markedCalendar.value = calendar
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideDeleteModal() {
|
function hideDeleteCalendarModal() {
|
||||||
isDeleteCalendarModalOpen.value = false
|
isDeleteCalendarModalOpen.value = false
|
||||||
markedCalendar.value = null
|
markedCalendar.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deployEditModal() {
|
||||||
|
isEditWorldModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideEditModal() {
|
||||||
|
isEditWorldModalOpen.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -119,7 +157,24 @@ function hideDeleteModal() {
|
|||||||
|
|
||||||
<header class="lg:w-1/2 mb-8">
|
<header class="lg:w-1/2 mb-8">
|
||||||
<Spacing>
|
<Spacing>
|
||||||
<Heading level="h1">{{ world.data.name }}</Heading>
|
<div class="flex items-center gap-2">
|
||||||
|
<Heading level="h1">{{ world.data.name }}</Heading>
|
||||||
|
|
||||||
|
<UiTooltipProvider :delay-duration="250">
|
||||||
|
<UiTooltip>
|
||||||
|
<UiTooltipTrigger as-child>
|
||||||
|
<UiButton size="icon" class="rounded-full h-8 w-8" @click="deployEditModal">
|
||||||
|
<PhPencil size="17" weight="fill" />
|
||||||
|
</UiButton>
|
||||||
|
</UiTooltipTrigger>
|
||||||
|
<UiTooltipContent :side-offset="12" side="right">
|
||||||
|
<p>
|
||||||
|
{{ $t('entity.world.editSingle') }}
|
||||||
|
</p>
|
||||||
|
</UiTooltipContent>
|
||||||
|
</UiTooltip>
|
||||||
|
</UiTooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>{{ world.data.description }}</p>
|
<p>{{ world.data.description }}</p>
|
||||||
</Spacing>
|
</Spacing>
|
||||||
@@ -131,53 +186,30 @@ function hideDeleteModal() {
|
|||||||
<Heading level="h2">
|
<Heading level="h2">
|
||||||
{{ $t('entity.calendar.namePlural') }}
|
{{ $t('entity.calendar.namePlural') }}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<UiTooltipProvider :delay-duration="250">
|
|
||||||
<UiTooltip>
|
|
||||||
<UiTooltipTrigger as-child>
|
|
||||||
<UiButton size="icon" class="rounded-full h-8 w-8" @click="() => isCreateCalendarModalOpen = true">
|
|
||||||
<PhPlus size="17"/>
|
|
||||||
</UiButton>
|
|
||||||
</UiTooltipTrigger>
|
|
||||||
<UiTooltipContent :side-offset="10">
|
|
||||||
<p>
|
|
||||||
{{ $t('entity.calendar.addSingle') }}
|
|
||||||
</p>
|
|
||||||
</UiTooltipContent>
|
|
||||||
</UiTooltip>
|
|
||||||
</UiTooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-if="world.data.calendars && world.data.calendars?.length > 0" class="grid md:grid-cols-3 gap-2">
|
<ul class="grid md:grid-cols-3 gap-2">
|
||||||
<li v-for="calendar in world.data.calendars" :key="calendar.id">
|
<li v-for="calendar in sortedCalendars" :key="calendar.id">
|
||||||
<UiCard
|
<CalendarPreviewCard :calendar="calendar" :gm-id="world.data.gmId" show-actions @on-delete="() => deployDeleteCalendarModal(calendar)" />
|
||||||
class="w-full transition-all hover:bg-slate-50 dark:bg-gray-950 dark:hover:bg-indigo-950 dark:focus-within:outline-gray-900"
|
</li>
|
||||||
:link="`/my/calendars/${calendar.id}`"
|
|
||||||
>
|
|
||||||
<UiCardHeader>
|
|
||||||
<UiCardTitle class="text-xl pr-12">{{ calendar.name }}</UiCardTitle>
|
|
||||||
</UiCardHeader>
|
|
||||||
|
|
||||||
<UiCardContent>
|
<li class="md:w-fit">
|
||||||
<p class="italic">Description future (ou alors des informations sur le nb d'évènements)</p>
|
<AddCard @on-click="() => isCreateCalendarModalOpen = true">
|
||||||
|
<template v-if="sortedCalendars.length > 0">
|
||||||
<UiButton size="icon" variant="ghost" class="absolute top-2 right-2 z-20 hover:text-white hover:bg-rose-400 dark:hover:bg-rose-700" @click="deployDeleteModal(calendar)">
|
{{ $t('entity.calendar.addSingle') }}
|
||||||
<PhTrash size="16" />
|
</template>
|
||||||
</UiButton>
|
<template v-else>
|
||||||
</UiCardContent>
|
{{ $t('entity.calendar.addSingleFirst') }}
|
||||||
</UiCard>
|
</template>
|
||||||
|
</AddCard>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<template v-else>
|
|
||||||
<p class="pl-6 opacity-75 italic">
|
|
||||||
{{ $t('entity.calendar.notFoundForWorld') }}
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
</Spacing>
|
</Spacing>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<WorldDialogEdit :world="world.data" :modal-state="isEditWorldModalOpen" @on-close="hideEditModal" />
|
||||||
<CalendarDialogCreate :world="world.data" :modal-state="isCreateCalendarModalOpen" @on-close="hideCreateDialog" />
|
<CalendarDialogCreate :world="world.data" :modal-state="isCreateCalendarModalOpen" @on-close="hideCreateDialog" />
|
||||||
<CalendarDialogDelete :calendar="markedCalendar" :modal-state="isDeleteCalendarModalOpen" @on-close="hideDeleteModal" />
|
<CalendarDialogDelete :calendar="markedCalendar" :modal-state="isDeleteCalendarModalOpen" @on-close="hideDeleteCalendarModal"/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="h-full w-full grid place-items-center">
|
<div class="h-full w-full grid place-items-center">
|
||||||
|
|||||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@@ -41,6 +41,9 @@ importers:
|
|||||||
lucide-vue-next:
|
lucide-vue-next:
|
||||||
specifier: ^0.475.0
|
specifier: ^0.475.0
|
||||||
version: 0.475.0(vue@3.5.13(typescript@5.7.3))
|
version: 0.475.0(vue@3.5.13(typescript@5.7.3))
|
||||||
|
luxon:
|
||||||
|
specifier: ^3.5.0
|
||||||
|
version: 3.5.0
|
||||||
nuxt:
|
nuxt:
|
||||||
specifier: ^3.15.4
|
specifier: ^3.15.4
|
||||||
version: 3.15.4(@parcel/watcher@2.5.1)(@types/node@22.13.5)(db0@0.2.4)(encoding@0.1.13)(eslint@9.21.0(jiti@2.4.2))(ioredis@5.5.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.8)(sass@1.85.1)(terser@5.39.0)(typescript@5.7.3)(vite@6.2.0(@types/node@22.13.5)(jiti@2.4.2)(sass@1.85.1)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0)
|
version: 3.15.4(@parcel/watcher@2.5.1)(@types/node@22.13.5)(db0@0.2.4)(encoding@0.1.13)(eslint@9.21.0(jiti@2.4.2))(ioredis@5.5.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.34.8)(sass@1.85.1)(terser@5.39.0)(typescript@5.7.3)(vite@6.2.0(@types/node@22.13.5)(jiti@2.4.2)(sass@1.85.1)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0)
|
||||||
@@ -84,6 +87,9 @@ importers:
|
|||||||
'@stylistic/eslint-plugin-js':
|
'@stylistic/eslint-plugin-js':
|
||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.1.0(eslint@9.21.0(jiti@2.4.2))
|
version: 3.1.0(eslint@9.21.0(jiti@2.4.2))
|
||||||
|
'@types/luxon':
|
||||||
|
specifier: ^3.4.2
|
||||||
|
version: 3.4.2
|
||||||
'@typescript-eslint/eslint-plugin':
|
'@typescript-eslint/eslint-plugin':
|
||||||
specifier: ^8.25.0
|
specifier: ^8.25.0
|
||||||
version: 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)
|
version: 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3)
|
||||||
@@ -1632,6 +1638,9 @@ packages:
|
|||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
|
'@types/luxon@3.4.2':
|
||||||
|
resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==}
|
||||||
|
|
||||||
'@types/node@22.13.5':
|
'@types/node@22.13.5':
|
||||||
resolution: {integrity: sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==}
|
resolution: {integrity: sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==}
|
||||||
|
|
||||||
@@ -3411,6 +3420,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: '>=3.0.1'
|
vue: '>=3.0.1'
|
||||||
|
|
||||||
|
luxon@3.5.0:
|
||||||
|
resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
magic-string-ast@0.7.0:
|
magic-string-ast@0.7.0:
|
||||||
resolution: {integrity: sha512-686fgAHaJY7wLTFEq7nnKqeQrhqmXB19d1HnqT35Ci7BN6hbAYLZUezTQ062uUHM7ggZEQlqJ94Ftls+KDXU8Q==}
|
resolution: {integrity: sha512-686fgAHaJY7wLTFEq7nnKqeQrhqmXB19d1HnqT35Ci7BN6hbAYLZUezTQ062uUHM7ggZEQlqJ94Ftls+KDXU8Q==}
|
||||||
engines: {node: '>=16.14.0'}
|
engines: {node: '>=16.14.0'}
|
||||||
@@ -6786,6 +6799,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
|
'@types/luxon@3.4.2': {}
|
||||||
|
|
||||||
'@types/node@22.13.5':
|
'@types/node@22.13.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.20.0
|
undici-types: 6.20.0
|
||||||
@@ -8798,6 +8813,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.13(typescript@5.7.3)
|
vue: 3.5.13(typescript@5.7.3)
|
||||||
|
|
||||||
|
luxon@3.5.0: {}
|
||||||
|
|
||||||
magic-string-ast@0.7.0:
|
magic-string-ast@0.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
magic-string: 0.30.17
|
magic-string: 0.30.17
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
today,
|
today,
|
||||||
months:calendar_months (*),
|
months:calendar_months (*),
|
||||||
state,
|
state,
|
||||||
|
createdAt:created_at,
|
||||||
|
updatedAt:updated_at,
|
||||||
eventNb:calendar_events(count)
|
eventNb:calendar_events(count)
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -29,6 +31,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
today,
|
today,
|
||||||
months:calendar_months (*),
|
months:calendar_months (*),
|
||||||
state,
|
state,
|
||||||
|
createdAt:created_at,
|
||||||
|
updatedAt:updated_at,
|
||||||
events:calendar_events (
|
events:calendar_events (
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
@@ -64,5 +68,5 @@ export default defineEventHandler(async (event) => {
|
|||||||
return output.eq("id", query.id).limit(1).single<Calendar>()
|
return output.eq("id", query.id).limit(1).single<Calendar>()
|
||||||
}
|
}
|
||||||
|
|
||||||
return output.returns<Calendar[]>()
|
return output.overrideTypes<Calendar[]>()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,15 +4,45 @@ import type { World } from "~/models/World";
|
|||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
id: z.number({ coerce: true }).positive().int().optional(),
|
id: z.number({ coerce: true }).positive().int().optional(),
|
||||||
full: z.boolean({ coerce: true }).optional()
|
full: z.boolean({ coerce: true }).optional(),
|
||||||
|
gmId: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const client = await serverSupabaseClient(event)
|
const client = await serverSupabaseClient(event)
|
||||||
const query = await getValidatedQuery(event, querySchema.parse)
|
const query = await getValidatedQuery(event, querySchema.parse)
|
||||||
|
|
||||||
const fullFields = "id, name, description, color, calendars (id, name, color, today)"
|
const fullFields = `
|
||||||
const partialFields = "id, name, description, color"
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
color,
|
||||||
|
createdAt:created_at,
|
||||||
|
updatedAt:updated_at,
|
||||||
|
gmId:gm_id,
|
||||||
|
calendars (
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
today,
|
||||||
|
createdAt:created_at,
|
||||||
|
updatedAt:updated_at,
|
||||||
|
eventNb:calendar_events(count)
|
||||||
|
)
|
||||||
|
`
|
||||||
|
const partialFields = `
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
color,
|
||||||
|
createdAt:created_at,
|
||||||
|
updatedAt:updated_at,
|
||||||
|
calendars (
|
||||||
|
createdAt:created_at,
|
||||||
|
updatedAt:updated_at,
|
||||||
|
eventNb:calendar_events(count)
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
let output
|
let output
|
||||||
|
|
||||||
@@ -23,10 +53,11 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (query.id) {
|
if (query.id) {
|
||||||
// const eventNb = await client.from('worlds').select('events:calendars(calendar_events(id))').order('events_count')
|
|
||||||
|
|
||||||
return output.eq("id", query.id).single<World>()
|
return output.eq("id", query.id).single<World>()
|
||||||
}
|
}
|
||||||
|
if (query.gmId) {
|
||||||
|
return output.eq("gm_id", query.gmId).overrideTypes<World[]>()
|
||||||
|
}
|
||||||
|
|
||||||
return output.returns<World[]>()
|
return output.overrideTypes<World[]>()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const useCalendar = defineStore("calendar", () => {
|
|||||||
|
|
||||||
function setReadStatus(gmId: string) {
|
function setReadStatus(gmId: string) {
|
||||||
// If the user is not logged in, or the calendar is not owned by the user, it's read-only
|
// If the user is not logged in, or the calendar is not owned by the user, it's read-only
|
||||||
isReadOnly.value = (!user) || (gmId !== user.value?.id)
|
isReadOnly.value = !user.value || gmId !== user.value.id
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
--
|
--
|
||||||
-- For use with https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone
|
-- For use with https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone
|
||||||
--
|
--
|
||||||
|
create extension if not exists moddatetime schema extensions;
|
||||||
|
|
||||||
-- Custom types
|
-- Custom types
|
||||||
create type public.app_permission as enum ('events.see.hidden', 'users.ban');
|
create type public.app_permission as enum ('events.see.hidden', 'users.ban');
|
||||||
@@ -44,9 +45,13 @@ create table public.worlds (
|
|||||||
description text,
|
description text,
|
||||||
color app_colors default 'black',
|
color app_colors default 'black',
|
||||||
state world_state default 'draft',
|
state world_state default 'draft',
|
||||||
|
created_at timestamptz default now(),
|
||||||
|
updated_at timestamptz,
|
||||||
gm_id uuid references public.users on delete cascade
|
gm_id uuid references public.users on delete cascade
|
||||||
);
|
);
|
||||||
comment on table public.worlds is 'Worlds belonging to a single user ; a game master.';
|
comment on table public.worlds is 'Worlds belonging to a single user ; a game master.';
|
||||||
|
create trigger handle_updated_at before update on public.worlds
|
||||||
|
for each row execute procedure moddatetime (updated_at);
|
||||||
|
|
||||||
-- World Players (join table)
|
-- World Players (join table)
|
||||||
create table public.world_players (
|
create table public.world_players (
|
||||||
@@ -66,9 +71,13 @@ create table public.calendars (
|
|||||||
today json not null,
|
today json not null,
|
||||||
color app_colors default 'black',
|
color app_colors default 'black',
|
||||||
state calendar_state default 'draft',
|
state calendar_state default 'draft',
|
||||||
|
created_at timestamptz default now(),
|
||||||
|
updated_at timestamptz,
|
||||||
world_id bigint references public.worlds on delete cascade not null
|
world_id bigint references public.worlds on delete cascade not null
|
||||||
);
|
);
|
||||||
comment on table public.calendars is 'Calendar settings and configuration attached to a single world.';
|
comment on table public.calendars is 'Calendar settings and configuration attached to a single world.';
|
||||||
|
create trigger handle_updated_at before update on public.calendars
|
||||||
|
for each row execute procedure moddatetime (updated_at);
|
||||||
|
|
||||||
-- Calendar Months
|
-- Calendar Months
|
||||||
create table public.calendar_months (
|
create table public.calendar_months (
|
||||||
@@ -100,6 +109,8 @@ create table public.calendar_events (
|
|||||||
category bigint references public.calendar_event_categories on delete cascade,
|
category bigint references public.calendar_event_categories on delete cascade,
|
||||||
hidden boolean default false,
|
hidden boolean default false,
|
||||||
wiki text,
|
wiki text,
|
||||||
|
created_at timestamptz default now(),
|
||||||
|
updated_at timestamptz,
|
||||||
calendar_id bigint references public.calendars on delete cascade not null
|
calendar_id bigint references public.calendars on delete cascade not null
|
||||||
);
|
);
|
||||||
comment on table public.calendar_events is 'Events linked to a world';
|
comment on table public.calendar_events is 'Events linked to a world';
|
||||||
@@ -123,9 +134,11 @@ comment on table public.calendar_event_categories_links is 'Link tables for mult
|
|||||||
|
|
||||||
-- Character categories
|
-- Character categories
|
||||||
create table public.character_categories (
|
create table public.character_categories (
|
||||||
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,
|
||||||
color app_colors default 'black',
|
color app_colors default 'black',
|
||||||
|
created_at timestamptz default now(),
|
||||||
|
updated_at timestamptz,
|
||||||
unique (name)
|
unique (name)
|
||||||
);
|
);
|
||||||
comment on table public.character_categories is 'Categories describing characters';
|
comment on table public.character_categories is 'Categories describing characters';
|
||||||
@@ -141,6 +154,8 @@ create table public.characters (
|
|||||||
hidden_birth boolean,
|
hidden_birth boolean,
|
||||||
hidden_death boolean,
|
hidden_death boolean,
|
||||||
wiki text,
|
wiki text,
|
||||||
|
created_at timestamptz default now(),
|
||||||
|
updated_at timestamptz,
|
||||||
world_id bigint references public.worlds on delete cascade not null
|
world_id bigint references public.worlds on delete cascade not null
|
||||||
);
|
);
|
||||||
comment on table public.characters is 'Characters linked to a world';
|
comment on table public.characters is 'Characters linked to a world';
|
||||||
|
|||||||
Reference in New Issue
Block a user