Merge branch 'code/cleanup' into dev
This commit is contained in:
@@ -15,7 +15,7 @@ function handleMenuItemAction(actionType: SidebarMenuActionType) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<menu class="flex flex-col gap-4">
|
||||||
<li class="mb-12">
|
<li class="mb-12">
|
||||||
<UiButton variant="ghost" size="icon" class="rounded-full" @click="console.log">
|
<UiButton variant="ghost" size="icon" class="rounded-full" @click="console.log">
|
||||||
@@ -66,3 +66,26 @@ function handleMenuItemAction(actionType: SidebarMenuActionType) {
|
|||||||
<UserCTA />
|
<UserCTA />
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</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>
|
<ClientOnly>
|
||||||
<UiDropdownMenu v-model:open="menuOpened">
|
<UiDropdownMenu v-model:open="menuOpened">
|
||||||
<UiDropdownMenuTrigger>
|
<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
|
<UiAvatarImage
|
||||||
:src="userMeta?.avatar_url"
|
:src="userMeta?.avatar_url"
|
||||||
:alt="userMeta?.full_name"
|
: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}",
|
greeting: "Connected as {user}",
|
||||||
anonymousGreeting: "Preferences",
|
anonymousGreeting: "Preferences",
|
||||||
|
backToProfile: "Back to profile",
|
||||||
sidebarMenu: {
|
sidebarMenu: {
|
||||||
profile: "Profile",
|
profile: "Profile",
|
||||||
appearance: "Appearance",
|
appearance: "Appearance",
|
||||||
@@ -80,14 +81,24 @@ export default defineI18nConfig(() => ({
|
|||||||
nameSingular: "World",
|
nameSingular: "World",
|
||||||
namePlural: "Worlds",
|
namePlural: "Worlds",
|
||||||
addSingle: "Add a world",
|
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: {
|
createDialog: {
|
||||||
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.",
|
||||||
},
|
},
|
||||||
@@ -101,6 +112,7 @@ export default defineI18nConfig(() => ({
|
|||||||
notFound: "Calendar not found",
|
notFound: "Calendar not found",
|
||||||
notFoundDescription: "The link is not valid or the calendar has been deleted / archived.",
|
notFoundDescription: "The link is not valid or the calendar has been deleted / archived.",
|
||||||
notFoundForWorld: "No calendar for this world… yet !",
|
notFoundForWorld: "No calendar for this world… yet !",
|
||||||
|
backToList: "Go back to calendars",
|
||||||
isLoading: "Calendar is loading…",
|
isLoading: "Calendar is loading…",
|
||||||
hasXEvents: "This calendar has {count} public events",
|
hasXEvents: "This calendar has {count} public events",
|
||||||
date: {
|
date: {
|
||||||
@@ -278,6 +290,7 @@ export default defineI18nConfig(() => ({
|
|||||||
},
|
},
|
||||||
greeting: "Connecté en tant que {user}",
|
greeting: "Connecté en tant que {user}",
|
||||||
anonymousGreeting: "Préférences",
|
anonymousGreeting: "Préférences",
|
||||||
|
backToProfile: "Retour au profil",
|
||||||
sidebarMenu: {
|
sidebarMenu: {
|
||||||
profile: "Profil",
|
profile: "Profil",
|
||||||
appearance: "Apparence",
|
appearance: "Apparence",
|
||||||
@@ -318,14 +331,24 @@ export default defineI18nConfig(() => ({
|
|||||||
nameSingular: "Monde",
|
nameSingular: "Monde",
|
||||||
namePlural: "Mondes",
|
namePlural: "Mondes",
|
||||||
addSingle: "Ajouter un monde",
|
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: {
|
createDialog: {
|
||||||
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.",
|
||||||
},
|
},
|
||||||
@@ -339,6 +362,7 @@ export default defineI18nConfig(() => ({
|
|||||||
notFound: "Aucun calendrier trouvé",
|
notFound: "Aucun calendrier trouvé",
|
||||||
notFoundDescription: "Le lien n'est pas valide ou le calendrier a été supprimé / archivé.",
|
notFoundDescription: "Le lien n'est pas valide ou le calendrier a été supprimé / archivé.",
|
||||||
notFoundForWorld: "Aucun calendrier pour ce monde… pour l'instant !",
|
notFoundForWorld: "Aucun calendrier pour ce monde… pour l'instant !",
|
||||||
|
backToList: "Retourner aux calendriers",
|
||||||
isLoading: "Chargement du calendrier…",
|
isLoading: "Chargement du calendrier…",
|
||||||
hasXEvents: "Ce calendrier contient {count} évènements publiques",
|
hasXEvents: "Ce calendrier contient {count} évènements publiques",
|
||||||
date: {
|
date: {
|
||||||
|
|||||||
@@ -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 { PhCalendarX, PhCircleNotch } from "@phosphor-icons/vue";
|
import { PhArrowBendDoubleUpLeft, PhCalendarX, PhCircleNotch } from "@phosphor-icons/vue";
|
||||||
import type { Calendar } from "~/models/CalendarConfig";
|
import type { Calendar } from "~/models/CalendarConfig";
|
||||||
import type { Category } from "~/models/Category";
|
import type { Category } from "~/models/Category";
|
||||||
|
|
||||||
@@ -8,8 +8,29 @@ const shortId = route.params.id
|
|||||||
|
|
||||||
const user = useSupabaseUser()
|
const user = useSupabaseUser()
|
||||||
|
|
||||||
const { data: calendarData, pending: calPending, refresh: calRefresh } = await useLazyFetch<{ data: Calendar }>("/api/calendars/query", { query: { shortId, full: true } })
|
const {
|
||||||
const { data: catData, pending: catPending, refresh: catRefresh } = await useLazyFetch<{ data: Category[] }>("/api/calendars/categories/query")
|
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, () => {
|
watch(user, () => {
|
||||||
calRefresh()
|
calRefresh()
|
||||||
@@ -18,7 +39,7 @@ watch(user, () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
<Head>
|
||||||
<Title>{{ $t("entity.calendar.nameSingular") }}</Title>
|
<Title>{{ $t("entity.calendar.nameSingular") }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
@@ -31,12 +52,12 @@ watch(user, () => {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<Head>
|
||||||
<Title>{{ calendarData.data.name }}</Title>
|
<Title>{{ calendar.data.name }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Calendar :calendar-data="calendarData.data" :categories="catData.data" />
|
<Calendar :calendar-data="calendar.data" :categories="categories.data" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="h-full w-full grid place-items-center">
|
<div v-else class="h-full w-full grid place-items-center">
|
||||||
@@ -54,6 +75,14 @@ watch(user, () => {
|
|||||||
<p>
|
<p>
|
||||||
{{ $t('entity.calendar.notFoundDescription') }}
|
{{ $t('entity.calendar.notFoundDescription') }}
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<script lang="ts" setup>
|
||||||
import type { Calendar } from "~/models/CalendarConfig";
|
import type { Calendar } from "~/models/CalendarConfig";
|
||||||
|
|
||||||
const { data } = await useFetch("/api/calendars/query", { key: "explore-calendars" })
|
const { data: availableCalendars } = await useLazyFetch<{ data: Calendar[] }>("/api/calendars/query", { key: "explore-calendars" })
|
||||||
|
|
||||||
const availableCalendars = data.value?.data as Calendar[]
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -22,8 +20,8 @@ const availableCalendars = data.value?.data as Calendar[]
|
|||||||
{{ $t("entity.calendar.namePublicPlural") }}
|
{{ $t("entity.calendar.namePublicPlural") }}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<ul v-if="availableCalendars && availableCalendars?.length > 0" class="grid md:grid-cols-3 gap-2">
|
<ul v-if="availableCalendars?.data" class="grid md:grid-cols-3 gap-2">
|
||||||
<li v-for="calendar in availableCalendars" :key="calendar.shortId">
|
<li v-for="calendar in availableCalendars.data" :key="calendar.shortId">
|
||||||
<UiCard
|
<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"
|
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}`"
|
:link="`/calendars/${calendar.shortId}`"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<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 { Calendar } from "~/models/CalendarConfig";
|
||||||
import type { Category } from "~/models/Category";
|
import type { Category } from "~/models/Category";
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ definePageMeta({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const user = useSupabaseUser()
|
const user = useSupabaseUser()
|
||||||
|
|
||||||
// Redirect user back home when they log out on the page
|
// Redirect user back home when they log out on the page
|
||||||
watch(user, (n) => {
|
watch(user, (n) => {
|
||||||
if (!n) {
|
if (!n) {
|
||||||
@@ -18,15 +19,31 @@ watch(user, (n) => {
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const id = route.params.id
|
const id = route.params.id
|
||||||
|
|
||||||
const { data: calendarData, pending: calPending } = await useLazyFetch("/api/calendars/query", { key: `calendar-${id}`, query: { id, full: true } })
|
const {
|
||||||
const { data: catData, pending: catPending } = await useLazyFetch("/api/calendars/categories/query", { key: `categories-${id}` })
|
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 {
|
||||||
const categories = computed<Category[]>(() => catData?.value?.data as Category[])
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
<Head>
|
||||||
<Title>{{ $t("entity.calendar.nameSingular") }}</Title>
|
<Title>{{ $t("entity.calendar.nameSingular") }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
@@ -39,11 +56,36 @@ const categories = computed<Category[]>(() => catData?.value?.data as Category[]
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<Head>
|
||||||
<Title>{{ cal.name }}</Title>
|
<Title>{{ calendar.data.name }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Calendar :calendar-data="cal" :categories />
|
<Calendar :calendar-data="calendar.data" :categories="categories.data" />
|
||||||
</div>
|
</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>
|
<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";
|
||||||
|
|
||||||
const supabase = useSupabaseClient()
|
const supabase = useSupabaseClient()
|
||||||
|
|
||||||
const { data: res } = await useFetch("/api/worlds/query")
|
const { data: worlds } = await useLazyFetch<{ data: World[] }>("/api/worlds/query")
|
||||||
|
|
||||||
const worlds = ref<World[]>(res.value?.data as World[])
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ["auth-guard"]
|
middleware: ["auth-guard"]
|
||||||
@@ -36,8 +34,10 @@ let worldChannel: RealtimeChannel
|
|||||||
|
|
||||||
/** Handles world insertion realtime events */
|
/** Handles world insertion realtime events */
|
||||||
function handleInsertedWorld(newWorld: World) {
|
function handleInsertedWorld(newWorld: World) {
|
||||||
|
if (!worlds.value?.data) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
worlds.value.push(newWorld)
|
worlds.value?.data.push(newWorld)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
}
|
}
|
||||||
@@ -45,8 +45,10 @@ function handleInsertedWorld(newWorld: World) {
|
|||||||
|
|
||||||
/** Handles world deletion realtime events */
|
/** Handles world deletion realtime events */
|
||||||
function handleDeletedWorld(id: number) {
|
function handleDeletedWorld(id: number) {
|
||||||
|
if (!worlds.value?.data) return
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
}
|
}
|
||||||
@@ -68,7 +70,9 @@ onMounted(() => {
|
|||||||
break
|
break
|
||||||
|
|
||||||
case "UPDATE":
|
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
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -87,21 +91,32 @@ 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>
|
||||||
<main class="p-8 after:fill-red-400">
|
<main class="p-8">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>{{ $t("entity.world.namePlural") }}</Title>
|
<Title>{{ $t("entity.world.namePlural") }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
@@ -131,8 +146,8 @@ function hideDeleteModal() {
|
|||||||
</UiTooltipProvider>
|
</UiTooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="grid lg:grid-cols-3 gap-2">
|
<ul v-if="worlds?.data" class="grid lg:grid-cols-3 gap-2">
|
||||||
<li v-for="world in worlds" :key="world.id">
|
<li v-for="world in worlds.data" :key="world.id">
|
||||||
<UiCard
|
<UiCard
|
||||||
class="w-full transition-all"
|
class="w-full transition-all"
|
||||||
:link="`/my/worlds/${world.id}`"
|
:link="`/my/worlds/${world.id}`"
|
||||||
@@ -165,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>
|
||||||
@@ -176,32 +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>
|
||||||
|
|
||||||
<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 { RealtimeChannel } from "@supabase/supabase-js"
|
||||||
import type { World } from "~/models/World";
|
import type { World } from "~/models/World";
|
||||||
import type { Calendar } from "~/models/CalendarConfig";
|
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 supabase = useSupabaseClient()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const id = route.params.id
|
const id = route.params.id
|
||||||
|
|
||||||
const { data: res, pending } = await useFetch("/api/worlds/query", { query: { id, full: true } })
|
const { data: world, status } = await useFetch<{ data: World }>("/api/worlds/query", { query: { id, full: true } })
|
||||||
|
|
||||||
const world = ref<World>(res.value?.data as World)
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ["auth-guard"]
|
middleware: ["auth-guard"]
|
||||||
@@ -40,8 +38,10 @@ let calendarChannel: RealtimeChannel
|
|||||||
|
|
||||||
/** Handles calendar insertion realtime events */
|
/** Handles calendar insertion realtime events */
|
||||||
function handleInsertedCalendar(newCalendar: Calendar) {
|
function handleInsertedCalendar(newCalendar: Calendar) {
|
||||||
|
if (!world.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
world.value.calendars?.push(newCalendar)
|
world.value.data.calendars?.push(newCalendar)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
}
|
}
|
||||||
@@ -49,8 +49,10 @@ function handleInsertedCalendar(newCalendar: Calendar) {
|
|||||||
|
|
||||||
/** Handles calendar deletion realtime events */
|
/** Handles calendar deletion realtime events */
|
||||||
function handleDeletedCalendar(id: number) {
|
function handleDeletedCalendar(id: number) {
|
||||||
|
if (!world.value) return
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
}
|
}
|
||||||
@@ -101,7 +103,7 @@ function hideDeleteModal() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="p-8">
|
<main class="p-8">
|
||||||
<template v-if="pending">
|
<template v-if="status === 'pending'">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>{{ $t("entity.world.namePlural") }}</Title>
|
<Title>{{ $t("entity.world.namePlural") }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
@@ -110,16 +112,16 @@ function hideDeleteModal() {
|
|||||||
{{ $t('entity.isLoading') }}
|
{{ $t('entity.isLoading') }}
|
||||||
</Heading>
|
</Heading>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="world">
|
<template v-else-if="world?.data">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>{{ world.name }}</Title>
|
<Title>{{ world.data.name }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<header class="lg:w-1/2 mb-8">
|
<header class="lg:w-1/2 mb-8">
|
||||||
<Spacing>
|
<Spacing>
|
||||||
<Heading level="h1">{{ world.name }}</Heading>
|
<Heading level="h1">{{ world.data.name }}</Heading>
|
||||||
|
|
||||||
<p>{{ world.description }}</p>
|
<p>{{ world.data.description }}</p>
|
||||||
</Spacing>
|
</Spacing>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -146,8 +148,8 @@ function hideDeleteModal() {
|
|||||||
</UiTooltipProvider>
|
</UiTooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-if="world.calendars && world.calendars?.length > 0" class="grid md:grid-cols-3 gap-2">
|
<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.calendars" :key="calendar.id">
|
<li v-for="calendar in world.data.calendars" :key="calendar.id">
|
||||||
<UiCard
|
<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"
|
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}`"
|
:link="`/my/calendars/${calendar.id}`"
|
||||||
@@ -173,35 +175,36 @@ function hideDeleteModal() {
|
|||||||
</template>
|
</template>
|
||||||
</Spacing>
|
</Spacing>
|
||||||
</section>
|
</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" />
|
<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>
|
</main>
|
||||||
</template>
|
</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