feat(sprint1-step6): polish, landing page, responsive, deploy config
- Landing.jsx : écran d'accueil "Entrer dans le Marais" sur / - Home.jsx : jeu sur /jeu, click animation float-up, sidebar responsive - formatNumber.ts : util partagé k/M/B/T (remplace 4 copies locales) - home.scss : rewrite classes (game-cover, click-zone, tadpole-sprite, game-sidebar) - Responsive : sidebar fixe desktop, drawer bottom mobile (<768px) - navbar : wildCoin → resource-counter, auth-nav stylé, dead code supprimé - GameSync.tsx : bridge useSaveSync ↔ Zustand (câblé dans App) - tadpole.svg : asset renommé (SantaClause-bag → tadpole) - deploy/ : Apache vhost SPA+proxy, deploy.sh, ecosystem.config.cjs PM2 - NavBarData : Jeu → /jeu - Cleanup : dead imports, commentaires legacy
This commit is contained in:
2254
Frontend/public/svg/tadpole.svg
Executable file
2254
Frontend/public/svg/tadpole.svg
Executable file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 127 KiB |
@@ -4,6 +4,8 @@ import { Outlet } from "react-router-dom";
|
|||||||
import Navbar from "./components/navbar";
|
import Navbar from "./components/navbar";
|
||||||
import Footer from "./components/footer";
|
import Footer from "./components/footer";
|
||||||
import Hud from "./components/Hud/Hud";
|
import Hud from "./components/Hud/Hud";
|
||||||
|
import { GameTick } from "./components/GameTick";
|
||||||
|
import { GameSync } from "./components/GameSync";
|
||||||
|
|
||||||
import "./scss/root.scss";
|
import "./scss/root.scss";
|
||||||
import "./scss/components/footer.scss";
|
import "./scss/components/footer.scss";
|
||||||
@@ -12,20 +14,22 @@ import navData from "./data/NavBarData.json";
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const [toggleSnow, setToggleSnow] = useState(false);
|
const [toggleRain, setToggleRain] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<GameTick />
|
||||||
|
<GameSync />
|
||||||
<Navbar
|
<Navbar
|
||||||
navData={navData}
|
navData={navData}
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
setIsVisible={setIsVisible}
|
setIsVisible={setIsVisible}
|
||||||
toggleSnow={toggleSnow}
|
toggleRain={toggleRain}
|
||||||
setToggleSnow={setToggleSnow}
|
setToggleRain={setToggleRain}
|
||||||
/>
|
/>
|
||||||
<Hud isVisible={isVisible} setIsVisible={setIsVisible} />
|
<Hud isVisible={isVisible} setIsVisible={setIsVisible} />
|
||||||
<main>
|
<main>
|
||||||
<Outlet context={[toggleSnow, setToggleSnow]} />
|
<Outlet context={[toggleRain, setToggleRain]} />
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
|
|||||||
22
Frontend/src/components/GameSync.tsx
Normal file
22
Frontend/src/components/GameSync.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// GameSync.tsx — Bridge useSaveSync ↔ Zustand store
|
||||||
|
// Monter une seule fois dans App. Silencieux en mode invité (pas de token).
|
||||||
|
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { useGameStore } from "../store/useGameStore";
|
||||||
|
import { useSaveSync } from "../hooks/useSaveSync";
|
||||||
|
|
||||||
|
export function GameSync() {
|
||||||
|
const state = useGameStore((s) => s.state);
|
||||||
|
const loadFromServer = useGameStore((s) => s.loadFromServer);
|
||||||
|
const playSeconds = useGameStore((s) => s.playSeconds);
|
||||||
|
|
||||||
|
const getGameState = useCallback(() => state, [state]);
|
||||||
|
|
||||||
|
useSaveSync({
|
||||||
|
getGameState,
|
||||||
|
onLoad: loadFromServer,
|
||||||
|
playTimeSeconds: playSeconds,
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -6,7 +6,8 @@ import "../scss/root.scss";
|
|||||||
|
|
||||||
import PrimaryButton from "./buttons/PrimaryButton";
|
import PrimaryButton from "./buttons/PrimaryButton";
|
||||||
import Burger from "./burger";
|
import Burger from "./burger";
|
||||||
import { useWildCoin } from "./WildCoin/WildCoinContext";
|
import { useGameStore } from "../store/useGameStore";
|
||||||
|
import { useAuth } from "../context/AuthContext";
|
||||||
import HUDON from "../../public/NavBar/HUDON.svg";
|
import HUDON from "../../public/NavBar/HUDON.svg";
|
||||||
import HUDOFF from "../../public/NavBar/HUDOFF.svg";
|
import HUDOFF from "../../public/NavBar/HUDOFF.svg";
|
||||||
import SnowOn from "../../public/NavBar/SnowOn.svg";
|
import SnowOn from "../../public/NavBar/SnowOn.svg";
|
||||||
@@ -17,24 +18,20 @@ export default function Navbar({
|
|||||||
navData,
|
navData,
|
||||||
isVisible,
|
isVisible,
|
||||||
setIsVisible,
|
setIsVisible,
|
||||||
toggleSnow,
|
toggleRain,
|
||||||
setToggleSnow,
|
setToggleRain,
|
||||||
}) {
|
}) {
|
||||||
Navbar.propTypes = {
|
Navbar.propTypes = {
|
||||||
isVisible: PropTypes.bool,
|
isVisible: PropTypes.bool,
|
||||||
setIsVisible: PropTypes.function,
|
setIsVisible: PropTypes.function,
|
||||||
setToggleSnow: PropTypes.function,
|
setToggleRain: PropTypes.function,
|
||||||
toggleSnow: PropTypes.bool,
|
toggleRain: PropTypes.bool,
|
||||||
}.isRequired;
|
}.isRequired;
|
||||||
|
|
||||||
const { wildCoin } = useWildCoin();
|
const resources = useGameStore((s) => s.state.resources);
|
||||||
|
const { user, logout } = useAuth();
|
||||||
const [imageSrc, setImageSrc] = useState(HUDON);
|
const [imageSrc, setImageSrc] = useState(HUDON);
|
||||||
const [snowImageSrc, setSnowImageSrc] = useState(SnowOff);
|
const [snowImageSrc, setSnowImageSrc] = useState(SnowOff);
|
||||||
const [timerVisible, setTimerVisible] = useState(false);
|
|
||||||
const handleClickWildCoin = () => {
|
|
||||||
setTimerVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleHud = () => {
|
const toggleHud = () => {
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
setIsVisible(true);
|
setIsVisible(true);
|
||||||
@@ -44,12 +41,12 @@ export default function Navbar({
|
|||||||
setImageSrc(HUDON);
|
setImageSrc(HUDON);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
function toggleSnowBtn() {
|
function toggleRainBtn() {
|
||||||
if (toggleSnow === false) {
|
if (toggleRain === false) {
|
||||||
setToggleSnow(true);
|
setToggleRain(true);
|
||||||
setSnowImageSrc(SnowOn);
|
setSnowImageSrc(SnowOn);
|
||||||
} else {
|
} else {
|
||||||
setToggleSnow(false);
|
setToggleRain(false);
|
||||||
setSnowImageSrc(SnowOff);
|
setSnowImageSrc(SnowOff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,11 +56,11 @@ export default function Navbar({
|
|||||||
className="logo"
|
className="logo"
|
||||||
to="/"
|
to="/"
|
||||||
aria-label="Retourner à la page d'accueil"
|
aria-label="Retourner à la page d'accueil"
|
||||||
title="Logo XmassClick"
|
title="Logo Clickerz"
|
||||||
/>
|
/>
|
||||||
<div className="navbar">
|
<div className="navbar">
|
||||||
<div className="wildCoin">
|
<div className="resource-counter">
|
||||||
{new Intl.NumberFormat().format(wildCoin)}
|
{new Intl.NumberFormat().format(Math.floor(resources))}
|
||||||
</div>
|
</div>
|
||||||
<ul className="nav-list">
|
<ul className="nav-list">
|
||||||
{navData.map((navIndex) => {
|
{navData.map((navIndex) => {
|
||||||
@@ -104,6 +101,18 @@ export default function Navbar({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
{user ? (
|
||||||
|
<div className="auth-nav">
|
||||||
|
<span className="auth-nickname">{user.nickname}</span>
|
||||||
|
<button className="auth-btn" onClick={logout} type="button">
|
||||||
|
Déconnexion
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Link className="mainLink" to="/login">
|
||||||
|
Connexion
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
<img
|
<img
|
||||||
onClick={() => toggleHud()}
|
onClick={() => toggleHud()}
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
@@ -111,7 +120,7 @@ export default function Navbar({
|
|||||||
alt="boutton on"
|
alt="boutton on"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
onClick={() => toggleSnowBtn()}
|
onClick={() => toggleRainBtn()}
|
||||||
src={snowImageSrc}
|
src={snowImageSrc}
|
||||||
style={{ height: "28px" }}
|
style={{ height: "28px" }}
|
||||||
alt="boutton on"
|
alt="boutton on"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
"id": "1",
|
"id": "1",
|
||||||
"linkname": "Jeu",
|
"linkname": "Jeu",
|
||||||
"linkurl": "/",
|
"linkurl": "/jeu",
|
||||||
"btn": false
|
"btn": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
import Landing from "./pages/Landing";
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
import ErrorPage from "./pages/404";
|
import ErrorPage from "./pages/404";
|
||||||
import { WildCoinProvider } from "./components/WildCoin/WildCoinContext";
|
import Login from "./pages/Login";
|
||||||
import Ameliorations from "./components/WildCoin/Amelioration";
|
import AuthCallback from "./pages/AuthCallback";
|
||||||
|
import { AuthProvider } from "./context/AuthContext";
|
||||||
import Boutique from "./pages/Boutique";
|
import Boutique from "./pages/Boutique";
|
||||||
import Achievements from "./pages/Achievements";
|
import Achievements from "./pages/Achievements";
|
||||||
import Legal from "./pages/Legal";
|
import Legal from "./pages/Legal";
|
||||||
@@ -17,16 +19,12 @@ const router = createBrowserRouter([
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
|
element: <Landing />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/jeu",
|
||||||
element: <Home />,
|
element: <Home />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "*",
|
|
||||||
element: <ErrorPage />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/ameliorations",
|
|
||||||
element: <Ameliorations />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/boutique",
|
path: "/boutique",
|
||||||
element: <Boutique />,
|
element: <Boutique />,
|
||||||
@@ -43,6 +41,18 @@ const router = createBrowserRouter([
|
|||||||
path: "/cookies",
|
path: "/cookies",
|
||||||
element: <Cookie />,
|
element: <Cookie />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/login",
|
||||||
|
element: <Login />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/callback",
|
||||||
|
element: <AuthCallback />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "*",
|
||||||
|
element: <ErrorPage />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -50,7 +60,7 @@ const router = createBrowserRouter([
|
|||||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<WildCoinProvider>
|
<AuthProvider>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</WildCoinProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import BoutiqueCard from "../components/BoutiqueCard";
|
import BoutiqueCard from "../components/BoutiqueCard";
|
||||||
// import { useWildCoin } from "..components/WildCoin/WildCoinContext";
|
|
||||||
import "../scss/shop.scss";
|
import "../scss/shop.scss";
|
||||||
import shop from "../data/shop";
|
import shop from "../data/shop";
|
||||||
|
|
||||||
|
|||||||
@@ -1,231 +1,131 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import PropTypes from "prop-types";
|
import { useEffect, useCallback } from "react";
|
||||||
import "../scss/home.scss";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
import { useWildCoin } from "../components/WildCoin/WildCoinContext";
|
import { useGameStore } from "../store/useGameStore";
|
||||||
|
import { formatNumber } from "../utils/formatNumber";
|
||||||
|
import { GeneratorShop } from "../components/GeneratorShop";
|
||||||
|
import { PrestigePanel } from "../components/PrestigePanel";
|
||||||
|
import { EvolutionTree } from "../components/EvolutionTree";
|
||||||
|
import { MilestoneBar } from "../components/MilestoneBar";
|
||||||
|
import "../scss/home.scss";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [toggleSnow, setToggleSnow] = useOutletContext();
|
const [toggleRain] = useOutletContext();
|
||||||
Home.propTypes = {
|
const click = useGameStore((s) => s.click);
|
||||||
setToggleSnow: PropTypes.function,
|
const resources = useGameStore((s) => s.state.resources);
|
||||||
toggleSnow: PropTypes.bool,
|
const clickMultiplier = useGameStore((s) => s.state.clickMultiplier);
|
||||||
}.isRequired;
|
|
||||||
|
|
||||||
const { biere, setBiere, santaDrunk, setSantaDrunk } = useWildCoin();
|
const createParticle = useCallback((clientX, clientY) => {
|
||||||
|
const particle = document.createElement("span");
|
||||||
|
particle.className = "click-particle";
|
||||||
|
particle.textContent = `+${formatNumber(clickMultiplier)}`;
|
||||||
|
particle.style.left = `${clientX}px`;
|
||||||
|
particle.style.top = `${clientY}px`;
|
||||||
|
document.body.appendChild(particle);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (particle.parentNode) particle.parentNode.removeChild(particle);
|
||||||
|
}, 800);
|
||||||
|
}, [clickMultiplier]);
|
||||||
|
|
||||||
var snow = {
|
const handleIncrement = useCallback((e) => {
|
||||||
wind: 0,
|
click();
|
||||||
maxXrange: 40,
|
createParticle(e.clientX, e.clientY);
|
||||||
minXrange: 20,
|
}, [click, createParticle]);
|
||||||
maxSpeed: 1,
|
|
||||||
minSpeed: 3,
|
|
||||||
color: "#fff",
|
|
||||||
char: "*",
|
|
||||||
maxSize: 32,
|
|
||||||
minSize: 10,
|
|
||||||
|
|
||||||
flakes: [],
|
// Rain effect (ambiance)
|
||||||
WIDTH: -10,
|
useEffect(() => {
|
||||||
HEIGHT: 0,
|
const rain = {
|
||||||
|
wind: 0, maxXrange: 40, minXrange: 20, maxSpeed: 1, minSpeed: 3,
|
||||||
init: function (nb) {
|
color: "#8ecae6", char: "~", maxSize: 32, minSize: 10,
|
||||||
var o = this,
|
flakes: [], WIDTH: -10, HEIGHT: 0, running: false,
|
||||||
frag = document.createDocumentFragment();
|
init(nb) {
|
||||||
o.getSize();
|
const frag = document.createDocumentFragment();
|
||||||
|
this.getSize();
|
||||||
for (var i = 0; i < nb; i++) {
|
this.running = true;
|
||||||
var flake = {
|
for (let i = 0; i < nb; i++) {
|
||||||
x: o.random(o.WIDTH),
|
const flake = {
|
||||||
y: -o.maxSize,
|
x: this.random(this.WIDTH), y: -this.maxSize,
|
||||||
xrange: o.minXrange + o.random(o.maxXrange - o.minXrange),
|
xrange: this.minXrange + this.random(this.maxXrange - this.minXrange),
|
||||||
yspeed: o.minSpeed + o.random(o.maxSpeed - o.minSpeed, 100),
|
yspeed: this.minSpeed + this.random(this.maxSpeed - this.minSpeed, 100),
|
||||||
life: 0,
|
life: 0, size: this.minSize + this.random(this.maxSize - this.minSize),
|
||||||
size: o.minSize + o.random(o.maxSize - o.minSize),
|
|
||||||
html: document.createElement("span"),
|
html: document.createElement("span"),
|
||||||
};
|
};
|
||||||
|
Object.assign(flake.html.style, {
|
||||||
flake.html.style.position = "absolute";
|
position: "absolute", top: `${flake.y}px`, left: `${flake.x}px`,
|
||||||
flake.html.style.top = flake.y + "px";
|
fontSize: `${flake.size}px`, color: this.color, userSelect: "none", overflow: "hidden",
|
||||||
flake.html.style.left = flake.x + "px";
|
});
|
||||||
flake.html.style.fontSize = flake.size + "px";
|
flake.html.appendChild(document.createTextNode(this.char));
|
||||||
flake.html.style.color = o.color;
|
|
||||||
flake.html.appendChild(document.createTextNode(o.char));
|
|
||||||
frag.appendChild(flake.html);
|
frag.appendChild(flake.html);
|
||||||
flake.html.style.userSelect = "none";
|
this.flakes.push(flake);
|
||||||
flake.html.style.overflow = "hidden";
|
|
||||||
o.flakes.push(flake);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.appendChild(frag);
|
document.body.appendChild(frag);
|
||||||
o.animate();
|
this.animate();
|
||||||
|
window.onresize = () => this.getSize();
|
||||||
window.onresize = function () {
|
|
||||||
o.getSize();
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
animate() {
|
||||||
animate: function () {
|
if (!this.running) return;
|
||||||
var o = this;
|
for (const flake of this.flakes) {
|
||||||
for (var i = 0, c = o.flakes.length; i < c; i++) {
|
const top = flake.y + flake.yspeed;
|
||||||
var flake = o.flakes[i],
|
const left = flake.x + Math.sin(flake.life) * flake.xrange + this.wind;
|
||||||
top = flake.y + flake.yspeed,
|
if (top < this.HEIGHT - flake.size - 10 && left < this.WIDTH - flake.size && left > 0) {
|
||||||
left = flake.x + Math.sin(flake.life) * flake.xrange + o.wind;
|
flake.html.style.top = `${top}px`;
|
||||||
if (
|
flake.html.style.left = `${left}px`;
|
||||||
top < o.HEIGHT - flake.size - 10 &&
|
|
||||||
left < o.WIDTH - flake.size &&
|
|
||||||
left > 0
|
|
||||||
) {
|
|
||||||
flake.html.style.top = top + "px";
|
|
||||||
flake.html.style.left = left + "px";
|
|
||||||
flake.y = top;
|
flake.y = top;
|
||||||
flake.x += o.wind;
|
flake.x += this.wind;
|
||||||
flake.life += 0.01;
|
flake.life += 0.01;
|
||||||
} else {
|
} else {
|
||||||
flake.html.style.top = -o.maxSize + "px";
|
flake.html.style.top = `${-this.maxSize}px`;
|
||||||
flake.x = o.random(o.WIDTH);
|
flake.x = this.random(this.WIDTH);
|
||||||
flake.y = -o.maxSize;
|
flake.y = -this.maxSize;
|
||||||
flake.html.style.left = flake.x + "px";
|
flake.html.style.left = `${flake.x}px`;
|
||||||
flake.life = 0;
|
flake.life = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setTimeout(function () {
|
setTimeout(() => this.animate(), 20);
|
||||||
o.animate();
|
|
||||||
}, 20);
|
|
||||||
},
|
},
|
||||||
|
stop() {
|
||||||
stop: function () {
|
this.running = false;
|
||||||
for (var i = 0, c = this.flakes.length; i < c; i++) {
|
for (const flake of this.flakes) {
|
||||||
document.body.removeChild(this.flakes[i].html);
|
if (flake.html.parentNode) flake.html.parentNode.removeChild(flake.html);
|
||||||
}
|
}
|
||||||
this.flakes = [];
|
this.flakes = [];
|
||||||
},
|
},
|
||||||
|
random(range, num = 1) {
|
||||||
random: function (range, num) {
|
|
||||||
num = num ? num : 1;
|
|
||||||
return Math.floor(Math.random() * (range + 1) * num) / num;
|
return Math.floor(Math.random() * (range + 1) * num) / num;
|
||||||
},
|
},
|
||||||
|
getSize() {
|
||||||
getSize: function () {
|
|
||||||
this.WIDTH = document.body.clientWidth || window.innerWidth;
|
this.WIDTH = document.body.clientWidth || window.innerWidth;
|
||||||
this.HEIGHT = document.body.clientHeight || window.innerHeight;
|
this.HEIGHT = document.body.clientHeight || window.innerHeight;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { incrementClick, incrementWildCoin } = useWildCoin();
|
if (toggleRain) rain.init(10);
|
||||||
|
return () => rain.stop();
|
||||||
const createParticle = (x, y) => {
|
}, [toggleRain]);
|
||||||
const cookieClicks = document.querySelector(".pieces");
|
|
||||||
|
|
||||||
const particle = document.createElement("a");
|
|
||||||
particle.style.backgroundImage = "url('/png/w-coin.png')";
|
|
||||||
particle.setAttribute("class", "pieces-particle");
|
|
||||||
particle.style.left = x + "%";
|
|
||||||
particle.style.bottom = y + "px";
|
|
||||||
|
|
||||||
cookieClicks.appendChild(particle);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
cookieClicks.removeChild(particle);
|
|
||||||
}, 1500);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleIncrement = () => {
|
|
||||||
incrementWildCoin(incrementClick);
|
|
||||||
createParticle(50, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (toggleSnow) {
|
|
||||||
snow.init(10);
|
|
||||||
} else {
|
|
||||||
snow.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
snow.stop();
|
|
||||||
};
|
|
||||||
}, [toggleSnow]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const main = document.querySelector(".bghomecover");
|
|
||||||
const santa = document.querySelector(".santaclaus");
|
|
||||||
if (main !== undefined) {
|
|
||||||
if (biere[1] >= 1) {
|
|
||||||
santa.style.background = `url("/svg/SantaClause-drink.svg")`;
|
|
||||||
if (santaDrunk === true) {
|
|
||||||
main.style.filter = `blur(${biere[1]}px)`;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
console.count("setTimeOut");
|
|
||||||
main.style.filter = `blur(0px)`;
|
|
||||||
setSantaDrunk(false);
|
|
||||||
}, biere[1] * 5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [biere, setBiere]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="bghomecover">
|
<main className="game-cover">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<meta
|
<meta name="description" content="Clickerz — Clicker idle dans le Tetard Universe." />
|
||||||
name="description"
|
<title>Clickerz — Tetard Universe</title>
|
||||||
content="Xmass Click votre nouveau Clicker préféré !"
|
|
||||||
/>
|
|
||||||
<meta name="robots" content="index, follow" />
|
|
||||||
<meta
|
|
||||||
name="googlebot"
|
|
||||||
content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="bingbot"
|
|
||||||
content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1"
|
|
||||||
/>
|
|
||||||
<link rel="canonical" href="https://xmass.click/" />
|
|
||||||
<meta property="og:url" content="https://xmass.click/" />
|
|
||||||
<meta property="og:site_name" content="Xmass Click" />
|
|
||||||
<meta property="og:locale" content="fr_FR" />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:title" content="mywebsite | title" />
|
|
||||||
<meta
|
|
||||||
property="og:description"
|
|
||||||
content="Xmass Click votre nouveau Clicker préféré !"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
property="og:image"
|
|
||||||
content="https://xmass.click/webp/share-cover.webp"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
property="og:image:secure_url"
|
|
||||||
content="https://xmass.click/webp/share-cover.webp"
|
|
||||||
/>
|
|
||||||
<meta property="og:image:width" content="584" />
|
|
||||||
<meta property="og:image:height" content="384" />
|
|
||||||
<meta property="fb:pages" content="" />
|
|
||||||
<meta property="fb:admins" content="" />
|
|
||||||
<meta property="fb:app_id" content="" />
|
|
||||||
<meta name="twitter:card" content="summary" />
|
|
||||||
<meta name="twitter:site" content="" />
|
|
||||||
<meta name="twitter:creator" content="" />
|
|
||||||
<meta name="twitter:title" content="Xmass Click" />
|
|
||||||
<meta
|
|
||||||
name="twitter:description"
|
|
||||||
content="Xmass Click votre nouveau Clicker préféré !"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="twitter:image"
|
|
||||||
content="https://xmass.click/webp/share-cover.webp"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<title>Xmass Click</title>
|
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="santaposition">
|
|
||||||
<div className="pieces" />
|
{/* Clicker area — centre */}
|
||||||
<div className="santaclaus" onClick={handleIncrement} />
|
<div className="click-zone" onClick={handleIncrement}>
|
||||||
|
<div className="tadpole-sprite" />
|
||||||
|
<div className="text-center text-3xl md:text-4xl font-bold text-white drop-shadow-lg font-[var(--font)] select-none pointer-events-none">
|
||||||
|
{formatNumber(resources)}
|
||||||
</div>
|
</div>
|
||||||
<div className="boostList"></div>
|
</div>
|
||||||
|
|
||||||
|
{/* Game panels — sidebar (right desktop, bottom mobile) */}
|
||||||
|
<aside className="game-sidebar">
|
||||||
|
<MilestoneBar />
|
||||||
|
<GeneratorShop />
|
||||||
|
<PrestigePanel />
|
||||||
|
<EvolutionTree />
|
||||||
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
37
Frontend/src/pages/Landing.jsx
Normal file
37
Frontend/src/pages/Landing.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function Landing() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>Clickerz — Tetard Universe</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Clickerz — Clicker idle dans le Tetard Universe. Fais éclore des têtards, évolue et domine le marais !"
|
||||||
|
/>
|
||||||
|
</Helmet>
|
||||||
|
<main className="min-h-[92vh] mt-20 flex flex-col items-center justify-center gap-8 bg-[var(--bg-color)]">
|
||||||
|
<div className="flex flex-col items-center gap-4 text-center px-4">
|
||||||
|
<h1 className="text-4xl md:text-6xl font-bold text-gray-800 font-[var(--font)]">
|
||||||
|
Clickerz
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg md:text-xl text-gray-600 font-[var(--font)] max-w-md">
|
||||||
|
Fais éclore des têtards, construis ton empire et domine le marais.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/jeu"
|
||||||
|
className="px-8 py-4 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white text-lg font-semibold font-[var(--font)] transition-all hover:scale-105 shadow-lg shadow-emerald-600/30"
|
||||||
|
>
|
||||||
|
Entrer dans le Marais
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-500 font-[var(--font)]">
|
||||||
|
Pas de compte requis — joue en mode invité
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wildCoin {
|
.resource-counter {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: var(--font);
|
font-family: var(--font);
|
||||||
@@ -282,3 +282,33 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-grey);
|
color: var(--color-grey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
font-family: var(--font);
|
||||||
|
|
||||||
|
.auth-nickname {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-btn {
|
||||||
|
padding: 0.3rem 0.8rem;
|
||||||
|
border: 1px solid var(--color-grey);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background: none;
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-grey);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-grey);
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,58 +1,105 @@
|
|||||||
.bghomecover {
|
// home.scss — Game view styles
|
||||||
|
|
||||||
|
.game-cover {
|
||||||
background-image: url("/webp/bg-cover.webp");
|
background-image: url("/webp/bg-cover.webp");
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: bottom;
|
background-position: bottom;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
filter: blur(0px);
|
min-height: 92vh;
|
||||||
transition: filter 1s ease-in-out;
|
position: relative;
|
||||||
}
|
|
||||||
.santaposition {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: end;
|
}
|
||||||
|
|
||||||
.santaclaus {
|
// --- Clicker zone ---
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 5vh;
|
|
||||||
|
|
||||||
min-width: 320px;
|
.click-zone {
|
||||||
width: 320px;
|
display: flex;
|
||||||
min-height: 320px;
|
flex-direction: column;
|
||||||
height: 320px;
|
align-items: center;
|
||||||
z-index: 1;
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
background: url("/svg/SantaClause-bag.svg");
|
padding-bottom: 2vh;
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
&:active {
|
// Desktop: center
|
||||||
transform: rotate(2deg);
|
@media (min-width: 768px) {
|
||||||
|
padding-right: 22rem; // offset for sidebar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pieces-particle {
|
.tadpole-sprite {
|
||||||
width: 30px;
|
width: 280px;
|
||||||
height: 30px;
|
height: 280px;
|
||||||
position: absolute;
|
background: url("/svg/tadpole.svg") no-repeat center / contain;
|
||||||
bottom: 0;
|
transition: transform 0.1s ease;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
width: 320px;
|
||||||
|
height: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-zone:active & {
|
||||||
|
transform: scale(0.95) rotate(2deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Click feedback particle ---
|
||||||
|
|
||||||
|
.click-particle {
|
||||||
|
position: fixed;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: pieces-up 1.5s linear forwards;
|
font-family: var(--font);
|
||||||
background-position: center;
|
font-size: 1.2rem;
|
||||||
background-repeat: no-repeat;
|
font-weight: 700;
|
||||||
background-size: contain;
|
color: #34d399;
|
||||||
|
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 100;
|
||||||
|
animation: float-up 0.8s ease-out forwards;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@keyframes pieces-up {
|
@keyframes float-up {
|
||||||
0% {
|
0% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-60px) scale(1.3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
// --- Game sidebar ---
|
||||||
transform: rotate3d(0, 1, 0, 180deg);
|
|
||||||
opacity: 0;
|
.game-sidebar {
|
||||||
bottom: 100%;
|
position: fixed;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 5.5rem;
|
||||||
|
bottom: 0.75rem;
|
||||||
|
width: 20rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 10;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
|
||||||
|
// Mobile: bottom drawer
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
top: auto;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 45vh;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border-top-left-radius: 1rem;
|
||||||
|
border-top-right-radius: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
Frontend/src/utils/formatNumber.ts
Normal file
9
Frontend/src/utils/formatNumber.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// formatNumber.ts — Affichage formaté des grands nombres
|
||||||
|
|
||||||
|
export function formatNumber(n: number): string {
|
||||||
|
if (n >= 1e12) return `${(n / 1e12).toFixed(2)}T`;
|
||||||
|
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
|
||||||
|
if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
|
||||||
|
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
||||||
|
return Math.floor(n).toString();
|
||||||
|
}
|
||||||
44
deploy/clickerz.tetardtek.com.conf
Normal file
44
deploy/clickerz.tetardtek.com.conf
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Apache vhost — clickerz.tetardtek.com
|
||||||
|
# Frontend: static build served from /var/www/clickerz
|
||||||
|
# Backend API: reverse proxy to pm2 on port 3310
|
||||||
|
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName clickerz.tetardtek.com
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
|
<VirtualHost *:443>
|
||||||
|
ServerName clickerz.tetardtek.com
|
||||||
|
|
||||||
|
# SSL (certbot)
|
||||||
|
SSLEngine on
|
||||||
|
SSLCertificateFile /etc/letsencrypt/live/clickerz.tetardtek.com/fullchain.pem
|
||||||
|
SSLCertificateKeyFile /etc/letsencrypt/live/clickerz.tetardtek.com/privkey.pem
|
||||||
|
|
||||||
|
# Frontend — SPA static files
|
||||||
|
DocumentRoot /var/www/clickerz
|
||||||
|
<Directory /var/www/clickerz>
|
||||||
|
Options -Indexes +FollowSymLinks
|
||||||
|
AllowOverride None
|
||||||
|
Require all granted
|
||||||
|
|
||||||
|
# SPA fallback — all non-file routes → index.html
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteBase /
|
||||||
|
RewriteRule ^index\.html$ - [L]
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule . /index.html [L]
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
# Backend API — reverse proxy
|
||||||
|
ProxyPreserveHost On
|
||||||
|
ProxyPass /api http://127.0.0.1:3310/api
|
||||||
|
ProxyPassReverse /api http://127.0.0.1:3310/api
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
Header always set X-Content-Type-Options "nosniff"
|
||||||
|
Header always set X-Frame-Options "DENY"
|
||||||
|
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
</VirtualHost>
|
||||||
39
deploy/deploy.sh
Executable file
39
deploy/deploy.sh
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# deploy.sh — Build & deploy clickerz to VPS
|
||||||
|
# Usage: ssh vps 'cd /opt/clickerz && bash deploy/deploy.sh'
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "=== Clickerz deploy ==="
|
||||||
|
|
||||||
|
# 1. Pull latest
|
||||||
|
git pull --ff-only
|
||||||
|
|
||||||
|
# 2. Build frontend
|
||||||
|
echo "--- Building frontend..."
|
||||||
|
cd Frontend
|
||||||
|
npm ci --production=false
|
||||||
|
npm run build
|
||||||
|
echo "--- Copying dist to /var/www/clickerz..."
|
||||||
|
sudo rm -rf /var/www/clickerz
|
||||||
|
sudo cp -r dist /var/www/clickerz
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# 3. Backend deps
|
||||||
|
echo "--- Installing backend deps..."
|
||||||
|
cd Backend
|
||||||
|
npm ci --production
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# 4. Run migrations
|
||||||
|
echo "--- Running DB migrations..."
|
||||||
|
cd Backend
|
||||||
|
npm run db:migrate
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# 5. Restart pm2
|
||||||
|
echo "--- Restarting pm2..."
|
||||||
|
pm2 startOrRestart ecosystem.config.cjs --env production
|
||||||
|
pm2 save
|
||||||
|
|
||||||
|
echo "=== Deploy complete ==="
|
||||||
17
ecosystem.config.cjs
Normal file
17
ecosystem.config.cjs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// ecosystem.config.cjs — PM2 config for clickerz backend
|
||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: "clickerz-api",
|
||||||
|
cwd: "./Backend",
|
||||||
|
script: "index.js",
|
||||||
|
instances: 1,
|
||||||
|
autorestart: true,
|
||||||
|
watch: false,
|
||||||
|
env: {
|
||||||
|
NODE_ENV: "production",
|
||||||
|
PORT: 3310,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user