🎉 Init monorepo Nuxt 4 + Laravel 12 (Story 1.1)
Setup complet de l'infrastructure projet : - Frontend Nuxt 4 (SSR, TypeScript, i18n, Pinia, TailwindCSS) - Backend Laravel 12 API-only avec middleware X-API-Key et CORS - Design tokens (sky-dark, sky-accent, sky-text) et polices (Merriweather, Inter) - Documentation planning et implementation artifacts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
# Story 4.4: Table easter_eggs et système de détection
|
||||
|
||||
Status: ready-for-dev
|
||||
|
||||
## Story
|
||||
|
||||
As a développeur,
|
||||
I want une infrastructure pour gérer les easter eggs cachés,
|
||||
so that je peux ajouter des surprises récompensant l'exploration.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. **Given** les migrations Laravel sont exécutées **When** `php artisan migrate` est lancé **Then** la table `easter_eggs` est créée (id, slug, location, trigger_type ENUM, reward_type ENUM, reward_key, difficulty, is_active, timestamps)
|
||||
2. **And** les trigger_types incluent : click, hover, konami, scroll, sequence
|
||||
3. **And** les reward_types incluent : snippet, anecdote, image, badge
|
||||
4. **And** les seeders insèrent 5-10 easter eggs avec leurs récompenses traduites
|
||||
5. **Given** l'API `/api/easter-eggs` est appelée **When** la requête est faite **Then** les métadonnées des easter eggs actifs sont retournées (slug, location, trigger_type)
|
||||
6. **And** les réponses/récompenses ne sont PAS incluses (pour éviter la triche)
|
||||
7. **Given** l'API `/api/easter-eggs/{slug}/validate` est appelée **When** un slug valide est fourni **Then** la récompense traduite est retournée
|
||||
8. **And** l'easter egg est marqué comme trouvé côté client (store)
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] **Task 1: Créer la migration table easter_eggs** (AC: #1, #2, #3)
|
||||
- [ ] Créer migration `create_easter_eggs_table`
|
||||
- [ ] Colonnes : id, slug (unique), location, trigger_type (ENUM), reward_type (ENUM), reward_key, difficulty (1-5), is_active (boolean), timestamps
|
||||
- [ ] ENUMs pour trigger_type et reward_type
|
||||
|
||||
- [ ] **Task 2: Créer le Model EasterEgg** (AC: #1)
|
||||
- [ ] Créer `app/Models/EasterEgg.php`
|
||||
- [ ] Définir les fillable et casts
|
||||
- [ ] Scope `active()` pour les easter eggs actifs
|
||||
- [ ] Relation avec translations pour reward_key
|
||||
|
||||
- [ ] **Task 3: Créer le Seeder des easter eggs** (AC: #4)
|
||||
- [ ] Créer `database/seeders/EasterEggSeeder.php`
|
||||
- [ ] 5-10 easter eggs avec variété de triggers et récompenses
|
||||
- [ ] Ajouter les traductions FR et EN pour les récompenses
|
||||
|
||||
- [ ] **Task 4: Créer l'endpoint liste des easter eggs** (AC: #5, #6)
|
||||
- [ ] Créer `app/Http/Controllers/Api/EasterEggController.php`
|
||||
- [ ] Méthode `index()` retournant slug, location, trigger_type
|
||||
- [ ] NE PAS inclure reward_key ou détails de la récompense
|
||||
|
||||
- [ ] **Task 5: Créer l'endpoint validation** (AC: #7)
|
||||
- [ ] Méthode `validate($slug)` retournant la récompense
|
||||
- [ ] Traduire selon Accept-Language
|
||||
- [ ] Retourner 404 si slug invalide
|
||||
|
||||
- [ ] **Task 6: Créer le store côté client** (AC: #8)
|
||||
- [ ] Ajouter `easterEggsFound: string[]` dans useProgressionStore
|
||||
- [ ] Méthode `markEasterEggFound(slug)`
|
||||
- [ ] Getter `easterEggsCount` (trouvés/total)
|
||||
|
||||
- [ ] **Task 7: Créer le composable useFetchEasterEggs**
|
||||
- [ ] Créer `frontend/app/composables/useFetchEasterEggs.ts`
|
||||
- [ ] Méthode `fetchList()` pour récupérer les métadonnées
|
||||
- [ ] Méthode `validate(slug)` pour valider un easter egg trouvé
|
||||
|
||||
- [ ] **Task 8: Tests et validation**
|
||||
- [ ] Exécuter les migrations
|
||||
- [ ] Vérifier le seeding
|
||||
- [ ] Tester l'API liste (sans récompenses)
|
||||
- [ ] Tester l'API validation (avec récompenses)
|
||||
|
||||
## Dev Notes
|
||||
|
||||
### Migration easter_eggs
|
||||
|
||||
```php
|
||||
<?php
|
||||
// database/migrations/2026_02_04_000003_create_easter_eggs_table.php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('easter_eggs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('slug')->unique();
|
||||
$table->string('location'); // Page ou zone où se trouve l'easter egg
|
||||
$table->enum('trigger_type', ['click', 'hover', 'konami', 'scroll', 'sequence']);
|
||||
$table->enum('reward_type', ['snippet', 'anecdote', 'image', 'badge']);
|
||||
$table->string('reward_key'); // Clé de traduction pour la récompense
|
||||
$table->unsignedTinyInteger('difficulty')->default(1); // 1-5
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('is_active');
|
||||
$table->index('location');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('easter_eggs');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Model EasterEgg
|
||||
|
||||
```php
|
||||
<?php
|
||||
// api/app/Models/EasterEgg.php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class EasterEgg extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'slug',
|
||||
'location',
|
||||
'trigger_type',
|
||||
'reward_type',
|
||||
'reward_key',
|
||||
'difficulty',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'difficulty' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeByLocation($query, string $location)
|
||||
{
|
||||
return $query->where('location', $location);
|
||||
}
|
||||
|
||||
public function getReward(string $lang = 'fr'): ?string
|
||||
{
|
||||
return Translation::getTranslation($this->reward_key, $lang);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Seeder des easter eggs
|
||||
|
||||
```php
|
||||
<?php
|
||||
// database/seeders/EasterEggSeeder.php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\EasterEgg;
|
||||
use App\Models\Translation;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class EasterEggSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$easterEggs = [
|
||||
// 1. Konami code sur la landing
|
||||
[
|
||||
'slug' => 'konami-master',
|
||||
'location' => 'landing',
|
||||
'trigger_type' => 'konami',
|
||||
'reward_type' => 'badge',
|
||||
'reward_key' => 'easter.konami.reward',
|
||||
'difficulty' => 3,
|
||||
],
|
||||
// 2. Clic sur l'araignée cachée (header)
|
||||
[
|
||||
'slug' => 'hidden-spider',
|
||||
'location' => 'header',
|
||||
'trigger_type' => 'click',
|
||||
'reward_type' => 'anecdote',
|
||||
'reward_key' => 'easter.spider.reward',
|
||||
'difficulty' => 2,
|
||||
],
|
||||
// 3. Hover sur un caractère spécial dans le code (page projets)
|
||||
[
|
||||
'slug' => 'secret-comment',
|
||||
'location' => 'projects',
|
||||
'trigger_type' => 'hover',
|
||||
'reward_type' => 'snippet',
|
||||
'reward_key' => 'easter.comment.reward',
|
||||
'difficulty' => 2,
|
||||
],
|
||||
// 4. Scroll jusqu'en bas de la page parcours
|
||||
[
|
||||
'slug' => 'journey-end',
|
||||
'location' => 'journey',
|
||||
'trigger_type' => 'scroll',
|
||||
'reward_type' => 'anecdote',
|
||||
'reward_key' => 'easter.journey_end.reward',
|
||||
'difficulty' => 1,
|
||||
],
|
||||
// 5. Séquence de clics sur les compétences (Vue, Laravel, TypeScript)
|
||||
[
|
||||
'slug' => 'tech-sequence',
|
||||
'location' => 'skills',
|
||||
'trigger_type' => 'sequence',
|
||||
'reward_type' => 'snippet',
|
||||
'reward_key' => 'easter.tech_seq.reward',
|
||||
'difficulty' => 4,
|
||||
],
|
||||
// 6. Clic sur le logo Skycel 5 fois
|
||||
[
|
||||
'slug' => 'logo-clicks',
|
||||
'location' => 'global',
|
||||
'trigger_type' => 'click',
|
||||
'reward_type' => 'image',
|
||||
'reward_key' => 'easter.logo.reward',
|
||||
'difficulty' => 2,
|
||||
],
|
||||
// 7. Hover sur la date "2022" dans le parcours
|
||||
[
|
||||
'slug' => 'founding-date',
|
||||
'location' => 'journey',
|
||||
'trigger_type' => 'hover',
|
||||
'reward_type' => 'anecdote',
|
||||
'reward_key' => 'easter.founding.reward',
|
||||
'difficulty' => 2,
|
||||
],
|
||||
// 8. Console.log dans les devtools
|
||||
[
|
||||
'slug' => 'dev-console',
|
||||
'location' => 'global',
|
||||
'trigger_type' => 'sequence',
|
||||
'reward_type' => 'badge',
|
||||
'reward_key' => 'easter.console.reward',
|
||||
'difficulty' => 3,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($easterEggs as $egg) {
|
||||
EasterEgg::create($egg);
|
||||
}
|
||||
|
||||
// Traductions des récompenses
|
||||
$translations = [
|
||||
// Konami
|
||||
['key' => 'easter.konami.reward', 'fr' => "🎮 Badge 'Gamer' débloqué ! Tu connais les classiques.", 'en' => "🎮 'Gamer' badge unlocked! You know the classics."],
|
||||
|
||||
// Spider
|
||||
['key' => 'easter.spider.reward', 'fr' => "🕷️ Tu m'as trouvé ! Je me cache partout sur ce site... Le Bug te surveille toujours.", 'en' => "🕷️ You found me! I hide everywhere on this site... The Bug is always watching."],
|
||||
|
||||
// Comment
|
||||
['key' => 'easter.comment.reward', 'fr' => "// Premier code écrit : console.log('Hello World'); // Tout a commencé là...", 'en' => "// First code written: console.log('Hello World'); // It all started there..."],
|
||||
|
||||
// Journey end
|
||||
['key' => 'easter.journey_end.reward', 'fr' => "Tu as lu jusqu'au bout ? Respect. Le prochain chapitre s'écrit peut-être avec toi.", 'en' => "You read all the way? Respect. The next chapter might be written with you."],
|
||||
|
||||
// Tech sequence
|
||||
['key' => 'easter.tech_seq.reward', 'fr' => "const stack = ['Vue', 'Laravel', 'TypeScript'];\n// La sainte trinité du dev moderne ⚡", 'en' => "const stack = ['Vue', 'Laravel', 'TypeScript'];\n// The holy trinity of modern dev ⚡"],
|
||||
|
||||
// Logo
|
||||
['key' => 'easter.logo.reward', 'fr' => "🖼️ Image secrète débloquée : La première version du logo Skycel (spoiler: c'était moche)", 'en' => "🖼️ Secret image unlocked: The first version of the Skycel logo (spoiler: it was ugly)"],
|
||||
|
||||
// Founding
|
||||
['key' => 'easter.founding.reward', 'fr' => "2022 : l'année où Le Bug est né. Littéralement un bug dans le code qui m'a donné l'idée de la mascotte.", 'en' => "2022: the year The Bug was born. Literally a bug in the code that gave me the mascot idea."],
|
||||
|
||||
// Console
|
||||
['key' => 'easter.console.reward', 'fr' => "🔧 Badge 'Développeur' débloqué ! Tu as vérifié la console comme un vrai dev.", 'en' => "🔧 'Developer' badge unlocked! You checked the console like a real dev."],
|
||||
];
|
||||
|
||||
foreach ($translations as $t) {
|
||||
Translation::firstOrCreate(
|
||||
['lang' => 'fr', 'key_name' => $t['key']],
|
||||
['value' => $t['fr']]
|
||||
);
|
||||
Translation::firstOrCreate(
|
||||
['lang' => 'en', 'key_name' => $t['key']],
|
||||
['value' => $t['en']]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Controller API
|
||||
|
||||
```php
|
||||
<?php
|
||||
// api/app/Http/Controllers/Api/EasterEggController.php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\EasterEgg;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EasterEggController extends Controller
|
||||
{
|
||||
/**
|
||||
* Liste les easter eggs actifs (sans révéler les récompenses)
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$easterEggs = EasterEgg::active()
|
||||
->select('slug', 'location', 'trigger_type', 'difficulty')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'data' => $easterEggs,
|
||||
'meta' => [
|
||||
'total' => $easterEggs->count(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide un easter egg et retourne la récompense
|
||||
*/
|
||||
public function validate(Request $request, string $slug)
|
||||
{
|
||||
$easterEgg = EasterEgg::active()->where('slug', $slug)->first();
|
||||
|
||||
if (!$easterEgg) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'code' => 'EASTER_EGG_NOT_FOUND',
|
||||
'message' => 'Easter egg not found or inactive',
|
||||
],
|
||||
], 404);
|
||||
}
|
||||
|
||||
$lang = $request->header('Accept-Language', 'fr');
|
||||
$reward = $easterEgg->getReward($lang);
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'slug' => $easterEgg->slug,
|
||||
'reward_type' => $easterEgg->reward_type,
|
||||
'reward' => $reward,
|
||||
'difficulty' => $easterEgg->difficulty,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Routes API
|
||||
|
||||
```php
|
||||
// api/routes/api.php
|
||||
Route::get('/easter-eggs', [EasterEggController::class, 'index']);
|
||||
Route::post('/easter-eggs/{slug}/validate', [EasterEggController::class, 'validate']);
|
||||
```
|
||||
|
||||
### Extension du store progression
|
||||
|
||||
```typescript
|
||||
// À ajouter dans frontend/app/stores/progression.ts
|
||||
|
||||
// État
|
||||
const easterEggsFound = ref<string[]>([])
|
||||
|
||||
// Actions
|
||||
function markEasterEggFound(slug: string) {
|
||||
if (!easterEggsFound.value.includes(slug)) {
|
||||
easterEggsFound.value.push(slug)
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
const easterEggsFoundCount = computed(() => easterEggsFound.value.length)
|
||||
|
||||
// Export
|
||||
return {
|
||||
// ... existing ...
|
||||
easterEggsFound,
|
||||
easterEggsFoundCount,
|
||||
markEasterEggFound,
|
||||
}
|
||||
```
|
||||
|
||||
### Composable useFetchEasterEggs
|
||||
|
||||
```typescript
|
||||
// frontend/app/composables/useFetchEasterEggs.ts
|
||||
interface EasterEggMeta {
|
||||
slug: string
|
||||
location: string
|
||||
trigger_type: 'click' | 'hover' | 'konami' | 'scroll' | 'sequence'
|
||||
difficulty: number
|
||||
}
|
||||
|
||||
interface EasterEggReward {
|
||||
slug: string
|
||||
reward_type: 'snippet' | 'anecdote' | 'image' | 'badge'
|
||||
reward: string
|
||||
difficulty: number
|
||||
}
|
||||
|
||||
export function useFetchEasterEggs() {
|
||||
const config = useRuntimeConfig()
|
||||
const { locale } = useI18n()
|
||||
|
||||
// Cache des easter eggs disponibles
|
||||
const availableEasterEggs = ref<EasterEggMeta[]>([])
|
||||
const isLoaded = ref(false)
|
||||
|
||||
async function fetchList(): Promise<EasterEggMeta[]> {
|
||||
if (isLoaded.value) return availableEasterEggs.value
|
||||
|
||||
const response = await $fetch<{ data: EasterEggMeta[] }>('/easter-eggs', {
|
||||
baseURL: config.public.apiUrl,
|
||||
headers: {
|
||||
'X-API-Key': config.public.apiKey,
|
||||
},
|
||||
})
|
||||
|
||||
availableEasterEggs.value = response.data
|
||||
isLoaded.value = true
|
||||
return response.data
|
||||
}
|
||||
|
||||
async function validate(slug: string): Promise<EasterEggReward | null> {
|
||||
try {
|
||||
const response = await $fetch<{ data: EasterEggReward }>(`/easter-eggs/${slug}/validate`, {
|
||||
method: 'POST',
|
||||
baseURL: config.public.apiUrl,
|
||||
headers: {
|
||||
'X-API-Key': config.public.apiKey,
|
||||
'Accept-Language': locale.value,
|
||||
},
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to validate easter egg:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getByLocation(location: string): EasterEggMeta[] {
|
||||
return availableEasterEggs.value.filter(e => e.location === location || e.location === 'global')
|
||||
}
|
||||
|
||||
return {
|
||||
availableEasterEggs,
|
||||
fetchList,
|
||||
validate,
|
||||
getByLocation,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tableau des easter eggs
|
||||
|
||||
| Slug | Location | Trigger | Type | Difficulté |
|
||||
|------|----------|---------|------|------------|
|
||||
| konami-master | landing | konami | badge | 3/5 |
|
||||
| hidden-spider | header | click | anecdote | 2/5 |
|
||||
| secret-comment | projects | hover | snippet | 2/5 |
|
||||
| journey-end | journey | scroll | anecdote | 1/5 |
|
||||
| tech-sequence | skills | sequence | snippet | 4/5 |
|
||||
| logo-clicks | global | click | image | 2/5 |
|
||||
| founding-date | journey | hover | anecdote | 2/5 |
|
||||
| dev-console | global | sequence | badge | 3/5 |
|
||||
|
||||
### Dépendances
|
||||
|
||||
**Cette story nécessite :**
|
||||
- Story 1.2 : Table translations
|
||||
- Story 3.5 : Store de progression
|
||||
|
||||
**Cette story prépare pour :**
|
||||
- Story 4.5 : Implémentation UI des easter eggs
|
||||
|
||||
### Project Structure Notes
|
||||
|
||||
**Fichiers à créer :**
|
||||
```
|
||||
api/
|
||||
├── app/Models/
|
||||
│ └── EasterEgg.php # CRÉER
|
||||
├── app/Http/Controllers/Api/
|
||||
│ └── EasterEggController.php # CRÉER
|
||||
└── database/
|
||||
├── migrations/
|
||||
│ └── 2026_02_04_000003_create_easter_eggs_table.php # CRÉER
|
||||
└── seeders/
|
||||
└── EasterEggSeeder.php # CRÉER
|
||||
|
||||
frontend/app/composables/
|
||||
└── useFetchEasterEggs.ts # CRÉER
|
||||
```
|
||||
|
||||
**Fichiers à modifier :**
|
||||
```
|
||||
api/routes/api.php # AJOUTER routes easter-eggs
|
||||
api/database/seeders/DatabaseSeeder.php # APPELER EasterEggSeeder
|
||||
frontend/app/stores/progression.ts # AJOUTER easterEggsFound
|
||||
```
|
||||
|
||||
### References
|
||||
|
||||
- [Source: docs/planning-artifacts/epics.md#Story-4.4]
|
||||
- [Source: docs/planning-artifacts/ux-design-specification.md#Easter-Eggs]
|
||||
- [Source: docs/brainstorming-gamification-2026-01-26.md#Easter-Eggs]
|
||||
|
||||
### Technical Requirements
|
||||
|
||||
| Requirement | Value | Source |
|
||||
|-------------|-------|--------|
|
||||
| Nombre d'easter eggs | 5-10 | Epics |
|
||||
| Trigger types | click, hover, konami, scroll, sequence | Epics |
|
||||
| Reward types | snippet, anecdote, image, badge | Epics |
|
||||
| API sans spoil | Liste sans récompenses | Epics |
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
|
||||
{{agent_model_name_version}}
|
||||
|
||||
### Debug Log References
|
||||
|
||||
### Completion Notes List
|
||||
|
||||
### Change Log
|
||||
| Date | Change | Author |
|
||||
|------|--------|--------|
|
||||
| 2026-02-04 | Story créée avec contexte complet | SM Agent |
|
||||
|
||||
### File List
|
||||
|
||||
Reference in New Issue
Block a user