feat: finalisation de la Sidebar.vue
This commit is contained in:
parent
d9e935f7de
commit
53365df456
16
.prettierrc
Normal file
16
.prettierrc
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"printWidth": 120,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"htmlWhitespaceSensitivity": "strict",
|
||||||
|
"singleAttributePerLine": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"jsxBracketSameLine": true,
|
||||||
|
"vueIndentScriptAndStyle": true,
|
||||||
|
"bracketSameLine": true
|
||||||
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
import Layout from '../shared/components/layout/Layout.vue';
|
import Layout from '../shared/components/layout/Layout.vue';
|
||||||
import HomePage from '../domain/manga/presentation/pages/HomePage.vue';
|
import HomePage from '../domain/manga/presentation/pages/HomePage.vue';
|
||||||
import MangaDetails from "../domain/manga/presentation/pages/MangaDetails.vue";
|
import MangaDetails from '../domain/manga/presentation/pages/MangaDetails.vue';
|
||||||
|
|
||||||
// Placeholder component for new routes
|
// Placeholder component for new routes
|
||||||
const PlaceholderComponent = {
|
const PlaceholderComponent = {
|
||||||
props: {
|
props: {
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<h1 class="text-2xl font-bold mb-4">{{ title }}</h1>
|
<h1 class="text-2xl font-bold mb-4">{{ title }}</h1>
|
||||||
<p class="text-gray-600">Cette fonctionnalité sera bientôt disponible.</p>
|
<p class="text-gray-600">Cette fonctionnalité sera bientôt disponible.</p>
|
||||||
@@ -20,118 +20,130 @@ const PlaceholderComponent = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
name: 'home',
|
name: 'home',
|
||||||
component: HomePage
|
component: HomePage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/manga/:id',
|
path: '/manga/:id',
|
||||||
name: 'manga-details',
|
name: 'manga-details',
|
||||||
component: MangaDetails
|
component: MangaDetails
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/add',
|
path: '/add',
|
||||||
name: 'add-manga',
|
name: 'add-manga',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Ajouter un manga' }
|
props: { title: 'Ajouter un manga' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/reader/:chapterId',
|
path: '/reader/:chapterId',
|
||||||
name: 'reader',
|
name: 'reader',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Lecteur' }
|
props: { title: 'Lecteur' }
|
||||||
},
|
},
|
||||||
// Pages placeholder avec chargement différé
|
// Pages placeholder avec chargement différé
|
||||||
{
|
{
|
||||||
path: '/import',
|
path: '/import',
|
||||||
name: 'import',
|
name: 'import',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Import de bibliothèque' }
|
props: { title: 'Import de bibliothèque' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/discover',
|
path: '/discover',
|
||||||
name: 'discover',
|
name: 'discover',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Découvrir' }
|
props: { title: 'Découvrir' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/convert',
|
path: '/convert',
|
||||||
name: 'convert',
|
name: 'convert',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Convertir CBR en CBZ' }
|
props: { title: 'Convertir CBR en CBZ' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/calendar',
|
path: '/calendar',
|
||||||
name: 'calendar',
|
name: 'calendar',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Calendrier' }
|
props: { title: 'Calendrier' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/activity',
|
path: '/activity',
|
||||||
name: 'activity',
|
name: 'activity',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Activité' }
|
props: { title: 'Activité' }
|
||||||
},
|
},
|
||||||
// Paramètres
|
// Paramètres
|
||||||
{
|
{
|
||||||
path: '/settings/general',
|
path: '/settings',
|
||||||
name: 'settings-general',
|
name: 'settings',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Paramètres généraux' }
|
props: { title: 'Paramètres' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings/folders',
|
path: '/settings/general',
|
||||||
name: 'settings-folders',
|
name: 'settings-general',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Gestion des dossiers' }
|
props: { title: 'Paramètres généraux' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings/scrappers',
|
path: '/settings/folders',
|
||||||
name: 'settings-scrappers',
|
name: 'settings-folders',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Configuration des scrappers' }
|
props: { title: 'Gestion des dossiers' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings/ui',
|
path: '/settings/scrappers',
|
||||||
name: 'settings-ui',
|
name: 'settings-scrappers',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: "Paramètres de l'interface" }
|
props: { title: 'Configuration des scrappers' }
|
||||||
},
|
},
|
||||||
// Système
|
{
|
||||||
{
|
path: '/settings/ui',
|
||||||
path: '/system/status',
|
name: 'settings-ui',
|
||||||
name: 'system-status',
|
component: PlaceholderComponent,
|
||||||
component: PlaceholderComponent,
|
props: { title: "Paramètres de l'interface" }
|
||||||
props: { title: 'Status du système' }
|
},
|
||||||
},
|
// Système
|
||||||
{
|
{
|
||||||
path: '/system/backup',
|
path: '/system',
|
||||||
name: 'system-backup',
|
name: 'system',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Sauvegarde' }
|
props: { title: 'Système' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/system/logs',
|
path: '/system/status',
|
||||||
name: 'system-logs',
|
name: 'system-status',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Journaux système' }
|
props: { title: 'Status du système' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/system/updates',
|
path: '/system/backup',
|
||||||
name: 'system-updates',
|
name: 'system-backup',
|
||||||
component: PlaceholderComponent,
|
component: PlaceholderComponent,
|
||||||
props: { title: 'Mises à jour' }
|
props: { title: 'Sauvegarde' }
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
}
|
path: '/system/logs',
|
||||||
|
name: 'system-logs',
|
||||||
|
component: PlaceholderComponent,
|
||||||
|
props: { title: 'Journaux système' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/system/updates',
|
||||||
|
name: 'system-updates',
|
||||||
|
component: PlaceholderComponent,
|
||||||
|
props: { title: 'Mises à jour' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: createWebHistory('/vue/'),
|
history: createWebHistory('/vue/'),
|
||||||
routes
|
routes
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,36 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-50">
|
<div class="min-h-screen bg-gray-50 flex">
|
||||||
<Header
|
<Header
|
||||||
@menu-click="toggleSidebar"
|
@menu-click="toggleSidebar"
|
||||||
@manga-click="$emit('manga-click', $event)"
|
@manga-click="$emit('manga-click', $event)"
|
||||||
@add-manga-click="$emit('add-manga-click', $event)"
|
@add-manga-click="$emit('add-manga-click', $event)" />
|
||||||
/>
|
<Sidebar :is-open="isSidebarOpen" @close="closeSidebar" @add-manga-click="$emit('add-manga-click', $event)" />
|
||||||
<Sidebar
|
|
||||||
:is-open="isSidebarOpen"
|
|
||||||
@close="closeSidebar"
|
|
||||||
@add-manga-click="$emit('add-manga-click', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<main class="pt-16 md:ml-60">
|
<main class="flex-1 pt-16 md:ml-60">
|
||||||
<router-view></router-view>
|
<RouterView></RouterView>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import Header from './Header.vue';
|
import Header from './Header.vue';
|
||||||
import Sidebar from './Sidebar.vue';
|
import Sidebar from './Sidebar.vue';
|
||||||
|
|
||||||
const isSidebarOpen = ref(false);
|
const isSidebarOpen = ref(false);
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
isSidebarOpen.value = !isSidebarOpen.value;
|
isSidebarOpen.value = !isSidebarOpen.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeSidebar = () => {
|
const closeSidebar = () => {
|
||||||
isSidebarOpen.value = false;
|
isSidebarOpen.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
defineEmits(['manga-click', 'add-manga-click']);
|
defineEmits(['manga-click', 'add-manga-click']);
|
||||||
</script>
|
</script>
|
||||||
@@ -1,155 +1,131 @@
|
|||||||
<template>
|
<template>
|
||||||
<aside
|
<aside
|
||||||
:class="[
|
:class="[
|
||||||
'fixed top-16 bottom-0 left-0 w-60 bg-white transform transition-transform duration-300 ease-in-out z-40',
|
'fixed top-16 left-0 w-60 bg-gray-600 text-white transform transition-transform duration-300 ease-in-out z-40 h-full',
|
||||||
isOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'
|
isOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'
|
||||||
]"
|
]"
|
||||||
>
|
role="navigation"
|
||||||
<nav class="h-full overflow-y-auto py-4">
|
aria-label="Menu principal">
|
||||||
<div v-for="(item, index) in menuItems" :key="index" class="mb-2">
|
<nav class="h-full overflow-y-auto">
|
||||||
<!-- Menu item with submenu -->
|
<ul class="h-full flex flex-col">
|
||||||
<template v-if="item.id">
|
<li v-for="(item, index) in menuItems" :key="index" class="mb-2">
|
||||||
<button
|
<template v-if="item.id">
|
||||||
@click="toggleMenu(item.id)"
|
<MenuGroup
|
||||||
class="w-full px-4 py-2 flex items-center justify-between hover:bg-gray-100"
|
:id="item.id"
|
||||||
>
|
:icon="item.icon"
|
||||||
<div class="flex items-center">
|
:text="item.text"
|
||||||
<component :is="item.icon" class="w-5 h-5 mr-3" />
|
:sub-items="item.subItems"
|
||||||
<span>{{ item.text }}</span>
|
:is-active="isActive(item)"
|
||||||
</div>
|
:to="item.to" />
|
||||||
<component
|
</template>
|
||||||
:is="expandedMenus[item.id] ? ChevronUpIcon : ChevronDownIcon"
|
<MenuItem
|
||||||
class="w-4 h-4"
|
v-else
|
||||||
/>
|
:icon="item.icon"
|
||||||
</button>
|
:text="item.text"
|
||||||
|
:to="item.to"
|
||||||
<div v-show="expandedMenus[item.id]">
|
:is-active="isActiveRoute(item.to)"
|
||||||
<template v-for="(subItem, subIndex) in item.subItems" :key="subIndex">
|
:badge="item.badge" />
|
||||||
<router-link
|
</li>
|
||||||
v-if="subItem.to"
|
</ul>
|
||||||
:to="subItem.to"
|
</nav>
|
||||||
class="block px-4 py-2 pl-12 hover:bg-gray-100"
|
</aside>
|
||||||
>
|
|
||||||
{{ subItem.text }}
|
|
||||||
</router-link>
|
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
@click="subItem.onClick"
|
|
||||||
class="w-full text-left px-4 py-2 pl-12 hover:bg-gray-100"
|
|
||||||
>
|
|
||||||
{{ subItem.text }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Simple menu item -->
|
|
||||||
<router-link
|
|
||||||
v-else
|
|
||||||
:to="item.to"
|
|
||||||
class="block px-4 py-2 hover:bg-gray-100"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<component :is="item.icon" class="w-5 h-5 mr-3" />
|
|
||||||
<span>{{ item.text }}</span>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
v-if="item.badge"
|
|
||||||
class="bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full"
|
|
||||||
>
|
|
||||||
{{ item.badge }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { useRoute } from 'vue-router';
|
||||||
import {
|
import {
|
||||||
BookOpenIcon,
|
BookOpenIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
ArrowDownTrayIcon,
|
ArrowDownTrayIcon,
|
||||||
GlobeAltIcon,
|
GlobeAltIcon,
|
||||||
ArrowsRightLeftIcon,
|
ArrowsRightLeftIcon,
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
Cog6ToothIcon,
|
Cog6ToothIcon,
|
||||||
ComputerDesktopIcon,
|
ComputerDesktopIcon
|
||||||
ChevronDownIcon,
|
} from '@heroicons/vue/24/outline';
|
||||||
ChevronUpIcon
|
import MenuItem from './sidebar/MenuItem.vue';
|
||||||
} from '@heroicons/vue/24/outline';
|
import MenuGroup from './sidebar/MenuGroup.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const route = useRoute();
|
||||||
isOpen: {
|
const props = defineProps({
|
||||||
type: Boolean,
|
isOpen: {
|
||||||
required: true
|
type: Boolean,
|
||||||
}
|
required: true
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'add-manga-click']);
|
const isActiveRoute = path => {
|
||||||
|
return route.path === path;
|
||||||
|
};
|
||||||
|
|
||||||
const expandedMenus = ref({
|
const isActive = item => {
|
||||||
mangas: true,
|
if (!item.to) {
|
||||||
settings: false,
|
return item.subItems?.some(subItem => route.path === subItem.to) || false;
|
||||||
system: false
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const toggleMenu = (menuId) => {
|
if (item.to === '/') {
|
||||||
expandedMenus.value[menuId] = !expandedMenus.value[menuId];
|
return route.path === '/' || ['/add', '/import', '/discover'].includes(route.path);
|
||||||
};
|
}
|
||||||
|
|
||||||
const menuItems = [
|
return route.path.startsWith(item.to);
|
||||||
{
|
};
|
||||||
icon: BookOpenIcon,
|
|
||||||
text: 'Mangas',
|
const menuItems = [
|
||||||
id: 'mangas',
|
{
|
||||||
subItems: [
|
icon: BookOpenIcon,
|
||||||
{ icon: PlusIcon, text: 'Ajouter un nouveau', onClick: () => emit('add-manga-click') },
|
text: 'Mangas',
|
||||||
{ icon: ArrowDownTrayIcon, text: 'Import bibliothèque', to: '/import' },
|
to: '/',
|
||||||
{ icon: GlobeAltIcon, text: 'Découvrir', to: '/discover' },
|
id: 'mangas',
|
||||||
]
|
subItems: [
|
||||||
},
|
{ icon: PlusIcon.render, text: 'Ajouter un nouveau', to: '/add' },
|
||||||
{
|
{
|
||||||
icon: ArrowsRightLeftIcon,
|
icon: ArrowDownTrayIcon,
|
||||||
text: 'Convertir CBR en CBZ',
|
text: 'Import bibliothèque',
|
||||||
to: '/convert'
|
to: '/import'
|
||||||
},
|
},
|
||||||
{
|
{ icon: GlobeAltIcon, text: 'Découvrir', to: '/discover' }
|
||||||
icon: CalendarIcon,
|
]
|
||||||
text: 'Calendrier',
|
},
|
||||||
to: '/calendar'
|
{
|
||||||
},
|
icon: ArrowsRightLeftIcon,
|
||||||
{
|
text: 'Convertir CBR en CBZ',
|
||||||
icon: ClockIcon,
|
to: '/convert'
|
||||||
text: 'Activité',
|
},
|
||||||
to: '/activity',
|
{
|
||||||
badge: '3'
|
icon: CalendarIcon,
|
||||||
},
|
text: 'Calendrier',
|
||||||
{
|
to: '/calendar'
|
||||||
icon: Cog6ToothIcon,
|
},
|
||||||
text: 'Paramètres',
|
{
|
||||||
id: 'settings',
|
icon: ClockIcon,
|
||||||
subItems: [
|
text: 'Activité',
|
||||||
{ text: 'Général', to: '/settings/general' },
|
to: '/activity',
|
||||||
{ text: 'Dossiers', to: '/settings/folders' },
|
badge: '3'
|
||||||
{ text: 'Scrappers', to: '/settings/scrappers' },
|
},
|
||||||
{ text: 'UI', to: '/settings/ui' }
|
{
|
||||||
]
|
icon: Cog6ToothIcon,
|
||||||
},
|
text: 'Paramètres',
|
||||||
{
|
to: '/settings',
|
||||||
icon: ComputerDesktopIcon,
|
id: 'settings',
|
||||||
text: 'Système',
|
subItems: [
|
||||||
id: 'system',
|
{ icon: null, text: 'Général', to: '/settings/general' },
|
||||||
subItems: [
|
{ icon: null, text: 'Dossiers', to: '/settings/folders' },
|
||||||
{ text: 'Status', to: '/system/status' },
|
{ icon: null, text: 'Scrappers', to: '/settings/scrappers' },
|
||||||
{ text: 'Backup', to: '/system/backup' },
|
{ icon: null, text: 'UI', to: '/settings/ui' }
|
||||||
{ text: 'Logs', to: '/system/logs' },
|
]
|
||||||
{ text: 'Updates', to: '/system/updates' }
|
},
|
||||||
]
|
{
|
||||||
},
|
icon: ComputerDesktopIcon,
|
||||||
];
|
text: 'Système',
|
||||||
|
to: '/system',
|
||||||
|
id: 'system',
|
||||||
|
subItems: [
|
||||||
|
{ icon: null, text: 'Status', to: '/system/status' },
|
||||||
|
{ icon: null, text: 'Backup', to: '/system/backup' },
|
||||||
|
{ icon: null, text: 'Logs', to: '/system/logs' },
|
||||||
|
{ icon: null, text: 'Updates', to: '/system/updates' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="border-l-4"
|
||||||
|
:class="{
|
||||||
|
'border-green-600': isActive,
|
||||||
|
'hover:bg-gray-700 border-transparent': !isActive
|
||||||
|
}">
|
||||||
|
<div class="flex w-full" @click="toggleExpanded">
|
||||||
|
<RouterLink
|
||||||
|
:to="to"
|
||||||
|
class="flex-grow px-4 py-2 flex items-center"
|
||||||
|
:class="{
|
||||||
|
'text-green-600 bg-gray-800': isActive
|
||||||
|
}">
|
||||||
|
<div class="flex items-center flex-grow">
|
||||||
|
<component :is="icon" class="w-5 h-5 mr-3" />
|
||||||
|
<span>{{ text }}</span>
|
||||||
|
</div>
|
||||||
|
<component :is="expanded ? ChevronUpIcon : ChevronDownIcon" class="w-4 h-4" />
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul v-if="subItems.length > 0" class="ml-8 mt-2 space-y-4" v-show="expanded">
|
||||||
|
<SubMenuItem
|
||||||
|
v-for="(subItem, index) in subItems"
|
||||||
|
:key="index"
|
||||||
|
:text="subItem.text"
|
||||||
|
:to="subItem.to"
|
||||||
|
@click="subItem.onClick" />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/24/outline';
|
||||||
|
import SubMenuItem from './SubMenuItem.vue';
|
||||||
|
import { useMenuStore } from '../../../stores/menuStore';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
subItems: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const menuStore = useMenuStore();
|
||||||
|
const expanded = ref(false);
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const isRouteMatching = path => {
|
||||||
|
return props.subItems.some(item => path.startsWith(item.to)) || path === props.to;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[() => route.path, () => menuStore.activeMenuId],
|
||||||
|
([newPath, newMenuId]) => {
|
||||||
|
if (isRouteMatching(newPath)) {
|
||||||
|
expanded.value = true;
|
||||||
|
menuStore.setActiveMenu(props.id);
|
||||||
|
} else if (newMenuId !== props.id) {
|
||||||
|
expanded.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleExpanded = () => {
|
||||||
|
if (expanded.value) {
|
||||||
|
menuStore.setActiveMenu(null);
|
||||||
|
} else {
|
||||||
|
menuStore.setActiveMenu(props.id);
|
||||||
|
}
|
||||||
|
expanded.value = !expanded.value;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
47
assets/vue/app/shared/components/layout/sidebar/MenuItem.vue
Normal file
47
assets/vue/app/shared/components/layout/sidebar/MenuItem.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<RouterLink :to="to" class="block px-4 py-2 hover:bg-gray-700 border-l-4 border-transparent" role="menuitem">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<component :is="icon" class="w-5 h-5 mr-3" aria-hidden="true" />
|
||||||
|
<span>{{ text }}</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="badge"
|
||||||
|
class="bg-green-500 text-white text-xs px-2 py-1 rounded-full"
|
||||||
|
aria-label="Nouveaux éléments: {{ badge }}">
|
||||||
|
{{ badge }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</RouterLink>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
icon: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="postcss" scoped>
|
||||||
|
.router-link-active {
|
||||||
|
@apply text-green-600 bg-gray-800 border-green-600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<li>
|
||||||
|
<RouterLink v-if="to" :to="to" class="block hover:text-green-600" role="menuitem">
|
||||||
|
{{ text }}
|
||||||
|
</RouterLink>
|
||||||
|
<button v-else @click="$emit('click')" class="w-full text-left hover:text-green-600" role="menuitem">
|
||||||
|
{{ text }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(['click']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="postcss" scoped>
|
||||||
|
.router-link-exact-active {
|
||||||
|
@apply text-green-600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
12
assets/vue/app/shared/stores/menuStore.js
Normal file
12
assets/vue/app/shared/stores/menuStore.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
export const useMenuStore = defineStore('menu', {
|
||||||
|
state: () => ({
|
||||||
|
activeMenuId: null
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setActiveMenu(menuId) {
|
||||||
|
this.activeMenuId = menuId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -27,17 +27,17 @@ class GetMangaBySlugHandlerTest extends TestCase
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$manga = new Manga(
|
$manga = new Manga(
|
||||||
new MangaId('1'),
|
id: new MangaId('1'),
|
||||||
new MangaTitle('One Piece'),
|
title: new MangaTitle('One Piece'),
|
||||||
new MangaSlug('one-piece'),
|
slug: new MangaSlug('one-piece'),
|
||||||
'Description',
|
description: 'Description',
|
||||||
'Eiichiro Oda',
|
author: 'Eiichiro Oda',
|
||||||
1997,
|
publicationYear: 1997,
|
||||||
['Action', 'Adventure'],
|
genres: ['Action', 'Adventure'],
|
||||||
'ongoing',
|
status: 'ongoing',
|
||||||
null,
|
externalId: null,
|
||||||
'https://example.com/image.jpg',
|
imageUrl: 'https://example.com/image.jpg',
|
||||||
4.5
|
rating: 4.5
|
||||||
);
|
);
|
||||||
$this->repository->save($manga);
|
$this->repository->save($manga);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user