Files
Portfolio-Game/frontend/app/stores/progression.ts
skycel 9fd66def12 🎲 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>
2026-02-05 21:00:49 +01:00

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,
},
})