🎲 Add Pinia progression store & GDPR consent banner (Story 1.6)
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>
This commit is contained in:
149
frontend/app/stores/progression.ts
Normal file
149
frontend/app/stores/progression.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
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,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user