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",
|
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.",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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">
|
||||||
|
<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" />
|
<PhTrash size="16" />
|
||||||
</UiButton>
|
</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>
|
||||||
|
|||||||
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