Implements useProgressionStore with conditional localStorage persistence (only after RGPD consent), immersive ConsentBanner with narrator style, WelcomeBack component for returning visitors, and connects progress bar in header to store. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
150 lines
3.6 KiB
TypeScript
150 lines
3.6 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
|
|
export type HeroType = 'recruteur' | 'client' | 'dev'
|
|
|
|
export interface ProgressionState {
|
|
sessionId: string
|
|
hero: HeroType | null
|
|
currentPath: string
|
|
visitedSections: string[]
|
|
completionPercent: number
|
|
easterEggsFound: string[]
|
|
challengeCompleted: boolean
|
|
contactUnlocked: boolean
|
|
narratorStage: number
|
|
choices: Record<string, string>
|
|
consentGiven: boolean | null
|
|
}
|
|
|
|
const SECTIONS = ['projets', 'competences', 'temoignages', 'parcours'] as const
|
|
|
|
/**
|
|
* Storage conditionnel : ne persiste que si consentGiven === true.
|
|
* La lecture est toujours autorisée (pour restaurer une session existante).
|
|
*/
|
|
const conditionalStorage: Storage = {
|
|
get length() {
|
|
return import.meta.client ? localStorage.length : 0
|
|
},
|
|
key(index: number) {
|
|
return import.meta.client ? localStorage.key(index) : null
|
|
},
|
|
getItem(key: string) {
|
|
if (import.meta.client) {
|
|
return localStorage.getItem(key)
|
|
}
|
|
return null
|
|
},
|
|
setItem(key: string, value: string) {
|
|
if (import.meta.client) {
|
|
try {
|
|
const parsed = JSON.parse(value)
|
|
if (parsed.consentGiven === true) {
|
|
localStorage.setItem(key, value)
|
|
}
|
|
} catch {
|
|
// Si parsing échoue, ne pas persister
|
|
}
|
|
}
|
|
},
|
|
removeItem(key: string) {
|
|
if (import.meta.client) {
|
|
localStorage.removeItem(key)
|
|
}
|
|
},
|
|
clear() {
|
|
if (import.meta.client) {
|
|
localStorage.clear()
|
|
}
|
|
},
|
|
}
|
|
|
|
export const useProgressionStore = defineStore('progression', {
|
|
state: (): ProgressionState => ({
|
|
sessionId: '',
|
|
hero: null,
|
|
currentPath: 'start',
|
|
visitedSections: [],
|
|
completionPercent: 0,
|
|
easterEggsFound: [],
|
|
challengeCompleted: false,
|
|
contactUnlocked: false,
|
|
narratorStage: 1,
|
|
choices: {},
|
|
consentGiven: null,
|
|
}),
|
|
|
|
getters: {
|
|
hasVisited: (state) => (section: string) => state.visitedSections.includes(section),
|
|
|
|
isContactUnlocked: (state) => state.visitedSections.length >= 2 || state.contactUnlocked,
|
|
|
|
progressPercent: (state) => Math.round((state.visitedSections.length / SECTIONS.length) * 100),
|
|
|
|
hasExistingProgress: (state) => state.visitedSections.length > 0 || state.hero !== null,
|
|
},
|
|
|
|
actions: {
|
|
initSession() {
|
|
if (!this.sessionId && import.meta.client) {
|
|
this.sessionId = crypto.randomUUID()
|
|
}
|
|
},
|
|
|
|
setHero(hero: HeroType) {
|
|
this.hero = hero
|
|
this.initSession()
|
|
},
|
|
|
|
visitSection(section: string) {
|
|
if (!this.visitedSections.includes(section)) {
|
|
this.visitedSections.push(section)
|
|
this.completionPercent = this.progressPercent
|
|
|
|
if (this.visitedSections.length >= 2) {
|
|
this.contactUnlocked = true
|
|
}
|
|
}
|
|
},
|
|
|
|
findEasterEgg(slug: string) {
|
|
if (!this.easterEggsFound.includes(slug)) {
|
|
this.easterEggsFound.push(slug)
|
|
}
|
|
},
|
|
|
|
completeChallenge() {
|
|
this.challengeCompleted = true
|
|
},
|
|
|
|
unlockContact() {
|
|
this.contactUnlocked = true
|
|
},
|
|
|
|
updateNarratorStage(stage: number) {
|
|
if (stage >= 1 && stage <= 5) {
|
|
this.narratorStage = stage
|
|
}
|
|
},
|
|
|
|
makeChoice(choiceId: string, value: string) {
|
|
this.choices[choiceId] = value
|
|
},
|
|
|
|
setConsent(given: boolean) {
|
|
this.consentGiven = given
|
|
if (given) {
|
|
this.initSession()
|
|
} else if (import.meta.client) {
|
|
// Si refus, supprimer les données stockées
|
|
localStorage.removeItem('skycel-progression')
|
|
}
|
|
},
|
|
},
|
|
|
|
persist: {
|
|
key: 'skycel-progression',
|
|
storage: conditionalStorage,
|
|
},
|
|
})
|