Merge pull request #53 from hubHarmony/Payement

handle subscription Payement on website
This commit is contained in:
Antoninop 2024-09-27 15:44:17 +02:00 committed by GitHub
commit 26302e5d88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1038 additions and 606 deletions

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="src/assets/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Servii - Hebergement facile</title>
<!-- charger les polices -->

23
package-lock.json generated
View File

@ -10,6 +10,8 @@
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@stripe/react-stripe-js": "^2.8.0",
"@stripe/stripe-js": "^4.5.0",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^16.0.0",
"@types/jest": "^29.5.12",
@ -3184,6 +3186,27 @@
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@stripe/react-stripe-js": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.8.0.tgz",
"integrity": "sha512-Vf1gNEuBxA9EtxiLghm2ZWmgbADNMJw4HW6eolUu0DON/6mZvWZgk0KHolN0sozNJwYp0i/8hBsDBcBUWcvnbw==",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"@stripe/stripe-js": "^1.44.1 || ^2.0.0 || ^3.0.0 || ^4.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@stripe/stripe-js": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-4.5.0.tgz",
"integrity": "sha512-dMOzc58AOlsF20nYM/avzV8RFhO/vgYTY7ajLMH6mjlnZysnOHZxsECQvjEmL8Q/ukPwHkOnxSPW/QGCCnp7XA==",
"engines": {
"node": ">=12.16"
}
},
"node_modules/@testing-library/dom": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.3.0.tgz",

View File

@ -13,6 +13,8 @@
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@stripe/react-stripe-js": "^2.8.0",
"@stripe/stripe-js": "^4.5.0",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^16.0.0",
"@types/jest": "^29.5.12",

View File

@ -2,14 +2,17 @@ import { useEffect, useState, Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { auth } from './service/firebase';
import { auth, getUserSubscription } from './service/firebase';
import styles from './App.module.scss';
import Loading from './pages/Loading/loading';
import Pricing from './pages/Payement/Pricing/Pricing';
import PaymentForm from './pages/Payement/PaymentForm/PaymentForm';
import Checkout from './pages/Payement/Checkout';
const LoginPage = lazy(() => import('./pages/LoginPage/LoginPage'));
const ServerDetails = lazy(() => import('./pages/ServerDetails/ServerDetails'));
const CreatePage = lazy(() => import('./pages/CreateServer/CreateServer'));
const Javapick = lazy(() => import('./pages/CreateServer/Javapick/java'));
const Javapick = lazy(() => import('./pages/CreateServer/java/java'));
const Modpack = lazy(() => import('./pages/CreateServer/modpack/modpack'));
const Bedrock = lazy(() => import('./pages/CreateServer/bedrock/bedrock'));
const DashboardPage = lazy(() => import('./pages/DashboardPage/DashboardPage'));
@ -18,6 +21,8 @@ const App = () => {
const [user, setUser] = useState(() => JSON.parse(localStorage.getItem('user')) || null);
const [loading, setLoading] = useState(true);
const [showLoading, setShowLoading] = useState(false);
const [subscription, setSubscription] = useState(0);
const [loadingSubscription, setLoadingSubscription] = useState(true);
useEffect(() => {
const timeoutId = setTimeout(() => setShowLoading(true), 6000);
@ -25,10 +30,11 @@ const App = () => {
const unsubscribe = auth.onAuthStateChanged((user) => {
if (user) {
localStorage.setItem('user', JSON.stringify(user));
setUser(user);
} else {
localStorage.removeItem('user');
setUser(null);
}
setUser(user);
setLoading(false);
clearTimeout(timeoutId);
});
@ -39,7 +45,27 @@ const App = () => {
};
}, []);
if (loading && showLoading) {
useEffect(() => {
if (user) {
const fetchSubscription = async () => {
try {
setLoadingSubscription(true);
const userSubscription = await getUserSubscription(user.uid);
console.log('User Subscription:', userSubscription);
setSubscription(userSubscription || 0);
} catch (error) {
console.error('Error fetching subscription:', error);
} finally {
setLoadingSubscription(false);
}
};
fetchSubscription();
} else {
setLoadingSubscription(false);
}
}, [user]);
if (loading || loadingSubscription) {
return <Loading />;
}
@ -48,17 +74,34 @@ const App = () => {
<div className={styles.container}>
<Suspense fallback={<Loading />}>
<Routes>
{/* Public Route */}
<Route path="/login" element={user ? <Navigate to="/dashboard" /> : <LoginPage />} />
{/* Protected Routes (Requires Authentication) */}
<Route path="/dashboard" element={user ? <DashboardPage user={user} /> : <Navigate to="/login" />} />
<Route path="/createServer" element={user ? <CreatePage user={user} /> : <Navigate to="/login" />} />
<Route path="/createServer/java" element={user ? <Javapick user={user} /> : <Navigate to="/login" />} />
<Route path="/createServer/bedrock" element={user ? <Bedrock user={user} /> : <Navigate to="/login" />} />
<Route path="/createServer/modpack" element={user ? <Modpack user={user} /> : <Navigate to="/login" />} />
{/* Routes with Subscription Levels */}
<Route path="/createServer/java" element={user ? (subscription > 0 ? <Javapick user={user} /> : <Navigate to="/payement?package=Basique" />) : <Navigate to="/login" />} />
<Route path="/createServer/bedrock" element={user ? (subscription > 1 ? <Bedrock user={user} /> : <Navigate to="/payement?package=Standard" />) : <Navigate to="/login" />} />
<Route path="/createServer/modpack" element={user ? (subscription > 2 ? <Modpack user={user} /> : <Navigate to="/payement?package=Premium" />) : <Navigate to="/login" />} />
{/* Server Details Route */}
<Route path="/server/:serverName/*" element={user ? <ServerDetails user={user} /> : <Navigate to="/login" />} />
{/* Pricing and Payment */}
<Route path="/pricing" element={user ? <Pricing user={user} /> : <Navigate to="/login" />} />
<Route path="/payment/*" element={user ? <PaymentForm user={user} /> : <Navigate to="/login" />} />
<Route path="/return/*" element={user ? <Checkout user={user} /> : <Navigate to="/login" />} />
{/* Default Route */}
<Route path="/" element={<Navigate to={user ? "/dashboard" : "/login"} />} />
{/* Catch-all route */}
<Route path="*" element={user ? <DashboardPage user={user} /> : <Navigate to="/login" />} />
</Routes>
</Suspense>
<ToastContainer
position="top-right"
autoClose={3500}

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
src/assets/tier/basique.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
src/assets/tier/premium.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -1,163 +0,0 @@
import { useState, useEffect } from 'react';
import styles from './CreateServer.module.scss';
import { getUserSubdomain } from "../../service/firebase";
import serviiApi from "../../service/api.tsx";
import PropTypes from "prop-types";
import { GoTag, GoCheck } from "react-icons/go";
const versions = {
paper: [
"1.16.2",
"1.14.4",
"1.13.2",
"1.16.1",
"1.18.2",
"1.19",
"1.20.4",
"1.19.3",
"1.19.4",
"1.19.2",
"1.20.1",
"1.21.1",
"1.20.5",
"1.20.6",
"1.21",
"1.20.2",
"1.20",
"1.9.4",
"1.10.2",
"1.11.2",
"1.12.2",
"1.13.1",
"1.13.2",
"1.14.1",
"1.14.2",
"1.14.3",
"1.14.4",
"1.15.1",
"1.15.2",
"1.16.1",
"1.16.2",
"1.16.3",
"1.16.4",
"1.16.5",
"1.17.1",
"1.18.1"
].sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))
};
const CreateServer = ({ user, onCreateServer, onSubdomainUpdate, onCancel, noServers }) => {
const [subdomain, setSubdomain] = useState(null);
const [subdomainInput, setSubdomainInput] = useState('');
const [serverName, setServerName] = useState('');
const [serverVersion, setServerVersion] = useState('1.21.1');
const loadSubdomain = async () => {
try {
if (user && user.uid) {
const userSubdomain = await getUserSubdomain(user.uid);
setSubdomain(userSubdomain || '');
onSubdomainUpdate(userSubdomain || '');
} else {
console.error('User or user.uid is undefined');
}
} catch (error) {
console.error('Error fetching subdomain:', error);
}
};
useEffect(() => {
loadSubdomain().then(r => r);
},
[user]);
const handleSaveSubdomain = async () => {
try {
await serviiApi.setSubdomain(subdomainInput);
setSubdomain(subdomainInput);
onSubdomainUpdate(subdomainInput);
} catch (error) {
console.error('Error setting subdomain:', error);
}
};
const handleCreateServer = async () => {
try {
const frameworkToSend = 'paper';
await onCreateServer(serverName, serverVersion, frameworkToSend);
} catch (error) {
console.error('Error creating server:', error);
}
};
const VersionChoice = (version) => {
return () => {
setServerVersion(version);
};
};
const validateInput = (input) => {
return input.replace(/[^a-zA-Z]/g, '');
};
return (
<div className={styles.container}>
{noServers ? (
<div className={styles.mainCardNoserveur}>
<div className={styles.nsSubTitle}>Bonjour</div>
<div className={styles.nsTitle}>Aucun serveur</div>
<button className={styles.btnnoServCreate} onClick={onCreateServer}>Créer un nouveau serveur</button>
</div>
) : (
subdomain === null ? (
<div>Loading...</div>
) : (
subdomain === '' ? (
<div className={styles.mainCardSubdomain}>
<div className={styles.title}>Ecrivez votre sous domaine</div>
<div className={styles.subtitle}>
Le sous-domaine est le nom sous lequel vos amis et vous rejoignez le serveur, un peu comme une adresse. Choisissez-le bien, car il nest pas facilement modifiable !
</div>
<input
className={styles.inputsubdomain}
type="text"
value={subdomainInput}
onChange={(e) => setSubdomainInput(validateInput(e.target.value))}
placeholder='Nom du sous domaine'
/>
<button className={styles.btnSubCreate} onClick={handleSaveSubdomain}>
Envoyer
</button>
<button className={styles.btnSubCreate} onClick={onCancel}>
Annuler
</button>
</div>
) : (
<div className={styles.GamesChoice}>
<div className={styles.title}>Création du serveur</div>
<div className={styles.subtitle}>Comment voulez vous jouer ?</div>
<div className={styles.GamesContainer}>dd</div>
</div>
)
)
)}
</div>
);
};
CreateServer.propTypes = {
user: PropTypes.oneOfType([
PropTypes.shape({
uid: PropTypes.string.isRequired,
displayName: PropTypes.string,
email: PropTypes.string,
photoURL: PropTypes.string,
}),
]),
onCreateServer: PropTypes.func.isRequired,
onSubdomainUpdate: PropTypes.func.isRequired,
onCancel: PropTypes.func,
noServers: PropTypes.any
};
export default CreateServer;

View File

@ -1,296 +0,0 @@
.mainCardCommon {
display: flex;
justify-content: start;
align-items: start;
flex-direction: column;
background-color: #1D1836;
border-radius: 1.5rem;
}
.title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1.5rem;
}
.subtitle {
font-size: 1.2rem;
font-weight: 300;
text-align: center;
margin-bottom: 2rem;
}
.input, .select {
width: 60rem;
padding: 1rem;
background-color: #090325;
border: none;
border-radius: 0.5rem;
color: white;
font-size: 1rem;
}
.btnSubCreate, .btnServCreate {
margin-top: 2rem;
padding: 1rem 2rem;
background-color: #090325;
color: white;
border: none;
border-radius: 0.5rem;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s, transform 0.3s;
margin-right: 1rem;
}
.containerNoserveur {
display: flex;
justify-content: center;
align-items: center;
}
.mainCardNoserveur {
background-color: #1D1836;
padding: 3rem;
margin-top: 5rem;
border-radius: 1rem;
}
.nsTitle {
font-size: 3rem;
color: #F2F2F2;
font-weight: 900;
}
.nsSubTitle {
font-size: 1.8rem;
font-weight: 300;
}
.btnnoServCreate {
width: 40rem;
height: 5rem;
margin-top: 5rem;
font-size: 2rem;
font-weight: 900;
background-color: #090325;
border-radius: 1rem;
color: white;
border: none;
cursor: pointer;
}
.btnServCreate {
width: 15rem;
height: 3.5rem;
margin-top: 2.5rem;
font-size: 1.5rem;
font-weight: 900;
border-radius: 1rem;
background-color: #090325;
}
.GamesChoice {
margin-top: var(--navbar-height);
width: 50rem;
height: 10rem;
background-color: red;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.mainCardSubdomain {
background-color: #1D1836;
padding: 3rem;
margin-top: 5rem;
border-radius: 1rem;
width: 55rem;
}
.subtitle {
font-size: 1.2rem;
font-weight: 600;
text-align: start;
margin-bottom: 1.5rem;
margin-top: 2rem;
}
.inputsubdomain {
width: 50rem;
padding: 1rem;
margin-top: 1rem;
background-color: #090325;
border: none;
border-radius: 0.5rem;
color: white;
font-size: 1rem;
}
.carreContainer {
display: flex;
justify-content: space-around;
align-items: center;
width: 100%;
height: 6rem;
}
.carreWrapper {
display: flex;
flex-direction: column;
align-items: center;
}
.carre {
width: 5rem;
height: 5rem;
object-fit: cover;
display: block;
cursor: pointer;
transition: transform 0.2s;
}
.carre:hover {
transform: scale(1.1);
}
.selected {
border: 2px solid #fff;
transform: scale(1.2);
}
.adviced {
border: 2px solid violet;
}
.recommendationLabel {
margin-top: 0.5rem;
font-size: 1rem;
color: violet;
font-weight: 700;
text-align: center;
}
.VersionContainer {
display: grid;
grid-template-columns: repeat(6, 1fr);
justify-items: center;
align-items: center;
width: 100%;
}
.VersionCard{
height: 4rem;
width: 8rem;
background-color: #090325;
display: flex;
align-items: center;
justify-content: center ;
font-size: 1.5rem;
margin-bottom: 1rem;
border-radius: .3rem;
}
.VersionCard svg {
margin-right: 0.5rem;
}
.VersionCard:hover{
background-color: #09032579;
cursor: pointer;
}
.selectedVersion {
border: .15rem solid #fff;
color: #fff;
}
@media (max-width: 800px) {
.VersionContainer {
grid-template-columns: repeat(3, 1fr);
}
.buttonContainer{
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
}
.title {
font-size: 1.8rem;
margin-bottom: 1rem;
}
.subtitle {
font-size: 1rem;
margin-bottom: 1.5rem;
}
.input, .select {
width: 100%;
padding: 0.75rem;
font-size: 0.875rem;
}
.btnSubCreate, .btnServCreate {
margin-top: 2rem;
padding: 1rem 2.5rem;
font-size: 1rem;
margin-right: 2rem;
display: flex;
justify-content: center;
align-items: center;
}
.mainCardNoserveur {
padding: 2rem;
margin-top: 3rem;
}
.nsTitle {
font-size: 2rem;
}
.nsSubTitle {
font-size: 1.2rem;
}
.btnnoServCreate {
width: 100%;
height: 3.5rem;
font-size: 1.5rem;
margin-top: 3rem;
}
.btnServCreate {
width: 100%;
height: 2.5rem;
font-size: 1.25rem;
}
.mainCardCreateServ {
margin-top: 3rem;
padding: 1.5rem;
width: 35rem;
}
.mainCardSubdomain {
padding: 2rem;
margin-top: 3rem;
width: 100%;
}
.inputsubdomain {
width: 100%;
padding: 0.75rem;
font-size: 0.875rem;
}
}

View File

@ -0,0 +1,20 @@
import styles from './DeleteConfirmationModal.module.scss';
const DeleteConfirmationModal = ({ isOpen, onClose, onDelete }) => {
if (!isOpen) return null;
return (
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<h2>Êtes-vous sûr de vouloir supprimer ?</h2>
<p>Cette action est irréversible.</p>
<div className={styles.modalButtons}>
<button className={styles.cancelButton} onClick={onClose}>Annuler</button>
<button className={styles.deleteButton} onClick={onDelete}>Supprimer</button>
</div>
</div>
</div>
);
};
export default DeleteConfirmationModal;

View File

@ -0,0 +1,62 @@
.modalOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal {
background-color: white;
border-radius: 0.67rem;
padding: 1.67rem;
box-shadow: 0 0.33rem 2.5rem rgba(0, 0, 0, 0.1);
max-width: 33.33rem;
width: 100%;
text-align: center;
}
h2 {
margin: 0 0 0.83rem;
}
p {
margin: 0 0 1.67rem;
}
.modalButtons {
display: flex;
justify-content: space-between;
}
.cancelButton {
background-color: #f0f0f0;
color: #333;
border: none;
padding: 0.83rem 1.25rem;
border-radius: 0.42rem;
font-size: 1rem;
cursor: pointer;
&:hover {
background-color: #e0e0e0;
}
}
.deleteButton {
background-color: #be3939;
color: white;
border: none;
padding: 0.83rem 1.25rem;
border-radius: 0.42rem;
font-size: 1rem;
cursor: pointer;
&:hover {
background-color: rgba(255, 59, 59, 0.76);
}
}

View File

@ -4,16 +4,19 @@ import styles from './Navbar.module.scss';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUser, faCog, faSignOutAlt, faArrowLeft } from '@fortawesome/free-solid-svg-icons';
import PropTypes from "prop-types";
import { useNavigate } from 'react-router-dom';
const Navbar = ({
user,
hasShadow = true,
showBackButton = false,
onBackClick,
backButtonText = "Retour au dashboard"
backButtonText = "Retour au dashboard",
Subsribebtn = true
}) => {
const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = useRef(null);
const navigate = useNavigate();
const handleLogout = () => {
auth.signOut();
@ -45,7 +48,15 @@ const Navbar = ({
{showBackButton && (
<div className={styles.backButton} onClick={onBackClick}>
<FontAwesomeIcon icon={faArrowLeft} className={styles.backIcon} />
<span>{backButtonText}</span> {}
<span>{backButtonText}</span>
</div>
)}
{Subsribebtn && (
<div className={styles.subscribeSection}>
<div className={styles.subscribeBtn} onClick={() => navigate('/Pricing')}>
S&apos;abonner
</div>
</div>
)}
@ -90,6 +101,7 @@ Navbar.propTypes = {
showBackButton: PropTypes.bool,
onBackClick: PropTypes.func,
backButtonText: PropTypes.string,
Subsribebtn: PropTypes.bool
};
export default Navbar;

View File

@ -89,6 +89,18 @@
font-size: 1.5rem;
}
.subscribeBtn{
padding: .5rem 1rem;
border-radius: .4rem;
background-color: #2f2f2f;
color: white;
cursor: pointer;
border: none;
margin-right: 2rem;
}
@media (max-width: 650px) {
.backButton{
font-size: 1.25rem;

View File

@ -10,7 +10,7 @@ const ServerCard = ({ status, version, name, framework, onRunClick, onStopClick,
const getFrameworkSource = () => {
switch (framework) {
case 'frabric':
case 'fabric':
return fabric;
case 'forge':
return forge;

View File

@ -105,6 +105,7 @@
.startStopButton {
padding: 1rem 1.5rem;
border-radius: 0.5rem;
font-size: 1rem;
border: none;
color: white;
cursor: pointer;

View File

@ -42,19 +42,19 @@ body {
@media (min-width: 1600px) {
html {
font-size: 17px;
font-size: 14px;
}
}
@media (min-width: 1800px) {
html {
font-size: 19px;
font-size: 15px;
}
}
@media (min-width: 2000px) {
html {
font-size: 15px;
font-size: 17px;
}
}

View File

@ -4,11 +4,26 @@ import Navbar from '../../components/navbar/Navbar';
import { useNavigate } from 'react-router-dom';
import bedrockimg from '../../assets/bedrock.png';
import modedimg from '../../assets/moded.png';
import javaimg from '../../assets/java.png'
import javaimg from '../../assets/java.png';
import { getUserSubscription } from "../../service/firebase";
import { useState, useEffect } from 'react';
const CreateServer = ({ user }) => {
const navigate = useNavigate();
const [subscription, setSubscription] = useState(0);
useEffect(() => {
const fetchSubscription = async () => {
try {
const userSubscription = await getUserSubscription(user.uid);
setSubscription(userSubscription || 0);
} catch (error) {
console.error('Error fetching subscription:', error);
}
};
fetchSubscription();
}, [user.uid]);
return (
<div className={styles.Container}>
@ -23,23 +38,25 @@ const CreateServer = ({ user }) => {
<div className={styles.title}>Création du serveur</div>
<div className={styles.subtitle}>De quelle façon voulez-vous jouer ?</div>
<div className={styles.GamesContainer}>
<div className={styles.GameCard} onClick={() => navigate('/CreateServer/java')}>
<img src={javaimg} className={styles.imgCard}/>
<div className={styles.GameCard} onClick={() => (subscription > 0) ? navigate('/CreateServer/java') : navigate('/payement?package=Gratuit')}>
<img src={javaimg} className={styles.imgCard} alt="Java Edition" />
<div className={styles.Gamesubtitle}>Java Edition</div>
<div className={styles.Gamedescription}>Découvrez la version classique de Minecraft sur PC, avec un large éventail de mises à jour et de fonctionnalités, couvrant plus de vingt versions !</div>
<button className={styles.GameButton}>Choisir ce modèle </button>
<button className={styles.GameButton}>Choisir ce modèle</button>
</div>
<div className={styles.GameCard} onClick={() => navigate('/CreateServer/bedrock')}>
<img src={bedrockimg} className={styles.imgCard}/>
<div className={styles.GameCard} onClick={() => (subscription > 1) ? navigate('/CreateServer/bedrock') : navigate('/payement?package=Standard')}>
<img src={bedrockimg} className={styles.imgCard} alt="Mini-jeu" />
<div className={styles.Gamesubtitle}>Mini-jeu</div>
<div className={styles.Gamedescription}>Plongez dans Minecraft avec des cartes personnalisées et des règles uniques, en solo ou avec vos amis.</div>
<button className={styles.GameButton}>Choisir ce modèle </button>
<button className={styles.GameButton}>Choisir ce modèle</button>
</div>
<div className={styles.GameCard} onClick={() => navigate('/CreateServer/modpack')}>
<img src={modedimg} className={styles.imgCard}/>
<div className={styles.GameCard} onClick={() => (subscription > 2) ? navigate('/CreateServer/modpack') : navigate('/payement?package=Premium')}>
<img src={modedimg} className={styles.imgCard} alt="Minecraft Modé" />
<div className={styles.Gamesubtitle}>Minecraft Modé</div>
<div className={styles.Gamedescription}>Explorez la version modifiée de Minecraft sur PC, avec des modpacks riches et variés, contenant plus de 200 mods pour une expérience de jeu personnalisée.</div>
<button className={styles.GameButton}>Choisir ce modèle </button>
<button className={styles.GameButton}>Choisir ce modèle</button>
</div>
</div>
</div>
@ -48,14 +65,12 @@ const CreateServer = ({ user }) => {
};
CreateServer.propTypes = {
user: PropTypes.oneOfType([
PropTypes.shape({
user: PropTypes.shape({
uid: PropTypes.string.isRequired,
displayName: PropTypes.string,
email: PropTypes.string,
photoURL: PropTypes.string,
}),
]),
}).isRequired,
};
export default CreateServer;

View File

@ -26,7 +26,6 @@
}
.GamesContainer {
margin-top: 2.5rem;
display: flex;
justify-content: center;
align-items: center;
@ -90,20 +89,23 @@ img {
.GamesContainer {
flex-direction: column;
flex-wrap: row;
margin-top: 40rem;
height: 100%;
margin-top: 45rem;
}
.GameCard {
margin: 1rem;
margin: 0rem 0rem .5rem 0rem;
width: 20rem;
padding: 2.5rem;
}
}
@media (max-width: 750px) {
.GamesContainer {
margin-top: 50rem;
.GamesChoice {
margin-top: 35rem;
}
.title {
margin-top: 2rem;
}
}

View File

@ -3,7 +3,7 @@ import { GoTag, GoCheck } from "react-icons/go";
import { useNavigate } from 'react-router-dom';
import styles from './java.module.scss';
import PropTypes from "prop-types";
import Navbar from '../../../components/navbar/Navbar';
import Navbar from '../../../components/navbar/Navbar.jsx';
import serviiApi from "../../../service/api.tsx";
const Javapick = ({ user }) => {

View File

@ -1,12 +1,56 @@
import { useEffect, useState } from 'react';
import styles from './modpack.module.scss';
import PropTypes from "prop-types";
import PropTypes from 'prop-types';
import Navbar from '../../../components/navbar/Navbar';
import { useNavigate } from 'react-router-dom';
import serviiApi from '../../../service/api';
const Modpack = ({ user }) => {
const navigate = useNavigate();
const [modpacks, setModpacks] = useState([]);
const [selectedModpackIndex, setSelectedModpackIndex] = useState(null);
const [error, setError] = useState('');
useEffect(() => {
const fetchModpacks = async () => {
try {
const response = await serviiApi.fetchModpacks();
if (response.return_code === 200) {
setModpacks(response.message);
} else {
setError(response.message);
}
} catch (error) {
console.error(error);
setError("Erreur lors du chargement des modpacks.");
}
};
fetchModpacks();
}, [user]);
const handleModpackClick = (index) => {
setSelectedModpackIndex(index);
};
const handleCancel = () => {
console.log("Annuler cliqué, index sélectionné avant :", selectedModpackIndex);
setSelectedModpackIndex(null);
};
const handleCreate = async () => {
if (selectedModpackIndex !== null) {
const selectedModpack = modpacks[selectedModpackIndex];
const { short_name, framework } = selectedModpack;
try {
await serviiApi.serverCreate(short_name, short_name, framework);
navigate('/Dashboard');
} catch (error) {
console.error('Error creating server:', error);
}
}
};
return (
<div className={styles.Container}>
@ -17,8 +61,37 @@ const Modpack = ({ user }) => {
backButtonText="Retour"
onBackClick={() => navigate('/CreateServer')}
/>
<div className={styles.hey}>
<h1>Prochainement disponible</h1>
<div className={styles.modpackGrid}>
{error ? <h2>{error}</h2> : (
modpacks.map((modpack, index) => (
<div
key={`modpack-${index}`}
className={`${styles.modpackCard} ${selectedModpackIndex === index ? styles.selected : ''}`}
onClick={() => handleModpackClick(index)}
>
<img src={`https://www.servii.fr/api/modpacks/image/${modpack.image}`} alt={modpack.name} className={styles.modpackImage} />
<h3>{modpack.name}</h3>
<p>{modpack.description}</p>
<div className={styles.tags}>
<span className={`${styles.tag} ${styles.adventure}`}>Aventure</span>
<span className={`${styles.tag} ${styles.combat}`}>Combat</span>
<span className={`${styles.tag} ${styles.pve}`}>PvE</span>
<span className={`${styles.tag} ${styles.forge}`}>{modpack.framework}</span>
</div>
<div className={styles.meta}>
<span>Version MC: {modpack.mcVersion}</span>
<span>Version Modpack: {modpack.version}</span>
</div>
{selectedModpackIndex === index && ( // Condition modifiée
<div className={styles.overlay}>
<div className={styles.shortNamtitle}>Créer le serveur {modpack.short_name} ?</div>
<button className={styles.createButton} onClick={handleCreate}>Créer</button>
<button className={styles.cancelButton} onClick={handleCancel}>Annuler</button>
</div>
)}
</div>
))
)}
</div>
</div>
);

View File

@ -1,3 +1,154 @@
.hey{
margin-top: 8em;
.Container {
display: flex;
flex-direction: column;
align-items: center;
padding: 6rem;
}
.modpackGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(30rem, 1fr));
gap: 2rem;
width: 100%;
padding: 1.5rem;
}
.modpackCard {
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: .7rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 2rem;
width: 30rem;
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
&:hover {
transform: translateY(-.2rem);
}
.content {
transition: filter 0.3s ease; /* Transition pour le flou */
filter: none; /* Par défaut, pas de flou */
}
&.selected .content {
filter: blur(0.8rem); /* Appliquer le flou si sélectionné */
}
h3 {
margin: .8rem 0;
font-size: 1.5em;
color: #333;
}
p {
font-size: 1em;
color: #666;
}
.modpackImage {
width: 100%;
height: auto;
margin-bottom: 1rem;
}
.tags {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: .5rem;
margin: 2.5rem 0rem;
.tag {
display: flex;
align-items: center;
padding: .4rem .8rem;
border-radius: 1.5rem;
font-size: 0.9em;
font-weight: 500;
color: #fff;
&.adventure {
background-color: #4CAF50;
}
&.combat {
background-color: #E91E63;
}
&.pve {
background-color: #2196F3;
}
&.forge {
background-color: #FFC107;
}
}
}
.meta {
display: flex;
justify-content: space-between;
font-size: .9rem;
color: #999;
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
z-index: 10;
h4 {
font-size: 1.8rem;
color: #333;
margin-bottom: 1rem;
}
.createButton {
background-color: black;
color: white;
padding: 0.5rem 1.5rem;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1.25rem;
}
.createButton:hover {
background-color: #252525bb;
}
.cancelButton {
background-color: gray;
color: white;
padding: 0.5rem 1.5rem;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1.25rem;
}
.cancelButton:hover {
background-color: rgba(95, 95, 95, 0.685);
}
.shortNamtitle {
font-weight: 400;
font-size: 1.5rem;
color: #333;
margin-bottom: 1rem;
}
}
}

View File

@ -3,10 +3,11 @@ import { useNavigate } from 'react-router-dom';
import ServerCard from '../../components/serverCards/DefaultServerCard';
import Navbar from '../../components/navbar/Navbar';
import Loading from '../Loading/loading';
import { getUserSubdomain } from "../../service/firebase";
import { getUserSubdomain, getUserSubscription } from "../../service/firebase";
import serviiApi from "../../service/api.tsx";
import PropTypes from "prop-types";
import styles from './DashboardPage.module.scss';
import DeleteConfirmationModal from '../../components/DeleteConfirmationModal/DeleteConfirmationModal';
const CACHE_KEY_SERVERS = 'cachedServers';
const CACHE_KEY_TIMESTAMP = 'cacheTimestamp';
@ -20,15 +21,21 @@ const DashboardPage = ({ user }) => {
});
const [subdomain, setSubdomain] = useState(' ');
const [subscription, setSubscription] = useState(0);
const [loading, setLoading] = useState(servers.length === 0);
const [searchTerm, setSearchTerm] = useState('');
const [newSubdomain, setNewSubdomain] = useState(' ');
const [isModalOpen, setModalOpen] = useState(false);
const [serverToDelete, setServerToDelete] = useState(null);
const updateServersFromApi = useCallback(async () => {
try {
if (user?.uid) {
const userSubdomain = await getUserSubdomain(user.uid);
setSubdomain(userSubdomain || null);
const userSubscription = await getUserSubscription(user.uid);
setSubscription(userSubscription || 0);
}
const ApiResponse = await serviiApi.fetchServers();
@ -51,15 +58,45 @@ const DashboardPage = ({ user }) => {
const handleCreateServer = () => navigate('/CreateServer');
const handleRunServer = async (serverName) => {
const handleRunServer = async (serverName, framework) => {
try {
if (subscription === 0) {
if (framework === "paper") {
navigate('/payement?package=Gratuit');
} if (framework === "Bedrock") {
navigate('/payement?package=Standard');
}
else {
navigate('/payement?package=Standard');
}
}
else if (subscription === 1) {
if (framework === "paper") {
await serviiApi.serverRun(serverName);
updateServersFromApi();
} else {
navigate('/payement?package=Standard');
}
}
else if (subscription === 2) {
if (framework === "paper" || framework === "Bedrock") {
await serviiApi.serverRun(serverName);
updateServersFromApi();
} else {
navigate('/payement?package=Premium');
}
}
else if (subscription === 3) {
await serviiApi.serverRun(serverName);
updateServersFromApi();
}
} catch (error) {
console.error('Error starting server:', error);
}
};
const handleStopServer = async (serverName) => {
try {
await serviiApi.serverStop(serverName);
@ -69,13 +106,17 @@ const DashboardPage = ({ user }) => {
}
};
const handleDeleteServer = async (serverName) => {
const handleDeleteServer = async () => {
if (serverToDelete) {
try {
await serviiApi.serverDelete(serverName);
await serviiApi.serverDelete(serverToDelete);
setModalOpen(false);
setServerToDelete(null);
updateServersFromApi();
} catch (error) {
console.error('Error deleting server:', error);
}
}
};
const handleCopyAddress = () => {
@ -185,9 +226,12 @@ const DashboardPage = ({ user }) => {
framework={favoriteServer.framework}
maxPlayers={favoriteServer.maxPlayers}
countPlayers={favoriteServer.onlinePlayers}
onRunClick={() => handleRunServer(favoriteServer.name)}
onRunClick={() => handleRunServer(favoriteServer.name , favoriteServer.framework)}
onStopClick={() => handleStopServer(favoriteServer.name)}
onDeleteClick={() => handleDeleteServer(favoriteServer.name)}
onDeleteClick={() => {
setServerToDelete(favoriteServer.name);
setModalOpen(true);
}}
subdomain={subdomain}
favoriteServer={true}
/>
@ -208,15 +252,24 @@ const DashboardPage = ({ user }) => {
framework={server.framework}
maxPlayers={server.maxPlayers}
countPlayers={server.onlinePlayers}
onRunClick={() => handleRunServer(server.name)}
onRunClick={() => handleRunServer(server.name , server.framework)}
onStopClick={() => handleStopServer(server.name)}
onDeleteClick={() => handleDeleteServer(server.name)}
onDeleteClick={() => {
setServerToDelete(server.name);
setModalOpen(true);
}}
subdomain={subdomain}
/>
))
) : null}
</div>
</div>
<DeleteConfirmationModal
isOpen={isModalOpen}
onClose={() => setModalOpen(false)}
onDelete={handleDeleteServer}
/>
</div>
);
};

View File

@ -0,0 +1,51 @@
import { useState, useEffect } from 'react';
const Checkout = () => {
const [sessionStatus, setSessionStatus] = useState(null);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const session_id = params.get('session_id');
console.log('session_id:', session_id);
if (session_id) {
const fetchSessionStatus = async () => {
try {
const response = await fetch(`https://www.servii.fr/api/get-session-status?session_id=${session_id}`);
const data = await response.json();
setSessionStatus(data);
} catch (error) {
console.error('Error fetching session status:', error);
}
};
fetchSessionStatus();
} else {
console.error('No session_id found in URL');
}
}, []);
if (!sessionStatus) {
return <div>Loading...</div>;
}
return (
<div>
{sessionStatus.status === 'open' && (
<div>
<h1>Checkout is still open</h1>
</div>
)}
{sessionStatus.status === 'complete' && (
<div>
<h1>Success!</h1>
<p>Payment Status: {sessionStatus.payment_status}</p>
<p>Customer Email: {sessionStatus.customer_email}</p>
</div>
)}
</div>
);
};
export default Checkout;

View File

@ -0,0 +1,97 @@
import { useCallback, useState, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import Navbar from '../../../components/navbar/Navbar';
import PropTypes from 'prop-types';
import { loadStripe } from '@stripe/stripe-js';
import { EmbeddedCheckoutProvider, EmbeddedCheckout } from '@stripe/react-stripe-js';
import styles from './PaymentForm.module.scss';
const stripePromise = loadStripe("pk_live_51PyIYTP3VLLeb9GlHpiK8p5lVC3kGPvAAb0Nn8m5qVAGzespGfGlDoP8Wmw4HbZJJgqxxHEIG7MJwP4IAWCpRBi100dYMpV1gv");
const PackageNumber = (selectedPackage) => {
switch (selectedPackage) {
case 'Basique':
return 1;
case 'Standard':
return 2;
case 'Premium':
return 3;
default:
return 1;
}
};
const CheckoutForm = ({ email }) => {
const location = useLocation();
const [clientSecret, setClientSecret] = useState('');
const queryParams = new URLSearchParams(location.search);
const selectedPackage = queryParams.get('package');
const fetchClientSecret = useCallback(() => {
return fetch('https://www.servii.fr/api/get-checkout-session', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'permission': PackageNumber(selectedPackage),
'email': email,
},
})
.then((res) => res.json())
.then((data) => {
if (data && data.clientSecret) {
setClientSecret(data.clientSecret);
}
})
.catch((error) => {
console.error('Error fetching client secret:', error);
});
}, [selectedPackage, email]);
useEffect(() => {
fetchClientSecret();
}, [fetchClientSecret]);
return { clientSecret };
};
const PaymentForm = ({ user }) => {
const navigate = useNavigate();
const { clientSecret } = CheckoutForm({ email: user.email });
const options = { clientSecret };
return (
<div className={styles.container}>
<Navbar
user={user}
hasShadow={true}
showBackButton={true}
onBackClick={() => navigate('/Pricing')}
backButtonText="Retour"
/>
<div id="checkout">
<EmbeddedCheckoutProvider stripe={stripePromise} options={options} >
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
</div>
</div>
);
};
PaymentForm.propTypes = {
user: PropTypes.shape({
uid: PropTypes.string.isRequired,
email: PropTypes.string,
photoURL: PropTypes.string,
}).isRequired,
};
export default PaymentForm;

View File

@ -0,0 +1,7 @@
.container{
margin-top: 5.5rem;
}
.Footer-PoweredBy-Text{
display: none;
}

View File

@ -0,0 +1,117 @@
import styles from './Pricing.module.scss';
import Navbar from '../../../components/navbar/Navbar';
import { useNavigate } from 'react-router-dom';
import PropTypes from 'prop-types';
import basique from '../../../assets/tier/basique.png';
import standard from '../../../assets/tier/standard.png';
import premium from '../../../assets/tier/premium.png';
// Import des icônes
import { FaCheckCircle, FaTimesCircle } from 'react-icons/fa';
const Pricing = ({ user }) => {
const navigate = useNavigate();
const groups = [
{
title: 'Basique',
price: '2.99€',
description: 'Fait pour les joueurs vanilla sur un petit serveur.',
features: [
{ name: '2gb de Ram', isAvailable: true },
{ name: 'Accès au serveurs vanilla', isAvailable: true },
{ name: 'Personalisation complète', isAvailable: true },
{ name: 'Support par e-mail', isAvailable: true },
{ name: 'Accès au serveurs Mini-jeux', isAvailable: false },
{ name: 'Accès au serveurs de modpacks', isAvailable: false },
],
image: basique
},
{
title: 'Standard',
price: '4.99€',
description: 'Fait pour les joueurs de mini-jeux et vanilla !',
features: [
{ name: '5gb de Ram', isAvailable: true },
{ name: 'Accès au serveurs vanilla', isAvailable: true },
{ name: 'Personalisation complète', isAvailable: true },
{ name: 'Support par e-mail', isAvailable: true },
{ name: 'Accès au serveurs Mini-jeux', isAvailable: true },
{ name: 'Accès au serveurs de modpacks', isAvailable: false }
],
image: standard
},
{
title: 'Premium',
price: '9.99€',
description: 'Conçu pour les joueurs de modpacks robustes.',
features: [
{ name: '10gb de Ram', isAvailable: true },
{ name: 'Accès au serveurs vanilla', isAvailable: true },
{ name: 'Personalisation complète', isAvailable: true },
{ name: 'Support par e-mail', isAvailable: true },
{ name: 'Accès au serveurs Mini-jeux', isAvailable: true },
{ name: 'Accès au serveurs de modpacks', isAvailable: true }
],
image: premium
},
];
const handleSubscribe = (pkg) => {
navigate(`/payment?package=${pkg.title}`);
};
return (
<div className={styles.pricingContainer}>
<Navbar
user={user}
hasShadow={true}
showBackButton={true}
onBackClick={() => navigate('/Dashboard')}
/>
<div className={styles.packageList}>
{groups.map((pkg, index) => (
<div key={index} className={styles.packageCard}>
<div className={styles.packageCardheader}>
<h2 className={styles.title}>{pkg.title}</h2>
<img src={pkg.image} alt={pkg.title} />
</div>
<div className={styles.priceContainer}>
<div className={styles.price}>{pkg.price}</div>
<span className={styles.mensuel}>/mois</span>
</div>
<div className={styles.description}>{pkg.description}</div>
<hr />
<ul className={styles.features}>
<div className={styles.inclut}>CE QUI EST INCLUS</div>
{pkg.features.map((feature, idx) => (
<li key={idx} className={feature.isAvailable ? styles.featureAvailable : styles.featureUnavailable}>
{feature.isAvailable ? (
<FaCheckCircle style={{ color: 'green', marginRight: '8px' }} />
) : (
<FaTimesCircle style={{ color: 'red', marginRight: '8px' }} />
)}
{feature.name}
</li>
))}
</ul>
<button className={styles.button} onClick={() => handleSubscribe(pkg)}>
Démarrez maintenant
</button>
</div>
))}
</div>
</div>
);
};
Pricing.propTypes = {
user: PropTypes.object.isRequired,
};
export default Pricing;

View File

@ -0,0 +1,122 @@
$primary-color: #000;
$secondary-color: #fff;
.pricingContainer {
margin-top: 4rem;
display: flex;
justify-content: center;
flex-direction: row;
gap: 2rem;
padding: 4rem;
}
.packageList {
display: flex;
flex-direction: row;
gap: 3rem;
}
.packageCard {
background-color: $secondary-color;
color: $primary-color;
border-radius: .5rem;
padding: .5rem 2rem 2rem 2rem;
text-align: start;
transition: transform 0.3s, box-shadow 0.3s;
border: 0.15rem solid #dfdcd5;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
.packageCardheader {
display: flex;
justify-content: space-between;
align-items: center;
img {
width: 3rem;
height: auto;
margin-left: 0.5rem;
}
}
.title {
font-size: 1.8rem;
font-weight: 600;
}
.priceContainer {
display: flex;
justify-content: start;
align-items: baseline;
margin-bottom: 1.5rem;
.price {
font-size: 2.5rem;
font-weight: bold;
color: #2F2F2F;
}
.mensuel {
font-size: 1rem;
margin-left: 0.25rem;
color: #7c7c7c;
}
}
.description {
width: 16.5rem;
height: 4rem;
font-size: 1rem;
margin-top: 0.25rem;
color: #3c3c3c;
}
.features {
list-style-type: none;
padding: 0;
margin: 1rem 0;
li {
margin: 0.5rem 0;
font-size: 1rem;
color: #555;
}
}
.inclut {
font-size: 1.25rem;
margin-bottom: 1rem;
font-weight: 500;
}
.button {
color: $primary-color;
border: none;
border-radius: 2px;
padding: 12px 24px;
cursor: pointer;
font-size: 1rem;
margin-top: 1rem;
background-color: #2F2F2F;
color: $secondary-color;
transition: background-color 0.3s, transform 0.2s;
width: 100%;
&:hover {
background-color: darken(#2F2F2F, 10%);
transform: translateY(-2px);
}
}
hr {
border: 0.1rem solid #dfdcd5;
margin: 1.5rem -2rem;
}
}
@media (max-width: 700px) {
.packageList {
flex-direction: column;
}
}

View File

@ -88,7 +88,23 @@ function toast_status(status: number, message: string) {
});
}
class serviiApi {
public static async fetchModpacks(): Promise<ApiResponse> {
try {
const response = await fetch(`https://www.servii.fr/api/modpacks/image/a-metadata.txt`, {
method: 'GET',
});
const json = await response.json();
return { return_code: response.status, message: json };
} catch (error) {
toast_status(666, "Couldn't fetch modpacks");
console.error(error);
return { return_code: 503, message: "Couldn't fetch modpacks" };
}
}
private static async call<T extends BaseRequest>(endpoint: serviiRequest, body: T, token: string): Promise<ApiResponse> {
const unreachable: string = "Couldn't find an available API";
try {

View File

@ -37,4 +37,16 @@ const getUserSubdomain = async (userId) => {
}
};
export { auth, googleProvider, signInWithPopup, getUserSubdomain, app };
const getUserSubscription = async (userId) => {
const userDocRef = doc(db, 'users', userId);
const userDocSnap = await getDoc(userDocRef);
if (userDocSnap.exists()) {
const userData = userDocSnap.data();
return userData.subscription;
} else {
throw new Error("No such document!");
}
};
export { auth, googleProvider, signInWithPopup, getUserSubdomain, getUserSubscription, app };