feat: add frontend as flat files (was submodule)
This commit is contained in:
30
frontend/src/App.jsx
Normal file
30
frontend/src/App.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useState } from 'react'
|
||||
import { IS_DEV } from './env'
|
||||
import DevBanner from './components/DevBanner'
|
||||
import Hero from './components/Hero'
|
||||
import GelShowcase from './components/GelShowcase'
|
||||
import KerboulistanBanner from './components/KerboulistanBanner'
|
||||
import GameCanvas from './components/GameCanvas'
|
||||
import Footer from './components/Footer'
|
||||
|
||||
function App() {
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
|
||||
if (isPlaying) {
|
||||
return <GameCanvas onBack={() => setIsPlaying(false)} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<DevBanner />
|
||||
{/* Offset content when dev banner is visible */}
|
||||
{IS_DEV && <div className="h-8" />}
|
||||
<Hero onPlay={() => setIsPlaying(true)} />
|
||||
<GelShowcase />
|
||||
<KerboulistanBanner />
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
26
frontend/src/components/DevBanner.jsx
Normal file
26
frontend/src/components/DevBanner.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { IS_DEV, theme } from '../env'
|
||||
|
||||
export default function DevBanner() {
|
||||
if (!IS_DEV) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed top-0 left-0 right-0 z-[100] flex items-center justify-center gap-2 py-1.5 text-xs font-mono tracking-wider text-black/80 select-none"
|
||||
style={{
|
||||
background: `repeating-linear-gradient(
|
||||
-45deg,
|
||||
${theme.accent},
|
||||
${theme.accent} 10px,
|
||||
${theme.gradientTo} 10px,
|
||||
${theme.gradientTo} 20px
|
||||
)`,
|
||||
}}
|
||||
>
|
||||
<span>{theme.badge}</span>
|
||||
<span className="font-bold uppercase">Environnement de développement</span>
|
||||
<span>—</span>
|
||||
<span className="opacity-70">Ne pas utiliser en conditions réelles</span>
|
||||
<span>{theme.badge}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
frontend/src/components/Footer.jsx
Normal file
27
frontend/src/components/Footer.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="py-8 px-4 border-t border-rolld-border/50">
|
||||
<div className="max-w-6xl mx-auto flex flex-col md:flex-row items-center justify-between gap-4 text-sm text-rolld-muted">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-rolld-text">ROLL'D</span>
|
||||
<span>·</span>
|
||||
<span>Marble MMO</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs">
|
||||
<span>Unity · React · Colyseus</span>
|
||||
<span>·</span>
|
||||
<span className="text-rolld-accent/60">v0.1.2-dev</span>
|
||||
<span>·</span>
|
||||
<a
|
||||
href="https://git.kerboul.me/kerboul/rolld_frontend"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-rolld-accent-light transition-colors"
|
||||
>
|
||||
Source
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
189
frontend/src/components/GameCanvas.jsx
Normal file
189
frontend/src/components/GameCanvas.jsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
|
||||
// Check if Unity build files exist
|
||||
const UNITY_BUILD_PATH = '/unity-build/Build'
|
||||
// Cache-busting version — update this after each Unity build
|
||||
const UNITY_BUILD_VERSION = '20260310c'
|
||||
const LOADER_URL = `${UNITY_BUILD_PATH}/nouveau_build.loader.js?v=${UNITY_BUILD_VERSION}`
|
||||
|
||||
// Game server URL (Colyseus WebSocket)
|
||||
const GAME_SERVER_URL = import.meta.env.VITE_GAME_SERVER_URL || 'ws://localhost:2567'
|
||||
|
||||
export default function GameCanvas({ onBack }) {
|
||||
const [loadingProgress, setLoadingProgress] = useState(0)
|
||||
const [isLoaded, setIsLoaded] = useState(false)
|
||||
const [hasUnityBuild, setHasUnityBuild] = useState(null) // null = checking, true/false = result
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Check if Unity build exists
|
||||
fetch(LOADER_URL, { method: 'HEAD' })
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
setHasUnityBuild(true)
|
||||
loadUnity()
|
||||
} else {
|
||||
setHasUnityBuild(false)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setHasUnityBuild(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const loadUnity = useCallback(() => {
|
||||
// Dynamically load Unity loader
|
||||
const script = document.createElement('script')
|
||||
script.src = LOADER_URL
|
||||
script.onload = () => {
|
||||
if (typeof window.createUnityInstance === 'function') {
|
||||
const canvas = document.getElementById('unity-canvas')
|
||||
window.createUnityInstance(canvas, {
|
||||
dataUrl: `${UNITY_BUILD_PATH}/nouveau_build.data?v=${UNITY_BUILD_VERSION}`,
|
||||
frameworkUrl: `${UNITY_BUILD_PATH}/nouveau_build.framework.js?v=${UNITY_BUILD_VERSION}`,
|
||||
codeUrl: `${UNITY_BUILD_PATH}/nouveau_build.wasm?v=${UNITY_BUILD_VERSION}`,
|
||||
streamingAssetsUrl: '/unity-build/StreamingAssets',
|
||||
companyName: 'ROLLD',
|
||||
productName: 'ROLLD',
|
||||
productVersion: '0.1',
|
||||
}, (progress) => {
|
||||
setLoadingProgress(Math.round(progress * 100))
|
||||
}).then((instance) => {
|
||||
setIsLoaded(true)
|
||||
window.__unityInstance = instance
|
||||
|
||||
// Patch requestPointerLock to catch SecurityError (user exits lock before promise resolves)
|
||||
const canvas = document.getElementById('unity-canvas')
|
||||
if (canvas) {
|
||||
const origLock = canvas.requestPointerLock.bind(canvas)
|
||||
canvas.requestPointerLock = function (...args) {
|
||||
try {
|
||||
const result = origLock(...args)
|
||||
if (result && typeof result.catch === 'function') {
|
||||
return result.catch((e) => {
|
||||
if (e.name === 'SecurityError') {
|
||||
console.warn('[ROLLD] Pointer lock interrupted (SecurityError) — ignored')
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
})
|
||||
}
|
||||
return result
|
||||
} catch (e) {
|
||||
if (e.name === 'SecurityError') {
|
||||
console.warn('[ROLLD] Pointer lock sync error — ignored')
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass game server URL to Unity's NetworkManager
|
||||
instance.SendMessage('NetworkManager', 'SetServerURL', GAME_SERVER_URL)
|
||||
console.log('[ROLLD] Unity loaded, server URL sent:', GAME_SERVER_URL)
|
||||
}).catch((err) => {
|
||||
setError(err.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
script.onerror = () => setError('Failed to load Unity loader')
|
||||
document.body.appendChild(script)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-rolld-bg z-50 flex flex-col">
|
||||
{/* Top bar */}
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-rolld-surface/80 backdrop-blur border-b border-rolld-border">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 text-rolld-muted hover:text-rolld-text transition-colors text-sm"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Retour
|
||||
</button>
|
||||
<span className="text-rolld-accent font-bold tracking-wide text-sm">ROLL'D</span>
|
||||
<div className="w-16" /> {/* Spacer */}
|
||||
</div>
|
||||
|
||||
{/* Game area */}
|
||||
<div className="flex-1 relative flex items-center justify-center">
|
||||
{/* Unity Canvas (hidden until build exists) */}
|
||||
<canvas
|
||||
id="unity-canvas"
|
||||
className={`w-full h-full ${hasUnityBuild && isLoaded ? 'block' : 'hidden'}`}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
|
||||
{/* Loading state */}
|
||||
{hasUnityBuild === true && !isLoaded && !error && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-6">
|
||||
<div className="text-4xl font-black">
|
||||
<span className="text-rolld-text">ROLL</span>
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-rolld-accent to-rolld-violet">'D</span>
|
||||
</div>
|
||||
<div className="w-64">
|
||||
<div className="h-2 rounded-full bg-rolld-surface overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-gradient-to-r from-rolld-accent to-rolld-violet transition-all duration-300"
|
||||
style={{ width: `${loadingProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-rolld-muted text-sm text-center mt-2">Chargement… {loadingProgress}%</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No build placeholder */}
|
||||
{hasUnityBuild === false && (
|
||||
<div className="flex flex-col items-center justify-center gap-6 text-center px-4">
|
||||
<div className="w-24 h-24 rounded-3xl bg-rolld-surface border border-rolld-border flex items-center justify-center text-5xl animate-float">
|
||||
🎮
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-rolld-text mb-2">Build Unity en attente</h2>
|
||||
<p className="text-rolld-muted max-w-md leading-relaxed">
|
||||
Le build WebGL n'est pas encore disponible. Placez les fichiers dans{' '}
|
||||
<code className="px-2 py-0.5 rounded bg-rolld-surface border border-rolld-border text-rolld-accent-light text-sm font-mono">
|
||||
public/unity-build/Build/
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
<div className="glass rounded-xl p-4 text-left text-sm text-rolld-muted font-mono max-w-sm w-full">
|
||||
<p className="text-rolld-accent-light mb-2">Fichiers requis :</p>
|
||||
<p>├── nouveau_build.loader.js</p>
|
||||
<p>├── nouveau_build.data</p>
|
||||
<p>├── nouveau_build.framework.js</p>
|
||||
<p>└── nouveau_build.wasm</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Checking state */}
|
||||
{hasUnityBuild === null && (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-8 h-8 border-2 border-rolld-accent border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-rolld-muted text-sm">Vérification du build…</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error state */}
|
||||
{error && (
|
||||
<div className="flex flex-col items-center gap-4 text-center px-4">
|
||||
<div className="text-5xl">⚠️</div>
|
||||
<h2 className="text-xl font-bold text-red-400">Erreur de chargement</h2>
|
||||
<p className="text-rolld-muted max-w-md text-sm">{error}</p>
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="px-4 py-2 rounded-lg bg-rolld-surface border border-rolld-border text-rolld-text hover:border-rolld-accent/40 transition-colors text-sm"
|
||||
>
|
||||
Retour à l'accueil
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
95
frontend/src/components/GelShowcase.jsx
Normal file
95
frontend/src/components/GelShowcase.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
const gels = [
|
||||
{
|
||||
name: 'Gel Orange',
|
||||
emoji: '🟠',
|
||||
description: 'Surface de boost — multiplie votre vitesse. Prenez de l\'élan et catapultez-vous vers de nouveaux sommets.',
|
||||
gradient: 'from-orange-500 to-amber-600',
|
||||
borderColor: 'border-orange-500/30',
|
||||
glowColor: 'hover:shadow-orange-500/20',
|
||||
bgGlow: 'bg-orange-500/5',
|
||||
},
|
||||
{
|
||||
name: 'Gel Violet',
|
||||
emoji: '🟣',
|
||||
description: 'Surface sticky — collez aux murs et plafonds. La gravité s\'inverse pour vous plaquer contre la surface.',
|
||||
gradient: 'from-purple-500 to-violet-600',
|
||||
borderColor: 'border-purple-500/30',
|
||||
glowColor: 'hover:shadow-purple-500/20',
|
||||
bgGlow: 'bg-purple-500/5',
|
||||
},
|
||||
{
|
||||
name: 'Gel Bleu',
|
||||
emoji: '🔵',
|
||||
description: 'Surface rebondissante — transformez votre bille en super balle. Plus vous arrivez vite, plus vous rebondissez haut.',
|
||||
gradient: 'from-blue-500 to-cyan-600',
|
||||
borderColor: 'border-blue-500/30',
|
||||
glowColor: 'hover:shadow-blue-500/20',
|
||||
bgGlow: 'bg-blue-500/5',
|
||||
},
|
||||
]
|
||||
|
||||
export default function GelShowcase() {
|
||||
return (
|
||||
<section id="gels" className="py-24 px-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Section header */}
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-rolld-text mb-4">
|
||||
Trois gels, infinies possibilités
|
||||
</h2>
|
||||
<p className="text-rolld-muted text-lg max-w-xl mx-auto">
|
||||
Chaque surface change radicalement votre physique. Maîtrisez-les pour dominer l'arène.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Gel cards */}
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{gels.map((gel) => (
|
||||
<div
|
||||
key={gel.name}
|
||||
className={`group relative rounded-2xl border ${gel.borderColor} bg-rolld-surface p-8 transition-all duration-500 hover:scale-[1.02] hover:shadow-2xl ${gel.glowColor}`}
|
||||
>
|
||||
{/* Glow effect */}
|
||||
<div className={`absolute inset-0 rounded-2xl ${gel.bgGlow} opacity-0 group-hover:opacity-100 transition-opacity duration-500`} />
|
||||
|
||||
<div className="relative z-10">
|
||||
{/* Icon */}
|
||||
<div className="text-5xl mb-4">{gel.emoji}</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className={`text-xl font-bold mb-3 bg-gradient-to-r ${gel.gradient} bg-clip-text text-transparent`}>
|
||||
{gel.name}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-rolld-muted leading-relaxed text-sm">
|
||||
{gel.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Controls hint */}
|
||||
<div className="mt-16 glass rounded-2xl p-8 text-center">
|
||||
<h3 className="text-xl font-semibold text-rolld-text mb-6">Contrôles</h3>
|
||||
<div className="flex flex-wrap justify-center gap-6">
|
||||
{[
|
||||
{ keys: 'ZQSD', label: 'Mouvement' },
|
||||
{ keys: 'ESPACE', label: 'Sauter (maintenir = +force)' },
|
||||
{ keys: 'SOURIS', label: 'Caméra' },
|
||||
{ keys: 'CLIC DROIT', label: 'Libérer le curseur' },
|
||||
].map((control) => (
|
||||
<div key={control.keys} className="flex flex-col items-center gap-2">
|
||||
<kbd className="px-3 py-1.5 rounded-lg bg-rolld-bg border border-rolld-border text-rolld-accent-light font-mono text-sm">
|
||||
{control.keys}
|
||||
</kbd>
|
||||
<span className="text-rolld-muted text-xs">{control.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
83
frontend/src/components/Hero.jsx
Normal file
83
frontend/src/components/Hero.jsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { IS_DEV, theme } from '../env'
|
||||
|
||||
export default function Hero({ onPlay }) {
|
||||
return (
|
||||
<section className="relative min-h-screen flex flex-col items-center justify-center px-4 overflow-hidden">
|
||||
{/* Background effects */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
{/* Gradient orbs */}
|
||||
<div
|
||||
className="absolute top-1/4 left-1/4 w-[500px] h-[500px] rounded-full blur-[120px] animate-float"
|
||||
style={{ background: `rgba(${theme.accentRgb}, 0.1)` }}
|
||||
/>
|
||||
<div className="absolute bottom-1/4 right-1/4 w-[400px] h-[400px] rounded-full bg-rolld-orange/8 blur-[100px] animate-float" style={{ animationDelay: '-3s' }} />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[300px] h-[300px] rounded-full bg-rolld-violet/8 blur-[80px]" />
|
||||
|
||||
{/* Grid */}
|
||||
<div className="absolute inset-0 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(rgba(${theme.accentRgb},0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(${theme.accentRgb},0.5) 1px, transparent 1px)`,
|
||||
backgroundSize: '60px 60px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 text-center max-w-4xl mx-auto animate-slide-up">
|
||||
{/* Badge */}
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full glass text-sm mb-8" style={{ color: theme.accentLight }}>
|
||||
<span className="w-2 h-2 rounded-full animate-pulse" style={{ background: theme.accent }} />
|
||||
{IS_DEV ? '🚧 DEV · ' : ''}Marble MMO · Multijoueur temps réel
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h1 className="text-6xl md:text-8xl font-black tracking-tight mb-6">
|
||||
<span className="text-rolld-text">ROLL</span>
|
||||
<span
|
||||
className="text-transparent bg-clip-text"
|
||||
style={{ backgroundImage: `linear-gradient(to right, ${theme.accent}, ${theme.gradientTo}, ${IS_DEV ? '#e67e22' : '#f39c12'})` }}
|
||||
>'D</span>
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="text-lg md:text-xl text-rolld-muted max-w-2xl mx-auto mb-12 leading-relaxed">
|
||||
Un monde de billes, de gels et de physique. Roulez sur des surfaces qui boostent,
|
||||
collent ou font rebondir — et affrontez d'autres joueurs en temps réel.
|
||||
</p>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<button
|
||||
onClick={onPlay}
|
||||
className="group relative px-8 py-4 rounded-2xl text-white font-bold text-lg transition-all duration-300 hover:scale-105"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(to right, ${theme.accent}, ${theme.gradientTo})`,
|
||||
boxShadow: `0 0 30px rgba(${theme.accentRgb}, 0.4)`,
|
||||
}}
|
||||
>
|
||||
<span className="relative z-10 flex items-center gap-3">
|
||||
<svg className="w-6 h-6 group-hover:translate-x-0.5 transition-transform" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M6.3 2.841A1.5 1.5 0 004 4.11V15.89a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z" />
|
||||
</svg>
|
||||
Jouer maintenant
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<a
|
||||
href="#gels"
|
||||
className="px-6 py-3 rounded-xl border border-rolld-border text-rolld-muted hover:text-rolld-text hover:border-rolld-accent/40 transition-all duration-300"
|
||||
>
|
||||
Découvrir les mécaniques ↓
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll indicator */}
|
||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce">
|
||||
<div className="w-5 h-8 rounded-full border-2 border-rolld-muted/30 flex items-start justify-center p-1">
|
||||
<div className="w-1 h-2 rounded-full bg-rolld-muted/50" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
122
frontend/src/components/KerboulistanBanner.jsx
Normal file
122
frontend/src/components/KerboulistanBanner.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
export default function KerboulistanBanner() {
|
||||
return (
|
||||
<section className="relative py-20 px-4 overflow-hidden">
|
||||
{/* Transition gradient — ROLL'D dark to gov gold tint and back */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-rolld-bg via-[#0d0c08] to-rolld-bg" />
|
||||
{/* Subtle gold fog */}
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[300px] rounded-full bg-amber-500/[0.04] blur-[100px]" />
|
||||
{/* Scanlines — bureaucratic CRT vibe */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.015]"
|
||||
style={{
|
||||
backgroundImage: 'repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(217,175,78,0.4) 2px, rgba(217,175,78,0.4) 3px)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 max-w-3xl mx-auto text-center">
|
||||
{/* Classification header */}
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded border border-amber-700/30 bg-amber-900/10 mb-8">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-amber-500/80 animate-pulse" />
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-amber-500/60 font-mono">
|
||||
Directive n°2026-042 · Programme homologué
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Government seal / emblem area */}
|
||||
<div className="flex items-center justify-center gap-4 mb-6">
|
||||
<div className="h-px flex-1 max-w-[80px] bg-gradient-to-r from-transparent to-amber-600/30" />
|
||||
<a
|
||||
href="https://gov.kerboul.me"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group relative"
|
||||
>
|
||||
{/* Seal ring */}
|
||||
<div className="w-20 h-20 rounded-full border border-amber-600/30 group-hover:border-amber-500/50 flex items-center justify-center transition-all duration-500 group-hover:shadow-[0_0_30px_rgba(217,175,78,0.1)]">
|
||||
<div className="w-16 h-16 rounded-full border border-amber-700/20 flex items-center justify-center">
|
||||
<span className="text-2xl grayscale group-hover:grayscale-0 transition-all duration-500">🏛️</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Seal text ring (simulated) */}
|
||||
<div className="absolute -inset-2 rounded-full border border-dashed border-amber-800/15 group-hover:border-amber-700/25 transition-colors duration-500 animate-[spin_60s_linear_infinite]" />
|
||||
</a>
|
||||
<div className="h-px flex-1 max-w-[80px] bg-gradient-to-l from-transparent to-amber-600/30" />
|
||||
</div>
|
||||
|
||||
{/* Title — gov style */}
|
||||
<h2 className="text-xs tracking-[0.3em] uppercase text-amber-500/50 font-medium mb-2">
|
||||
République Libre Populaire Démocratique
|
||||
</h2>
|
||||
<h3 className="text-2xl md:text-3xl font-bold tracking-tight mb-2">
|
||||
<span className="text-amber-100/80">du </span>
|
||||
<a
|
||||
href="https://gov.kerboul.me"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-amber-600 hover:from-amber-300 hover:to-amber-500 transition-all duration-300"
|
||||
>
|
||||
Kerboulistan
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="flex items-center justify-center gap-3 my-6">
|
||||
<div className="h-px w-12 bg-amber-700/30" />
|
||||
<span className="text-amber-600/30 text-xs">✦</span>
|
||||
<div className="h-px w-12 bg-amber-700/30" />
|
||||
</div>
|
||||
|
||||
{/* Body text — bureaucratic tone */}
|
||||
<p className="text-rolld-muted/70 text-sm leading-relaxed max-w-lg mx-auto mb-2">
|
||||
Ce programme vidéoludique a été développé sous l'autorité directe du{' '}
|
||||
<span className="text-amber-400/60">Couple Présidentiel</span> et financé par le{' '}
|
||||
<span className="text-amber-400/60">Ministère de la Défense</span> dans le cadre de
|
||||
la Directive de Divertissement Stratégique.
|
||||
</p>
|
||||
<p className="text-rolld-muted/40 text-xs leading-relaxed max-w-md mx-auto mb-8">
|
||||
Budget alloué : 35 Memes ($K) · Temps de développement : classifié ·
|
||||
Taux de satisfaction prévu : 420%
|
||||
</p>
|
||||
|
||||
{/* Stamps / tags */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-3 mb-8">
|
||||
{[
|
||||
{ label: 'Homologué RLPDK', icon: '🛡️' },
|
||||
{ label: 'Approuvé FBC', icon: '📡' },
|
||||
{ label: 'MEMECON 5', icon: '🟢' },
|
||||
].map((stamp) => (
|
||||
<div
|
||||
key={stamp.label}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded border border-amber-800/20 bg-amber-900/5 text-amber-500/40 text-[10px] tracking-[0.15em] uppercase font-mono"
|
||||
>
|
||||
<span className="text-xs">{stamp.icon}</span>
|
||||
{stamp.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA — gov link */}
|
||||
<a
|
||||
href="https://gov.kerboul.me"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group inline-flex items-center gap-2 px-5 py-2.5 rounded-lg border border-amber-700/25 hover:border-amber-600/40 bg-amber-900/5 hover:bg-amber-900/10 transition-all duration-300"
|
||||
>
|
||||
<span className="text-amber-500/50 group-hover:text-amber-400/70 text-xs tracking-[0.1em] uppercase transition-colors duration-300">
|
||||
Consulter le Gouvernement
|
||||
</span>
|
||||
<svg className="w-3 h-3 text-amber-600/40 group-hover:text-amber-500/60 group-hover:translate-x-0.5 transition-all duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
{/* Bottom motto */}
|
||||
<p className="mt-8 text-[9px] tracking-[0.25em] uppercase text-amber-700/25 font-mono">
|
||||
« Endgame, Combined Arms, et Zougoulag ! »
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
31
frontend/src/env.js
Normal file
31
frontend/src/env.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// Environment configuration
|
||||
// Set VITE_ENV=dev in Coolify build args for the dev application
|
||||
// Defaults to 'prod' if not set
|
||||
|
||||
export const ENV = import.meta.env.VITE_ENV || 'prod'
|
||||
export const IS_DEV = ENV === 'dev'
|
||||
export const IS_PROD = ENV === 'prod'
|
||||
|
||||
// Theme overrides per environment
|
||||
export const envTheme = {
|
||||
dev: {
|
||||
accent: '#f39c12', // Orange
|
||||
accentLight: '#f1c40f', // Yellow-orange
|
||||
accentRgb: '243, 156, 18',
|
||||
gradientFrom: '#f39c12',
|
||||
gradientTo: '#e67e22',
|
||||
label: 'DEV',
|
||||
badge: '🚧',
|
||||
},
|
||||
prod: {
|
||||
accent: '#6c5ce7', // Purple (default)
|
||||
accentLight: '#a29bfe',
|
||||
accentRgb: '108, 92, 231',
|
||||
gradientFrom: '#6c5ce7',
|
||||
gradientTo: '#9b59b6',
|
||||
label: 'PROD',
|
||||
badge: '🚀',
|
||||
},
|
||||
}
|
||||
|
||||
export const theme = envTheme[ENV] || envTheme.prod
|
||||
43
frontend/src/index.css
Normal file
43
frontend/src/index.css
Normal file
@@ -0,0 +1,43 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--rolld-accent: #6c5ce7;
|
||||
--rolld-accent-light: #a29bfe;
|
||||
--rolld-accent-rgb: 108, 92, 231;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #0a0a0f;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--rolld-accent);
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.gel-gradient-orange {
|
||||
background: linear-gradient(135deg, #f39c12, #e67e22);
|
||||
}
|
||||
.gel-gradient-violet {
|
||||
background: linear-gradient(135deg, #9b59b6, #8e44ad);
|
||||
}
|
||||
.gel-gradient-blue {
|
||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
||||
}
|
||||
.glass {
|
||||
background: rgba(18, 18, 26, 0.7);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(var(--rolld-accent-rgb), 0.15);
|
||||
}
|
||||
}
|
||||
10
frontend/src/main.jsx
Normal file
10
frontend/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
Reference in New Issue
Block a user