Migration to nuxt 4

Used codemods CLI and reworked most alias'd path that stopped working
This commit is contained in:
Alexis
2025-07-27 14:30:30 +02:00
parent 68b9a38f83
commit 7fdab8601f
280 changed files with 847 additions and 496 deletions

View File

@@ -0,0 +1,84 @@
<script lang="ts" setup>
import { PhArchive, PhFile, PhFileDashed, PhFilePlus, PhPencil, PhPencilSimpleLine, PhTrash } from "@phosphor-icons/vue"
import type { World } from "@@/models/World"
import { DateTime } from "luxon"
import { cn } from "@/lib/utils"
const props = defineProps<{
world: World
}>()
const emit = defineEmits(["on-edit", "on-delete"])
const cardRef = ref(null)
const isCardHovered = useElementHover(cardRef)
const { focused: isCardFocused } = useFocusWithin(cardRef)
const { locale } = useI18n()
const createdAt = DateTime.fromISO(props.world.createdAt!).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value })
const updatedAt = computed<string>(() => props.world.updatedAt ? DateTime.fromISO(props.world.updatedAt).toLocaleString(DateTime.DATETIME_MED, { locale: locale.value }) : "")
</script>
<template>
<UiCard
ref="cardRef"
class="w-full h-full flex flex-col"
:link="`/my/worlds/${world.id}`"
:class="cn(
world.color ? `card-color element-${world.color}` : '',
)"
>
<UiCardHeader class="gap-4">
<UiCardTitle>
{{ world.name }}
</UiCardTitle>
<div v-if="world.state === 'published'" class="flex items-center gap-1 text-sm">
<PhFile size="20" weight="fill" />
<span>{{ $t('ui.contentState.published') }}</span>
</div>
<div v-if="world.state === 'draft'" class="flex items-center gap-1 text-sm">
<PhFileDashed size="20" weight="fill" />
<span>{{ $t('ui.contentState.draft') }}</span>
</div>
<div v-if="world.state === 'archived'" class="flex items-center gap-1 text-sm">
<PhArchive size="20" weight="fill" />
<span>{{ $t('ui.contentState.archived') }}</span>
</div>
</UiCardHeader>
<UiCardContent class="grow">
<p class="italic">{{ world.description }}</p>
<div
v-if="isCardHovered || isCardFocused"
class="flex gap-2 absolute top-4 right-4 z-20"
>
<Transition name="fade" appear>
<UiButton size="icon" variant="outline" class="size-8 border-foreground/20 group-hover:border-foreground/30 hover:text-background hover:border-foreground hover:bg-foreground" @click="emit('on-edit')">
<PhPencil size="15" weight="fill" />
</UiButton>
</Transition>
<Transition name="fade" appear>
<UiButton size="icon" variant="outline" class="size-8 border-foreground/25 group-hover:border-foreground/40 hover:text-background hover:border-rose-500 hover:bg-rose-500" @click="emit('on-delete')">
<PhTrash size="15" weight="fill" />
</UiButton>
</Transition>
</div>
</UiCardContent>
<hr>
<UiCardFooter class="border-t-0">
<ul class="grid gap-1 text-sm">
<li class="flex gap-1 items-center">
<PhFilePlus size="18" />
<span>{{ $t('common.createdAt', { createdAt }) }}</span>
</li>
<li v-if="updatedAt" class="flex gap-1 items-center">
<PhPencilSimpleLine size="18" />
<span>{{ $t('common.updatedAt', { updatedAt }) }}</span>
</li>
</ul>
</UiCardFooter>
</UiCard>
</template>

View File

@@ -0,0 +1,49 @@
<script lang="ts" setup>
import { PhX } from "@phosphor-icons/vue";
defineProps<{
modalState?: boolean
}>()
const worldSkeletonName = ref<string>("")
function onChangedName(newName: string) {
worldSkeletonName.value = newName
}
const emit = defineEmits(["on-close"])
function handleClose() {
emit("on-close")
worldSkeletonName.value = ""
}
</script>
<template>
<UiAlertDialog :open="modalState">
<UiAlertDialogContent class="gap-4" @close-auto-focus="(e) => e.preventDefault()">
<header>
<UiAlertDialogTitle>
<span class="text-2xl">
<span v-if="worldSkeletonName">
{{ $t('entity.world.createDialog.title') }} — {{ worldSkeletonName }}
</span>
<span v-else>
{{ $t('entity.world.createDialog.title') }}
</span>
</span>
</UiAlertDialogTitle>
<UiAlertDialogDescription>
{{ $t('entity.world.createDialog.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>
<WorldFormCreate @on-changed-name="onChangedName" @on-close="handleClose" />
</UiAlertDialogContent>
</UiAlertDialog>
</template>

View File

@@ -0,0 +1,103 @@
<script lang="ts" setup>
import { PhCircleNotch } from "@phosphor-icons/vue";
import { useToast } from "~/components/ui/toast";
import { ToastLifetime } from "~/components/ui/toast/use-toast";
import type { World } from "@@/models/World";
const { toast } = useToast()
const { t } = useI18n()
const props = defineProps<{
modalState: boolean,
world: World | null
}>()
const isLoading = ref<boolean>(false)
const emit = defineEmits(["on-close"])
async function handleAction(): Promise<void> {
if (isLoading.value || !props.world) return
isLoading.value = true
const { error } = await tryCatch(
$fetch(`/api/worlds/${props.world.id}`, { method: "DELETE" })
)
if (error) {
toast({
title: error.message,
variant: "destructive"
})
isLoading.value = false
return
}
toast({
title: t("entity.world.deletedToast.title", { world: props.world.name }),
variant: "success",
duration: ToastLifetime.SHORT
})
emit("on-close")
isLoading.value = false
}
/**
* Prevents the modal from closing if's still loading
*
* @param e The closing event (can be keydown or click)
*/
function handleClosing() {
if (!isLoading.value) {
emit("on-close")
}
}
</script>
<template>
<UiAlertDialog :open="modalState">
<UiAlertDialogContent
:disable-outside-pointer-events="true"
:trap-focus="true"
class="min-w-96"
@escape-key-down="handleClosing"
@focus-outside="handleClosing"
@interact-outside="handleClosing"
@pointer-down-outside="handleClosing"
@close-auto-focus="(e) => e.preventDefault()"
>
<UiAlertDialogTitle>
{{ $t('entity.world.deleteDialog.title') }}
</UiAlertDialogTitle>
<UiAlertDialogDescription>
{{ $t('entity.world.deleteDialog.subtitle') }}
</UiAlertDialogDescription>
<form @submit.prevent="handleAction">
<footer class="flex gap-2 justify-between">
<UiButton type="button" size="sm" variant="outline" @click="handleClosing">
{{ $t('ui.action.back') }}
</UiButton>
<div class="flex gap-2 justify-end">
<Transition name="fade-delay">
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive">
{{ $t('ui.action.cancel') }}
</UiButton>
</Transition>
<UiButton v-if="world" size="sm" variant="destructive" :disabled="isLoading">
<Transition name="fade">
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
</Transition>
{{ $t('entity.deleteOne', { entity: world?.name }) }}
</UiButton>
</div>
</footer>
</form>
</UiAlertDialogContent>
</UiAlertDialog>
</template>

View File

@@ -0,0 +1,54 @@
<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")
worldSkeletonName.value = props.world?.name ?? ""
}
</script>
<template>
<UiAlertDialog :open="modalState">
<UiAlertDialogContent class="gap-4" @close-auto-focus="(e) => e.preventDefault()">
<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>
<WorldFormUpdate :world @on-changed-name="onChangedName" @on-close="handleClose" />
</UiAlertDialogContent>
</UiAlertDialog>
</template>

View File

@@ -0,0 +1,114 @@
<script lang="ts" setup>
import { PhCircleNotch } from "@phosphor-icons/vue";
import type { World } from "@@/models/World";
const user = useSupabaseUser()
const defaultWorld: World = { name: "", description: "", gmId: user.value?.id, color: "white", state: "draft" }
const worldSkeleton = ref<World>({ ...defaultWorld })
onMounted(() => {
worldSkeleton.value = { ...defaultWorld }
})
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) return
isLoading.value = true
const { error } = await tryCatch(
$fetch("/api/worlds/create", { method: "POST", body: { ...worldSkeleton.value } })
)
if (error) {
console.log(error.message)
isLoading.value = false
return
}
emit("on-close")
isLoading.value = false
}
/**
* === Watch for name changes to display above ===
*/
const emit = defineEmits<{
"on-changed-name": [calendarName: string]
"on-close": []
}>()
/** 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 class="grid gap-4">
<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-hidden focus-visible:border-blue-600"
@input="handleNameChange"
>
<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-hidden focus-visible:border-blue-600"
/>
<div class="-mx-1 grid gap-3">
<UiLabel for="new-world-state">
{{ $t('ui.contentState.label') }}
</UiLabel>
<InputContentState id="new-world-state" v-model="worldSkeleton.state" />
</div>
<div class="-mx-1 grid gap-3">
<UiLabel for="new-world-color">
{{ $t('ui.colors.label') }}
</UiLabel>
<InputColor id="new-world-color" v-model="worldSkeleton.color" />
</div>
</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

@@ -0,0 +1,126 @@
<script lang="ts" setup>
import { PhCircleNotch } from "@phosphor-icons/vue";
import { useToast } from "~/components/ui/toast";
import { ToastLifetime } from "~/components/ui/toast/use-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
isLoading.value = true
const { error } = await tryCatch(
$fetch(`/api/worlds/${worldSkeleton.value.id}`, { method: "PATCH", body: { ...worldSkeleton.value } })
)
if (error) {
console.log(error.message)
}
toast({
title: t("entity.world.updatedToast.title", { world: worldSkeleton.value.name }),
variant: "success",
duration: ToastLifetime.SHORT
})
emit("on-close")
isLoading.value = false
}
/**
* === Watch for name changes to display above ===
*/
const emit = defineEmits<{
"on-changed-name": [calendarName: string]
"on-close": []
}>()
/** 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 class="grid gap-4">
<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 py-2 -mx-1 px-1 text-xl border-b-[1px] bg-transparent focus-visible:outline-hidden focus-visible:border-blue-600"
@input="handleNameChange"
>
<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-hidden focus-visible:border-blue-600"
/>
<div class="-mx-1 grid gap-3">
<UiLabel for="new-world-state">
{{ $t('ui.contentState.label') }}
</UiLabel>
<InputContentState id="new-world-state" v-model="worldSkeleton.state" />
</div>
<div class="-mx-1 grid gap-3">
<UiLabel for="new-world-color">
{{ $t('ui.colors.label') }}
</UiLabel>
<InputColor id="new-world-color" v-model="worldSkeleton.color" />
</div>
</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>