designtopack/src/components/Menu.vue
2025-10-08 14:58:29 +02:00

407 lines
9.4 KiB
Vue

<template>
<button
@click="toggleExpand()"
class="btn btn--white | rounded-xl"
aria-controls="menu"
:aria-expanded="isExpanded"
>
<svg
aria-labelledby="menu-toggle"
class="icon"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title id="menu-toggle">
{{ isExpanded ? 'Masquer le menu' : 'Afficher le menu' }}
</title>
<path
d="M10.6751 2.625L3.00007 10.3125C2.94028 10.3686 2.89263 10.4364 2.86005 10.5116C2.82748 10.5869 2.81067 10.668 2.81067 10.75C2.81067 10.832 2.82748 10.9131 2.86005 10.9884C2.89263 11.0636 2.94028 11.1314 3.00007 11.1875L10.6751 18.875M17.1876 2.625L9.50007 10.3125C9.38555 10.4293 9.32141 10.5864 9.32141 10.75C9.32141 10.9136 9.38555 11.0707 9.50007 11.1875L17.1876 18.875"
stroke="currentColor"
stroke-width="1.25"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<div v-if="isExpanded" id="menu" class="flex | rounded-xl">
<header class="w-full | flex">
<!-- TODO: à dynamiser en récupérant le $site->title() -->
<p lang="en">Design to Pack</p>
</header>
<nav class="w-full | flow">
<ul class="flex">
<li
v-for="mainItem in mainItems"
:data-icon="mainItem.icon"
:key="mainItem.path"
:class="{ active: isCurrent(mainItem), disabled: mainItem.disabled }"
:data-count="
mainItem.title === 'Notifications'
? unreadNotificationsCount
: undefined
"
>
<router-link
:to="mainItem.path"
:aria-current="isCurrent(mainItem)"
@click="collapse()"
>{{ mainItem.title }}</router-link
>
<span
v-if="mainItem.title === 'Inspirations' && page?.newInspirations"
class="pill pill--secondary"
>{{ 'Nouveautés' }}</span
>
</li>
</ul>
<details :class="{ skeleton: !currentProjects }" open>
<summary>Projets en cours</summary>
<ul v-if="currentProjects.length > 0">
<li
v-for="project in currentProjects"
:class="{ active: isCurrent(project) }"
>
<router-link
:to="
isEmptyBrief(project)
? project.uri + '/client-brief'
: project.uri
"
:class="hasUnreadNotification(project) ? 'new' : undefined"
:data-dtl="project.isDTLEnabled ? 'true' : undefined"
@click="collapse()"
>{{ project.title }}</router-link
>
</li>
</ul>
</details>
<details v-if="archivedProjects.length">
<summary>Projets archivés</summary>
<ul>
<li
v-for="project in archivedProjects"
:class="{ active: isCurrent(project) }"
>
<router-link :to="project.uri" @click="collapse()">{{
project.title
}}</router-link>
</li>
</ul>
</details>
</nav>
<footer class="w-full">
<ul class="flex">
<li data-icon="user">
<a
:href="user.role === 'admin' ? '/panel/account' : '/account'"
@click="collapse()"
>Profil</a
>
</li>
<li data-icon="logout">
<a href="/logout" @click="collapse()">Déconnexion</a>
</li>
</ul>
</footer>
</div>
</template>
<script setup>
import { storeToRefs } from 'pinia';
import { computed, ref } from 'vue';
import { useProjectsStore } from '../stores/projects';
import { useRoute } from 'vue-router';
import { useUserStore } from '../stores/user';
import { usePageStore } from '../stores/page';
import { useProjectStore } from '../stores/project';
const route = useRoute();
const isExpanded = ref(true);
const { user, notifications } = storeToRefs(useUserStore());
const { currentProjects, archivedProjects } = storeToRefs(useProjectsStore());
const { isEmptyBrief } = useProjectStore();
const { page } = storeToRefs(usePageStore());
const unreadNotificationsCount = computed(() => {
if (!user.value) return 0;
const count =
notifications.value?.filter((notification) => !notification.isRead)
.length ?? 0;
return count === 0 ? 0 : count;
});
const mainItems = [
{
title: 'Home',
path: '/',
icon: 'home',
},
{
title: 'Notifications',
path: '/notifications',
icon: 'megaphone',
},
{
title: 'Réunions',
path: '/reunions',
icon: 'calendar',
disabled: true,
},
{
title: 'Design to Light',
path: '/design-to-light',
icon: 'leaf',
},
{
title: 'Inspirations',
path: '/inspirations',
icon: 'inspiration',
},
];
function toggleExpand() {
isExpanded.value = !isExpanded.value;
}
function isCurrent(navItem) {
if (navItem.path) {
return navItem.path === route.path;
}
if (navItem.uri) {
return navItem.uri === route.path;
}
}
function hasUnreadNotification(project) {
if (!user.value) return false;
return notifications.value.some((notification) => {
return (
notification.isread != 'true' &&
project.uri.includes(notification.location.project.uri)
);
});
}
function collapse() {
if (window.innerWidth < 1024) {
isExpanded.value = false;
}
}
</script>
<style>
button[aria-controls='menu'] {
position: fixed;
z-index: 101;
width: 2.5rem;
height: 2.5rem;
}
button[aria-controls='menu'] svg {
width: 100%;
height: 100%;
}
button[aria-controls='menu'][aria-expanded='true'] {
margin-top: 1rem;
padding: 0.625rem; /* 10px */
left: var(--gutter);
transform: translateX(calc(var(--sidebar-width) - 100% - 1rem));
}
button[aria-controls='menu'][aria-expanded='false'] {
min-width: 3.5rem;
min-height: 3.5rem;
padding: 1.125rem;
transform: rotate(180deg);
}
button[aria-controls='menu'][aria-expanded='false']
+ main
> header:not([role='tablist']) {
margin-left: 4rem;
width: calc(100% - 4rem);
}
/* Menu */
#menu {
--flow-space: var(--space-32);
flex-direction: column;
align-items: flex-start;
flex-wrap: nowrap;
position: -webkit-sticky;
position: sticky;
top: var(--gutter);
width: 100%;
max-width: var(--sidebar-width);
height: calc(100vh - var(--gutter) * 2);
height: calc(100dvh - var(--gutter) * 2);
padding: var(--gap);
background: var(--color-background);
overflow-y: auto;
}
@media (max-width: 1023px) {
button[aria-controls='menu'][aria-expanded='true'] {
left: 0;
margin-top: 0.4rem;
}
#menu {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100;
border-radius: 0;
height: 100vh;
padding-top: 2.5rem;
}
button[aria-controls='menu'][aria-expanded='true'] + #menu::before {
content: '';
position: fixed;
top: 0;
right: 0;
left: var(--sidebar-width);
bottom: 0;
z-index: -1;
background: var(--color-black-50);
}
}
#menu [data-icon]::before {
background-color: var(--color-grey-700);
}
#menu header {
align-items: center;
padding-left: var(--gap);
min-height: var(--space-40);
font-family: var(--font-serif);
font-size: var(--text-md);
background: var(--color-background);
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 1;
}
#menu header::before {
content: '';
display: block;
position: absolute;
top: -1rem;
left: 0;
right: 0;
bottom: -2rem;
z-index: -1;
background: linear-gradient(
to bottom,
var(--color-background) 0%,
var(--color-background) 75%,
transparent 100%
);
}
#menu nav {
--flow-space: var(--space-32);
flex: 1 1 0%;
}
#menu ul {
--direction: column;
--items: flex-start;
--row-gap: var(--space-4);
width: 100%;
}
#menu li,
#menu summary {
display: flex;
align-items: center;
width: 100%;
min-height: 2.5rem; /* 40px */
padding: 0.66rem var(--space-16);
gap: var(--space-12);
border-radius: var(--rounded-lg);
}
#menu li {
position: relative;
min-height: 2.75rem; /* 44px */
}
#menu li.disabled {
opacity: 0.5;
}
#menu li.disabled a::before {
cursor: not-allowed;
}
#menu li[data-count]::after {
content: attr(data-count);
display: inline-block;
color: var(--color-primary);
font-size: var(--text-sm);
font-weight: 500;
margin-left: auto;
}
#menu li .pill {
margin-left: auto;
}
#menu a {
display: flex;
align-items: center;
}
#menu li a::before {
content: '';
position: absolute;
inset: 0;
z-index: 1;
cursor: pointer;
}
#menu li:hover {
background-color: var(--color-grey-50);
}
#menu .active {
background-color: var(--color-primary-10);
}
#menu details {
font-family: var(--font-serif);
background-size: 1px 21px;
background-image: repeating-linear-gradient(
0deg,
var(--color-grey-50),
var(--color-grey-50) 1px,
transparent 1px,
transparent
);
background-repeat: repeat-x;
}
#menu details > summary {
font-family: var(--font-sans);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-grey-700);
background-color: var(--color-background);
width: fit-content;
}
#menu details .new::after {
content: '' / 'Nouvelles modifications';
color: transparent;
position: relative;
display: inline-block;
width: 0.375rem; /* 6px */
height: 0.375rem; /* 6px */
border-radius: var(--rounded-full);
background-color: var(--color-primary-100);
margin-left: var(--space-8);
transform: translateY(-0.1em);
overflow: hidden;
}
</style>