Updated data loading strategy on world page
This commit is contained in:
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>
|
||||||
@@ -38,6 +38,8 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log(error.message)
|
console.log(error.message)
|
||||||
|
isLoading.value = false
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const { data: calendars, status } = useLazyAsyncData<{ data: Calendar[] }>("expl
|
|||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<div v-if="status === 'pending'" 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">
|
||||||
<LazyLoadingCard />
|
<LazyCardLoading />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else-if="status === 'success' && calendars?.data">
|
<template v-else-if="status === 'success' && calendars?.data">
|
||||||
|
|||||||
@@ -100,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, refresh } = await useLazyFetch<{ data: World }>("/api/worlds/query", { query: { id, full: true }, key: "user-world", keepalive: 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,44 +17,45 @@ watch(user, (n) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ["auth-guard"]
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: world, status, refresh } = await useAsyncData<{ data: World }>('user-world', () => {
|
||||||
|
return $fetch("/api/worlds/query", { query: { id }})
|
||||||
|
})
|
||||||
|
const worldGmId = world.value?.data.gmId
|
||||||
|
|
||||||
|
const { data: calendars, status: calendarStatus, refresh: refreshCalendars } = useLazyFetch<{ data: Calendar[] }>('/api/calendars/query', { query: { worldId: world.value?.data.id, full: true }})
|
||||||
|
const sortedCalendars = computed(() => calendars.value?.data ? calendars.value.data.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)) : [])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* === Subscriptions ===
|
* === Subscriptions ===
|
||||||
*/
|
*/
|
||||||
/** Active calendar channel */
|
|
||||||
let calendarChannel: RealtimeChannel
|
let calendarChannel: RealtimeChannel
|
||||||
/** Active world channel */
|
|
||||||
let worldChannel: RealtimeChannel
|
let worldChannel: RealtimeChannel
|
||||||
|
|
||||||
onMounted(() => {
|
// Register channels
|
||||||
calendarChannel = supabase.channel("realtime-calendar-channel")
|
|
||||||
.on(
|
|
||||||
"postgres_changes",
|
|
||||||
{ event: "*", schema: "public", table: "calendars" },
|
|
||||||
async (payload) => {
|
|
||||||
console.log("refresh")
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.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()
|
||||||
refresh()
|
)
|
||||||
}
|
.subscribe()
|
||||||
|
calendarChannel = supabase.channel("realtime-calendar-channel")
|
||||||
|
.on(
|
||||||
|
"postgres_changes",
|
||||||
|
{ event: "*", schema: "public", table: "calendars" },
|
||||||
|
async () => refreshCalendars()
|
||||||
)
|
)
|
||||||
.subscribe()
|
.subscribe()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Unsubscribe from realtime
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// Unsubscribe from realtime
|
supabase.removeChannel(calendarChannel)
|
||||||
supabase.removeChannel(worldChannel)
|
supabase.removeChannel(worldChannel)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -108,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>
|
||||||
@@ -151,70 +194,71 @@ function hideEditModal() {
|
|||||||
</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>
|
||||||
|
|||||||
@@ -241,6 +241,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: {
|
||||||
@@ -571,6 +574,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: {
|
||||||
|
|||||||
@@ -69,4 +69,4 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
eslint: {},
|
eslint: {},
|
||||||
compatibilityDate: "latest",
|
compatibilityDate: "latest",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user