add hosting plans via fossbilling
Deploy Nexus Portal to HestiaCP (FTP) / build-and-deploy (push) Successful in 33s
Deploy Nexus Portal to HestiaCP (FTP) / build-and-deploy (push) Successful in 33s
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getProductList } from '../../services/api';
|
||||
import { Server, Database, Cloud, Globe, ShoppingCart, Loader, AlertCircle, Check } from 'lucide-react';
|
||||
|
||||
export default function Store() {
|
||||
const navigate = useNavigate();
|
||||
const [products, setProducts] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// Récupération du catalogue
|
||||
useEffect(() => {
|
||||
const fetchCatalog = async () => {
|
||||
try {
|
||||
const data = await getProductList();
|
||||
// FOSSBilling renvoie les produits dans data.list
|
||||
setProducts(data.list || []);
|
||||
} catch (err) {
|
||||
setError("Impossible de contacter le serveur d'approvisionnement.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCatalog();
|
||||
}, []);
|
||||
|
||||
// Radar visuel : Associe une icône selon le nom de la catégorie ou du produit
|
||||
const getCategoryIcon = (text) => {
|
||||
const t = text.toLowerCase();
|
||||
if (t.includes('vps') || t.includes('serveur')) return <Server className="w-8 h-8 text-cyan-400" />;
|
||||
if (t.includes('cloud') || t.includes('nextcloud')) return <Cloud className="w-8 h-8 text-blue-400" />;
|
||||
if (t.includes('db') || t.includes('base')) return <Database className="w-8 h-8 text-purple-400" />;
|
||||
return <Globe className="w-8 h-8 text-emerald-400" />;
|
||||
};
|
||||
|
||||
// Extracteur de prix : L'API FOSSBilling a une structure de prix assez profonde
|
||||
const extractMonthlyPrice = (pricing) => {
|
||||
if (pricing?.type === 'free') return 'Gratuit';
|
||||
if (pricing?.type === 'once') return `${pricing.once.price} € (Une fois)`;
|
||||
|
||||
// CORRECTION ICI : Utilisation des crochets pour la clé numérique '1'
|
||||
if (pricing?.type === 'recurrent' && pricing.recurrent['1']) {
|
||||
return `${pricing.recurrent['1'].price} € / mois`;
|
||||
}
|
||||
|
||||
return 'Prix sur demande';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-7xl p-6 mx-auto">
|
||||
|
||||
<header className="mb-12 text-center">
|
||||
<h1 className="text-4xl font-bold text-white tracking-widest mb-4">
|
||||
CATALOGUE <span className="text-cyan-400">NEXUS</span>
|
||||
</h1>
|
||||
<p className="text-gray-400 max-w-2xl mx-auto">
|
||||
Déployez de nouvelles instances et étendez votre infrastructure en quelques secondes. Provisionnement automatisé 24/7.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* GESTION DES ERREURS & CHARGEMENT */}
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center space-x-3 text-cyan-400 my-20">
|
||||
<Loader className="w-8 h-8 animate-spin" />
|
||||
<span className="text-lg tracking-widest">TÉLÉCHARGEMENT DU CATALOGUE...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="flex justify-center my-20">
|
||||
<div className="flex items-center space-x-3 text-red-400 bg-red-400/10 border border-red-400 p-6 rounded-lg max-w-lg">
|
||||
<AlertCircle className="w-8 h-8 flex-shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* GRILLE DU CATALOGUE */}
|
||||
{!isLoading && !error && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
|
||||
{products.map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
className="bg-gray-900 border border-gray-800 rounded-2xl overflow-hidden hover:border-cyan-400/50 transition-all duration-300 group flex flex-col relative"
|
||||
>
|
||||
{/* En-tête de la carte */}
|
||||
<div className="p-8 border-b border-gray-800 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:scale-110 group-hover:opacity-20 transition-all duration-500">
|
||||
{getCategoryIcon(product.category || product.title)}
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-white mb-2 relative z-10">
|
||||
{product.title}
|
||||
</h3>
|
||||
<div className="text-cyan-400 font-mono text-xl relative z-10">
|
||||
{extractMonthlyPrice(product.pricing)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description du produit */}
|
||||
<div className="p-8 flex-grow flex flex-col justify-between">
|
||||
{/* FOSSBilling permet de mettre du HTML dans la description.
|
||||
Ici, on triche un peu en simulant des puces pour le design,
|
||||
mais tu pourras utiliser dangerouslySetInnerHTML si tu as mis du vrai HTML. */}
|
||||
<div className="text-gray-400 text-sm mb-8 space-y-3">
|
||||
{product.description ? (
|
||||
<p>{product.description}</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center space-x-2"><Check className="w-4 h-4 text-cyan-400"/> <span>Provisionnement instantané</span></div>
|
||||
<div className="flex items-center space-x-2"><Check className="w-4 h-4 text-cyan-400"/> <span>Support technique 24/7</span></div>
|
||||
<div className="flex items-center space-x-2"><Check className="w-4 h-4 text-cyan-400"/> <span>SLA 99.9%</span></div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bouton d'action */}
|
||||
<button
|
||||
onClick={() => navigate(`/checkout/${product.id}`)}
|
||||
className="w-full bg-cyan-400/10 hover:bg-cyan-400 text-cyan-400 hover:text-gray-900 border border-cyan-400 py-3 rounded-lg font-bold tracking-widest transition-all flex justify-center items-center space-x-2 group-hover:shadow-[0_0_20px_rgba(34,211,238,0.2)]"
|
||||
>
|
||||
<ShoppingCart className="w-5 h-5" />
|
||||
<span>COMMANDER</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user