feat: initial import — ClickerZ formation project (Express + React/Vite)

This commit is contained in:
2026-03-15 14:29:33 +01:00
commit 4e93753250
118 changed files with 71039 additions and 0 deletions

35
Frontend/src/App.jsx Executable file
View File

@@ -0,0 +1,35 @@
import { useState } from "react";
import { Outlet } from "react-router-dom";
import Navbar from "./components/navbar";
import Footer from "./components/footer";
import Hud from "./components/Hud/Hud";
import "./scss/root.scss";
import "./scss/components/footer.scss";
import navData from "./data/NavBarData.json";
function App() {
const [isVisible, setIsVisible] = useState(false);
const [toggleSnow, setToggleSnow] = useState(false);
return (
<>
<Navbar
navData={navData}
isVisible={isVisible}
setIsVisible={setIsVisible}
toggleSnow={toggleSnow}
setToggleSnow={setToggleSnow}
/>
<Hud isVisible={isVisible} setIsVisible={setIsVisible} />
<main>
<Outlet context={[toggleSnow, setToggleSnow]} />
</main>
<Footer />
</>
);
}
export default App;

View File

@@ -0,0 +1,28 @@
import "../scss/components/achievementscard.scss";
import PropTypes from "prop-types";
function AchievementsCard({ name, description, image, key }) {
AchievementsCard.propTypes = {
name: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
key: PropTypes.number.isRequired,
};
return (
<div className="achievCardcontainer">
<img
className="achievecardpicture"
key={key}
src={image}
alt="cartes speciales"
/>
<div className="achievetitle">
<p className="achievname">{name}</p>
<p className="achievdescription">{description}</p>
</div>
</div>
);
}
export default AchievementsCard;

View File

@@ -0,0 +1,130 @@
import { useWildCoin } from "./WildCoin/WildCoinContext";
import "../scss/components/boutiquecard.scss";
import "../scss/components/buttons.scss";
import PropTypes from "prop-types";
export default function BoutiqueCard({
name,
price,
incrementValue,
description,
image,
link,
type,
buyed,
}) {
BoutiqueCard.propTypes = {
name: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
incrementValue: PropTypes.number.isRequired,
description: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
buyed: PropTypes.bool.isRequired,
};
const {
wildCoin,
incrementClick,
setWildCoin,
setIncrementClick,
incrementPerSecond,
setIncrementPerSecond,
setCoffee,
setSantaDrunk,
setManic,
setSnowman,
setBonnet,
setSugar,
setCookie,
setCouronne,
setEpice,
setBiere,
} = useWildCoin();
const acheterAmelioration = (type, price, name) => {
const prices = price;
const value = prices;
if (wildCoin >= value) {
if (type === "actif") {
setIncrementClick(incrementClick + incrementValue);
} else if (type === "passif") {
setIncrementPerSecond(incrementPerSecond + incrementValue);
}
setWildCoin(wildCoin - value);
switch (name) {
case "Tasse à café":
setCoffee((prevCoffee) => [true, prevCoffee[1] + 1]);
break;
case "Manic":
setManic((prevManic) => [true, prevManic[1] + 1]);
break;
case "Bonnet":
setBonnet((prevBonnet) => [true, prevBonnet[1] + 1]);
break;
case "Mr Bonhomme":
setSnowman((prevSnowman) => [true, prevSnowman[1] + 1]);
break;
case "Canne en sucre":
setSugar((prevSugar) => [true, prevSugar[1] + 1]);
break;
case "Cookie":
setCookie((prevCookie) => [true, prevCookie[1] + 1]);
break;
case "Couronne d'hiver":
setCouronne((prevCouronne) => [true, prevCouronne[1] + 1]);
break;
case "Mr pain d'épice":
setEpice((prevEpice) => [true, prevEpice[1] + 1]);
break;
case "Bière":
setBiere((prevBiere) => [true, prevBiere[1] + 1]);
setSantaDrunk(true);
break;
default:
break;
}
} else {
console.log("Pas assez de WildCoin pour acheter cette amélioration.");
}
};
return (
<div className="shopcardcontainer">
<div className="shopcontainer">
<div
className="cardpicture"
style={{ backgroundImage: `url(${image})` }}
alt={`image de ${name}`}
/>
<div>
<div className="titlesection">
<p className="itemname">{name}</p>
<div className="price">
<p className="itemprice">{price}</p>
<div className="priceicon" />
</div>
</div>
<div className="description">
<p className="itemdesc">
<em>
{type} + {incrementValue}
</em>
</p>
<p className="itemdesc">{description}</p>
</div>
</div>
<button
onClick={() => acheterAmelioration(type, price, name)}
className="primary-button"
>
Acheter
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,157 @@
import "../../scss/components/Hud.scss";
import { useWildCoin } from "../WildCoin/WildCoinContext";
import Timer from "../timer/Timer";
import propTypes from "prop-types";
function Hud({ isVisible }) {
Hud.propTypes = {
isVisible: propTypes.bool.isRequired,
};
const {
manic,
snowman,
bonnet,
sugar,
cookie,
couronne,
epice,
biere,
coffee,
} = useWildCoin();
const { incrementClick, incrementPerSecond } = useWildCoin();
const hiddenDiv = isVisible ? "none" : null;
return (
<div className="hudContainer">
<div style={{ display: hiddenDiv }} className="hudStats">
<div className="time section">
<p>Temps de jeu</p>
<p><Timer /></p>
</div>
<div className="auto section">
<p>Auto CPS</p>
<p>{incrementPerSecond}</p>
</div>
<div className="player section">
<p>Player Click</p>
<p>{incrementClick}</p>
</div>
</div>
<div className="hudBooster">
{coffee[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Tasse.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{coffee[1]}</p>
</div>
</div>
) : null}
{manic[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Hand.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{manic[1]}</p>
</div>
</div>
) : null}
{snowman[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Bonhome.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{snowman[1]}</p>
</div>
</div>
) : null}
{bonnet[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Bonnet.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{bonnet[1]}</p>
</div>
</div>
) : null}
{sugar[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Canne.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{sugar[1]}</p>
</div>
</div>
) : null}
{cookie[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Cookie.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{cookie[1]}</p>
</div>
</div>
) : null}
{couronne[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Courone.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{couronne[1]}</p>
</div>
</div>
) : null}
{epice[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/PainDep.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{epice[1]}</p>
</div>
</div>
) : null}
{biere[0] === true ? (
<div className="boosterItem">
<div
className="boosterIcon"
style={{ backgroundImage: `url(/svg/Beer.svg)` }}
alt="coffee"
/>
<div className="countbox">
<p className="boosterCount">{biere[1]}</p>
</div>
</div>
) : null}
</div>
</div>
);
}
export default Hud;

View File

@@ -0,0 +1,75 @@
import { useWildCoin } from "./WildCoinContext";
function Ameliorations() {
const {
wildCoin,
incrementClick,
setWildCoin,
setIncrementClick,
incrementPerSecond,
setIncrementPerSecond,
} = useWildCoin();
const activePrices = [5, 15, 50, 500]; // prix
const passivePrices = [5, 15, 50, 500];
const activeIncrementValues = [1, 3, 10, 100]; // boost = incrementValue
const passiveIncrementValues = [1, 3, 10, 100]; // = incrementValue
const acheterAmelioration = (type, amount) => {
const prices = type === "actif" ? activePrices : passivePrices;
const incrementValues =
type === "actif" ? activeIncrementValues : passiveIncrementValues;
const price = prices[amount - 1];
const incrementValue = incrementValues[amount - 1];
if (wildCoin >= price) {
if (type === "actif") {
setIncrementClick(incrementClick + incrementValue);
} else if (type === "passif") {
setIncrementPerSecond(incrementPerSecond + incrementValue);
}
setWildCoin(wildCoin - price);
} else {
console.log("Pas assez de WildCoin pour acheter cette amélioration.");
}
};
return (
<div className="divMagasinAmelio">
<h2>Magasin d'Améliorations</h2>
<div className="divAmelioActives">
<p>Améliorations Actives :</p>
{[1, 2, 3, 4].map((amount) => (
<div key={amount}>
Price: {activePrices[amount - 1]} - (+
{activeIncrementValues[amount - 1]})
<button
className="amelioActives"
onClick={() => acheterAmelioration("actif", amount)}
>
Acheter
</button>
</div>
))}
</div>
<div className="divAmelioPassives">
<p>Améliorations Passives :</p>
{[1, 2, 3, 4].map((amount) => (
<div key={amount}>
Price: {passivePrices[amount - 1]} - (+
{passiveIncrementValues[amount - 1]})
<button
className="amelioPassives"
onClick={() => acheterAmelioration("passif", amount)}
>
Acheter
</button>
</div>
))}
</div>
</div>
);
}
export default Ameliorations;

View File

@@ -0,0 +1,43 @@
import { createContext, useContext, useState, useEffect } from "react";
export const WildCoinContext = createContext();
export const useWildCoin = () => {
return useContext(WildCoinContext);
};
export function WildCoinProvider({ children }) {
// Value of coin
const [wildCoin, setWildCoin] = useState(0);
// increment by click state
const [incrementClick, setIncrementClick] = useState(1);
// increment inner useEffect state
const [incrementPerSecond, setIncrementPerSecond] = useState(1);
const incrementWildCoin = (amount) => {
setWildCoin((prevWildCoin) => prevWildCoin + amount);
};
/**
* @passiveGenerationInterval incre per sec wild coin in wildCoin
* */
useEffect(() => {
const passiveGenerationInterval = setInterval(() => {
incrementWildCoin(incrementPerSecond);
}, 1000);
return () => clearInterval(passiveGenerationInterval);
}, [incrementPerSecond]);
const value = {
wildCoin,
setWildCoin,
incrementClick,
incrementWildCoin,
};
return (
<WildCoinContext.Provider value={value}>
{children}
</WildCoinContext.Provider>
);
}

View File

@@ -0,0 +1,138 @@
import { createContext, useContext, useState, useEffect } from "react";
export const WildCoinContext = createContext();
export const useWildCoin = () => {
return useContext(WildCoinContext);
};
export function WildCoinProvider({ children }) {
const initialState = {
wildCoin: 0,
incrementClick: 1,
incrementPerSecond: 0,
};
const [state, setState] = useState(() => {
const storedContext = JSON.parse(localStorage.getItem("wildCoinContext"));
return {
...initialState,
...(storedContext || {}),
};
});
const [coffee, setCoffee] = useState([false, 0]);
const [manic, setManic] = useState([false, 0]);
const [snowman, setSnowman] = useState([false, 0]);
const [bonnet, setBonnet] = useState([false, 0]);
const [sugar, setSugar] = useState([false, 0]);
const [cookie, setCookie] = useState([false, 0]);
const [couronne, setCouronne] = useState([false, 0]);
const [epice, setEpice] = useState([false, 0]);
const [biere, setBiere] = useState([false, 0]);
const [santaDrunk, setSantaDrunk] = useState(false);
const updateWildCoin = (amount) => {
setState((prev) => ({
...prev,
wildCoin: prev.wildCoin + amount,
}));
};
const incrementWildCoin = (amount) => {
updateWildCoin(amount);
};
const setIncrementClick = (amount) => {
setState((prev) => ({
...prev,
incrementClick: amount,
}));
};
const setIncrementPerSecond = (amount) => {
setState((prev) => ({
...prev,
incrementPerSecond: amount,
}));
};
const setWildCoin = (amount) => {
setState((prev) => ({
...prev,
wildCoin: amount,
}));
};
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds((prevSeconds) => prevSeconds + 1);
}, 1000);
return () => clearInterval(intervalId);
}, []);
const formatTime = (time) => {
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time % 3600) / 60);
const seconds = time % 60;
const formattedTime = `${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
return formattedTime;
};
useEffect(() => {
localStorage.setItem("wildCoinContext", JSON.stringify(state));
}, [state]);
useEffect(() => {
const passiveGenerationInterval = setInterval(() => {
updateWildCoin(state.incrementPerSecond);
}, 1000);
return () => clearInterval(passiveGenerationInterval);
}, [state.incrementPerSecond]);
const contextValue = {
...state,
incrementWildCoin,
setIncrementClick,
setIncrementPerSecond,
setWildCoin,
coffee,
setCoffee,
manic,
setManic,
snowman,
setSnowman,
bonnet,
setBonnet,
sugar,
setSugar,
cookie,
setCookie,
couronne,
setCouronne,
epice,
setEpice,
biere,
setBiere,
setSantaDrunk,
santaDrunk,
seconds,
setSeconds,
formatTime,
};
return (
<WildCoinContext.Provider value={contextValue}>
{children}
</WildCoinContext.Provider>
);
}

View File

@@ -0,0 +1,16 @@
import { useWildCoin } from "./WildCoinContext";
import WildCoinS from "../../../public/WildCoin.svg";
function WildCoinIncrementAction() {
const { incrementClick, incrementWildCoin } = useWildCoin();
const handleIncrement = () => {
incrementWildCoin(incrementClick);
};
return (
<img src={WildCoinS} className="wildCoinBtn" style={{width:"40px", height:"40px"}} alt="Clique pour augmenter le score" aria-label="Clique pour augmenter le score" onClick={handleIncrement} />
);
}
export default WildCoinIncrementAction;

View File

@@ -0,0 +1,76 @@
import { NavLink as Link } from "react-router-dom";
import PropTypes from "prop-types";
import "../scss/components/navbar.scss";
import "../scss/root.scss";
import PrimaryButton from "./buttons/PrimaryButton";
export default function Burger({ navData }) {
return (
<nav className="menuToggle">
<input type="checkbox" aria-label="Menu" />
<span />
<span />
<span />
<ul className="menu">
{navData.map((navIndex) => {
if (navIndex.dropdown === undefined) {
return navIndex.btn === false ? (
<li key={navIndex.id}>
<Link className="mainLink" to={navIndex.linkurl}>
{navIndex.linkname}
</Link>
</li>
) : (
<li key={navIndex.id}>
<PrimaryButton
btnText={navIndex.linkname}
btnLink={navIndex.linkurl}
/>
</li>
);
}
return (
<li key={navIndex.id} className="dropdown">
<Link className="mainLink" to={navIndex.linkurl}>
{navIndex.linkname}
</Link>
<ul className="sousmenu">
{navIndex.dropdown.map((dropdown) => (
<li key={dropdown.id}>
<Link
className="dropLink"
to={navIndex.linkurl + dropdown.linkurl}
>
{dropdown.linkname}
</Link>
</li>
))}
</ul>
</li>
);
})}
</ul>
</nav>
);
}
Burger.propTypes = {
navData: PropTypes.arrayOf(
PropTypes.shape({
btn: PropTypes.bool,
id: PropTypes.string.isRequired,
linkname: PropTypes.string.isRequired,
linkurl: PropTypes.string.isRequired,
dropdown: PropTypes.arrayOf(
PropTypes.shape({
btn: PropTypes.bool,
id: PropTypes.string.isRequired,
linkname: PropTypes.string.isRequired,
linkurl: PropTypes.string.isRequired,
})
),
})
),
}.isRequired;

View File

@@ -0,0 +1,16 @@
import PropTypes from "prop-types";
import "../../scss/components/buttons.scss";
import { Link } from "react-router-dom";
export default function PrimaryButton({ btnText, btnLink }) {
PrimaryButton.propTypes = {
btnText: PropTypes.string.isRequired,
btnLink: PropTypes.string.isRequired,
};
return (
<Link className="primary-button" to={btnLink}>
{btnText}
</Link>
);
}

View File

@@ -0,0 +1,16 @@
import "../../scss/components/buttons.scss";
import PropTypes from "prop-types";
import { Link } from "react-router";
export default function SecondaryButton({ btnText, btnLink }) {
SecondaryButton.propTypes = {
btnText: PropTypes.string.isRequired,
btnLink: PropTypes.string.isRequired,
};
return (
<Link className="secondary-button" to={btnLink}>
{btnText}
</Link>
);
}

View File

@@ -0,0 +1,30 @@
import { Link } from "react-router-dom";
import PropTypes from "prop-types";
import "../../scss/components/CardContact.scss";
CardContact.propTypes = {
name: PropTypes.string.isRequired,
gitHub: PropTypes.string.isRequired,
};
function CardContact({ name, gitHub }) {
return (
<div className="cardContact">
<Link to={gitHub} target="_blank">
<button className="Btn">
<svg
className="svgIcon"
viewBox="0 0 496 512"
height="1.4em"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path>
</svg>
<span className="text">GitHub</span>
</button>
</Link>
<p>{name}</p>
</div>
);
}
export default CardContact;

View File

@@ -0,0 +1,84 @@
import "../scss/components/footer.scss";
import { NavLink as Link } from "react-router-dom";
import CardContact from "./cardsContact/CardContact";
const infoDev = [
{
id: "1",
name: "Alix C",
gitHub: "https://github.com/Halicksse",
},
{
id: "2",
name: "Sebatien L",
gitHub: "https://github.com/Lambseb",
},
{
id: "3",
name: "Baptiste S",
gitHub: "https://github.com/Batsave",
},
{
id: "4",
name: "Kevin T",
gitHub: "https://github.com/tetardtek",
},
{
id: "5",
name: "Nicolas DF",
gitHub: "https://github.com/Defreitasnicolas",
},
];
export default function Footer() {
return (
<footer className="footer">
<div className="footer-container">
<Link
to="/#home"
className="footer-logo"
alt="Logo"
aria-label="Logo Officiel"
title="Aller à la page d'accueil"
/>
<div className="section">
<p className="section-title">A Propos</p>
<p className="section-text">
Ce site est un prototype d'exercice développé lors d'un Hackathon
dans le cadre dune formation de Développeur Web et Mobile au sein
de la Wild Code School sur le campus Remote de Septembre 2023.
</p>
</div>
<div className="section">
<p className="section-title">Légale</p>
<ul className="section-list">
<li className="section-item">
<Link
to="/mentionslegales"
title="Aller à la page Mentions Légales"
>
Mentions Légales
</Link>
</li>
<li className="section-item">
<Link to="/cookies" title="Aller à la page Cookies">
Cookies
</Link>
</li>
<li className="section-item"></li>
</ul>
</div>
<div className="spacing" />
</div>
<div className="footer-section">
<div className="cardContactContainer">
{infoDev.map((info) => (
<CardContact key={info.id} name={info.name} gitHub={info.gitHub} />
))}
</div>
<p className="copyright">© 2023 | TrueQuiLeaks. Tous droits réservés.</p>
</div>
</footer>
);
}

View File

@@ -0,0 +1,146 @@
import { NavLink as Link } from "react-router-dom";
import PropTypes from "prop-types";
import "../scss/components/navbar.scss";
import "../scss/root.scss";
import PrimaryButton from "./buttons/PrimaryButton";
import Burger from "./burger";
import { useWildCoin } from "./WildCoin/WildCoinContext";
import HUDON from "../../public/NavBar/HUDON.svg";
import HUDOFF from "../../public/NavBar/HUDOFF.svg";
import SnowOn from "../../public/NavBar/SnowOn.svg";
import SnowOff from "../../public/NavBar/SnowOff.svg";
import { useState } from "react";
export default function Navbar({
navData,
isVisible,
setIsVisible,
toggleSnow,
setToggleSnow,
}) {
Navbar.propTypes = {
isVisible: PropTypes.bool,
setIsVisible: PropTypes.function,
setToggleSnow: PropTypes.function,
toggleSnow: PropTypes.bool,
}.isRequired;
const { wildCoin } = useWildCoin();
const [imageSrc, setImageSrc] = useState(HUDON);
const [snowImageSrc, setSnowImageSrc] = useState(SnowOff);
const [timerVisible, setTimerVisible] = useState(false);
const handleClickWildCoin = () => {
setTimerVisible(true);
};
const toggleHud = () => {
if (!isVisible) {
setIsVisible(true);
setImageSrc(HUDOFF);
} else {
setIsVisible(false);
setImageSrc(HUDON);
}
};
function toggleSnowBtn() {
if (toggleSnow === false) {
setToggleSnow(true);
setSnowImageSrc(SnowOn);
} else {
setToggleSnow(false);
setSnowImageSrc(SnowOff);
}
}
return (
<nav className="header-main">
<Link
className="logo"
to="/"
aria-label="Retourner à la page d'accueil"
title="Logo XmassClick"
/>
<div className="navbar">
<div className="wildCoin">
{new Intl.NumberFormat().format(wildCoin)}
</div>
<ul className="nav-list">
{navData.map((navIndex) => {
if (navIndex.dropdown === undefined) {
return navIndex.btn === false ? (
<li key={navIndex.id}>
<Link className="mainLink" to={navIndex.linkurl}>
{navIndex.linkname}
</Link>
</li>
) : (
<li key={navIndex.id}>
<PrimaryButton
btnText={navIndex.linkname}
btnLink={navIndex.linkurl}
/>
</li>
);
}
return (
<li key={navIndex.id} className="dropdown">
<Link className="mainLink" to={navIndex.linkurl}>
{navIndex.linkname}
</Link>
<ul className="dropdown-content">
{navIndex.dropdown.map((dropdown) => (
<li key={dropdown.id}>
<Link
className="dropLink"
to={navIndex.linkurl + dropdown.linkurl}
>
{dropdown.linkname}
</Link>
</li>
))}
</ul>
</li>
);
})}
</ul>
<img
onClick={() => toggleHud()}
src={imageSrc}
style={{ height: "28px" }}
alt="boutton on"
/>
<img
onClick={() => toggleSnowBtn()}
src={snowImageSrc}
style={{ height: "28px" }}
alt="boutton on"
/>
<Burger navData={navData} />
</div>
</nav>
);
}
Navbar.propTypes = {
navData: PropTypes.arrayOf(
PropTypes.shape({
btn: PropTypes.bool,
id: PropTypes.string,
linkname: PropTypes.string,
linkurl: PropTypes.string,
dropdown: PropTypes.arrayOf(
PropTypes.shape({
btn: PropTypes.bool,
id: PropTypes.string,
linkname: PropTypes.string,
linkurl: PropTypes.string,
})
),
})
),
};
Navbar.defaultProps = {
navData: [],
};

View File

@@ -0,0 +1,13 @@
import { useWildCoin } from "../WildCoin/WildCoinContext";
function Timer() {
const { formatTime, seconds } = useWildCoin();
return (
<div>
<p>{formatTime(seconds)}</p>
</div>
);
}
export default Timer;

View File

@@ -0,0 +1,142 @@
import React, {
createContext,
useContext,
useState,
useMemo,
useEffect,
} from "react";
import PropTypes from "prop-types";
import axios from "axios";
import { jwtDecode } from "jwt-decode";
const AuthContext = createContext();
const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
const jwtToken = localStorage.getItem("token");
if (jwtToken) {
try {
const decodedPayload = jwtDecode(jwtToken);
const res = await axios.get(
`http://localhost:3310/api/users/${decodedPayload.user}`
);
setUser(res.data);
} catch (error) {
console.error("Error fetching user data:", error);
} finally {
setLoading(false);
}
} else {
setLoading(false);
}
};
fetchData();
}, []);
const logout = () => {
localStorage.removeItem("token");
setUser(null);
};
const editUser = async (updatedFields) => {
try {
const response = await fetch(
`${import.meta.env.VITE_BACKEND_URL}/api/users/${user.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
body: JSON.stringify(updatedFields),
}
);
if (response.ok) {
const updatedUser = await response.json();
setUser((prevUser) => ({
...prevUser,
...updatedUser.user,
}));
return "User updated successfully";
}
if (response.status === 400) {
console.error("Bad Request:", response.statusText);
throw new Error("Bad Request");
} else if (response.status === 401) {
console.error("Unauthorized:", response.statusText);
throw new Error("Unauthorized");
} else {
console.error("Error updating user:", response.statusText);
throw new Error("Error updating user");
}
} catch (error) {
console.error("Error updating user:", error);
throw new Error("An error occurred during user update");
}
};
const sendPasswordResetEmail = async (email) => {
try {
const response = await fetch(
`${import.meta.env.VITE_BACKEND_URL}/api/forgot-password`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ mail: email }),
}
);
if (response.ok) {
return "Password reset email sent successfully";
}
const data = await response.json();
throw new Error(data.message || "Error sending password reset email");
} catch (error) {
console.error("Error sending password reset email:", error);
throw new Error(
"An error occurred while sending the password reset email"
);
}
};
const authContextValue = useMemo(() => {
return {
user,
loading,
logout,
editUser,
sendPasswordResetEmail,
setUser: (newUser) => {
setUser(newUser);
},
};
}, [user, loading, logout]);
return (
<AuthContext.Provider value={authContextValue}>
{children}
</AuthContext.Provider>
);
};
AuthProvider.propTypes = {
children: PropTypes.node.isRequired,
};
const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};

View File

@@ -0,0 +1,229 @@
{
"v": "5.4.4",
"fr": 29.9700012207031,
"ip": 0,
"op": 120.0000048877,
"w": 1080,
"h": 620,
"nm": "Comp 2",
"ddd": 0,
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "Shape Layer 1",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": { "a": 0, "k": 0, "ix": 10 },
"p": { "a": 0, "k": [539.5, 310, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 81],
[0, 0],
[-77, 0],
[0, -39],
[0, 0],
[17, -20],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, -81],
[0, 0],
[77, 0],
[0, 39],
[0, 0],
[-17, 20],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0]
],
"v": [
[-539.25, 234],
[-244, 234],
[-244, 126],
[-385, 126],
[-385, 62],
[-263, -238],
[-178, -238],
[-178, 54],
[-150, 54],
[-150, 120],
[-176, 120],
[-176, 234],
[-59, 234],
[-101, 143],
[-101, -160],
[-9, -248],
[100, -165],
[101, 157],
[80, 220],
[50, 240],
[288, 240],
[287, 123],
[148, 123],
[148, 56],
[268, -239],
[357, -239],
[357, 51],
[380, 51],
[380, 122],
[359, 122],
[359, 241.75],
[540.5, 242]
],
"c": false
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "st",
"c": {
"a": 0,
"k": [0.1, 0.1, 0.1, 1],
"ix": 3
},
"o": { "a": 0, "k": 100, "ix": 4 },
"w": { "a": 0, "k": 5, "ix": 5 },
"lc": 1,
"lj": 1,
"ml": 4,
"bm": 0,
"nm": "Stroke 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Shape 1",
"np": 3,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "tm",
"s": {
"a": 1,
"k": [
{
"i": { "x": [0.667], "y": [0.992] },
"o": { "x": [0.534], "y": [0.224] },
"t": 47,
"s": [0],
"e": [100]
},
{ "t": 95.0000038694293 }
],
"ix": 1
},
"e": {
"a": 1,
"k": [
{
"i": { "x": [0.667], "y": [0.96] },
"o": { "x": [0.677], "y": [0.024] },
"t": 15,
"s": [0],
"e": [100]
},
{ "t": 82.0000033399285 }
],
"ix": 2
},
"o": { "a": 0, "k": 0, "ix": 3 },
"m": 1,
"ix": 2,
"nm": "Trim Paths 1",
"mn": "ADBE Vector Filter - Trim",
"hd": false
}
],
"ip": 0,
"op": 120.0000048877,
"st": 0,
"bm": 0
}
],
"markers": []
}

View File

@@ -0,0 +1,386 @@
[
{
"id": 1,
"name": "Véritable cactus en peluche",
"founded": false,
"image": "https://i.goopics.net/n0tuti.jpg"
},
{
"id": 2,
"name": "Brosse à dents sans poil",
"founded": false,
"image": "https://i.goopics.net/hd42tk.jpg"
},
{
"id": 3,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Un jour, Dark Vador sest attaqué à Chuck Norris. Depuis, il fait de lasthme."
},
{
"id": 4,
"name": "Photo d'Ayoub",
"founded": false,
"image": "https://i.goopics.net/fpanvh.jpg"
},
{
"id": 5,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Pourquoi on met une selle sur un cheval ? Parce qu'en dessous, elle tomberait."
},
{
"id": 6,
"name": "Parapluie invisible",
"founded": false,
"image": "https://i.goopics.net/b8ms4o.jpg"
},
{
"id": 7,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Qu'est-ce qui est plus merveilleux que de faire tourner un enfant sur un tourniquet ? L'arrêter avec une pelle."
},
{
"id": 8,
"name": "Savon qui gratte",
"founded": false,
"image": "https://i.goopics.net/oeefev.jpg"
},
{
"id": 9,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Pourquoi les moutons aiment le chewing-gum ? Car c'est bon pour la laine."
},
{
"id": 10,
"name": "Moule à glaçons géant",
"founded": false,
"image": "https://i.goopics.net/2sdm53.png"
},
{
"id": 11,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Quand Chuck Norris fait un programme, il installe les modules, code et vend le programme... ensuite il demande à quoi il doit servir."
},
{
"id": 12,
"name": "Casquette avec ventilateur intégré",
"founded": false,
"image": "https://i.goopics.net/wjhe5i.jpg"
},
{
"id": 13,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Chuck Norris a invité Albert Einstein...à son dîner de cons."
},
{
"id": 14,
"name": "Game boy color de tonton",
"founded": false,
"image": "https://i.goopics.net/w82iwu.jpg"
},
{
"id": 15,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Vous savez pourquoi les pets puent ? Pour que les sourds en profitent !"
},
{
"id": 16,
"name": "Mug moustache",
"founded": false,
"image": "https://i.goopics.net/csu9z1.jpg"
},
{
"id": 17,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Qu'est ce qui est jaune et qui n'attend pas ? Un citron pressé"
},
{
"id": 18,
"name": "un pull de noël",
"founded": false,
"image": "https://i.goopics.net/uwjwn1.jpg"
},
{
"id": 19,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Que met un développeur sur sa voiture en hiver ? Une bash."
},
{
"id": 20,
"name": "Stickers Gnia gnia gnia 5 minutes la présentation",
"founded": false,
"image": "https://i.goopics.net/8jids3.png"
},
{
"id": 21,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Comment une blonde fait-elle pour faire un double de ses clefs ? Elle les photocopie."
},
{
"id": 22,
"name": "Photo de ta mamie...",
"founded": false,
"image": "https://i.goopics.net/htqzwa.jpg"
},
{
"id": 23,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Qu'est-ce qu'un cochon volant ? Un aéroport."
},
{
"id": 24,
"name": "Livre de names de Tonton",
"founded": false,
"image": "https://i.goopics.net/avr4b4.png"
},
{
"id": 25,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Mon grand-père avait prédit que le Titanic coulerait, il l'avait répété maintes fois...Mais on a préféré le virer de la salle de cinéma"
},
{
"id": 26,
"name": "Oreiller qui ronfle",
"founded": false,
"image": "https://i.goopics.net/4ydby9.png"
},
{
"id": 27,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Un jour, les Power Rangers ont combattu Chuck Norris. Depuis, on les appelle les Teletubbies."
},
{
"id": 28,
"name": "Haut-parleur de douche non étanche",
"founded": false,
"image": "https://i.goopics.net/ugqwdj.jpg"
},
{
"id": 29,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Qu'est-ce qui a deux pattes et qui saigne ? Un demi-chien..."
},
{
"id": 30,
"name": "Calendrier de l'Avent avec des chocolats au wasabi et moutarde forte",
"founded": false,
"image": "https://i.goopics.net/dakyj9.png"
},
{
"id": 31,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Sur quel site internet peut-on trouver un lave-vaisselle pas cher ? Meetic."
},
{
"id": 32,
"name": "Lunettes de soleil pour joueur de valorant",
"founded": false,
"image": "https://i.goopics.net/dxjicl.png"
},
{
"id": 33,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Chuck Norris mine de la crypto-monnaie...avec la calculette de sa montre Casio"
},
{
"id": 34,
"name": "Parfum au PHP: aucune odeur",
"founded": false,
"image": "https://i.goopics.net/o2435t.png"
},
{
"id": 35,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Quand Google ne trouve pas quelque chose, Il demande à Chuck Norris."
},
{
"id": 36,
"name": "Chapeau de Noël clignotant",
"founded": false,
"image": "https://i.goopics.net/d4su7e.png"
},
{
"id": 37,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Quelles sont les choses les plus lourdes de l'univers ? Soleil, Étoiles, Trou noir...et node_modules..."
},
{
"id": 38,
"name": "Kit de survie pour la fin du monde",
"founded": false,
"image": "https://i.goopics.net/ltcik6.png"
},
{
"id": 39,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Quelle est la fée la plus paresseuse ? La fée Néante"
},
{
"id": 40,
"name": "Bougie parfumée à l'essence de pizza",
"founded": false,
"image": "https://i.goopics.net/a3hv8n.jpg"
},
{
"id": 41,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Que mettre dans un kit de survie pour la fin du monde ? Du chocolat, des cookies et un DVD de !"
},
{
"id": 42,
"name": "Le livre mein... craft",
"founded": false,
"image": "https://i.goopics.net/fuy8kq.png"
},
{
"id": 43,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Un jour Hulk sest battu contre Chuck Norris. Depuis, il fait de la pub pour du maïs."
},
{
"id": 44,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Pourquoi les chaussettes ont-elles des orteils séparés ? Parce que même les pieds ont besoin d'intimité !"
},
{
"id": 45,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "J'ai dit non ! Mon gynécologue m'a dit pas de sexe pendant 3 semaines. Et que t'as dit ton dentiste ?"
},
{
"id": 46,
"name": "Chaussettes avec orteils séparés",
"founded": false,
"image": "https://i.goopics.net/vn3xht.png"
},
{
"id": 47,
"name": "Blague:",
"founded": false,
"image": "https://images.pexels.com/photos/1115680/pexels-photo-1115680.jpeg",
"description": "Quel est le jeu préféré des Portugais ?Call of d'outils"
},
{
"id": 48,
"name": "Christopher",
"description": "Un ange parmi les cieux, un nuage subtil et un glorieux soldat",
"founded": false,
"image": "./png/chris.png"
},
{
"id": 49,
"name": "Marie-Hermine",
"description": "La logique, le franc parlé, sans nul doute une perle dans un monde de brutes ",
"founded": false,
"image": "./png/m-h.png"
},
{
"id": 50,
"name": "Yavuz",
"description": "L'innovation culinaire, le cuisinier au grand coeur, l'avion petillant, l'or du Bayou. Il sait faire la fete, on l'appelle le PHP. ",
"founded": false,
"image": "./png/yavuz.png"
},
{
"id": 51,
"name": "Ayoub",
"description": "Un ami cher, loyal et de bon conseil, il saura vous épauler, vous ecouter, mais gare à toi, âme sensible ses mots peuvent etre francs et coupants mais toujours bienveillants. Un ami quoi!",
"founded": false,
"image": "./png/ayoub.png"
},
{
"id": 52,
"name": "Anthony",
"description": "Cupidon et Apollon n'ont qu'à bien se tenir, connu pour son physique legendaire, gardez vos femmes et vos hommes a double tour, a son passage c'est un ravage.",
"founded": false,
"image": "./png/antho.png"
},
{
"id": 53,
"name": "Vincent",
"description": "L'âme voyageuse, le vagabond au grand coeur et la main sur la bouteille. Il est le compagnon idéal pour un voyage animé. Et vogue, vogue la galère...",
"founded": false,
"image": "./png/vincent.png"
},
{
"id": 54,
"name": "Julien",
"description": "Il est l'élu, celui qui voit au dela de ce que vous voyez.",
"founded": false,
"image": "./png/julien.png"
},
{
"id": 55,
"name": "Benoit",
"description": "Maitre du temps, maitre de l'espace, sa parole est d'or et si tu ne respectes pas son temps, tu sors. ",
"founded": false,
"image": "./png/benoit.png"
},
{
"id": 56,
"name": "Samuel",
"description": "Des yeux perçants, les crocs acérés, tu le rencontreras dans la pénombre ou une foret enchantée. N'aies pas peur de ses canines affutées, son sourire et sa gentillesse sauront te rassurer. ",
"founded": false,
"image": "./png/samuel.png"
},
{
"id": 57,
"name": "Ayoub Ultime",
"description": "La carte ultime, c'est l'immunité à toutes épreuves! Un style, une classe, un élan d'élégance et une intelligence raffraîchissante. La meilleure carte. Tout simplement. ",
"founded": false,
"image": "./png/ayoub-ultimate.png"
},
{
"id": 58,
"name": "Une photo des formateurs de la Wild",
"founded": false,
"image": "https://i.goopics.net/8m6t45.jpg"
}
]

View File

@@ -0,0 +1,20 @@
[
{
"id": "1",
"linkname": "Jeu",
"linkurl": "/",
"btn": false
},
{
"id": "2",
"linkname": "Boutique",
"linkurl": "/boutique",
"btn": false
},
{
"id": "3",
"linkname": "Succès",
"linkurl": "/achievements",
"btn": false
}
]

92
Frontend/src/data/shop.json Executable file
View File

@@ -0,0 +1,92 @@
[
{
"name": "Manic",
"price": 15,
"incrementValue": 1,
"description": "Evite de vous bruler quand vous sortez les cookies du four, vous gagnez 5 CPS",
"link": "/",
"image": "./svg/Hand.svg",
"buyed": false,
"type": "actif"
},
{
"name": "Tasse à café",
"price": 15,
"incrementValue": 1,
"description": "Bien chaud vous permet de tenir sur la durée",
"link": "/",
"image": "./svg/Tasse.svg",
"buyed": false,
"type": "passif"
},
{
"name": "Mr Bonhomme",
"price": 150,
"incrementValue": 10,
"description": "Un assistant idéal pour le click",
"link": "/",
"image": "./svg/Bonhome.svg",
"buyed": false,
"type": "actif"
},
{
"name": "Bonnet",
"price": 150,
"incrementValue": 10,
"description": "Garder vos oreilles bien à l'abri du froid et click !",
"link": "/",
"image": "./svg/Bonnet.svg",
"buyed": false,
"type": "passif"
},
{
"name": "Cookie",
"price": 1500,
"incrementValue": 100,
"description": "Fait avec amour",
"link": "/",
"image": "./svg/Cookie.svg",
"buyed": false,
"type": "actif"
},
{
"name": "Canne en sucre",
"price": 1500,
"incrementValue": 100,
"description": "Le sucre c'est connu, ca reboost",
"link": "/",
"image": "./svg/Canne.svg",
"buyed": false,
"type": "passif"
},
{
"name": "Couronne d'hiver",
"price": 15000,
"incrementValue": 1000,
"description": "Un bisous ou rien du tout !",
"link": "/",
"image": "./svg/Courone.svg",
"buyed": false,
"type": "actif"
},
{
"name": "Mr pain d'épice",
"price": 15000,
"incrementValue": 1000,
"description": "Le meilleur c'est la tête",
"link": "/",
"image": "./svg/PainDep.svg",
"buyed": false,
"type": "passif"
},
{
"name": "Bière",
"price": 8000,
"incrementValue": 1000,
"description": "Boisson de qualité, double tout les CPS, attention à ne pas trop en abuser",
"link": "/",
"image": "./svg/Beer.svg",
"buyed": false,
"type": "actif"
}
]

10
Frontend/src/index.css Executable file
View File

@@ -0,0 +1,10 @@
:root {
margin: 0;
padding: 0;
}
::-webkit-scrollbar {
width: 1px;
display: none;
}

56
Frontend/src/main.jsx Executable file
View File

@@ -0,0 +1,56 @@
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import App from "./App";
import Home from "./pages/Home";
import ErrorPage from "./pages/404";
import { WildCoinProvider } from "./components/WildCoin/WildCoinContext";
import Ameliorations from "./components/WildCoin/Amelioration";
import Boutique from "./pages/Boutique";
import Achievements from "./pages/Achievements";
import Legal from "./pages/Legal";
import Cookie from "./pages/Cookie";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{
path: "/",
element: <Home />,
},
{
path: "*",
element: <ErrorPage />,
},
{
path: "/ameliorations",
element: <Ameliorations />,
},
{
path: "/boutique",
element: <Boutique />,
},
{
path: "/achievements",
element: <Achievements />,
},
{
path: "/mentionslegales",
element: <Legal />,
},
{
path: "/cookies",
element: <Cookie />,
},
],
},
]);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<WildCoinProvider>
<RouterProvider router={router} />
</WildCoinProvider>
);

0
Frontend/src/pages/.gitkeep Executable file
View File

26
Frontend/src/pages/404.jsx Executable file
View File

@@ -0,0 +1,26 @@
import { Link } from "react-router-dom";
import "../scss/pages.scss";
import Lottie from "react-lottie-player";
import animation404 from "../data/404-animation.json";
export default function NotFound() {
return (
<section>
<div className="containererror">
<Lottie
loop
animationData={animation404}
play
style={{ width: 260, height: 150 }}
/>
<h1>Oops! Il semble que la page n'existe pas.</h1>
<p className="message">
Nous vous conseillons de retourner à la page d'accueil.
</p>
<Link className="btn-return" to="/">
retourner à la page d'accueil
</Link>
</div>
</section>
);
}

View File

@@ -0,0 +1,37 @@
import { useState } from "react";
import AchievementsCard from "../components/AchievementsCard";
import "../scss/achievements.scss";
import { useWildCoin } from "../components/WildCoin/WildCoinContext";
import achievements from "../data/Achievements.json";
function Achievements() {
const { wildCoin } = useWildCoin();
let score = 1;
if (wildCoin >= 25) {
score = Math.floor((wildCoin - 25) / 400) + 1;
} else {
score = 0;
}
return (
<div className="fullachieve">
<h1>Succès</h1>
<div className="achievementscontainer">
<div className="achievementscardcontainer">
{achievements &&
achievements.slice(0, score).map((a) => {
return (
<AchievementsCard
key={a.id}
name={a.name}
description={a.description}
image={a.image}
/>
);
})}
</div>
</div>
</div>
);
}
export default Achievements;

33
Frontend/src/pages/Boutique.jsx Executable file
View File

@@ -0,0 +1,33 @@
import BoutiqueCard from "../components/BoutiqueCard";
// import { useWildCoin } from "..components/WildCoin/WildCoinContext";
import "../scss/shop.scss";
import shop from "../data/shop";
export default function Boutique() {
return (
<main>
<div className="shoppagecontainer">
<h1>Boutique</h1>
<div className="cardcontainer">
{shop.map((item) => {
return (
<BoutiqueCard
key={item.name}
name={item.name}
price={item.price}
incrementValue={item.incrementValue}
description={item.description}
image={item.image}
link={item.link}
type={item.type}
buyed={item.buyed}
/>
);
})}
</div>
</div>
</main>
);
}

100
Frontend/src/pages/Cookie.jsx Executable file
View File

@@ -0,0 +1,100 @@
import "../scss/Cookie.scss";
function Cookie() {
return (
<div className="container">
<div className="item">
<h2>Quest-ce quun cookie ?</h2>
<p>
Un cookie est un petit fichier texte sauvegardé sur votre ordinateur
lorsque vous visitez un site web. Ce fichier texte enregistre des
informations qui peuvent être lues par un site web lorsque vous le
visitez de nouveau plus tard. Certains de ces cookies sont nécessaires
pour accéder à certaines fonctionnalités dun site. Dautres cookies
sont dutilité pratique pour le visiteur : ils sauvegardent de manière
sécurisée votre nom dutilisateur ou vos préférences linguistiques par
exemple. Les cookies signifient tout simplement quà chaque fois que
vous visitez un site web, vous navez pas besoin de saisir à nouveau
les mêmes informations.
</p>
</div>
<div className="item">
<h2>Pourquoi Xmass Clicker utilise des cookies ?</h2>
<p>
Nous utilisons des cookies pour vous fournir une expérience
utilisateur optimale et adaptée à vos préférences personnelles. En
utilisant les cookies, Les cookies sont également utilisés pour
optimiser la performance du site. Xmass Clicker a pris toutes les
mesures organisationnelles et techniques pour protéger vos données
personnelles ainsi que dune éventuelle perte dinformations ou de
toute forme de traitement illicite. Pour davantage dinformations,
consultez notre Politique de confidentialité.
</p>
</div>
<div className="item">
<h2>Comment puis-je désactiver les cookies ?</h2>
<p>
Vous pouvez paramétrer votre navigateur Internet pour désactiver les
cookies. Notez toutefois que si vous désactivez les cookies, votre nom
dutilisateur ainsi que votre mot de passe ne seront plus sauvegardés
sur aucun site web.
</p>
</div>
<div className="item">
<h2>Firefox :</h2>
<p>
1. Ouvrez Firefox <br />
2. Appuyez sur la touche « Alt » <br />
3. Dans le menu en haut de la page cliquez sur « Outils » puis «
Options » <br />
4. Sélectionnez longlet « Vie privée » <br />
5. Dans le menu déroulant à droite de « Règles de conservation »,
cliquez sur « utiliser les paramètres personnalisés pour lhistorique
» <br />
6. Un peu plus bas, décochez « Accepter les cookies » <br />
7. Sauvegardez vos préférences en cliquant sur « OK »
</p>
</div>
<div className="item">
<h2>Internet Explorer/Edge :</h2>
<p>
1. Ouvrez Internet Explorer <br />
2. Dans le menu « Outils », sélectionnez « Options Internet » <br />
3. Cliquez sur longlet « Confidentialité » <br />
4. Cliquez sur « Avancé » et décochez « Accepter » <br />
5. Sauvegardez vos préférences en cliquant sur « OK »
</p>
</div>
<div className="item">
<h2>Safari :</h2>
<p>
1. Ouvrez Safari <br />
2. Dans la barre de menu en haut, cliquez sur « Safari », puis «
Préférences » <br />
3. Sélectionnez licône « Sécurité » <br />
4. À côté de « Accepter les cookies », cochez « Jamais » <br />
5. Si vous souhaitez voir les cookies qui sont déjà sauvegardés sur
votre ordinateur, cliquez sur « Afficher les cookies »
</p>
</div>
<div className="item">
<h2>Google Chrome :</h2>
<p>
1. Ouvrez Google Chrome <br />
2. Cliquez sur licône doutils dans la barre de menu <br />
3. Sélectionnez « Options » <br />
4. Cliquez sur longlet « Options avancées » <br />
5. Dans le menu déroulant « Paramètres des cookies », sélectionnez «
Bloquer tous les cookies »
</p>
</div>
</div>
);
}
export default Cookie;

231
Frontend/src/pages/Home.jsx Executable file
View File

@@ -0,0 +1,231 @@
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 { useWildCoin } from "../components/WildCoin/WildCoinContext";
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);
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 (
<main className="bghomecover">
<Helmet>
<meta
name="description"
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>
<div className="santaposition">
<div className="pieces" />
<div className="santaclaus" onClick={handleIncrement} />
</div>
<div className="boostList"></div>
</main>
);
}

53
Frontend/src/pages/Legal.jsx Executable file
View File

@@ -0,0 +1,53 @@
import "../scss/Legal.scss";
function Legal() {
return (
<div className="mentionslegales">
<h2>Éditeur :</h2>
<p>
Xmass'Click est un projet réalisé dans le cadre d'un hackathon sur 2
jours.
</p>
<h2>Coordonnées :</h2>
<p>
Téléphone : 04 22 52 10 10 <br />
E-mail : pere-noel@laposte.net <br />
Adresse : 250 avenue des Nuages, 1000 Pôle Nord <br />
</p>
<h2>Responsabilité :</h2>
<p>
Xmass'Click décline toute responsabilité quant à l'utilisation du site.
Les informations fournies sont à titre informatif et peuvent être
sujettes à des erreurs.
</p>
<h2>Propriété Intellectuelle :</h2>
<p>
Tout le contenu du site (textes, images, etc.) reste la propriété de
Xmass'Click. Toute reproduction est interdite sans autorisation
préalable.
</p>
<h2>Protection des Données Personnelles :</h2>
<p>
Xmass'Click ne collecte pas de données personnelles. Aucune information
personnelle n'est stockée lors de l'utilisation du site.
</p>
<h2>Conditions Générales d'Utilisation :</h2>
<p>
Aucune condition générale d'utilisation n'est applicable. L'utilisation
du site Xmass'Click se fait à titre gratuit et sans engagement.
</p>
<h2>Loi Applicable :</h2>
<p>
Le présent site est régi par la loi du Pôle Nord. En cas de litige, les
tribunaux du Père Noël seront compétents.
</p>
</div>
);
}
export default Legal;

3
Frontend/src/scss/App.scss Executable file
View File

@@ -0,0 +1,3 @@
a {
text-decoration: none;
}

19
Frontend/src/scss/Cookie.scss Executable file
View File

@@ -0,0 +1,19 @@
.container {
width: 100%;
margin: 0 auto;
max-width: 1280px;
font-family: var(--font);
display: flex;
flex-direction: column;
gap: 3rem;
padding: 15rem 1rem 4rem;
}
h2 {
font-family: var(--font);
font-size: 2rem;
font-weight: 600;
color: var(--color-grey);
}

19
Frontend/src/scss/Legal.scss Executable file
View File

@@ -0,0 +1,19 @@
.mentionslegales {
width: 100%;
margin: 0 auto;
max-width: 1280px;
font-family: var(--font);
display: flex;
flex-direction: column;
gap: 3rem;
padding: 15rem 1rem 4rem;
}
h2 {
font-family: var(--font);
font-size: 2rem;
font-weight: 600;
color: var(--color-grey);
}

View File

@@ -0,0 +1,31 @@
.fullachieve {
display: flex;
flex-direction: column;
padding-top: 10rem;
background-color: var(--color-blue-light);
width: 100%;
max-width: 1280px;
margin: 0 auto;
}
.achievementscontainer {
margin: auto;
display: flex;
align-items: center;
}
h1 {
text-align: center;
font-family: var(--font);
font-size: 3rem;
color: var(--color-grey);
margin-bottom: 3rem;
}
.achievementscardcontainer {
display: flex;
justify-content: center;
flex-wrap: wrap;
min-height: 300px;
gap: 3rem;
}

View File

@@ -0,0 +1,73 @@
.Btn {
border: none;
border-radius: 50%;
width: 45px;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
transition-duration: .4s;
cursor: pointer;
position: relative;
background-color: rgb(31, 31, 31);
overflow: hidden;
}
.svgIcon {
transition-duration: .3s;
}
.svgIcon path {
fill: var(--color-white)
}
.text {
position: absolute;
font-family: var(--font);
color: var(--color-white);
width: 120px;
font-weight: 600;
opacity: 0;
transition-duration: .4s;
}
.Btn:hover {
width: 110px;
transition-duration: .4s;
border-radius: 30px;
}
.Btn:hover .text {
opacity: 1;
transition-duration: .4s;
}
.Btn:hover .svgIcon {
opacity: 0;
transition-duration: .3s;
}
.cardContact {
display: flex;
min-width: 100px ;
align-items: center;
flex-direction: column;
}
.cardContactContainer {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 5rem;
margin-bottom: 1rem;
}
.cardContact p {
font-family: var(--font);
color: var(--color-grey);
}

View File

@@ -0,0 +1,102 @@
.hudContainer {
display: flex;
flex-direction: column;
flex-wrap: wrap;
align-items: center;
justify-content: space-around;
min-width: 260px;
width: fit-content;
max-width: 1280px;
height: fit-content;
gap: 1rem;
padding: 1rem;
border-radius: 8px;
box-sizing: border-box;
background-color: var(--color-grey);
color: var(--color-white);
font-family: var(--font);
color: aliceblue;
font-size: 1rem;
text-align: center;
position: absolute;
left: 50%;
transform: translate(-50%);
z-index: 2;
.hudStats {
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
.section {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
}
.hudBooster {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
align-items: center;
padding: 1rem;
gap: 1rem;
min-width: 280px;
width: auto;
max-width: 1280px;
height: fit-content;
color: var(--color-white);
font-family: var(--font);
color: aliceblue;
font-size: 1rem;
text-align: center;
border-radius: 8px;
box-sizing: border-box;
.boosterItem {
display: flex;
flex-wrap: wrap;
width: 30px;
height: 30px;
gap: 0.6rem;
.boosterIcon {
width: 100%;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.countbox {
position: absolute;
margin-top: 20px;
margin-left: 15px;
border: solid 1px var(--color-white);
background-color: var(--color-white);
border-radius: 20%;
padding: 0.1rem;
color: var(--color-grey);
min-width: 20px;
width: fit-content;
height: 20px;
box-shadow: -1px -1px 7px 0px var(--color-grey);
}
.boosterCount {
font-family: var(--font);
font-size: 0.7rem;
text-align: center;
font-weight: 900;
}
}
}
}

View File

@@ -0,0 +1,33 @@
.achievCardcontainer {
display: flex;
flex-direction: column;
align-items: center;
border: solid 0.05rem;
max-width: 250px;
border-radius: 1rem;
background-color: rgb(255, 255, 255);
margin-bottom: 1rem;
box-shadow: 1px 1px 10px 2px var(--color-grey);
}
.achievecardpicture {
width: 100%;
border-radius: 1rem 1rem 0 0;
}
.achievname {
font-size: 1.2rem;
margin-top: 0.2rem;
font-family: var(--font);
text-align: center;
color:rgb(29, 30, 30);
}
.achievdescription {
font-size: 1rem;
font-weight: 400;
color: var(--color-grey);
font-family: var(--font);
color:rgb(25, 25, 26);
padding: 1rem;
}

View File

@@ -0,0 +1,76 @@
.shopcardcontainer {
display: flex;
flex-direction: column;
width: 300px;
min-height: 520px;
padding: 1rem;
border-radius: 1rem;
align-items: center;
box-sizing: border-box;
background-color: var(--color-white);
font-family: var(--font);
.shopcontainer {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 1rem;
.cardpicture {
width: 100%;
height: 300px;
padding: 3rem;
background-size: 50%;
background-position: center;
background-repeat: no-repeat;
border-radius: 10px;
background-color: var(--color-purple-light);
box-sizing: border-box;
}
.titlesection {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 1rem;
.itemname {
font-size: 1.2rem;
font-weight: 600;
color: var(--color-grey);
}
.price {
display: flex;
gap: 0.2rem;
font-size: 1rem;
font-weight: 400;
color: var(--color-grey);
.itemprice {
font-weight: 600;
color: var(--color-red-light);
}
.priceicon {
width: 22px;
height: 22px;
background-image: url("/png/w-coin.png");
background-size: contain;
background-position: center;
background-repeat: no-repeat;
}
}
}
.description {
font-size: 1rem;
font-weight: 400;
color: var(--color-grey);
}
}
.buttoncard {
width: 100%;
}
}

View File

@@ -0,0 +1,46 @@
.primary-button {
display: flex;
padding: 0.6rem 1rem;
height: fit-content;
background-color: var(--color-red-light);
border-radius: 0.6rem;
justify-content: center;
text-decoration: none;
font-family: var(--font);
color: var(--color-white) !important;
text-align: center;
font-size: 1rem;
font-weight: 400;
transition: transform 0.1s ease-in-out;
border: none;
&:hover {
transform: scale(0.95);
background-color: var(--color-red-light);
}
}
.secondary-button {
display: flex;
padding: 1rem 1rem;
background-color: var(--color-white);
border-radius: 0.6rem;
justify-content: center;
width: fit-content;
height: fit-content;
text-decoration: none;
font-family: var(--font);
color: var(--color-grey)!important;
text-align: center;
font-size: 1rem;
transition: transform 0.1s ease-in-out;
border: none;
&:hover {
transform: scale(0.95);
background-color: var(--color-grey-hover);
}
}

View File

@@ -0,0 +1,92 @@
.footer {
display: flex;
flex-direction: column;
position: relative;
align-items: center;
bottom: 0;
left: 0;
width: 100%;
background-color: var(--bg-color);
border-top: solid 1px var(--color-grey);
padding: 2rem 0;
gap: 2rem;
.footer-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
width: 90%;
gap: 2rem;
}
.footer-logo {
background-image: url(/svg/logo.svg);
background-size: contain;
background-position: center;
background-repeat: no-repeat;
width: 250px;
height: 100px;
transition: all 0.15s ease-in-out;
&:hover {
transform: scale(0.9);
}
}
.section {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 1.4rem;
.section-title {
font-family: var(--font);
font-size: 1.2rem;
color: var(--color-grey);
text-decoration-line: underline;
text-underline-offset: 0.5rem;
}
.section-text {
max-width: 26ch;
font-family: var(--font);
font-size: 1rem;
color: var(--color-grey);
}
.section-list {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 1rem;
list-style: none;
.section-item,
a {
width: fit-content;
font-family: var(--font);
font-size: 1rem;
color: var(--color-grey);
transition: all 0.15s ease-in-out;
&:hover {
transform: scale(0.9);
}
}
}
}
.spacing {
min-width: 150px;
width: 10%;
}
.copyright {
font-family: var(--font);
font-size: 0.8rem;
font-weight: 300;
color: var(--color-grey);
text-align: center;
}
}

View File

@@ -0,0 +1,284 @@
.header-main {
display: flex;
justify-content: space-between;
position: absolute;
width: 100%;
height: 80px;
padding: 0rem 2rem;
top: 0;
background-color: var(--bg-color);
background-blend-mode: darken;
background-size: cover;
z-index: 99;
box-sizing: border-box;
@media (max-width: 999px) {
padding: 0rem 0.4rem;
box-sizing: border-box;
}
}
.logo {
width: 5rem;
content: url(/svg/logo.svg);
transition: 0.2s;
&:hover {
width: 5rem;
transition: 0.2s;
transform: scale(0.9);
}
}
.navbar {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 1rem;
box-sizing: border-box;
cursor: pointer;
.nav-list {
display: flex;
justify-content: space-between;
gap: 1.6rem;
align-items: center;
@media screen and (max-width: 999px) {
display: none;
}
li {
list-style: none;
font-family: var(--font);
font-weight: 300;
font-size: 1rem;
color: var(--color-white);
height: 100%;
.mainLink {
text-decoration: none;
color: var(--color-grey);
font-weight: 500;
padding: 30px 0;
&:hover {
color: var(--color-red-light);
font-weight: 500;
}
}
.dropLink {
text-decoration: none;
color: var(--color-white);
font-weight: 400;
&:hover {
color: var(--color-red-light);
font-weight: 400;
}
}
}
}
ul {
list-style-type: none;
@media screen and (max-width: 999px) {
color: var(--color-white) !important;
}
li {
float: left;
width: fit-content;
.dropdown-content {
display: none;
position: absolute;
background: var(--color-black);
transform: translateY(30px);
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(10, 10, 10, 0.2);
z-index: 1;
}
li.dropdown {
display: inline-block;
}
a {
color: var(--color-white);
width: fit-content;
}
a:hover,
.dropdown:hover {
color: var(--color-red-light);
font-weight: 400;
}
}
}
.dropdown-content a {
color: var(--color-black);
padding: 12px 16px;
text-decoration: none;
display: block;
text-align: left;
&:hover {
background-color: var(--color-black);
color: var(--color-gold);
}
}
.dropdown:hover .dropdown-content {
display: block;
}
}
@media screen and (min-width: 1000px) {
.menuToggle {
display: none;
}
}
@media screen and (max-width: 999px) {
.menuToggle {
float: left;
position: relative;
box-sizing: border-box;
top: 2px;
left: -10px;
z-index: 99;
-webkit-user-select: none;
user-select: none;
}
.menuToggle a {
text-decoration: none;
color: var(--color-grey);
transition: color 0.3s ease;
}
.menuToggle a:hover {
color: var(--color-red-light);
}
.menuToggle input {
display: block;
width: 40px;
height: 32px;
position: absolute;
top: -7px;
left: -5px;
cursor: pointer;
opacity: 0;
z-index: 2;
-webkit-touch-callout: none;
}
.menuToggle span {
display: block;
width: 33px;
height: 4px;
margin-bottom: 5px;
position: relative;
background: var(--color-grey);
border-radius: 3px;
z-index: 1;
transform-origin: 4px 0px;
transition: transform 0.2s cubic-bezier(0.77, 0.2, 0.05, 1),
background 0.2s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease;
}
.menuToggle span:first-child {
transform-origin: 0% 0%;
}
.menuToggle span:nth-last-child(2) {
transform-origin: 0% 100%;
}
.menuToggle input:checked ~ span {
opacity: 1;
transform: rotate(45deg) translate(-2px, -1px);
background: var(--color-white);
}
.menuToggle input:checked ~ span:nth-last-child(3) {
opacity: 0;
transform: rotate(0deg) scale(0.2, 0.2);
}
.menuToggle input:checked ~ span:nth-last-child(2) {
transform: rotate(-45deg) translate(0, -1px);
}
.menu {
position: absolute;
display: flex;
flex-direction: column;
width: 280px;
height: 110vh;
margin: -100px 0 0 -231px;
padding: 1.2rem;
padding-top: 100px;
background: var(--color-grey);
list-style-type: none;
transform-origin: 0% 0%;
overflow: hidden !important;
visibility: hidden;
opacity: 0%;
transition: opacity 0.2s ease, visibility 0.2s ease;
}
.menu li {
padding: 10px 0;
font-size: 1.2rem;
font-family: var(--font);
font-weight: 500;
color: var(--color-white);
}
.menuToggle input:checked ~ ul {
visibility: visible;
opacity: 100;
}
.sousmenu {
display: flex;
flex-direction: column;
margin-left: 1.2rem;
color: var(--color-white);
font-size: 1.2rem;
font-family: var(--font);
font-weight: 500;
padding-bottom: 1rem;
}
.empty {
line-height: 20rem;
}
}
.wildCoin {
display: flex;
align-items: center;
font-family: var(--font);
font-size: 1rem;
font-weight: 600;
color: var(--color-grey);
}

58
Frontend/src/scss/home.scss Executable file
View File

@@ -0,0 +1,58 @@
.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%;
}
}

171
Frontend/src/scss/pages.scss Executable file
View File

@@ -0,0 +1,171 @@
.container {
display: flex;
flex-direction: column;
max-width: 132ch;
width: 80%;
gap: 3rem;
margin: 150px auto 50px;
h1 {
font-family: var(--font);
color: var(--color-black);
font-size: 1.8rem;
text-align: center;
width: fit-content;
}
.separator {
border: solid 1px var(--color-gold-hover);
}
.massageinfo {
display: flex;
flex-direction: column;
gap: 0.4rem;
.info {
font-family: var(--font);
color: var(--color-grey);
font-size: 1rem;
font-weight: 400;
}
}
.subtitle {
font-family: var(--font);
color: var(--color-black);
font-size: 1.2rem;
font-weight: 600;
text-align: left;
margin-bottom: 0.8rem;
}
.content {
display: flex;
flex-direction: column;
gap: 0.6rem;
.subtitle {
font-family: var(--font);
color: var(--color-black);
font-size: 1.2rem;
font-weight: 600;
text-align: left;
margin-bottom: 0.5rem;
}
.paragraphe {
font-family: var(--font);
color: var(--color-grey);
font-size: 1rem;
font-weight: 400;
margin-bottom: 0.5rem;
list-style: inside;
a {
font-family: var(--font);
color: var(--color-gold-link);
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.5rem;
list-style: inside;
text-decoration: none;
&:hover {
color: var(--color-gold-hover);
}
}
}
.picture-container {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 1rem;
@media (max-width: 449px) {
flex-direction: column;
}
.picture {
background-position: center;
background-size: cover;
width: 100%;
height: 300px;
}
}
}
//Massages pages
.listing-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 3rem;
.listcontent {
display: flex;
flex-direction: column;
.listdetail {
font-family: var(--font);
color: var(--color-grey);
font-size: 0.95rem;
font-weight: 400;
margin-bottom: 0.5rem;
list-style-type: none;
padding-left: 1rem;
}
}
}
}
//error pages
section {
display: flex;
flex-direction: column;
height: 90vh;
justify-content: center;
width: 100%;
.containererror {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
h1 {
font-family: var(--font);
color: var(--color-black);
font-size: 2rem;
text-align: center;
width: fit-content;
}
.message {
font-family: var(--font);
color: var(--color-grey);
font-size: 1rem;
font-weight: 300;
text-align: center;
}
.btn-return {
display: flex;
justify-content: center;
width: fit-content;
margin: auto;
padding: 0.5rem 1rem;
background-color: var(--color-grey);
border: none;
border-radius: 0.6rem;
font-family: var(--font);
color: var(--color-white);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
&:hover {
background-color: var(--color-grey);
transform: scale(0.9);
}
}
}
}

31
Frontend/src/scss/root.scss Executable file
View File

@@ -0,0 +1,31 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
:root {
--color-blue-light: #dcecf3;
--color-purple-light: #e4e3f3;
--color-red-light: #c33636;
--color-white: #ffffff;
--color-light: #eaeaea;
--color-grey: #202020;
--color-grey-hover: #606060;
--bg-color: var(--color-blue-light);
--font: "Hanken Grotesk", sans-serif;
}
a {
text-decoration: none;
}
main {
min-height: 92vh;
margin-top: 80px;
padding: 0 0 2rem;
background-color: var(--bg-color);
}

26
Frontend/src/scss/shop.scss Executable file
View File

@@ -0,0 +1,26 @@
.shoppagecontainer {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
padding: 12rem 0 4rem;
h1 {
font-family: var(--font);
font-size: 2rem;
font-weight: 600;
color: var(--color-grey);
}
.cardcontainer {
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
max-width: 1280px;
}
}

0
Frontend/src/services/.gitkeep Executable file
View File