Files
projet_gestion_commande/frontend-prep/src/pages/User/ProfilePage.tsx
T
Xor290 066443e228
Frontend Client - EAS Build / build (push) Failing after 57m7s
Frontend Web - Build & Lint / build (push) Failing after 15m48s
chore: build
2026-06-26 19:36:56 +02:00

796 lines
33 KiB
TypeScript

import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import Navbar from "../../components/Navbar";
import {
isUserAuthenticated,
extractUsernameFromToken,
getMyProfile,
updateMyProfile,
getTelegramStatus,
generateTelegramLinkToken,
unlinkTelegram,
get2FAStatus,
toggle2FA,
getPublicSettings,
} from "../../api/api";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faUser,
faMapMarkerAlt,
faPhone,
faCommentDots,
faSave,
faCheckCircle,
faExclamationTriangle,
faPaperPlane,
faUnlink,
faTimes,
faLock,
faShieldAlt,
} from "@fortawesome/free-solid-svg-icons";
import "./ProfilePage.css";
const STORAGE_ADDRESS = "profile_default_address";
const STORAGE_PHONE = "profile_default_phone";
const STORAGE_SIGNAL = "profile_signal_pseudo";
export default function ProfilePage() {
const navigate = useNavigate();
// Données compte (backend)
const [nom, setNom] = useState("");
const [prenom, setPrenom] = useState("");
const [telephone, setTelephone] = useState("");
const [loadingProfile, setLoadingProfile] = useState(true);
// Données locales (localStorage)
const [defaultAddress, setDefaultAddress] = useState(
() => localStorage.getItem(STORAGE_ADDRESS) ?? "",
);
const [defaultPhone, setDefaultPhone] = useState(
() => localStorage.getItem(STORAGE_PHONE) ?? "",
);
const [signalPseudo, setSignalPseudo] = useState(
() => localStorage.getItem(STORAGE_SIGNAL) ?? "",
);
const [savingContact, setSavingContact] = useState(false);
// Modals succès / erreur (pattern identique à l'app mobile)
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [successTitle, setSuccessTitle] = useState("");
const [successMsg, setSuccessMsg] = useState("");
const [showErrorModal, setShowErrorModal] = useState(false);
const [errorMsg, setErrorMsg] = useState("");
// Telegram
const [tgLinked, setTgLinked] = useState(false);
const [tgEnabled, setTgEnabled] = useState(false);
const [tgLoading, setTgLoading] = useState(false);
// 2FA
const [twoFAEnabled, setTwoFAEnabled] = useState(false);
const [twoFAAdminEnabled, setTwoFAAdminEnabled] = useState(false);
const [twoFALoading, setTwoFALoading] = useState(false);
// SAV Telegram
const [savTelegramUrl, setSavTelegramUrl] = useState("");
// Modals confirmation
const [showSaveModal, setShowSaveModal] = useState(false);
const [showConfirmAddressModal, setShowConfirmAddressModal] =
useState(false);
const [showConfirmContactModal, setShowConfirmContactModal] =
useState(false);
const [showUnlinkModal, setShowUnlinkModal] = useState(false);
const [showUnlinkSuccessModal, setShowUnlinkSuccessModal] = useState(false);
const username = extractUsernameFromToken() ?? "";
useEffect(() => {
if (!isUserAuthenticated()) {
navigate("/login/client", { replace: true });
return;
}
// Statut Telegram
getTelegramStatus().then((s) => {
setTgLinked(s.linked);
setTgEnabled(s.enabled);
});
// Statut 2FA
Promise.all([get2FAStatus(), getPublicSettings()]).then(
([status, pub]) => {
setTwoFAEnabled(status.two_fa_enabled);
setTwoFAAdminEnabled(pub.two_fa_enabled);
if (pub.contact_telegram) {
const ct = pub.contact_telegram;
setSavTelegramUrl(
ct.startsWith("http")
? ct
: `https://t.me/${ct.replace(/^@/, "")}`,
);
}
},
);
// Charger depuis backend
getMyProfile().then((res) => {
if (res.success && res.client) {
setNom(res.client.nom ?? "");
setPrenom(res.client.prenom ?? "");
setTelephone(res.client.telephone ?? "");
// Initialiser le téléphone par défaut si pas encore défini
if (
!localStorage.getItem(STORAGE_PHONE) &&
res.client.telephone
) {
setDefaultPhone(res.client.telephone);
}
}
setLoadingProfile(false);
});
}, [navigate]);
const showSuccess = (title: string, msg: string) => {
setSuccessTitle(title);
setSuccessMsg(msg);
setShowSuccessModal(true);
};
const showError = (msg: string) => {
setErrorMsg(msg);
setShowErrorModal(true);
};
const saveAddress = () => {
localStorage.setItem(STORAGE_ADDRESS, defaultAddress.trim());
setShowConfirmAddressModal(false);
showSuccess(
"Adresse enregistrée",
"Votre adresse par défaut a été sauvegardée et sera pré-remplie à votre prochaine commande.",
);
};
const saveLocal = () => {
localStorage.setItem(STORAGE_ADDRESS, defaultAddress.trim());
localStorage.setItem(STORAGE_PHONE, defaultPhone.trim());
localStorage.setItem(STORAGE_SIGNAL, signalPseudo.trim());
setShowSaveModal(false);
showSuccess(
"Infos enregistrées",
"Adresse, téléphone et pseudo Signal sauvegardés. Ils seront pré-remplis à votre prochaine commande.",
);
};
const handleLinkTelegram = async () => {
setTgLoading(true);
const res = await generateTelegramLinkToken();
setTgLoading(false);
if (res.error || !res.link_url) {
showError(res.error || "Service Telegram non disponible");
return;
}
window.open(res.link_url, "_blank");
};
const handleUnlinkTelegram = () => {
setShowUnlinkModal(true);
};
const confirmUnlinkTelegram = async () => {
setShowUnlinkModal(false);
await unlinkTelegram();
setTgLinked(false);
setTwoFAEnabled(false);
setShowUnlinkSuccessModal(true);
};
const handleToggle2FA = async () => {
const newVal = !twoFAEnabled;
setTwoFALoading(true);
const res = await toggle2FA(newVal);
setTwoFALoading(false);
if (res.success) {
setTwoFAEnabled(newVal);
showSuccess(
newVal ? "2FA activée" : "2FA désactivée",
newVal
? "Un code vous sera envoyé sur Telegram à chaque connexion."
: "La double authentification a été désactivée.",
);
} else {
showError(res.error || "Erreur lors de la modification");
}
};
const saveContact = async () => {
setShowConfirmContactModal(false);
setSavingContact(true);
const res = await updateMyProfile({
nom: nom.trim(),
prenom: prenom.trim(),
telephone: telephone.trim(),
});
setSavingContact(false);
if (res.success) {
showSuccess(
"Profil mis à jour",
"Vos informations de compte ont été enregistrées avec succès.",
);
} else {
showError(
res.message ?? "Erreur lors de la mise à jour du profil.",
);
}
};
if (loadingProfile) {
return (
<>
<Navbar />
<div className="profile-container">
<div className="profile-loading">
<div className="spinner" />
</div>
</div>
</>
);
}
return (
<>
<Navbar />
<div className="profile-container">
<div className="profile-header">
<div className="profile-avatar">
<FontAwesomeIcon icon={faUser} />
</div>
<div>
<h1 className="profile-title">Mon Profil</h1>
<p className="profile-username">{username}</p>
</div>
</div>
{/* Section compte */}
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon
icon={faUser}
className="profile-card-icon"
/>
Mon compte
</h2>
<div className="profile-fields">
<div className="profile-row">
<div className="profile-group">
<label>Prénom</label>
<input
type="text"
value={prenom}
onChange={(e) => setPrenom(e.target.value)}
placeholder="Votre prénom"
/>
</div>
<div className="profile-group">
<label>Nom</label>
<input
type="text"
value={nom}
onChange={(e) => setNom(e.target.value)}
placeholder="Votre nom"
/>
</div>
</div>
<div className="profile-group">
<label>Téléphone (compte)</label>
<input
type="tel"
value={telephone}
onChange={(e) => setTelephone(e.target.value)}
placeholder="+33 6 12 34 56 78"
/>
</div>
</div>
<button
className="profile-btn"
onClick={() => setShowConfirmContactModal(true)}
disabled={savingContact}
>
<FontAwesomeIcon icon={faSave} />
{savingContact
? " Enregistrement..."
: " Enregistrer le compte"}
</button>
<button
className="profile-btn profile-btn--secondary"
onClick={() => navigate("/user/change-password")}
style={{ marginTop: "0.6rem" }}
>
<FontAwesomeIcon icon={faLock} /> Changer le mot de
passe
</button>
</div>
{/* Section adresse par défaut */}
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon
icon={faMapMarkerAlt}
className="profile-card-icon profile-card-icon--address"
/>
Adresse par défaut
</h2>
<p className="profile-hint">
Sera pré-remplie dans le formulaire de commande. Vous
pourrez la modifier si vous n'êtes pas à cette adresse.
</p>
<div className="profile-group">
<label>Adresse</label>
<input
type="text"
value={defaultAddress}
onChange={(e) => setDefaultAddress(e.target.value)}
placeholder="Numéro, rue, ville, code postal"
/>
</div>
<button
className="profile-btn profile-btn--secondary"
onClick={() => setShowConfirmAddressModal(true)}
style={{ marginTop: "0.8rem" }}
>
<FontAwesomeIcon icon={faSave} /> Enregistrer l'adresse
</button>
</div>
{/* Section contact commande */}
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon
icon={faPhone}
className="profile-card-icon profile-card-icon--phone"
/>
Contact livraison
</h2>
<p className="profile-hint">
Numéro utilisé par le livreur lors de la livraison. Peut
être différent du numéro de votre compte.
</p>
<div className="profile-group">
<label>Téléphone par défaut</label>
<input
type="tel"
value={defaultPhone}
onChange={(e) => setDefaultPhone(e.target.value)}
placeholder="+33 6 12 34 56 78"
/>
</div>
<div
className="profile-group"
style={{ marginTop: "1rem" }}
>
<label>
<FontAwesomeIcon
icon={faCommentDots}
style={{ marginRight: "0.4rem" }}
/>
Pseudo Signal (optionnel)
</label>
<input
type="text"
value={signalPseudo}
onChange={(e) => setSignalPseudo(e.target.value)}
placeholder="@votre.pseudo.signal"
/>
</div>
<button
className="profile-btn profile-btn--secondary"
onClick={() => setShowSaveModal(true)}
>
<FontAwesomeIcon icon={faSave} /> Enregistrer les infos
par défaut
</button>
</div>
{/* Section Telegram */}
{tgEnabled && (
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon
icon={faPaperPlane}
className="profile-card-icon profile-card-icon--telegram"
/>
Notifications Telegram
</h2>
<p className="profile-hint">
Recevez vos notifications sur Telegram, même quand
le site est fermé.
</p>
{tgLinked ? (
<div className="profile-telegram-linked">
<span className="profile-telegram-status">
<FontAwesomeIcon icon={faCheckCircle} />{" "}
Compte Telegram lié
</span>
<button
className="profile-btn profile-btn--danger"
onClick={handleUnlinkTelegram}
>
<FontAwesomeIcon icon={faUnlink} /> Délier
Telegram
</button>
</div>
) : (
<button
className="profile-btn profile-btn--telegram"
onClick={handleLinkTelegram}
disabled={tgLoading}
>
<FontAwesomeIcon icon={faPaperPlane} />
{tgLoading
? " Génération du lien..."
: " Lier mon compte Telegram"}
</button>
)}
</div>
)}
{/* Section SAV Telegram */}
{savTelegramUrl && (
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon
icon={faPaperPlane}
className="profile-card-icon profile-card-icon--telegram"
/>
Contacter le SAV
</h2>
<p className="profile-hint">
Un problème avec votre commande ? Contactez notre
service après-vente directement sur Telegram.
</p>
<a
href={savTelegramUrl}
target="_blank"
rel="noopener noreferrer"
className="profile-btn profile-btn--telegram"
style={{ textDecoration: "none" }}
>
<FontAwesomeIcon icon={faPaperPlane} /> Contacter
le SAV sur Telegram
</a>
</div>
)}
{/* Section 2FA — visible uniquement si l'admin l'a activé ET Telegram est lié */}
{twoFAAdminEnabled && tgLinked && (
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon
icon={faShieldAlt}
className="profile-card-icon"
style={{ color: "#6366f1" }}
/>
Double authentification (2FA)
</h2>
<p className="profile-hint">
À chaque connexion, un code à 6 chiffres vous sera
envoyé sur Telegram avant d'accéder à votre compte.
</p>
<div className="profile-2fa-row">
<div className="profile-2fa-label">
<span>
{twoFAEnabled ? "Activée" : "Désactivée"}
</span>
{twoFAEnabled && (
<span className="profile-2fa-badge">
<FontAwesomeIcon icon={faCheckCircle} />{" "}
Protection active
</span>
)}
</div>
{twoFALoading ? (
<div className="profile-2fa-spinner" />
) : (
<button
role="switch"
aria-checked={twoFAEnabled}
className={`profile-toggle ${twoFAEnabled ? "profile-toggle--on" : ""}`}
onClick={handleToggle2FA}
aria-label="Activer ou désactiver la double authentification"
>
<span className="profile-toggle-thumb" />
</button>
)}
</div>
</div>
)}
</div>
{showSaveModal && (
<div
className="profile-modal-overlay"
onClick={() => setShowSaveModal(false)}
>
<div
className="profile-modal"
onClick={(e) => e.stopPropagation()}
>
<button
className="profile-modal-close"
onClick={() => setShowSaveModal(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon">
<FontAwesomeIcon icon={faSave} />
</div>
<h3 className="profile-modal-title">
Enregistrer les infos par défaut ?
</h3>
<p className="profile-modal-body">
Adresse, téléphone de livraison et pseudo Signal
seront sauvegardés localement et pré-remplis lors de
vos prochaines commandes.
</p>
<div className="profile-modal-actions">
<button
className="profile-modal-btn profile-modal-btn--cancel"
onClick={() => setShowSaveModal(false)}
>
Annuler
</button>
<button
className="profile-modal-btn profile-modal-btn--confirm"
onClick={saveLocal}
>
<FontAwesomeIcon icon={faCheckCircle} />{" "}
Confirmer
</button>
</div>
</div>
</div>
)}
{showConfirmAddressModal && (
<div
className="profile-modal-overlay"
onClick={() => setShowConfirmAddressModal(false)}
>
<div
className="profile-modal"
onClick={(e) => e.stopPropagation()}
>
<button
className="profile-modal-close"
onClick={() => setShowConfirmAddressModal(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon">
<FontAwesomeIcon icon={faMapMarkerAlt} />
</div>
<h3 className="profile-modal-title">
Enregistrer l'adresse ?
</h3>
<p className="profile-modal-body">
Cette adresse sera sauvegardée localement et
pré-remplie lors de vos prochaines commandes.
</p>
<div className="profile-modal-actions">
<button
className="profile-modal-btn profile-modal-btn--cancel"
onClick={() =>
setShowConfirmAddressModal(false)
}
>
Annuler
</button>
<button
className="profile-modal-btn profile-modal-btn--confirm"
onClick={saveAddress}
>
<FontAwesomeIcon icon={faCheckCircle} />{" "}
Confirmer
</button>
</div>
</div>
</div>
)}
{showConfirmContactModal && (
<div
className="profile-modal-overlay"
onClick={() => setShowConfirmContactModal(false)}
>
<div
className="profile-modal"
onClick={(e) => e.stopPropagation()}
>
<button
className="profile-modal-close"
onClick={() => setShowConfirmContactModal(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon">
<FontAwesomeIcon icon={faUser} />
</div>
<h3 className="profile-modal-title">
Enregistrer le compte ?
</h3>
<p className="profile-modal-body">
Vos informations (prénom, nom, téléphone) seront
mises à jour sur votre compte.
</p>
<div className="profile-modal-actions">
<button
className="profile-modal-btn profile-modal-btn--cancel"
onClick={() =>
setShowConfirmContactModal(false)
}
>
Annuler
</button>
<button
className="profile-modal-btn profile-modal-btn--confirm"
onClick={saveContact}
disabled={savingContact}
>
<FontAwesomeIcon icon={faCheckCircle} />{" "}
{savingContact
? "Enregistrement..."
: "Confirmer"}
</button>
</div>
</div>
</div>
)}
{showUnlinkModal && (
<div
className="profile-modal-overlay"
onClick={() => setShowUnlinkModal(false)}
>
<div
className="profile-modal"
onClick={(e) => e.stopPropagation()}
>
<button
className="profile-modal-close"
onClick={() => setShowUnlinkModal(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon profile-modal-icon--danger">
<FontAwesomeIcon icon={faUnlink} />
</div>
<h3 className="profile-modal-title">
Délier Telegram ?
</h3>
<p className="profile-modal-body">
Vous ne recevrez plus de notifications Telegram. La
double authentification sera également désactivée.
</p>
<div className="profile-modal-actions">
<button
className="profile-modal-btn profile-modal-btn--cancel"
onClick={() => setShowUnlinkModal(false)}
>
Annuler
</button>
<button
className="profile-modal-btn profile-modal-btn--danger"
onClick={confirmUnlinkTelegram}
>
<FontAwesomeIcon icon={faUnlink} /> Délier
</button>
</div>
</div>
</div>
)}
{/* Modal succès */}
{showSuccessModal && (
<div
className="profile-modal-overlay"
onClick={() => setShowSuccessModal(false)}
>
<div
className="profile-modal"
onClick={(e) => e.stopPropagation()}
>
<button
className="profile-modal-close"
onClick={() => setShowSuccessModal(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon profile-modal-icon--success">
<FontAwesomeIcon icon={faCheckCircle} />
</div>
<h3 className="profile-modal-title">{successTitle}</h3>
<p className="profile-modal-body">{successMsg}</p>
<div className="profile-modal-actions">
<button
className="profile-modal-btn profile-modal-btn--confirm profile-modal-btn--success"
onClick={() => setShowSuccessModal(false)}
>
Fermer
</button>
</div>
</div>
</div>
)}
{/* Modal erreur */}
{showErrorModal && (
<div
className="profile-modal-overlay"
onClick={() => setShowErrorModal(false)}
>
<div
className="profile-modal"
onClick={(e) => e.stopPropagation()}
>
<button
className="profile-modal-close"
onClick={() => setShowErrorModal(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon profile-modal-icon--danger">
<FontAwesomeIcon icon={faExclamationTriangle} />
</div>
<h3 className="profile-modal-title">
Une erreur est survenue
</h3>
<p className="profile-modal-body">{errorMsg}</p>
<div className="profile-modal-actions">
<button
className="profile-modal-btn profile-modal-btn--danger"
onClick={() => setShowErrorModal(false)}
>
Fermer
</button>
</div>
</div>
</div>
)}
{showUnlinkSuccessModal && (
<div
className="profile-modal-overlay"
onClick={() => setShowUnlinkSuccessModal(false)}
>
<div
className="profile-modal"
onClick={(e) => e.stopPropagation()}
>
<button
className="profile-modal-close"
onClick={() => setShowUnlinkSuccessModal(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon profile-modal-icon--success">
<FontAwesomeIcon icon={faCheckCircle} />
</div>
<h3 className="profile-modal-title">Compte délié</h3>
<p className="profile-modal-body">
Votre compte Telegram a é délié avec succès. Vous
ne recevrez plus de notifications via Telegram.
</p>
<div className="profile-modal-actions">
<button
className="profile-modal-btn profile-modal-btn--confirm"
onClick={() => setShowUnlinkSuccessModal(false)}
>
<FontAwesomeIcon icon={faCheckCircle} /> Fermer
</button>
</div>
</div>
</div>
)}
</>
);
}