Files
ClickerZ/Frontend/src/routes/jeu/+page.svelte
Tetardtek f6bff6e389
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 22s
feat: migrate frontend React 18 → Svelte 5 + SvelteKit
Core logic portable (economy, balance, cosmetics, migrateSave) — zero rewrite.
136 tests green, identiques. Backend inchangé.

- Svelte 5 runes stores (game, auth, toast) remplacent Zustand
- SvelteKit adapter-static SPA (dist/ output, fallback index.html)
- Tailwind v4 conservé, design system .gp-* porté
- Transitions natives : slide, fly, scale, fade sur toute l'UI
- Sidebar tabbée (Production/Evolution/Collection) + CollapsiblePanel
- Mobile bottom sheet avec FAB toggle + backdrop blur
- Click particles réactifs Svelte (plus de DOM impératif)
- TadpoleSprite bounce + glow ring au clic
- Guide refait en accordéon, Achievements avec filtres
- a11y : focus-visible, Escape modals, aria-current, aria-labels
- CI/CD adapté (tests + build + rsync)
- Build 504K (vs ~1.2MB React)
2026-03-28 20:03:21 +01:00

152 lines
5.5 KiB
Svelte

<script lang="ts">
import { fly, scale, fade } from 'svelte/transition';
import { quintOut, elasticOut } from 'svelte/easing';
import { gameStore } from '$lib/stores/game.svelte';
import { formatNumber } from '$lib/utils/formatNumber';
import CockpitHeader from '$lib/components/CockpitHeader.svelte';
import GeneratorShop from '$lib/components/GeneratorShop.svelte';
import PrestigePanel from '$lib/components/PrestigePanel.svelte';
import EvolutionTree from '$lib/components/EvolutionTree.svelte';
import MilestoneBar from '$lib/components/MilestoneBar.svelte';
import MilestonesPanel from '$lib/components/MilestonesPanel.svelte';
import CosmeticsPanel from '$lib/components/CosmeticsPanel.svelte';
import TadpoleSprite from '$lib/components/TadpoleSprite.svelte';
import PrestigeScreen from '$lib/components/PrestigeScreen.svelte';
import ClickParticles from '$lib/components/ClickParticles.svelte';
import SidebarTabs from '$lib/components/SidebarTabs.svelte';
import { ACHIEVEMENTS } from '$lib/data/achievements';
let achieveCount = $derived(ACHIEVEMENTS.filter((a) => a.check(gameStore.state)).length);
const sidebarTabs = [
{ id: 'production', label: 'Production', icon: '🏭' },
{ id: 'evolution', label: 'Evolution', icon: '🧬' },
{ id: 'collection', label: 'Collection', icon: '✨' },
];
// Component refs for imperative calls
let clickParticles: ReturnType<typeof ClickParticles>;
let tadpoleSprite: ReturnType<typeof TadpoleSprite>;
function handleClick(e: MouseEvent) {
gameStore.click();
clickParticles?.spawn(e.clientX, e.clientY, gameStore.lastClickGain, gameStore.lastClickDouble, gameStore.lastClickCrit);
tadpoleSprite?.bounce();
}
// Mobile sidebar toggle
let sidebarOpen = $state(false);
</script>
<svelte:head>
<title>Clickerz — Tetard Universe</title>
<meta name="description" content="Clickerz — Clicker idle dans le Tetard Universe." />
</svelte:head>
{#if !gameStore.ready}
<div class="flex items-center justify-center min-h-[80vh]" in:fade>
<div class="flex flex-col items-center gap-4">
<div class="w-16 h-16 border-4 border-emerald-500/30 border-t-emerald-500 rounded-full animate-spin"></div>
<p class="text-slate-400" style="font-family: var(--font);">Chargement de ta progression...</p>
</div>
</div>
{:else}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="zone" data-zone="swamp" in:fade={{ duration: 400 }}>
<PrestigeScreen />
<ClickParticles bind:this={clickParticles} />
<!-- Click zone -->
<div class="click-zone" onclick={handleClick}>
<div in:scale={{ duration: 500, start: 0.8, easing: elasticOut }}>
<TadpoleSprite bind:this={tadpoleSprite} />
</div>
<div
class="click-zone-counter"
in:fly={{ y: 20, duration: 400, easing: quintOut }}
>
{formatNumber(gameStore.state.resources)}
</div>
</div>
<!-- Desktop sidebar -->
<aside class="game-sidebar hidden md:flex" in:fly={{ x: 100, duration: 500, easing: quintOut }}>
<!-- Pinned: always visible -->
<CockpitHeader />
<MilestoneBar />
<!-- Tabbed content -->
<SidebarTabs tabs={sidebarTabs}>
{#snippet children(activeTab)}
{#if activeTab === 'production'}
<GeneratorShop />
<PrestigePanel />
{:else if activeTab === 'evolution'}
<EvolutionTree />
<MilestonesPanel />
{:else if activeTab === 'collection'}
<CosmeticsPanel />
<a href="/achievements" class="achieve-badge">
{achieveCount}/{ACHIEVEMENTS.length} succes
</a>
<a href="/guide" class="achieve-badge" style="border-color: rgba(139, 92, 246, 0.2); background: rgba(139, 92, 246, 0.08); color: #a78bfa;">
Guide du Gardien
</a>
{/if}
{/snippet}
</SidebarTabs>
</aside>
<!-- Mobile bottom bar: toggle button -->
<button
class="md:hidden fixed bottom-4 right-4 z-30 w-14 h-14 rounded-full flex items-center justify-center shadow-xl"
style="background: rgba(17,17,17,0.9); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.1);"
onclick={() => sidebarOpen = !sidebarOpen}
>
<span class="text-2xl">{sidebarOpen ? '✕' : '🎮'}</span>
</button>
<!-- Mobile bottom sheet -->
{#if sidebarOpen}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="md:hidden fixed inset-0 z-20"
style="background: rgba(0,0,0,0.5);"
transition:fade={{ duration: 200 }}
onclick={() => sidebarOpen = false}
></div>
<div
class="md:hidden fixed bottom-0 left-0 right-0 z-25 flex flex-col gap-3 max-h-[75vh] overflow-y-auto rounded-t-2xl p-4 pb-20"
style="background: rgba(10,10,10,0.95); backdrop-filter: blur(12px); border-top: 1px solid rgba(255,255,255,0.08);"
transition:fly={{ y: 300, duration: 350, easing: quintOut }}
>
<!-- Drag handle -->
<div class="flex justify-center">
<div class="w-10 h-1 rounded-full" style="background: rgba(255,255,255,0.2);"></div>
</div>
<CockpitHeader />
<MilestoneBar />
<SidebarTabs tabs={sidebarTabs}>
{#snippet children(activeTab)}
{#if activeTab === 'production'}
<GeneratorShop />
<PrestigePanel />
{:else if activeTab === 'evolution'}
<EvolutionTree />
<MilestonesPanel />
{:else if activeTab === 'collection'}
<CosmeticsPanel />
<a href="/achievements" class="achieve-badge">
{achieveCount}/{ACHIEVEMENTS.length} succes
</a>
{/if}
{/snippet}
</SidebarTabs>
</div>
{/if}
</div>
{/if}