Files
Lesstime/frontend/pages/help.vue

169 lines
9.2 KiB
Vue

<script setup lang="ts">
import { marked } from 'marked'
useHead({ title: 'Aide' })
type Section = {
id: string
title: string
icon: string
accent: string
roles: ('admin' | 'user' | 'client')[]
content: string
}
const rawModules = import.meta.glob('~/content/help/*.md', { eager: true, query: '?raw', import: 'default' }) as Record<string, string>
const META: Record<string, { title: string, icon: string, accent: string, roles: ('admin' | 'user' | 'client')[] }> = {
'01-getting-started': { title: 'Bienvenue', icon: 'mdi:hand-wave', accent: 'from-amber-400 to-rose-500', roles: ['admin', 'user', 'client'] },
'02-projects-workflows': { title: 'Projets & Workflows', icon: 'mdi:view-column-outline', accent: 'from-indigo-500 to-fuchsia-500', roles: ['admin', 'user'] },
'03-my-tasks': { title: 'Mes tâches', icon: 'mdi:checkbox-marked-circle-outline', accent: 'from-sky-500 to-cyan-500', roles: ['admin', 'user'] },
'04-time-tracking': { title: 'Time tracking', icon: 'mdi:timer-outline', accent: 'from-emerald-500 to-teal-500', roles: ['admin', 'user'] },
'05-tasks-detail': { title: 'Tâches en détail', icon: 'mdi:file-document-edit-outline', accent: 'from-violet-500 to-purple-600', roles: ['admin', 'user'] },
'06-client-portal': { title: 'Portal client', icon: 'mdi:account-tie-outline', accent: 'from-orange-500 to-amber-500', roles: ['admin', 'client'] },
'07-admin': { title: 'Administration', icon: 'mdi:shield-crown-outline', accent: 'from-rose-500 to-pink-600', roles: ['admin'] },
'08-integrations': { title: 'Intégrations', icon: 'mdi:puzzle-outline', accent: 'from-blue-500 to-indigo-500', roles: ['admin', 'user'] },
'09-mcp-api': { title: 'Token MCP & API', icon: 'mdi:robot-outline', accent: 'from-slate-700 to-slate-900', roles: ['admin', 'user'] },
}
const sections = computed<Section[]>(() => {
return Object.entries(rawModules).map(([path, raw]) => {
const id = path.split('/').pop()!.replace(/\.md$/, '')
const meta = META[id] ?? { title: id, icon: 'mdi:file-document-outline', accent: 'from-neutral-500 to-neutral-700', roles: ['admin', 'user', 'client'] as ('admin' | 'user' | 'client')[] }
return { id, ...meta, content: raw }
}).sort((a, b) => a.id.localeCompare(b.id))
})
const auth = useAuthStore()
const userRole = computed<'admin' | 'user' | 'client'>(() => {
const roles = auth.user?.roles ?? []
if (roles.includes('ROLE_ADMIN')) return 'admin'
if (roles.includes('ROLE_CLIENT')) return 'client'
return 'user'
})
const visibleSections = computed(() =>
sections.value.filter(s => s.roles.includes(userRole.value)),
)
const route = useRoute()
const router = useRouter()
const activeId = ref(visibleSections.value[0]?.id ?? '')
onMounted(() => {
const hash = (route.query.section as string) ?? route.hash.replace('#', '')
if (hash && visibleSections.value.some(s => s.id === hash)) {
activeId.value = hash
}
})
watch(activeId, (id) => {
router.replace({ query: { ...route.query, section: id } })
})
const activeSection = computed(() => visibleSections.value.find(s => s.id === activeId.value) ?? visibleSections.value[0])
const renderedHtml = computed(() => {
if (!activeSection.value) return ''
return marked.parse(activeSection.value.content, { async: false }) as string
})
const prevSection = computed(() => {
const idx = visibleSections.value.findIndex(s => s.id === activeId.value)
return idx > 0 ? visibleSections.value[idx - 1] : null
})
const nextSection = computed(() => {
const idx = visibleSections.value.findIndex(s => s.id === activeId.value)
return idx >= 0 && idx < visibleSections.value.length - 1 ? visibleSections.value[idx + 1] : null
})
</script>
<template>
<div class="flex min-h-[calc(100vh-60px)] flex-col lg:flex-row">
<!-- Sidebar -->
<aside class="shrink-0 border-b border-neutral-200 bg-gradient-to-b from-white to-neutral-50 px-3 py-4 lg:w-72 lg:border-b-0 lg:border-r lg:px-4 lg:py-6">
<div class="mb-4 flex items-center gap-2 lg:mb-6">
<div class="flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-primary-500 to-primary-700 text-white shadow-sm">
<Icon name="mdi:lifebuoy" size="20" />
</div>
<div>
<h1 class="text-base font-bold text-neutral-900">Centre d'aide</h1>
<p class="text-xs text-neutral-500">Lesstime — Guide utilisateur</p>
</div>
</div>
<nav class="flex flex-row gap-1 overflow-x-auto pb-1 lg:flex-col lg:overflow-visible lg:pb-0">
<button
v-for="section in visibleSections"
:key="section.id"
type="button"
class="group flex shrink-0 items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm font-medium transition-all lg:shrink"
:class="activeId === section.id
? 'bg-white text-neutral-900 shadow-sm ring-1 ring-neutral-200'
: 'text-neutral-600 hover:bg-white hover:text-neutral-900'"
@click="activeId = section.id"
>
<span
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br text-white shadow-sm"
:class="section.accent"
>
<Icon :name="section.icon" size="16" />
</span>
<span class="whitespace-nowrap lg:whitespace-normal">{{ section.title }}</span>
</button>
</nav>
</aside>
<!-- Content -->
<main class="flex-1 px-4 py-6 sm:px-8 lg:px-12 lg:py-10">
<div v-if="activeSection" class="mx-auto max-w-3xl">
<!-- Hero header -->
<div
class="mb-8 overflow-hidden rounded-2xl bg-gradient-to-br p-6 text-white shadow-lg sm:p-8"
:class="activeSection.accent"
>
<div class="flex items-center gap-4">
<div class="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-white/20 backdrop-blur-sm">
<Icon :name="activeSection.icon" size="28" />
</div>
<div>
<p class="text-xs font-semibold uppercase tracking-wider text-white/80">Section</p>
<h2 class="text-2xl font-bold tracking-tight sm:text-3xl">{{ activeSection.title }}</h2>
</div>
</div>
</div>
<!-- Markdown content -->
<article
class="prose prose-neutral max-w-none prose-headings:font-bold prose-headings:tracking-tight prose-h1:hidden prose-h2:mt-10 prose-h2:border-b prose-h2:border-neutral-200 prose-h2:pb-2 prose-h3:text-neutral-800 prose-a:text-primary-600 prose-strong:text-neutral-900 prose-code:rounded prose-code:bg-neutral-100 prose-code:px-1.5 prose-code:py-0.5 prose-code:text-sm prose-code:font-medium prose-code:text-rose-600 prose-code:before:content-none prose-code:after:content-none prose-pre:rounded-xl prose-pre:bg-slate-900 prose-table:border prose-table:border-neutral-200 prose-th:bg-neutral-50 prose-th:px-3 prose-th:py-2 prose-td:px-3 prose-td:py-2 prose-blockquote:rounded-r-lg prose-blockquote:border-l-4 prose-blockquote:border-amber-400 prose-blockquote:bg-amber-50 prose-blockquote:px-4 prose-blockquote:py-2 prose-blockquote:not-italic prose-blockquote:text-amber-900"
v-html="renderedHtml"
/>
<!-- Footer nav -->
<div class="mt-12 flex items-center justify-between border-t border-neutral-200 pt-6">
<button
type="button"
class="inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-neutral-900 disabled:invisible"
:disabled="!prevSection"
@click="prevSection && (activeId = prevSection.id)"
>
<Icon name="mdi:arrow-left" size="18" />
<span>{{ prevSection?.title ?? '' }}</span>
</button>
<button
type="button"
class="inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-neutral-600 transition-colors hover:bg-neutral-100 hover:text-neutral-900 disabled:invisible"
:disabled="!nextSection"
@click="nextSection && (activeId = nextSection.id)"
>
<span>{{ nextSection?.title ?? '' }}</span>
<Icon name="mdi:arrow-right" size="18" />
</button>
</div>
</div>
</main>
</div>
</template>