Migration to nuxt 4
Used codemods CLI and reworked most alias'd path that stopped working
This commit is contained in:
15
app/components/calendar/category/List.vue
Normal file
15
app/components/calendar/category/List.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { Category } from "@@/models/Category";
|
||||
|
||||
defineProps<{
|
||||
categories: Category[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul>
|
||||
<li v-for="category in categories" :key="category.id">
|
||||
<CalendarCategoryListItem :category="category" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
15
app/components/calendar/category/ListItem.vue
Normal file
15
app/components/calendar/category/ListItem.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { Category } from "@@/models/Category";
|
||||
|
||||
defineProps<{
|
||||
category: Category
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ category.name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
70
app/components/calendar/category/Table.vue
Normal file
70
app/components/calendar/category/Table.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import type { Category } from "@@/models/Category";
|
||||
import { ScrollAreaRoot, ScrollAreaViewport, ScrollAreaScrollbar, ScrollAreaThumb } from "radix-vue"
|
||||
|
||||
const { categories } = defineProps<{
|
||||
categories: Category[]
|
||||
}>()
|
||||
|
||||
const sortedCategories = computed(() => categories.toSorted((a, b) => a.name.localeCompare(b.name)))
|
||||
|
||||
const deleteDialogOpened = ref<boolean>(false)
|
||||
|
||||
function openDeleteDialog() {
|
||||
deleteDialogOpened.value = true
|
||||
}
|
||||
|
||||
function closeDeleteDialog() {
|
||||
deleteDialogOpened.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<CalendarCategoryTableHeader />
|
||||
|
||||
<ScrollAreaRoot class="h-36 grow overflow-hidden ">
|
||||
<ScrollAreaViewport class="w-full h-full pr-4" as-child>
|
||||
<div class="last:border-0">
|
||||
<CalendarCategoryTableRow
|
||||
v-for="item in sortedCategories"
|
||||
:key="item.id"
|
||||
:category="item"
|
||||
@on-delete-category="openDeleteDialog"
|
||||
/>
|
||||
</div>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar
|
||||
class="flex select-none touch-none p-0.5 bg-background transition-colors duration-100 ease-out hover:bg-foreground/5 data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:h-2.5"
|
||||
orientation="vertical"
|
||||
>
|
||||
<ScrollAreaThumb
|
||||
class="flex-1 bg-foreground/20 rounded-[10px] relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full"
|
||||
/>
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
|
||||
<CalendarCategoryTableFooter />
|
||||
</div>
|
||||
|
||||
<UiDialog v-model:open="deleteDialogOpened">
|
||||
<UiDialogContent
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||
@escape-key-down="closeDeleteDialog"
|
||||
@focus-outside="closeDeleteDialog"
|
||||
@interact-outside="closeDeleteDialog"
|
||||
@pointer-down-outside="closeDeleteDialog"
|
||||
>
|
||||
<UiDialogTitle>
|
||||
{{ $t('entity.category.deleteDialog.title') }}
|
||||
</UiDialogTitle>
|
||||
<UiDialogDescription>
|
||||
{{ $t('entity.category.deleteDialog.subtitle') }}
|
||||
</UiDialogDescription>
|
||||
|
||||
<CalendarFormDeleteCategory />
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</template>
|
||||
130
app/components/calendar/category/TableFooter.vue
Normal file
130
app/components/calendar/category/TableFooter.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhPlus } from "@phosphor-icons/vue"
|
||||
import { useToast } from "~/components/ui/toast"
|
||||
import { ToastLifetime } from "~/components/ui/toast/use-toast"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const { toast } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
type FooterMode = "add" | "view"
|
||||
const currentMode = ref<FooterMode>("view")
|
||||
|
||||
const rowRef = ref<HTMLDivElement | null>(null)
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
const { focused: inputFocused } = useFocus(inputRef)
|
||||
|
||||
/**
|
||||
* Toggle view mode options
|
||||
*/
|
||||
type ToggleViewOptions = {
|
||||
execution?: "now" | "nextTick"
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle view mode
|
||||
* @param options.execution - When to execute the toggle. "now" or "nextTick"
|
||||
*/
|
||||
function toggleView(options: ToggleViewOptions = { execution: "now" }) {
|
||||
currentMode.value = "view"
|
||||
|
||||
if (options.execution === "now") {
|
||||
inputFocused.value = false
|
||||
} else {
|
||||
nextTick(() => {
|
||||
inputFocused.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle add mode
|
||||
*/
|
||||
function toggleAdd() {
|
||||
currentMode.value = "add"
|
||||
categorySkeleton.value = { name: "", color: "black" }
|
||||
|
||||
nextTick(() => {
|
||||
inputFocused.value = true
|
||||
})
|
||||
}
|
||||
|
||||
onClickOutside(rowRef, () => toggleView({ execution: "nextTick" }))
|
||||
onKeyStroke("Escape", () => toggleView({ execution: "now" }))
|
||||
|
||||
const { addCategoryFromSkeleton } = useCategoryStore()
|
||||
const { categorySkeleton } = storeToRefs(useCategoryStore())
|
||||
|
||||
/**
|
||||
* Submit the update
|
||||
*/
|
||||
async function submitNew() {
|
||||
if (!categorySkeleton.value) return
|
||||
const newCategoryName = toRaw(categorySkeleton.value).name
|
||||
|
||||
const { error } = await tryCatch(addCategoryFromSkeleton())
|
||||
|
||||
if (error) {
|
||||
toast({
|
||||
title: t("entity.category.addedToast.titleError", { category: newCategoryName }),
|
||||
variant: "destructive",
|
||||
duration: ToastLifetime.LONG
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
toggleView({ execution: "now" })
|
||||
toast({
|
||||
title: t("entity.category.addedToast.title", { category: newCategoryName }),
|
||||
variant: "success",
|
||||
duration: ToastLifetime.SHORT
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border-y-[1px] border-b-foreground/10 mr-4">
|
||||
<form
|
||||
ref="rowRef"
|
||||
class="grid grid-cols-12 items-center gap-4 p-1 bg-transparent hover:bg-slate-50 dark:bg-transparent dark:hover:bg-slate-900"
|
||||
@submit.prevent="submitNew"
|
||||
>
|
||||
<div v-if="currentMode === 'add'" class="col-span-1" />
|
||||
<div
|
||||
:class="cn({
|
||||
'col-span-6': currentMode === 'view',
|
||||
'col-span-5': currentMode === 'add'
|
||||
})"
|
||||
>
|
||||
<template v-if="currentMode === 'view'">
|
||||
<button
|
||||
class="p-2 h-full w-full text-left underline-offset-4 hover:underline cursor-pointer"
|
||||
@click="toggleAdd"
|
||||
>
|
||||
<PhPlus size="18" />
|
||||
</button>
|
||||
</template>
|
||||
<template v-if="currentMode === 'add' && categorySkeleton">
|
||||
<input
|
||||
ref="inputRef"
|
||||
v-model="categorySkeleton.name"
|
||||
type="text"
|
||||
class="p-1 h-full w-full bg-transparent focus-visible:outline-hidden italic"
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
<div class="col-span-4">
|
||||
<template v-if="currentMode === 'add' && categorySkeleton">
|
||||
<div class="-mx-2">
|
||||
<InputColor
|
||||
id="category-color"
|
||||
v-model="categorySkeleton.color"
|
||||
position="item-aligned"
|
||||
theme="subtle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
19
app/components/calendar/category/TableHeader.vue
Normal file
19
app/components/calendar/category/TableHeader.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { PhListBullets } from "@phosphor-icons/vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="grid grid-cols-12 gap-4 px-2 py-4 border-b-[1px] border-b-border font-bold mr-4">
|
||||
<div class="col-span-1 flex items-end">
|
||||
<PhListBullets size="20" />
|
||||
</div>
|
||||
<div class="col-span-5">
|
||||
Nom
|
||||
</div>
|
||||
<div class="col-span-4">
|
||||
Couleur
|
||||
</div>
|
||||
<div class="col-span-2" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
187
app/components/calendar/category/TableRow.vue
Normal file
187
app/components/calendar/category/TableRow.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<script setup lang="ts">
|
||||
import { PhCheck, PhTrash } from "@phosphor-icons/vue"
|
||||
import { useToast } from "~/components/ui/toast"
|
||||
import { ToastLifetime } from "~/components/ui/toast/use-toast"
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { Category } from "@@/models/Category"
|
||||
|
||||
const { toast } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const { category } = defineProps<{
|
||||
category: Category
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
"on-delete-category": [payload: Category]
|
||||
}>()
|
||||
|
||||
type RowMode = "edit" | "view"
|
||||
const currentMode = ref<RowMode>("view")
|
||||
|
||||
const rowRef = ref<HTMLDivElement | null>(null)
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
const { focused: inputFocused } = useFocus(inputRef)
|
||||
const rowHovered = useElementHover(rowRef)
|
||||
|
||||
/**
|
||||
* Toggle view mode options
|
||||
*/
|
||||
type ToggleViewOptions = {
|
||||
execution?: "now" | "nextTick"
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle view mode
|
||||
* @param options.execution - When to execute the toggle. "now" or "nextTick"
|
||||
*/
|
||||
function toggleView(options: ToggleViewOptions = { execution: "now" }) {
|
||||
currentMode.value = "view"
|
||||
|
||||
if (options.execution === "now") {
|
||||
inputFocused.value = false
|
||||
} else {
|
||||
nextTick(() => {
|
||||
inputFocused.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle edit mode
|
||||
*/
|
||||
function toggleEdit() {
|
||||
currentMode.value = "edit"
|
||||
categorySkeleton.value = structuredClone(toRaw(category))
|
||||
|
||||
nextTick(() => {
|
||||
inputFocused.value = true
|
||||
})
|
||||
}
|
||||
|
||||
onClickOutside(rowRef, () => toggleView({ execution: "nextTick" }))
|
||||
onKeyStroke("Escape", () => toggleView({ execution: "now" }))
|
||||
|
||||
const { resetSkeleton, updateCategoryFromSkeleton } = useCategoryStore()
|
||||
const { categorySkeleton } = storeToRefs(useCategoryStore())
|
||||
|
||||
onUnmounted(() => {
|
||||
resetSkeleton()
|
||||
})
|
||||
|
||||
/**
|
||||
* Submit the update
|
||||
*/
|
||||
async function submitUpdate() {
|
||||
const { error } = await tryCatch(updateCategoryFromSkeleton())
|
||||
|
||||
if (error) {
|
||||
toast({
|
||||
title: t("entity.category.updatedToast.titleError", { category: category.name }),
|
||||
variant: "destructive",
|
||||
duration: ToastLifetime.LONG
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
toggleView({ execution: "now" })
|
||||
toast({
|
||||
title: t("entity.category.updatedToast.title", { category: category.name }),
|
||||
variant: "success",
|
||||
duration: ToastLifetime.SHORT
|
||||
})
|
||||
}
|
||||
|
||||
function handleQueryDelete() {
|
||||
categorySkeleton.value = structuredClone(toRaw(category))
|
||||
emit("on-delete-category", category)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="rowRef" class="relative">
|
||||
<form
|
||||
class="grid grid-cols-12 items-center gap-4 p-1 border-b-[1px] border-b-foreground/10"
|
||||
:class="cn(
|
||||
{ 'bg-slate-100 hover:bg-slate-200 dark:bg-slate-900 dark:hover:bg-slate-800': currentMode === 'edit' },
|
||||
{ 'bg-transparent hover:bg-slate-50 dark:bg-transparent dark:hover:bg-slate-900': currentMode !== 'edit' }
|
||||
)"
|
||||
@submit.prevent="submitUpdate"
|
||||
>
|
||||
<div class="col-span-1 pointer-events-none">
|
||||
<span class="opacity-50 p-2">{{ category.id }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="col-span-5"
|
||||
>
|
||||
<template v-if="currentMode === 'view'">
|
||||
<button
|
||||
class="py-2 px-1 h-full w-full text-left underline-offset-4 hover:underline cursor-pointer"
|
||||
@click="toggleEdit"
|
||||
>
|
||||
{{ category.name }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="currentMode === 'edit' && categorySkeleton">
|
||||
<input
|
||||
ref="inputRef"
|
||||
v-model="categorySkeleton.name"
|
||||
type="text"
|
||||
class="p-1 h-full w-full bg-transparent focus-visible:outline-hidden italic"
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
<div class="col-span-4">
|
||||
<template v-if="currentMode === 'view'">
|
||||
<button
|
||||
class="p-1 h-full w-full text-left text-sm cursor-pointer"
|
||||
@click="toggleEdit"
|
||||
>
|
||||
<span
|
||||
class="bgc"
|
||||
:class="cn(`element-${category.color}`)"
|
||||
>
|
||||
{{ $t(`ui.colors.${category.color}`) }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="currentMode === 'edit' && categorySkeleton">
|
||||
<div class="-mx-2">
|
||||
<InputColor
|
||||
id="category-color"
|
||||
v-model="categorySkeleton.color"
|
||||
position="item-aligned"
|
||||
theme="subtle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</form>
|
||||
<menu class="w-fit absolute top-1/2 -translate-y-1/2 right-2 flex items-center gap-2">
|
||||
<li
|
||||
v-if="currentMode === 'edit' && categorySkeleton"
|
||||
>
|
||||
<UiButton
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
class="w-6 h-6 rounded-full bg-emerald-500 hover:bg-emerald-600 text-white"
|
||||
:title="$t('ui.actions.edit')"
|
||||
@click="submitUpdate"
|
||||
>
|
||||
<PhCheck size="14" weight="bold" />
|
||||
</UiButton>
|
||||
</li>
|
||||
<li v-else-if="rowHovered">
|
||||
<UiButton
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
class="w-6 h-6 rounded-full hover:bg-red-600 hover:text-white"
|
||||
:title="$t('ui.actions.delete')"
|
||||
@click="handleQueryDelete"
|
||||
>
|
||||
<PhTrash size="14" weight="bold" />
|
||||
</UiButton>
|
||||
</li>
|
||||
</menu>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user