- {new Intl.NumberFormat().format(wildCoin)}
+
+ {new Intl.NumberFormat().format(Math.floor(resources))}
{navData.map((navIndex) => {
@@ -104,6 +101,18 @@ export default function Navbar({
);
})}
+ {user ? (
+
+ {user.nickname}
+
+
+ ) : (
+
+ Connexion
+
+ )}
![]()
toggleHud()}
src={imageSrc}
@@ -111,7 +120,7 @@ export default function Navbar({
alt="boutton on"
/>
![]()
toggleSnowBtn()}
+ onClick={() => toggleRainBtn()}
src={snowImageSrc}
style={{ height: "28px" }}
alt="boutton on"
diff --git a/Frontend/src/data/NavBarData.json b/Frontend/src/data/NavBarData.json
index 07d45e3..abc7fa1 100755
--- a/Frontend/src/data/NavBarData.json
+++ b/Frontend/src/data/NavBarData.json
@@ -2,7 +2,7 @@
{
"id": "1",
"linkname": "Jeu",
- "linkurl": "/",
+ "linkurl": "/jeu",
"btn": false
},
{
diff --git a/Frontend/src/main.jsx b/Frontend/src/main.jsx
index e78b11a..f99294f 100755
--- a/Frontend/src/main.jsx
+++ b/Frontend/src/main.jsx
@@ -1,10 +1,12 @@
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import App from "./App";
+import Landing from "./pages/Landing";
import Home from "./pages/Home";
import ErrorPage from "./pages/404";
-import { WildCoinProvider } from "./components/WildCoin/WildCoinContext";
-import Ameliorations from "./components/WildCoin/Amelioration";
+import Login from "./pages/Login";
+import AuthCallback from "./pages/AuthCallback";
+import { AuthProvider } from "./context/AuthContext";
import Boutique from "./pages/Boutique";
import Achievements from "./pages/Achievements";
import Legal from "./pages/Legal";
@@ -17,16 +19,12 @@ const router = createBrowserRouter([
children: [
{
path: "/",
+ element:
,
+ },
+ {
+ path: "/jeu",
element:
,
},
- {
- path: "*",
- element:
,
- },
- {
- path: "/ameliorations",
- element:
,
- },
{
path: "/boutique",
element:
,
@@ -43,6 +41,18 @@ const router = createBrowserRouter([
path: "/cookies",
element:
,
},
+ {
+ path: "/login",
+ element:
,
+ },
+ {
+ path: "/callback",
+ element:
,
+ },
+ {
+ path: "*",
+ element:
,
+ },
],
},
]);
@@ -50,7 +60,7 @@ const router = createBrowserRouter([
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
-
+
-
+
);
diff --git a/Frontend/src/pages/Boutique.jsx b/Frontend/src/pages/Boutique.jsx
index 7cfd896..78bb908 100755
--- a/Frontend/src/pages/Boutique.jsx
+++ b/Frontend/src/pages/Boutique.jsx
@@ -1,5 +1,4 @@
import BoutiqueCard from "../components/BoutiqueCard";
-// import { useWildCoin } from "..components/WildCoin/WildCoinContext";
import "../scss/shop.scss";
import shop from "../data/shop";
diff --git a/Frontend/src/pages/Home.jsx b/Frontend/src/pages/Home.jsx
index 95e3d98..0b5a270 100755
--- a/Frontend/src/pages/Home.jsx
+++ b/Frontend/src/pages/Home.jsx
@@ -1,231 +1,131 @@
import { Helmet } from "react-helmet";
import { useOutletContext } from "react-router-dom";
-import PropTypes from "prop-types";
-import "../scss/home.scss";
-import { useEffect } from "react";
+import { useEffect, useCallback } 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() {
- const [toggleSnow, setToggleSnow] = useOutletContext();
- Home.propTypes = {
- setToggleSnow: PropTypes.function,
- toggleSnow: PropTypes.bool,
- }.isRequired;
-
- const { biere, setBiere, santaDrunk, setSantaDrunk } = useWildCoin();
-
- var snow = {
- wind: 0,
- maxXrange: 40,
- minXrange: 20,
- maxSpeed: 1,
- minSpeed: 3,
- color: "#fff",
- char: "*",
- maxSize: 32,
- minSize: 10,
-
- flakes: [],
- WIDTH: -10,
- HEIGHT: 0,
-
- init: function (nb) {
- var o = this,
- frag = document.createDocumentFragment();
- o.getSize();
-
- for (var i = 0; i < nb; i++) {
- var flake = {
- x: o.random(o.WIDTH),
- y: -o.maxSize,
- xrange: o.minXrange + o.random(o.maxXrange - o.minXrange),
- yspeed: o.minSpeed + o.random(o.maxSpeed - o.minSpeed, 100),
- life: 0,
- size: o.minSize + o.random(o.maxSize - o.minSize),
- html: document.createElement("span"),
- };
-
- flake.html.style.position = "absolute";
- flake.html.style.top = flake.y + "px";
- flake.html.style.left = flake.x + "px";
- flake.html.style.fontSize = flake.size + "px";
- flake.html.style.color = o.color;
- flake.html.appendChild(document.createTextNode(o.char));
- frag.appendChild(flake.html);
- flake.html.style.userSelect = "none";
- flake.html.style.overflow = "hidden";
- o.flakes.push(flake);
- }
-
- document.body.appendChild(frag);
- o.animate();
-
- window.onresize = function () {
- o.getSize();
- };
- },
-
- animate: function () {
- var o = this;
- for (var i = 0, c = o.flakes.length; i < c; i++) {
- var flake = o.flakes[i],
- top = flake.y + flake.yspeed,
- left = flake.x + Math.sin(flake.life) * flake.xrange + o.wind;
- if (
- 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.x += o.wind;
- flake.life += 0.01;
- } else {
- flake.html.style.top = -o.maxSize + "px";
- flake.x = o.random(o.WIDTH);
- flake.y = -o.maxSize;
- flake.html.style.left = flake.x + "px";
- flake.life = 0;
- }
- }
- setTimeout(function () {
- o.animate();
- }, 20);
- },
-
- stop: function () {
- for (var i = 0, c = this.flakes.length; i < c; i++) {
- document.body.removeChild(this.flakes[i].html);
- }
- this.flakes = [];
- },
-
- random: function (range, num) {
- num = num ? num : 1;
- return Math.floor(Math.random() * (range + 1) * num) / num;
- },
-
- getSize: function () {
- this.WIDTH = document.body.clientWidth || window.innerWidth;
- this.HEIGHT = document.body.clientHeight || window.innerHeight;
- },
- };
-
- const { incrementClick, incrementWildCoin } = useWildCoin();
-
- const createParticle = (x, y) => {
- 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);
+ const [toggleRain] = useOutletContext();
+ const click = useGameStore((s) => s.click);
+ const resources = useGameStore((s) => s.state.resources);
+ const clickMultiplier = useGameStore((s) => s.state.clickMultiplier);
+ 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(() => {
- cookieClicks.removeChild(particle);
- }, 1500);
- };
+ if (particle.parentNode) particle.parentNode.removeChild(particle);
+ }, 800);
+ }, [clickMultiplier]);
- const handleIncrement = () => {
- incrementWildCoin(incrementClick);
- createParticle(50, 300);
- };
+ const handleIncrement = useCallback((e) => {
+ click();
+ createParticle(e.clientX, e.clientY);
+ }, [click, createParticle]);
+ // Rain effect (ambiance)
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);
+ const rain = {
+ wind: 0, maxXrange: 40, minXrange: 20, maxSpeed: 1, minSpeed: 3,
+ color: "#8ecae6", char: "~", maxSize: 32, minSize: 10,
+ flakes: [], WIDTH: -10, HEIGHT: 0, running: false,
+ init(nb) {
+ const frag = document.createDocumentFragment();
+ this.getSize();
+ this.running = true;
+ for (let i = 0; i < nb; i++) {
+ const flake = {
+ x: this.random(this.WIDTH), y: -this.maxSize,
+ xrange: this.minXrange + this.random(this.maxXrange - this.minXrange),
+ yspeed: this.minSpeed + this.random(this.maxSpeed - this.minSpeed, 100),
+ life: 0, size: this.minSize + this.random(this.maxSize - this.minSize),
+ html: document.createElement("span"),
+ };
+ Object.assign(flake.html.style, {
+ position: "absolute", top: `${flake.y}px`, left: `${flake.x}px`,
+ fontSize: `${flake.size}px`, color: this.color, userSelect: "none", overflow: "hidden",
+ });
+ flake.html.appendChild(document.createTextNode(this.char));
+ frag.appendChild(flake.html);
+ this.flakes.push(flake);
}
- }
- }
- }, [biere, setBiere]);
+ document.body.appendChild(frag);
+ this.animate();
+ window.onresize = () => this.getSize();
+ },
+ animate() {
+ if (!this.running) return;
+ for (const flake of this.flakes) {
+ const top = flake.y + flake.yspeed;
+ const left = flake.x + Math.sin(flake.life) * flake.xrange + this.wind;
+ if (top < this.HEIGHT - flake.size - 10 && left < this.WIDTH - flake.size && left > 0) {
+ flake.html.style.top = `${top}px`;
+ flake.html.style.left = `${left}px`;
+ flake.y = top;
+ flake.x += this.wind;
+ flake.life += 0.01;
+ } else {
+ flake.html.style.top = `${-this.maxSize}px`;
+ flake.x = this.random(this.WIDTH);
+ flake.y = -this.maxSize;
+ flake.html.style.left = `${flake.x}px`;
+ flake.life = 0;
+ }
+ }
+ setTimeout(() => this.animate(), 20);
+ },
+ stop() {
+ this.running = false;
+ for (const flake of this.flakes) {
+ if (flake.html.parentNode) flake.html.parentNode.removeChild(flake.html);
+ }
+ this.flakes = [];
+ },
+ random(range, num = 1) {
+ return Math.floor(Math.random() * (range + 1) * num) / num;
+ },
+ getSize() {
+ this.WIDTH = document.body.clientWidth || window.innerWidth;
+ this.HEIGHT = document.body.clientHeight || window.innerHeight;
+ },
+ };
+
+ if (toggleRain) rain.init(10);
+ return () => rain.stop();
+ }, [toggleRain]);
return (
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Xmass Click
+
+ Clickerz — Tetard Universe
-
-
-
+
+ {/* Clicker area — centre */}
+
+
+
+ {formatNumber(resources)}
+
-
+
+ {/* Game panels — sidebar (right desktop, bottom mobile) */}
+
);
}
diff --git a/Frontend/src/pages/Landing.jsx b/Frontend/src/pages/Landing.jsx
new file mode 100644
index 0000000..95b56bd
--- /dev/null
+++ b/Frontend/src/pages/Landing.jsx
@@ -0,0 +1,37 @@
+import { Helmet } from "react-helmet";
+import { Link } from "react-router-dom";
+
+export default function Landing() {
+ return (
+ <>
+
+ Clickerz — Tetard Universe
+
+
+
+
+
+ Clickerz
+
+
+ Fais éclore des têtards, construis ton empire et domine le marais.
+
+
+
+
+ Entrer dans le Marais
+
+
+
+ Pas de compte requis — joue en mode invité
+
+
+ >
+ );
+}
diff --git a/Frontend/src/scss/components/navbar.scss b/Frontend/src/scss/components/navbar.scss
index 10b9d31..490bf4f 100755
--- a/Frontend/src/scss/components/navbar.scss
+++ b/Frontend/src/scss/components/navbar.scss
@@ -274,7 +274,7 @@
}
}
-.wildCoin {
+.resource-counter {
display: flex;
align-items: center;
font-family: var(--font);
@@ -282,3 +282,33 @@
font-weight: 600;
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);
+ }
+ }
+}
diff --git a/Frontend/src/scss/home.scss b/Frontend/src/scss/home.scss
index 1e350f0..4eda785 100755
--- a/Frontend/src/scss/home.scss
+++ b/Frontend/src/scss/home.scss
@@ -1,58 +1,105 @@
-.bghomecover {
- background-image: url("/webp/bg-cover.webp");
- background-size: cover;
- background-repeat: no-repeat;
- background-position: bottom;
- width: 100%;
- filter: blur(0px);
- transition: filter 1s ease-in-out;
-}
-.santaposition {
- display: flex;
- justify-content: center;
- align-items: end;
-
- .santaclaus {
- display: block;
- position: absolute;
- bottom: 5vh;
-
- min-width: 320px;
- width: 320px;
- min-height: 320px;
- height: 320px;
- z-index: 1;
-
- background: url("/svg/SantaClause-bag.svg");
- background-repeat: no-repeat;
- background-size: contain;
- cursor: pointer;
-
- &:active {
- transform: rotate(2deg);
- }
- }
-
- .pieces-particle {
- width: 30px;
- height: 30px;
- position: absolute;
- bottom: 0;
- pointer-events: none;
- animation: pieces-up 1.5s linear forwards;
- background-position: center;
- background-repeat: no-repeat;
- background-size: contain;
- }
-}
-@keyframes pieces-up {
- 0% {
- opacity: 1;
- }
-
- 100% {
- transform: rotate3d(0, 1, 0, 180deg);
- opacity: 0;
- bottom: 100%;
- }
-}
+// home.scss — Game view styles
+
+.game-cover {
+ background-image: url("/webp/bg-cover.webp");
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: bottom;
+ width: 100%;
+ min-height: 92vh;
+ position: relative;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+}
+
+// --- Clicker zone ---
+
+.click-zone {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 1rem;
+ padding-bottom: 2vh;
+ cursor: pointer;
+ flex: 1;
+
+ // Desktop: center
+ @media (min-width: 768px) {
+ padding-right: 22rem; // offset for sidebar
+ }
+}
+
+.tadpole-sprite {
+ width: 280px;
+ height: 280px;
+ background: url("/svg/tadpole.svg") no-repeat center / contain;
+ 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;
+ font-family: var(--font);
+ font-size: 1.2rem;
+ font-weight: 700;
+ 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 float-up {
+ 0% {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+ 100% {
+ opacity: 0;
+ transform: translateY(-60px) scale(1.3);
+ }
+}
+
+// --- Game sidebar ---
+
+.game-sidebar {
+ 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;
+ }
+}
diff --git a/Frontend/src/utils/formatNumber.ts b/Frontend/src/utils/formatNumber.ts
new file mode 100644
index 0000000..4377057
--- /dev/null
+++ b/Frontend/src/utils/formatNumber.ts
@@ -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();
+}
diff --git a/deploy/clickerz.tetardtek.com.conf b/deploy/clickerz.tetardtek.com.conf
new file mode 100644
index 0000000..57f0cbf
--- /dev/null
+++ b/deploy/clickerz.tetardtek.com.conf
@@ -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
+
+
+ ServerName clickerz.tetardtek.com
+ RewriteEngine On
+ RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
+
+
+
+ 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
+
+ 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]
+
+
+ # 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"
+
diff --git a/deploy/deploy.sh b/deploy/deploy.sh
new file mode 100755
index 0000000..d4133a5
--- /dev/null
+++ b/deploy/deploy.sh
@@ -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 ==="
diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs
new file mode 100644
index 0000000..a2107c3
--- /dev/null
+++ b/ecosystem.config.cjs
@@ -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,
+ },
+ },
+ ],
+};