Added edit world functions

This commit is contained in:
Alexis
2025-02-28 19:24:53 +01:00
parent 04940aacc3
commit fe035da7c3
6 changed files with 285 additions and 6 deletions

View 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>

View 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>

View File

@@ -88,10 +88,17 @@ export default defineI18nConfig(() => ({
title: "Create a world", title: "Create a world",
subtitle: "Worlds are the building blocks which hold all your characters, your calendars…" subtitle: "Worlds are the building blocks which hold all your characters, your calendars…"
}, },
editDialog: {
title: "Edit world",
subtitle: "Update world data",
},
deleteDialog: { deleteDialog: {
title: "Delete this world ?", title: "Delete this world ?",
subtitle: "This world will be deleted permanently, and all of its associated data will be lost !", 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: { deletedToast: {
title: "The world \"{world}\" has been successfuly deleted.", title: "The world \"{world}\" has been successfuly deleted.",
}, },
@@ -331,10 +338,17 @@ export default defineI18nConfig(() => ({
title: "Créer un monde", title: "Créer un monde",
subtitle: "Un monde est la brique de base qui contient vos personnages, vos calendriers…" 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: { deleteDialog: {
title: "Supprimer ce monde ?", title: "Supprimer ce monde ?",
subtitle: "Le monde sera supprimé définitivement, vous perdrez toutes les données associées !", 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: { deletedToast: {
title: "Le monde \"{world}\" a été supprimé avec succès.", title: "Le monde \"{world}\" a été supprimé avec succès.",
}, },

View File

@@ -2,6 +2,8 @@ import { z } from "zod"
import type { Calendar } from "./CalendarConfig" import type { Calendar } from "./CalendarConfig"
import { rpgColorSchema, type RPGColor } from "./Color" import { rpgColorSchema, type RPGColor } from "./Color"
export type WorldState = "published" | "draft" | "archived"
export interface World { export interface World {
id?: number id?: number
name: string name: string
@@ -9,6 +11,7 @@ export interface World {
color?: RPGColor, color?: RPGColor,
calendars?: Calendar[], calendars?: Calendar[],
gmId?: string gmId?: string
state?: WorldState
} }
export const postWorldSchema = z.object({ export const postWorldSchema = z.object({
@@ -16,4 +19,5 @@ export const postWorldSchema = z.object({
description: z.string().optional().nullable(), description: z.string().optional().nullable(),
color: rpgColorSchema, color: rpgColorSchema,
gmId: z.string().optional().nullable(), gmId: z.string().optional().nullable(),
state: z.string().optional().nullable().default("draft"),
}) })

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <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 { RealtimeChannel } from "@supabase/supabase-js"
import type { World } from "~/models/World"; import type { World } from "~/models/World";
@@ -91,17 +91,28 @@ onUnmounted(() => {
}) })
const markedWorld = ref<World | null>(null) const markedWorld = ref<World | null>(null)
const isEditWorldModalOpen = ref<boolean>(false)
const isDeleteWorldModalOpen = ref<boolean>(false) const isDeleteWorldModalOpen = ref<boolean>(false)
function deployDeleteModal(calendar: World) { function deployDeleteModal(world: World) {
isDeleteWorldModalOpen.value = true isDeleteWorldModalOpen.value = true
markedWorld.value = calendar markedWorld.value = world
} }
function hideDeleteModal() { function hideDeleteModal() {
isDeleteWorldModalOpen.value = false isDeleteWorldModalOpen.value = false
markedWorld.value = null markedWorld.value = null
} }
function deployEditModal(world: World) {
isEditWorldModalOpen.value = true
markedWorld.value = world
}
function hideEditModal() {
isEditWorldModalOpen.value = false
markedWorld.value = null
}
</script> </script>
<template> <template>
@@ -169,9 +180,15 @@ function hideDeleteModal() {
<UiCardContent> <UiCardContent>
<p class="italic">{{ world.description }}</p> <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">
<PhTrash size="16" /> <UiButton size="icon" variant="ghost" class=" hover:text-white hover:bg-indigo-400 dark:hover:bg-indigo-700" @click="deployEditModal(world)">
</UiButton> <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> </UiCardContent>
</UiCard> </UiCard>
</li> </li>
@@ -180,6 +197,7 @@ function hideDeleteModal() {
</section> </section>
<WorldDialogCreate :modal-state="isCreateWorldModalOpen" @on-close="hideCreateDialog" /> <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" /> <WorldDialogDelete :world="markedWorld" :modal-state="isDeleteWorldModalOpen" @on-close="hideDeleteModal" />
</main> </main>
</template> </template>

View 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."
})
}
})