170 lines
9.2 KiB
Vue
170 lines
9.2 KiB
Vue
<script setup lang="ts">
|
|
import { marked } from 'marked'
|
|
|
|
definePageMeta({ middleware: ['auth'] })
|
|
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>
|