Added edit world functions
This commit is contained in:
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>
|
||||
@@ -88,10 +88,17 @@ export default defineI18nConfig(() => ({
|
||||
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.",
|
||||
},
|
||||
@@ -331,10 +338,17 @@ export default defineI18nConfig(() => ({
|
||||
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.",
|
||||
},
|
||||
|
||||
@@ -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 { 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";
|
||||
|
||||
@@ -91,17 +91,28 @@ 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>
|
||||
@@ -169,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)">
|
||||
<PhTrash size="16" />
|
||||
</UiButton>
|
||||
<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>
|
||||
@@ -180,6 +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>
|
||||
|
||||
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