Merge branch 'code/cleanup' into dev
This commit is contained in:
@@ -15,7 +15,7 @@ function handleMenuItemAction(actionType: SidebarMenuActionType) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="w-16 py-6 border-r-[1px] bg-indigo-700 dark:bg-black text-white border-r-indigo-700 dark:border-r-indigo-950 grid grid-rows-[1fr_auto] justify-center transition-colors">
|
||||
<nav class="w-16 py-6 border-r-[1px] bg-indigo-700 dark:bg-black text-white border-r-indigo-700 dark:border-r-indigo-950 grid grid-rows-[1fr_auto] justify-center transition-colors after:opacity-50 after:contrast-125 dark:after:opacity-75 dark:after:contrast-175 after:-hue-rotate-60">
|
||||
<menu class="flex flex-col gap-4">
|
||||
<li class="mb-12">
|
||||
<UiButton variant="ghost" size="icon" class="rounded-full" @click="console.log">
|
||||
@@ -66,3 +66,26 @@ function handleMenuItemAction(actionType: SidebarMenuActionType) {
|
||||
<UserCTA />
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
nav {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline: 0;
|
||||
height: 25rem;
|
||||
max-height: 100%;
|
||||
background-image: url("/images/sidebar.png");
|
||||
background-position: bottom;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
mask-image: linear-gradient(to top, black 25%, transparent 50%, transparent);
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -60,7 +60,7 @@ function pushRoute(to: AvailableRoutes) {
|
||||
<ClientOnly>
|
||||
<UiDropdownMenu v-model:open="menuOpened">
|
||||
<UiDropdownMenuTrigger>
|
||||
<UiAvatar v-if="user" class="cursor-pointer">
|
||||
<UiAvatar v-if="user" id="user-avatar" class="ring-[.2rem] ring-indigo-700 dark:ring-neutral-900 cursor-pointer">
|
||||
<UiAvatarImage
|
||||
:src="userMeta?.avatar_url"
|
||||
:alt="userMeta?.full_name"
|
||||
|
||||
53
components/world/dialog/Edit.vue
Normal file
53
components/world/dialog/Edit.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhX } from "@phosphor-icons/vue";
|
||||
import type { World } from "~/models/World";
|
||||
|
||||
const props = defineProps<{
|
||||
modalState?: boolean
|
||||
world: World | null
|
||||
}>()
|
||||
|
||||
const worldSkeletonName = ref<string>(props.world?.name ?? "")
|
||||
watch(() => props.world, (newWorld) => {
|
||||
worldSkeletonName.value = newWorld?.name ?? ""
|
||||
})
|
||||
|
||||
function onChangedName(newName: string) {
|
||||
worldSkeletonName.value = newName
|
||||
}
|
||||
|
||||
const emit = defineEmits(["on-close"])
|
||||
|
||||
function handleClose() {
|
||||
emit("on-close")
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiAlertDialog :open="modalState">
|
||||
<UiAlertDialogContent class="gap-4">
|
||||
<header>
|
||||
<UiAlertDialogTitle>
|
||||
<span class="text-2xl">
|
||||
<span v-if="worldSkeletonName">
|
||||
{{ $t('entity.world.editDialog.title') }} — {{ worldSkeletonName }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('entity.world.editDialog.title') }}
|
||||
</span>
|
||||
</span>
|
||||
</UiAlertDialogTitle>
|
||||
|
||||
<UiAlertDialogDescription>
|
||||
{{ $t('entity.world.editDialog.subtitle') }}
|
||||
</UiAlertDialogDescription>
|
||||
</header>
|
||||
|
||||
<UiButton size="icon" variant="ghost" class="absolute top-4 right-4" title="Fermer la fenêtre" @click="handleClose">
|
||||
<PhX size="20" />
|
||||
</UiButton>
|
||||
|
||||
<WorldFormEdit :world @on-changed-name="onChangedName" @on-close="handleClose" />
|
||||
</UiAlertDialogContent>
|
||||
</UiAlertDialog>
|
||||
</template>
|
||||
113
components/world/form/Edit.vue
Normal file
113
components/world/form/Edit.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhCircleNotch } from "@phosphor-icons/vue";
|
||||
import { useToast } from "~/components/ui/toast";
|
||||
import type { World } from "~/models/World";
|
||||
|
||||
const { toast } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const user = useSupabaseUser()
|
||||
|
||||
const props = defineProps<{
|
||||
world: World | null
|
||||
}>()
|
||||
|
||||
const worldSkeleton = ref<World>({ ...props.world } as World)
|
||||
|
||||
onMounted(() => {
|
||||
worldSkeleton.value = { ...props.world } as World
|
||||
})
|
||||
|
||||
const isLoading = ref<boolean>(false)
|
||||
|
||||
/**
|
||||
* === Form Validation ===
|
||||
*/
|
||||
/** Whether the skeleton has a valid name */
|
||||
const validSkeleton = computed(() => worldSkeleton.value.name)
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!user.value || !worldSkeleton.value) return
|
||||
|
||||
try {
|
||||
isLoading.value = true
|
||||
await $fetch(`/api/worlds/${worldSkeleton.value.id}`, { method: "PATCH", body: { ...worldSkeleton.value } })
|
||||
|
||||
toast({
|
||||
title: t("entity.world.updatedToast.title", { world: worldSkeleton.value.name }),
|
||||
variant: "success",
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
emit("on-close")
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* === Watch for name changes to display above ===
|
||||
*/
|
||||
const emit = defineEmits<{
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
(e: "on-changed-name", calendarName: string): void
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
(e: "on-close"): void
|
||||
}>()
|
||||
|
||||
/** Hook to emit a debounced event for the changed skeleton name */
|
||||
const handleNameChange = useDebounceFn(() => {
|
||||
emit("on-changed-name", worldSkeleton.value.name)
|
||||
}, 400)
|
||||
|
||||
function handleFormCancel() {
|
||||
emit("on-close")
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="worldSkeleton">
|
||||
<form class="h-full grid grid-rows-[1fr_auto]" @submit.prevent="handleSubmit">
|
||||
<div>
|
||||
<input
|
||||
id="new-world-name"
|
||||
v-model="worldSkeleton.name"
|
||||
type="text"
|
||||
name="new-world-name"
|
||||
required
|
||||
:placeholder="$t('common.title')"
|
||||
class="w-full -my-1 mb-4 py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-none focus-visible:border-blue-600"
|
||||
@input="handleNameChange"
|
||||
>
|
||||
|
||||
<div class="-mx-1 mb-4">
|
||||
<InputColor v-model="worldSkeleton.color" />
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
id="new-world-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-none focus-visible:border-blue-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<footer class="flex justify-end gap-2 mt-6">
|
||||
<UiButton type="button" variant="destructive" @click="handleFormCancel">
|
||||
{{ $t('ui.action.cancel') }}
|
||||
</UiButton>
|
||||
|
||||
<UiButton type="submit" :disabled="!validSkeleton || isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="opacity-50 animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
{{ $t('ui.action.save') }}
|
||||
</UiButton>
|
||||
</footer>
|
||||
</form>
|
||||
</template>
|
||||
</template>
|
||||
@@ -40,6 +40,7 @@ export default defineI18nConfig(() => ({
|
||||
},
|
||||
greeting: "Connected as {user}",
|
||||
anonymousGreeting: "Preferences",
|
||||
backToProfile: "Back to profile",
|
||||
sidebarMenu: {
|
||||
profile: "Profile",
|
||||
appearance: "Appearance",
|
||||
@@ -80,14 +81,24 @@ export default defineI18nConfig(() => ({
|
||||
nameSingular: "World",
|
||||
namePlural: "Worlds",
|
||||
addSingle: "Add a world",
|
||||
notFound: "World not found",
|
||||
notFoundDescription: "The link is not valid or the world has been deleted / archived.",
|
||||
backToList: "Go back to worlds",
|
||||
createDialog: {
|
||||
title: "Create a world",
|
||||
subtitle: "Worlds are the building blocks which hold all your characters, your calendars…"
|
||||
},
|
||||
editDialog: {
|
||||
title: "Edit world",
|
||||
subtitle: "Update world data",
|
||||
},
|
||||
deleteDialog: {
|
||||
title: "Delete this world ?",
|
||||
subtitle: "This world will be deleted permanently, and all of its associated data will be lost !",
|
||||
},
|
||||
updatedToast: {
|
||||
title: "The world \"{world}\" has been successfuly updated.",
|
||||
},
|
||||
deletedToast: {
|
||||
title: "The world \"{world}\" has been successfuly deleted.",
|
||||
},
|
||||
@@ -101,6 +112,7 @@ export default defineI18nConfig(() => ({
|
||||
notFound: "Calendar not found",
|
||||
notFoundDescription: "The link is not valid or the calendar has been deleted / archived.",
|
||||
notFoundForWorld: "No calendar for this world… yet !",
|
||||
backToList: "Go back to calendars",
|
||||
isLoading: "Calendar is loading…",
|
||||
hasXEvents: "This calendar has {count} public events",
|
||||
date: {
|
||||
@@ -278,6 +290,7 @@ export default defineI18nConfig(() => ({
|
||||
},
|
||||
greeting: "Connecté en tant que {user}",
|
||||
anonymousGreeting: "Préférences",
|
||||
backToProfile: "Retour au profil",
|
||||
sidebarMenu: {
|
||||
profile: "Profil",
|
||||
appearance: "Apparence",
|
||||
@@ -318,14 +331,24 @@ export default defineI18nConfig(() => ({
|
||||
nameSingular: "Monde",
|
||||
namePlural: "Mondes",
|
||||
addSingle: "Ajouter un monde",
|
||||
notFound: "Aucun monde trouvé",
|
||||
notFoundDescription: "Le lien n'est pas valide ou le monde a été supprimé / archivé.",
|
||||
backToList: "Retourner aux mondes",
|
||||
createDialog: {
|
||||
title: "Créer un monde",
|
||||
subtitle: "Un monde est la brique de base qui contient vos personnages, vos calendriers…"
|
||||
},
|
||||
editDialog: {
|
||||
title: "Modifier le monde",
|
||||
subtitle: "Mettre à jour les données du monde",
|
||||
},
|
||||
deleteDialog: {
|
||||
title: "Supprimer ce monde ?",
|
||||
subtitle: "Le monde sera supprimé définitivement, vous perdrez toutes les données associées !",
|
||||
},
|
||||
updatedToast: {
|
||||
title: "Le monde \"{world}\" a été modifié avec succès.",
|
||||
},
|
||||
deletedToast: {
|
||||
title: "Le monde \"{world}\" a été supprimé avec succès.",
|
||||
},
|
||||
@@ -339,6 +362,7 @@ export default defineI18nConfig(() => ({
|
||||
notFound: "Aucun calendrier trouvé",
|
||||
notFoundDescription: "Le lien n'est pas valide ou le calendrier a été supprimé / archivé.",
|
||||
notFoundForWorld: "Aucun calendrier pour ce monde… pour l'instant !",
|
||||
backToList: "Retourner aux calendriers",
|
||||
isLoading: "Chargement du calendrier…",
|
||||
hasXEvents: "Ce calendrier contient {count} évènements publiques",
|
||||
date: {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { z } from "zod"
|
||||
import type { Calendar } from "./CalendarConfig"
|
||||
import { rpgColorSchema, type RPGColor } from "./Color"
|
||||
|
||||
export type WorldState = "published" | "draft" | "archived"
|
||||
|
||||
export interface World {
|
||||
id?: number
|
||||
name: string
|
||||
@@ -9,6 +11,7 @@ export interface World {
|
||||
color?: RPGColor,
|
||||
calendars?: Calendar[],
|
||||
gmId?: string
|
||||
state?: WorldState
|
||||
}
|
||||
|
||||
export const postWorldSchema = z.object({
|
||||
@@ -16,4 +19,5 @@ export const postWorldSchema = z.object({
|
||||
description: z.string().optional().nullable(),
|
||||
color: rpgColorSchema,
|
||||
gmId: z.string().optional().nullable(),
|
||||
state: z.string().optional().nullable().default("draft"),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhCalendarX, PhCircleNotch } from "@phosphor-icons/vue";
|
||||
import { PhArrowBendDoubleUpLeft, PhCalendarX, PhCircleNotch } from "@phosphor-icons/vue";
|
||||
import type { Calendar } from "~/models/CalendarConfig";
|
||||
import type { Category } from "~/models/Category";
|
||||
|
||||
@@ -8,8 +8,29 @@ const shortId = route.params.id
|
||||
|
||||
const user = useSupabaseUser()
|
||||
|
||||
const { data: calendarData, pending: calPending, refresh: calRefresh } = await useLazyFetch<{ data: Calendar }>("/api/calendars/query", { query: { shortId, full: true } })
|
||||
const { data: catData, pending: catPending, refresh: catRefresh } = await useLazyFetch<{ data: Category[] }>("/api/calendars/categories/query")
|
||||
const {
|
||||
data: calendar,
|
||||
status: calendarStatus,
|
||||
refresh: calRefresh
|
||||
} = await useLazyFetch<{ data: Calendar }>("/api/calendars/query",
|
||||
{
|
||||
key: `calendar-${shortId}`,
|
||||
query: {
|
||||
shortId,
|
||||
full: true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const {
|
||||
data: categories,
|
||||
status: categoriesStatus,
|
||||
refresh: catRefresh
|
||||
} = await useLazyFetch<{ data: Category[] }>("/api/calendars/categories/query",
|
||||
{ key: `categories-${shortId}` }
|
||||
)
|
||||
|
||||
const isLoading = computed(() => calendarStatus.value === "pending" || categoriesStatus.value === "pending")
|
||||
|
||||
watch(user, () => {
|
||||
calRefresh()
|
||||
@@ -18,7 +39,7 @@ watch(user, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="calPending || catPending" class="h-full w-full grid place-items-center">
|
||||
<div v-if="isLoading" class="h-full w-full grid place-items-center">
|
||||
<Head>
|
||||
<Title>{{ $t("entity.calendar.nameSingular") }}</Title>
|
||||
</Head>
|
||||
@@ -31,12 +52,12 @@ watch(user, () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="calendarData && catData" class="h-full w-full">
|
||||
<div v-else-if="calendar?.data && categories?.data" class="h-full w-full">
|
||||
<Head>
|
||||
<Title>{{ calendarData.data.name }}</Title>
|
||||
<Title>{{ calendar.data.name }}</Title>
|
||||
</Head>
|
||||
|
||||
<Calendar :calendar-data="calendarData.data" :categories="catData.data" />
|
||||
<Calendar :calendar-data="calendar.data" :categories="categories.data" />
|
||||
</div>
|
||||
|
||||
<div v-else class="h-full w-full grid place-items-center">
|
||||
@@ -54,6 +75,14 @@ watch(user, () => {
|
||||
<p>
|
||||
{{ $t('entity.calendar.notFoundDescription') }}
|
||||
</p>
|
||||
|
||||
<UiButton variant="default" class="mt-4 gap-2" as-child>
|
||||
<RouterLink to="/explore">
|
||||
<PhArrowBendDoubleUpLeft size="24" />
|
||||
|
||||
{{ $t('entity.calendar.backToList') }}
|
||||
</RouterLink>
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<main class="p-8">
|
||||
<Head>
|
||||
<Title>{{ $t("entity.world.namePlural") }}</Title>
|
||||
</Head>
|
||||
|
||||
<Heading level="h1">Calendriers publics</Heading>
|
||||
</main>
|
||||
</template>
|
||||
@@ -1,9 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Calendar } from "~/models/CalendarConfig";
|
||||
|
||||
const { data } = await useFetch("/api/calendars/query", { key: "explore-calendars" })
|
||||
|
||||
const availableCalendars = data.value?.data as Calendar[]
|
||||
const { data: availableCalendars } = await useLazyFetch<{ data: Calendar[] }>("/api/calendars/query", { key: "explore-calendars" })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -22,8 +20,8 @@ const availableCalendars = data.value?.data as Calendar[]
|
||||
{{ $t("entity.calendar.namePublicPlural") }}
|
||||
</Heading>
|
||||
|
||||
<ul v-if="availableCalendars && availableCalendars?.length > 0" class="grid md:grid-cols-3 gap-2">
|
||||
<li v-for="calendar in availableCalendars" :key="calendar.shortId">
|
||||
<ul v-if="availableCalendars?.data" class="grid md:grid-cols-3 gap-2">
|
||||
<li v-for="calendar in availableCalendars.data" :key="calendar.shortId">
|
||||
<UiCard
|
||||
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}`"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhCircleNotch } from "@phosphor-icons/vue";
|
||||
import { PhArrowBendDoubleUpLeft, PhCalendarX, PhCircleNotch } from "@phosphor-icons/vue";
|
||||
import type { Calendar } from "~/models/CalendarConfig";
|
||||
import type { Category } from "~/models/Category";
|
||||
|
||||
@@ -8,6 +8,7 @@ definePageMeta({
|
||||
})
|
||||
|
||||
const user = useSupabaseUser()
|
||||
|
||||
// Redirect user back home when they log out on the page
|
||||
watch(user, (n) => {
|
||||
if (!n) {
|
||||
@@ -18,15 +19,31 @@ watch(user, (n) => {
|
||||
const route = useRoute()
|
||||
const id = route.params.id
|
||||
|
||||
const { data: calendarData, pending: calPending } = await useLazyFetch("/api/calendars/query", { key: `calendar-${id}`, query: { id, full: true } })
|
||||
const { data: catData, pending: catPending } = await useLazyFetch("/api/calendars/categories/query", { key: `categories-${id}` })
|
||||
const {
|
||||
data: calendar,
|
||||
status: calendarStatus
|
||||
} = await useLazyFetch<{ data: Calendar }>("/api/calendars/query",
|
||||
{
|
||||
key: `calendar-${id}`,
|
||||
query: {
|
||||
id,
|
||||
full: true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const cal = computed<Calendar>(() => calendarData?.value?.data as Calendar)
|
||||
const categories = computed<Category[]>(() => catData?.value?.data as Category[])
|
||||
const {
|
||||
data: categories,
|
||||
status: categoriesStatus
|
||||
} = await useLazyFetch<{ data: Category[] }>("/api/calendars/categories/query",
|
||||
{ key: `categories-${id}` }
|
||||
)
|
||||
|
||||
const isLoading = computed(() => calendarStatus.value === "pending" || categoriesStatus.value === "pending")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="calPending || catPending" class="h-full w-full grid place-items-center">
|
||||
<div v-if="isLoading" class="h-full w-full grid place-items-center">
|
||||
<Head>
|
||||
<Title>{{ $t("entity.calendar.nameSingular") }}</Title>
|
||||
</Head>
|
||||
@@ -39,11 +56,36 @@ const categories = computed<Category[]>(() => catData?.value?.data as Category[]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="cal && categories" class="h-full w-full">
|
||||
<div v-else-if="calendar?.data && categories?.data" class="h-full w-full">
|
||||
<Head>
|
||||
<Title>{{ cal.name }}</Title>
|
||||
<Title>{{ calendar.data.name }}</Title>
|
||||
</Head>
|
||||
|
||||
<Calendar :calendar-data="cal" :categories />
|
||||
<Calendar :calendar-data="calendar.data" :categories="categories.data" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="h-full w-full grid place-items-center">
|
||||
<Head>
|
||||
<Title>{{ $t("entity.calendar.notFound") }}</Title>
|
||||
</Head>
|
||||
|
||||
<div class="grid justify-items-center opacity-80">
|
||||
<PhCalendarX size="75" class="opacity-60" />
|
||||
|
||||
<Heading level="h1">
|
||||
{{ $t("entity.calendar.notFound") }}
|
||||
</Heading>
|
||||
|
||||
<p>
|
||||
{{ $t('entity.calendar.notFoundDescription') }}
|
||||
</p>
|
||||
|
||||
<UiButton variant="default" class="mt-4 gap-2" as-child>
|
||||
<RouterLink to="/my">
|
||||
<PhArrowBendDoubleUpLeft size="24" />
|
||||
|
||||
{{ $t('entity.calendar.backToList') }}
|
||||
</RouterLink>
|
||||
</UiButton>
|
||||
</div>
|
||||
</div></template>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhPlus, PhTrash } from "@phosphor-icons/vue";
|
||||
import { PhPencil, PhPlus, PhTrash } from "@phosphor-icons/vue";
|
||||
import type { RealtimeChannel } from "@supabase/supabase-js"
|
||||
import type { World } from "~/models/World";
|
||||
|
||||
const supabase = useSupabaseClient()
|
||||
|
||||
const { data: res } = await useFetch("/api/worlds/query")
|
||||
|
||||
const worlds = ref<World[]>(res.value?.data as World[])
|
||||
const { data: worlds } = await useLazyFetch<{ data: World[] }>("/api/worlds/query")
|
||||
|
||||
definePageMeta({
|
||||
middleware: ["auth-guard"]
|
||||
@@ -36,8 +34,10 @@ let worldChannel: RealtimeChannel
|
||||
|
||||
/** Handles world insertion realtime events */
|
||||
function handleInsertedWorld(newWorld: World) {
|
||||
if (!worlds.value?.data) return
|
||||
|
||||
try {
|
||||
worlds.value.push(newWorld)
|
||||
worlds.value?.data.push(newWorld)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
@@ -45,8 +45,10 @@ function handleInsertedWorld(newWorld: World) {
|
||||
|
||||
/** Handles world deletion realtime events */
|
||||
function handleDeletedWorld(id: number) {
|
||||
if (!worlds.value?.data) return
|
||||
|
||||
try {
|
||||
worlds.value.splice(worlds.value.findIndex(w => w.id === id))
|
||||
worlds.value.data.splice(worlds.value.data.findIndex(w => w.id === id))
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
@@ -68,7 +70,9 @@ onMounted(() => {
|
||||
break
|
||||
|
||||
case "UPDATE":
|
||||
worlds.value = (await $fetch("/api/worlds/query")).data as World[]
|
||||
if (!worlds.value?.data) return
|
||||
|
||||
worlds.value.data = (await $fetch("/api/worlds/query")).data as World[]
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -87,21 +91,32 @@ onUnmounted(() => {
|
||||
})
|
||||
|
||||
const markedWorld = ref<World | null>(null)
|
||||
const isEditWorldModalOpen = ref<boolean>(false)
|
||||
const isDeleteWorldModalOpen = ref<boolean>(false)
|
||||
|
||||
function deployDeleteModal(calendar: World) {
|
||||
function deployDeleteModal(world: World) {
|
||||
isDeleteWorldModalOpen.value = true
|
||||
markedWorld.value = calendar
|
||||
markedWorld.value = world
|
||||
}
|
||||
|
||||
function hideDeleteModal() {
|
||||
isDeleteWorldModalOpen.value = false
|
||||
markedWorld.value = null
|
||||
}
|
||||
|
||||
function deployEditModal(world: World) {
|
||||
isEditWorldModalOpen.value = true
|
||||
markedWorld.value = world
|
||||
}
|
||||
|
||||
function hideEditModal() {
|
||||
isEditWorldModalOpen.value = false
|
||||
markedWorld.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="p-8 after:fill-red-400">
|
||||
<main class="p-8">
|
||||
<Head>
|
||||
<Title>{{ $t("entity.world.namePlural") }}</Title>
|
||||
</Head>
|
||||
@@ -131,8 +146,8 @@ function hideDeleteModal() {
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
|
||||
<ul class="grid lg:grid-cols-3 gap-2">
|
||||
<li v-for="world in worlds" :key="world.id">
|
||||
<ul v-if="worlds?.data" class="grid lg:grid-cols-3 gap-2">
|
||||
<li v-for="world in worlds.data" :key="world.id">
|
||||
<UiCard
|
||||
class="w-full transition-all"
|
||||
:link="`/my/worlds/${world.id}`"
|
||||
@@ -165,9 +180,15 @@ function hideDeleteModal() {
|
||||
<UiCardContent>
|
||||
<p class="italic">{{ world.description }}</p>
|
||||
|
||||
<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(world)">
|
||||
<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>
|
||||
@@ -176,32 +197,7 @@ function hideDeleteModal() {
|
||||
</section>
|
||||
|
||||
<WorldDialogCreate :modal-state="isCreateWorldModalOpen" @on-close="hideCreateDialog" />
|
||||
<WorldDialogEdit :world="markedWorld" :modal-state="isEditWorldModalOpen" @on-close="hideEditModal" />
|
||||
<WorldDialogDelete :world="markedWorld" :modal-state="isDeleteWorldModalOpen" @on-close="hideDeleteModal" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
main {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: clip;
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 2.4rem;
|
||||
bottom: -5%;
|
||||
height: 75%;
|
||||
width: 100%;
|
||||
background-image: url('/images/galaxy.svg');
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position-y: bottom;
|
||||
background-position-x: right;
|
||||
z-index: -1;
|
||||
mask-image: radial-gradient(ellipse 100% 100% at 120% 80%, black, transparent);
|
||||
opacity: .3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
import type { RealtimeChannel } from "@supabase/supabase-js"
|
||||
import type { World } from "~/models/World";
|
||||
import type { Calendar } from "~/models/CalendarConfig";
|
||||
import { PhPlus, PhTrash } from "@phosphor-icons/vue";
|
||||
import { PhArrowBendDoubleUpLeft, PhGlobeHemisphereWest, PhPlus, PhTrash } from "@phosphor-icons/vue";
|
||||
|
||||
const supabase = useSupabaseClient()
|
||||
const route = useRoute()
|
||||
const id = route.params.id
|
||||
|
||||
const { data: res, pending } = await useFetch("/api/worlds/query", { query: { id, full: true } })
|
||||
|
||||
const world = ref<World>(res.value?.data as World)
|
||||
const { data: world, status } = await useFetch<{ data: World }>("/api/worlds/query", { query: { id, full: true } })
|
||||
|
||||
definePageMeta({
|
||||
middleware: ["auth-guard"]
|
||||
@@ -40,8 +38,10 @@ let calendarChannel: RealtimeChannel
|
||||
|
||||
/** Handles calendar insertion realtime events */
|
||||
function handleInsertedCalendar(newCalendar: Calendar) {
|
||||
if (!world.value) return
|
||||
|
||||
try {
|
||||
world.value.calendars?.push(newCalendar)
|
||||
world.value.data.calendars?.push(newCalendar)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
@@ -49,8 +49,10 @@ function handleInsertedCalendar(newCalendar: Calendar) {
|
||||
|
||||
/** Handles calendar deletion realtime events */
|
||||
function handleDeletedCalendar(id: number) {
|
||||
if (!world.value) return
|
||||
|
||||
try {
|
||||
world.value.calendars?.splice(world.value.calendars.findIndex(c => c.id === id))
|
||||
world.value.data.calendars?.splice(world.value.data.calendars.findIndex(c => c.id === id))
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
@@ -101,7 +103,7 @@ function hideDeleteModal() {
|
||||
|
||||
<template>
|
||||
<main class="p-8">
|
||||
<template v-if="pending">
|
||||
<template v-if="status === 'pending'">
|
||||
<Head>
|
||||
<Title>{{ $t("entity.world.namePlural") }}</Title>
|
||||
</Head>
|
||||
@@ -110,16 +112,16 @@ function hideDeleteModal() {
|
||||
{{ $t('entity.isLoading') }}
|
||||
</Heading>
|
||||
</template>
|
||||
<template v-else-if="world">
|
||||
<template v-else-if="world?.data">
|
||||
<Head>
|
||||
<Title>{{ world.name }}</Title>
|
||||
<Title>{{ world.data.name }}</Title>
|
||||
</Head>
|
||||
|
||||
<header class="lg:w-1/2 mb-8">
|
||||
<Spacing>
|
||||
<Heading level="h1">{{ world.name }}</Heading>
|
||||
<Heading level="h1">{{ world.data.name }}</Heading>
|
||||
|
||||
<p>{{ world.description }}</p>
|
||||
<p>{{ world.data.description }}</p>
|
||||
</Spacing>
|
||||
</header>
|
||||
|
||||
@@ -146,8 +148,8 @@ function hideDeleteModal() {
|
||||
</UiTooltipProvider>
|
||||
</div>
|
||||
|
||||
<ul v-if="world.calendars && world.calendars?.length > 0" class="grid md:grid-cols-3 gap-2">
|
||||
<li v-for="calendar in world.calendars" :key="calendar.id">
|
||||
<ul v-if="world.data.calendars && world.data.calendars?.length > 0" class="grid md:grid-cols-3 gap-2">
|
||||
<li v-for="calendar in world.data.calendars" :key="calendar.id">
|
||||
<UiCard
|
||||
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="`/my/calendars/${calendar.id}`"
|
||||
@@ -173,35 +175,36 @@ function hideDeleteModal() {
|
||||
</template>
|
||||
</Spacing>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<CalendarDialogCreate :world :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" />
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
main {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: clip;
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 2.4rem;
|
||||
bottom: -5%;
|
||||
height: 75%;
|
||||
width: 100%;
|
||||
background-image: url('/images/planet.svg');
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position-y: bottom;
|
||||
background-position-x: right;
|
||||
z-index: -1;
|
||||
mask-image: radial-gradient(ellipse 100% 100% at 120% 80%, black, transparent);
|
||||
opacity: .3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#6366f1" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 611.999 611.999" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M611.502,203.367c-3.889-53.216-30.921-100.838-93.276-125.006c-69.361-26.884-151.96-13.033-216.77,16.813
|
||||
c-67.784,31.216-151.313,93.935-171.682,171.605c-7.962,30.357-5.303,63.94,10.093,91.288
|
||||
c19.789,35.152,59.002,56.863,99.094,61.324c55.248,6.147,112.773-16.493,155.435-50.741c24.317-19.521,46.5-44.992,58.997-73.774
|
||||
c11.212-25.821,13.864-56.419,1.925-81.911c-16.243-34.679-54.683-49.835-91.121-44.132c-4.787,0.735-9.337,1.776-13.657,3.016
|
||||
c-4.322,1.234-8.419,2.662-12.295,4.256c-7.765,3.148-14.653,6.919-20.773,10.925c-6.109,4.03-11.447,8.348-16.022,12.747
|
||||
c-1.037,1.043-2.056,2.067-3.054,3.071c-1.08,1.074-2.021,2.094-2.964,3.1c-1.877,2.012-3.588,3.995-5.207,5.882
|
||||
c-3.208,3.799-5.955,7.307-8.307,10.499c-4.731,6.35-8.047,11.323-10.284,14.671c-1.1,1.684-1.934,2.961-2.494,3.817
|
||||
c-0.55,0.863-0.829,1.301-0.829,1.301c-0.92,1.449-0.97,3.367,0.036,4.894c1.366,2.076,4.157,2.652,6.233,1.285
|
||||
c0,0,0.433-0.285,1.288-0.846c0.846-0.567,2.108-1.413,3.772-2.529c3.307-2.238,8.187-5.589,14.48-9.753
|
||||
c6.333-4.129,14.012-9.048,23.248-13.695c2.175-1.069,4.431-2.096,6.846-3.028c2.399-0.946,4.893-1.86,7.522-2.638
|
||||
c5.229-1.598,10.883-2.863,16.803-3.589c2.961-0.352,5.978-0.597,9.027-0.671c3.05-0.067,6.128,0.03,9.183,0.299
|
||||
c10.759,1.003,24.149,4.372,30.659,13.714c4.245,6.092,4.815,14.123,3.391,21.409c-2.471,12.644-10.28,23.585-18.883,33.174
|
||||
c-30.278,33.744-74.119,56.946-119.526,60.718c-37.814,2.947-73.075-14.422-78.121-55.732
|
||||
c-3.607-29.523,10.253-58.809,29.349-81.612c26.954-32.186,60.518-59.114,98.754-76.011c14.09-6.227,28.7-11.46,43.667-15.311
|
||||
c39.803-10.321,86.856-13.345,125.381,3.353c36.924,16.004,60.458,52.144,59.714,92.483c-0.223,11.808-2.412,23.851-6.114,35.878
|
||||
c-3.66,12.037-8.996,24.017-15.499,35.56c-6.539,11.542-14.238,22.664-22.764,33.124C454.222,364.819,387.62,404.605,326.223,430.7
|
||||
c-74.109,31.498-215.664,68.734-268.588-17.843c-27.588-45.131-17.468-102.863-2.502-150.461c0.541-1.722,0.006-3.684-1.497-4.867
|
||||
c-1.942-1.528-4.754-1.193-6.282,0.748c0,0-1.277,1.622-3.752,4.764c-0.624,0.768-1.287,1.718-2.039,2.741
|
||||
c-0.746,1.027-1.563,2.153-2.451,3.375C17.106,299.452,2.396,336.089,0.264,373.622c-1.962,34.546,6.992,69.97,26.952,98.236
|
||||
c23.939,33.9,62.223,55.79,102.375,66.286c63.644,16.637,134.014,11.022,196.48-7.885c14.716-4.454,29.379-9.645,43.893-15.61
|
||||
c14.505-5.984,28.826-12.8,42.955-20.315c51.202-27.233,99.882-64.099,136.409-109.452
|
||||
C588.79,335.885,616.063,265.779,611.502,203.367z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none" /><path fill="#6366f1" d="M245.11,60.68c-7.65-13.19-27.85-16.16-58.5-8.66A96,96,0,0,0,32.81,140.3C5.09,169,5.49,186,10.9,195.32,16,204.16,26.64,208,40.64,208a124.11,124.11,0,0,0,28.79-4,96,96,0,0,0,153.78-88.25c12.51-13,20.83-25.35,23.66-35.92C248.83,72.51,248.24,66.07,245.11,60.68Zm-13.69,15c-6.11,22.78-48.65,57.31-87.52,79.64-67.81,39-113.62,41.52-119.16,32-1.46-2.51-.65-7.24,2.22-13a80.06,80.06,0,0,1,10.28-15.05,95.53,95.53,0,0,0,6.23,14.18,4,4,0,0,0,4,2.12,122.14,122.14,0,0,0,16.95-3.32c21.23-5.55,46.63-16.48,71.52-30.78s47-30.66,62.45-46.15A122.74,122.74,0,0,0,209.7,82.45a4,4,0,0,0,.17-4.52,96.26,96.26,0,0,0-9.1-12.46c14.21-2.35,27.37-2.17,30.5,3.24C232.19,70.28,232.24,72.63,231.42,75.69Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 817 B |
BIN
public/images/sidebar.png
Normal file
BIN
public/images/sidebar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 MiB |
77
server/api/worlds/[id].patch.ts
Normal file
77
server/api/worlds/[id].patch.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { z } from "zod"
|
||||
import { serverSupabaseClient } from "#supabase/server"
|
||||
import { postWorldSchema, type World } from "~/models/World"
|
||||
|
||||
const paramsSchema = z.object({
|
||||
id: z.number({ coerce: true }).positive().int()
|
||||
})
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const client = await serverSupabaseClient(event)
|
||||
|
||||
const { data: params, error: paramsError} = await getValidatedRouterParams(event, paramsSchema.safeParse)
|
||||
const { data: bodyData, error: bodyError } = await readValidatedBody(event, body => postWorldSchema.safeParse(body))
|
||||
|
||||
if (paramsError) {
|
||||
throw createError({
|
||||
cause: "Utilisateur",
|
||||
fatal: false,
|
||||
message: "L'identifiant du monde est manquant ou mal renseigné.",
|
||||
status: 401,
|
||||
})
|
||||
}
|
||||
|
||||
if (bodyError) {
|
||||
const error = createError({
|
||||
cause: "Utilisateur",
|
||||
fatal: false,
|
||||
statusCode: 401,
|
||||
statusMessage: "Validation Error",
|
||||
message: "Erreur de validation du schéma, probablement dûe à une erreur utilisateur.",
|
||||
data: {
|
||||
errors: bodyError.issues.map(issue => ({
|
||||
path: issue.path,
|
||||
message: issue.message,
|
||||
code: issue.code
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await client
|
||||
.from("worlds")
|
||||
.update(
|
||||
{
|
||||
name: bodyData.name,
|
||||
description: bodyData.description,
|
||||
state: bodyData.state,
|
||||
gm_id: bodyData.gmId,
|
||||
color: bodyData.color
|
||||
} as never
|
||||
)
|
||||
.eq("id", params.id)
|
||||
.select(`
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
gm_id,
|
||||
state
|
||||
`)
|
||||
.single<World>()
|
||||
|
||||
if (error) throw error
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
cause: "Serveur",
|
||||
status: 500,
|
||||
fatal: false,
|
||||
message: "Une erreur inconnue est survenue."
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user