feat: add frontend as flat files (was submodule)
This commit is contained in:
1
frontend
1
frontend
Submodule frontend deleted from 6803f05f5e
6
frontend/.gitignore
vendored
Normal file
6
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.log
|
||||||
|
public/unity-build/TemplateData/
|
||||||
66
frontend/Dockerfile
Normal file
66
frontend/Dockerfile
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# --- Build stage ---
|
||||||
|
FROM node:18-alpine AS build
|
||||||
|
|
||||||
|
# Environment: 'dev' or 'prod' (controls theming)
|
||||||
|
ARG VITE_ENV=prod
|
||||||
|
ENV VITE_ENV=$VITE_ENV
|
||||||
|
|
||||||
|
# Game server WebSocket URL (Colyseus)
|
||||||
|
ARG VITE_GAME_SERVER_URL
|
||||||
|
ENV VITE_GAME_SERVER_URL=$VITE_GAME_SERVER_URL
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# --- Production stage ---
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy built assets
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy Unity build if present
|
||||||
|
COPY --from=build /app/public/unity-build /usr/share/nginx/html/unity-build
|
||||||
|
|
||||||
|
# Nginx config for SPA + gzip precompressed Unity files
|
||||||
|
RUN cat > /etc/nginx/conf.d/default.conf << 'EOF'
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript application/wasm;
|
||||||
|
|
||||||
|
location /unity-build/Build/ {
|
||||||
|
types {
|
||||||
|
application/javascript js;
|
||||||
|
application/wasm wasm;
|
||||||
|
application/octet-stream data;
|
||||||
|
}
|
||||||
|
add_header Cache-Control "no-cache, must-revalidate";
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
gzip_types application/javascript application/wasm application/octet-stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Aggressive cache for hashed Vite assets (NOT Unity build files)
|
||||||
|
location ~* ^(?!/unity-build/).*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
54
frontend/README.md
Normal file
54
frontend/README.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# ROLL'D — Frontend
|
||||||
|
|
||||||
|
Client web pour le jeu ROLL'D. Héberge le build Unity WebGL dans une interface moderne.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **React 18** + **Vite 5** — build rapide, HMR
|
||||||
|
- **Tailwind CSS 3** — styling utility-first
|
||||||
|
- **Unity WebGL Loader** — intégration du build Unity
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev # http://localhost:5173
|
||||||
|
npm run build # production build → dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unity WebGL Build
|
||||||
|
|
||||||
|
Placer le build Unity dans `public/unity-build/` :
|
||||||
|
```
|
||||||
|
public/unity-build/
|
||||||
|
├── Build/
|
||||||
|
│ ├── build.data.gz
|
||||||
|
│ ├── build.framework.js.gz
|
||||||
|
│ ├── build.loader.js
|
||||||
|
│ └── build.wasm.gz
|
||||||
|
└── TemplateData/ (optionnel)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t rolld-frontend .
|
||||||
|
docker run -p 80:80 rolld-frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
├── public/
|
||||||
|
│ ├── unity-build/ # Build WebGL (non versionné)
|
||||||
|
│ └── favicon.svg
|
||||||
|
├── src/
|
||||||
|
│ ├── components/ # Composants React
|
||||||
|
│ ├── assets/ # Images, fonts
|
||||||
|
│ ├── App.jsx
|
||||||
|
│ └── main.jsx
|
||||||
|
├── index.html
|
||||||
|
├── tailwind.config.js
|
||||||
|
├── vite.config.js
|
||||||
|
└── Dockerfile
|
||||||
|
```
|
||||||
17
frontend/index.html
Normal file
17
frontend/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr" class="scroll-smooth">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="description" content="ROLL'D — Un MMO de billes multijoueur avec des mécaniques de gel uniques. Jouez directement dans votre navigateur." />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<title>ROLL'D — Marble MMO</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-[#0a0a0f] text-[#e8e8f0] antialiased overflow-x-hidden">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2641
frontend/package-lock.json
generated
Normal file
2641
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "rolld-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-unity-webgl": "^9.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"postcss": "^8.4.32",
|
||||||
|
"tailwindcss": "^3.4.0",
|
||||||
|
"vite": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
11
frontend/public/favicon.svg
Normal file
11
frontend/public/favicon.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#6c5ce7"/>
|
||||||
|
<stop offset="100%" stop-color="#9b59b6"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<circle cx="50" cy="50" r="45" fill="url(#g)"/>
|
||||||
|
<circle cx="50" cy="50" r="38" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="2"/>
|
||||||
|
<circle cx="38" cy="38" r="8" fill="rgba(255,255,255,0.25)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 481 B |
2
frontend/public/unity-build/.gitkeep
Normal file
2
frontend/public/unity-build/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Unity WebGL build goes here
|
||||||
|
Place your Unity WebGL build files in a `Build/` subfolder.
|
||||||
133
frontend/public/unity-build/index.html
Normal file
133
frontend/public/unity-build/index.html
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-us">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<title>Unity Web Player | BallProject</title>
|
||||||
|
<link rel="shortcut icon" href="TemplateData/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="TemplateData/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="unity-container" class="unity-desktop">
|
||||||
|
<canvas id="unity-canvas" width=960 height=600 tabindex="-1"></canvas>
|
||||||
|
<div id="unity-loading-bar">
|
||||||
|
<div id="unity-logo"></div>
|
||||||
|
<div id="unity-progress-bar-empty">
|
||||||
|
<div id="unity-progress-bar-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="unity-warning"> </div>
|
||||||
|
<div id="unity-footer">
|
||||||
|
<div id="unity-logo-title-footer"></div>
|
||||||
|
<div id="unity-fullscreen-button"></div>
|
||||||
|
<div id="unity-build-title">BallProject</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
var canvas = document.querySelector("#unity-canvas");
|
||||||
|
|
||||||
|
// Shows a temporary message banner/ribbon for a few seconds, or
|
||||||
|
// a permanent error message on top of the canvas if type=='error'.
|
||||||
|
// If type=='warning', a yellow highlight color is used.
|
||||||
|
// Modify or remove this function to customize the visually presented
|
||||||
|
// way that non-critical warnings and error messages are presented to the
|
||||||
|
// user.
|
||||||
|
function unityShowBanner(msg, type) {
|
||||||
|
var warningBanner = document.querySelector("#unity-warning");
|
||||||
|
function updateBannerVisibility() {
|
||||||
|
warningBanner.style.display = warningBanner.children.length ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.innerHTML = msg;
|
||||||
|
warningBanner.appendChild(div);
|
||||||
|
if (type == 'error') div.style = 'background: red; padding: 10px;';
|
||||||
|
else {
|
||||||
|
if (type == 'warning') div.style = 'background: yellow; padding: 10px;';
|
||||||
|
setTimeout(function() {
|
||||||
|
warningBanner.removeChild(div);
|
||||||
|
updateBannerVisibility();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
updateBannerVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
var buildUrl = "Build";
|
||||||
|
var loaderUrl = buildUrl + "/nouveau_build.loader.js";
|
||||||
|
var config = {
|
||||||
|
arguments: [],
|
||||||
|
dataUrl: buildUrl + "/nouveau_build.data",
|
||||||
|
frameworkUrl: buildUrl + "/nouveau_build.framework.js",
|
||||||
|
codeUrl: buildUrl + "/nouveau_build.wasm",
|
||||||
|
streamingAssetsUrl: "StreamingAssets",
|
||||||
|
companyName: "DefaultCompany",
|
||||||
|
productName: "BallProject",
|
||||||
|
productVersion: "0.1.0",
|
||||||
|
showBanner: unityShowBanner,
|
||||||
|
// errorHandler: function(err, url, line) {
|
||||||
|
// alert("error " + err + " occurred at line " + line);
|
||||||
|
// // Return 'true' if you handled this error and don't want Unity
|
||||||
|
// // to process it further, 'false' otherwise.
|
||||||
|
// return true;
|
||||||
|
// },
|
||||||
|
};
|
||||||
|
|
||||||
|
// By default, Unity keeps WebGL canvas render target size matched with
|
||||||
|
// the DOM size of the canvas element (scaled by window.devicePixelRatio)
|
||||||
|
// Set this to false if you want to decouple this synchronization from
|
||||||
|
// happening inside the engine, and you would instead like to size up
|
||||||
|
// the canvas DOM size and WebGL render target sizes yourself.
|
||||||
|
// config.matchWebGLToCanvasSize = false;
|
||||||
|
|
||||||
|
// If you would like all file writes inside Unity Application.persistentDataPath
|
||||||
|
// directory to automatically persist so that the contents are remembered when
|
||||||
|
// the user revisits the site the next time, uncomment the following line:
|
||||||
|
// config.autoSyncPersistentDataPath = true;
|
||||||
|
// This autosyncing is currently not the default behavior to avoid regressing
|
||||||
|
// existing user projects that might rely on the earlier manual
|
||||||
|
// JS_FileSystem_Sync() behavior, but in future Unity version, this will be
|
||||||
|
// expected to change.
|
||||||
|
|
||||||
|
if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
|
||||||
|
// Mobile device style: fill the whole browser client area with the game canvas:
|
||||||
|
|
||||||
|
var meta = document.createElement('meta');
|
||||||
|
meta.name = 'viewport';
|
||||||
|
meta.content = 'width=device-width, height=device-height, initial-scale=1.0, user-scalable=no, shrink-to-fit=yes';
|
||||||
|
document.getElementsByTagName('head')[0].appendChild(meta);
|
||||||
|
document.querySelector("#unity-container").className = "unity-mobile";
|
||||||
|
canvas.className = "unity-mobile";
|
||||||
|
|
||||||
|
// To lower canvas resolution on mobile devices to gain some
|
||||||
|
// performance, uncomment the following line:
|
||||||
|
// config.devicePixelRatio = 1;
|
||||||
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Desktop style: Render the game canvas in a window that can be maximized to fullscreen:
|
||||||
|
canvas.style.width = "960px";
|
||||||
|
canvas.style.height = "600px";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector("#unity-loading-bar").style.display = "block";
|
||||||
|
|
||||||
|
var script = document.createElement("script");
|
||||||
|
script.src = loaderUrl;
|
||||||
|
script.onload = () => {
|
||||||
|
createUnityInstance(canvas, config, (progress) => {
|
||||||
|
document.querySelector("#unity-progress-bar-full").style.width = 100 * progress + "%";
|
||||||
|
}).then((unityInstance) => {
|
||||||
|
document.querySelector("#unity-loading-bar").style.display = "none";
|
||||||
|
document.querySelector("#unity-fullscreen-button").onclick = () => {
|
||||||
|
unityInstance.SetFullscreen(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
}).catch((message) => {
|
||||||
|
alert(message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.appendChild(script);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
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>,
|
||||||
|
)
|
||||||
49
frontend/tailwind.config.js
Normal file
49
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
'rolld': {
|
||||||
|
bg: '#0a0a0f',
|
||||||
|
surface: '#12121a',
|
||||||
|
border: '#1e1e2e',
|
||||||
|
accent: '#6c5ce7',
|
||||||
|
'accent-light': '#a29bfe',
|
||||||
|
orange: '#f39c12',
|
||||||
|
violet: '#9b59b6',
|
||||||
|
blue: '#3498db',
|
||||||
|
text: '#e8e8f0',
|
||||||
|
muted: '#6b7280',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
display: ['Inter', 'system-ui', 'sans-serif'],
|
||||||
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'float': 'float 6s ease-in-out infinite',
|
||||||
|
'pulse-glow': 'pulseGlow 2s ease-in-out infinite',
|
||||||
|
'slide-up': 'slideUp 0.8s ease-out',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
float: {
|
||||||
|
'0%, 100%': { transform: 'translateY(0px)' },
|
||||||
|
'50%': { transform: 'translateY(-20px)' },
|
||||||
|
},
|
||||||
|
pulseGlow: {
|
||||||
|
'0%, 100%': { boxShadow: '0 0 20px rgba(108, 92, 231, 0.3)' },
|
||||||
|
'50%': { boxShadow: '0 0 40px rgba(108, 92, 231, 0.6)' },
|
||||||
|
},
|
||||||
|
slideUp: {
|
||||||
|
'0%': { opacity: 0, transform: 'translateY(30px)' },
|
||||||
|
'100%': { opacity: 1, transform: 'translateY(0)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
14
frontend/vite.config.js
Normal file
14
frontend/vite.config.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 5173,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
assetsInlineLimit: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user