refactor(frontend) : reorganize components into subdirectories and fix imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 23:59:24 +01:00
parent d28f385918
commit 1efa0fa9ca
30 changed files with 9 additions and 9 deletions

View File

@@ -0,0 +1,68 @@
<template>
<Teleport v-if="modelValue" to="body">
<Transition name="drawer" appear>
<div
class="fixed inset-0 z-40 flex justify-end"
>
<div
class="absolute inset-0 bg-black/30"
@click="close"
/>
<div
class="relative z-50 flex h-full w-full max-w-md flex-col bg-white shadow-xl"
>
<div class="flex items-center justify-between border-b border-neutral-200 px-6 py-4">
<h2 class="text-lg font-bold text-neutral-900">{{ title }}</h2>
<button
type="button"
class="rounded p-1 text-neutral-400 hover:text-neutral-600"
@click="close"
>
<Icon name="mdi:close" size="24" />
</button>
</div>
<div class="flex-1 overflow-y-auto px-6 py-4">
<slot />
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: boolean
title: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
function close() {
emit('update:modelValue', false)
}
</script>
<style scoped>
.drawer-enter-active,
.drawer-leave-active {
transition: opacity 0.2s ease;
}
.drawer-enter-active > div:last-child,
.drawer-leave-active > div:last-child {
transition: transform 0.3s ease;
}
.drawer-enter-from,
.drawer-leave-to {
opacity: 0;
}
.drawer-enter-from > div:last-child,
.drawer-leave-to > div:last-child {
transform: translateX(100%);
}
</style>

View File

@@ -0,0 +1,42 @@
<template>
<header class="border-b border-neutral-200 bg-primary-500 p-5 text-white">
<div class="flex h-full items-center justify-end">
<div class="flex gap-12 text-xl text-white">
<div class="group relative flex gap-4">
<Icon name="mdi:account-circle-outline" class="self-center cursor-pointer" size="36" />
<p class="self-center cursor-pointer">{{ user?.username }}</p>
<div class="invisible absolute right-0 top-full z-20 mt-2 w-44 rounded-md border border-neutral-200 bg-white py-1 text-sm text-neutral-800 opacity-0 shadow-lg transition-all group-hover:visible group-hover:opacity-100">
<button
type="button"
class="block w-full px-3 py-2 text-left hover:bg-neutral-100"
>
Mon profil
</button>
<button
type="button"
class="block w-full px-3 py-2 text-left hover:bg-neutral-100"
@click="handleLogout"
>
Déconnexion
</button>
</div>
</div>
</div>
</div>
</header>
</template>
<script setup lang="ts">
import type { UserData } from '~/services/dto/user-data'
defineProps<{
user?: UserData
}>()
const auth = useAuthStore()
const handleLogout = async () => {
await auth.logout()
await navigateTo('/login')
}
</script>

View File

@@ -0,0 +1,31 @@
<template>
<div>
<p class="mb-2 text-sm font-medium text-neutral-700">Couleur</p>
<div class="flex flex-wrap gap-3">
<button
v-for="color in colors"
:key="color"
type="button"
class="h-10 w-10 rounded-full border-2 transition-transform hover:scale-110"
:class="modelValue === color ? 'border-neutral-900 scale-110' : 'border-transparent'"
:style="{ backgroundColor: color }"
@click="emit('update:modelValue', color)"
/>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
const colors = [
'#222783', '#26A69A', '#E91E63', '#4A90D9',
'#7E57C2', '#8BC34A', '#FDD835', '#80DEEA', '#FF7043',
]
</script>

View File

@@ -0,0 +1,96 @@
<template>
<Teleport v-if="modelValue" to="body">
<Transition name="modal" appear>
<div class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/30" @click="cancel" />
<div class="relative z-10 w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
<h3 class="text-lg font-bold text-neutral-900">Supprimer le statut « {{ statusLabel }} »</h3>
<p class="mt-3 text-sm text-neutral-600">
{{ taskCount }} tâche{{ taskCount > 1 ? 's sont liées' : ' est liée' }} à ce statut.
Choisissez les déplacer :
</p>
<div class="mt-4">
<MalioSelect
v-model="targetStatusId"
:options="targetOptions"
label="Déplacer vers"
empty-option-label="Backlog (sans statut)"
min-width="w-full"
/>
</div>
<div class="mt-6 flex justify-end gap-3">
<button
type="button"
class="rounded-md border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 hover:bg-neutral-50"
@click="cancel"
>
Annuler
</button>
<button
type="button"
class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-700 disabled:opacity-50"
:disabled="isProcessing"
@click="confirm"
>
Supprimer
</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import type { TaskStatus } from '~/services/dto/task-status'
const props = defineProps<{
modelValue: boolean
statusLabel: string
taskCount: number
availableStatuses: TaskStatus[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'confirm', targetStatusId: number | null): void
}>()
const targetStatusId = ref<number | null>(null)
const isProcessing = ref(false)
const targetOptions = computed(() =>
props.availableStatuses.map(s => ({ label: s.label, value: s.id }))
)
watch(() => props.modelValue, (open) => {
if (open) {
targetStatusId.value = null
isProcessing.value = false
}
})
function cancel() {
emit('update:modelValue', false)
}
function confirm() {
isProcessing.value = true
emit('confirm', targetStatusId.value)
}
</script>
<style scoped>
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.2s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<div class="mt-6 overflow-x-auto rounded-lg border border-neutral-200">
<table class="w-full text-left text-sm">
<thead class="border-b border-neutral-200 bg-neutral-50">
<tr>
<th
v-for="col in columns"
:key="col.key"
class="px-4 py-3 font-semibold text-neutral-700"
>
{{ col.label }}
</th>
<th v-if="deletable" class="px-4 py-3 font-semibold text-neutral-700">
Actions
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in items"
:key="item.id"
class="cursor-pointer border-b border-neutral-100 hover:bg-neutral-50"
@click="$emit('row-click', item)"
>
<td
v-for="col in columns"
:key="col.key"
class="px-4 py-3"
:class="[col.class, { 'font-semibold text-primary-500': col.primary }]"
>
<slot :name="`cell-${col.key}`" :item="item" :value="item[col.key]">
{{ item[col.key] }}
</slot>
</td>
<td v-if="deletable" class="px-4 py-3">
<button
class="text-red-500 hover:text-red-700"
@click.stop="$emit('delete', item)"
>
<Icon name="mdi:delete-outline" size="20" />
</button>
</td>
</tr>
<tr v-if="items.length === 0 && !loading">
<td
:colspan="columns.length + (deletable ? 1 : 0)"
class="px-4 py-8 text-center text-neutral-400"
>
{{ emptyMessage }}
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
export interface DataTableColumn {
key: string
label: string
primary?: boolean
class?: string
}
defineProps<{
columns: DataTableColumn[]
items: Record<string, any>[]
loading?: boolean
emptyMessage?: string
deletable?: boolean
}>()
defineEmits<{
(e: 'row-click', item: any): void
(e: 'delete', item: any): void
}>()
</script>

View File

@@ -0,0 +1,52 @@
<template>
<NuxtLink
:to="to"
class="group/link relative flex items-center transition-colors hover:text-primary-500"
:class="linkClasses"
:active-class="exact ? '' : activeClass"
:exact-active-class="exact ? activeClass : ''"
>
<Icon :name="icon" :size="sub ? '20' : '24'" class="flex-shrink-0" />
<span
v-if="!collapsed"
class="self-baseline whitespace-nowrap overflow-hidden transition-opacity duration-300"
:class="sub ? 'text-sm' : 'text-md'"
>
{{ label }}
</span>
<div
v-if="collapsed"
class="pointer-events-none absolute left-full z-50 ml-2 rounded-md bg-neutral-800 px-2 py-1 text-xs text-white opacity-0 shadow-lg transition-opacity group-hover/link:pointer-events-auto group-hover/link:opacity-100 whitespace-nowrap"
>
{{ label }}
</div>
</NuxtLink>
</template>
<script setup lang="ts">
const props = defineProps<{
to: string
icon: string
label: string
collapsed: boolean
sub?: boolean
exact?: boolean
}>()
const activeClass = computed(() => {
if (props.collapsed) {
return '!text-primary-500 bg-primary-500/10'
}
return '!text-primary-500 bg-tertiary-500'
})
const linkClasses = computed(() => {
if (props.collapsed) {
return 'justify-center w-10 h-10 mx-auto my-1 p-2 rounded-lg text-neutral-600 hover:text-primary-500 hover:bg-primary-500/10'
}
if (props.sub) {
return 'gap-3 px-4 py-2 pl-12 text-sm font-semibold text-neutral-700'
}
return 'gap-3 px-4 py-3 text-md font-semibold text-neutral-700'
})
</script>

View File

@@ -0,0 +1,26 @@
<template>
<button
class="flex w-full items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-semibold text-white transition"
:class="timerStore.isRunning
? 'bg-red-500 hover:bg-red-600'
: 'bg-primary-500 hover:bg-primary-600'"
:title="timerStore.isRunning ? 'Arrêter le timer' : 'Démarrer un timer'"
@click="timerStore.isRunning ? timerStore.stop() : timerStore.start()"
>
<Icon
:name="timerStore.isRunning ? 'mdi:stop' : 'mdi:play'"
size="16"
/>
<span v-if="!collapsed" class="font-mono tracking-wide">
{{ timerStore.elapsedFormatted }}
</span>
</button>
</template>
<script setup lang="ts">
defineProps<{
collapsed: boolean
}>()
const timerStore = useTimerStore()
</script>