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