Files
ClickerZ/Frontend/src/pages/Settings.jsx
Tetardtek b58d39e707 feat: migrate SCSS → Tailwind CSS + remove sass dependency
12 SCSS files (1167 lines) replaced by centralized index.css with
Tailwind v4 @theme tokens, @layer components, and utility classes.
Game panel design system (gp-*) preserved as CSS components.
Inline styles in Settings/Login/MilestoneBar converted to Tailwind utilities.
sass removed from dependencies. Build clean, 53 tests pass.
2026-03-28 11:19:45 +01:00

214 lines
6.0 KiB
JavaScript

import { useEffect, useState } from "react";
import { useAuth } from "../context/AuthContext";
const OAUTH_URL = import.meta.env.VITE_OAUTH_URL || "";
const PROVIDERS = ["discord", "github", "google", "twitch"];
const EMOJIS = { discord: "🎮", github: "🐙", google: "🌐", twitch: "🎬" };
function getOAuthToken() {
return localStorage.getItem("clkz_oauth_token");
}
async function oauthFetch(path, options = {}) {
const token = getOAuthToken();
if (!token) throw new Error("Not authenticated with SuperOAuth");
const res = await fetch(`${OAUTH_URL}/api/v1${path}`, {
...options,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
...options.headers,
},
});
if (res.status === 401) {
localStorage.removeItem("clkz_oauth_token");
throw new Error("SuperOAuth token expired — re-login required");
}
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.message || `HTTP ${res.status}`);
}
return res.json();
}
export default function Settings() {
const { user, logout } = useAuth();
const [profile, setProfile] = useState(null);
const [linkedProviders, setLinkedProviders] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [actionLoading, setActionLoading] = useState(null);
// Handle ?linked= or ?error= from link callback
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const linked = params.get("linked");
const err = params.get("error");
if (linked) {
window.history.replaceState({}, "", "/settings");
}
if (err) {
setError(err);
window.history.replaceState({}, "", "/settings");
}
}, []);
useEffect(() => {
if (!user) return;
fetchProfile();
}, [user]);
async function fetchProfile() {
setLoading(true);
setError(null);
try {
const data = await oauthFetch("/user/profile");
setProfile(data.data.user);
setLinkedProviders(data.data.linkedProviders);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
}
async function handleLink(provider) {
setActionLoading(provider);
setError(null);
try {
const data = await oauthFetch(`/oauth/${provider}/link`, {
method: "POST",
body: JSON.stringify({
returnUrl: `${window.location.origin}/settings`,
}),
});
window.location.href = data.data.authUrl;
} catch (e) {
setError(e.message);
setActionLoading(null);
}
}
async function handleUnlink(provider) {
if (!confirm(`Délier ${provider} ?`)) return;
setActionLoading(provider);
setError(null);
try {
await oauthFetch(`/oauth/${provider}/unlink`, { method: "DELETE" });
await fetchProfile();
} catch (e) {
setError(e.message);
} finally {
setActionLoading(null);
}
}
if (!user) {
return (
<section>
<div className="containererror">
<p className="message">Connecte-toi pour accéder aux paramètres.</p>
</div>
</section>
);
}
if (loading) {
return (
<section>
<div className="containererror">
<p className="message">Chargement du profil...</p>
</div>
</section>
);
}
const linkedNames = new Set(linkedProviders.map((p) => p.provider));
const canUnlink = linkedProviders.length > 1;
return (
<section>
<div className="containererror max-w-[500px]">
<h1>Paramètres</h1>
{error && (
<p className="text-red-500 text-[13px] mb-4">{error}</p>
)}
{/* Profile info */}
{profile && (
<div className="mb-6 text-left">
<p className="text-sm text-gray-400 my-1">
<strong>Pseudo :</strong> {profile.nickname}
</p>
<p className="text-sm text-gray-400 my-1">
<strong>Email :</strong> {profile.email || ""}
</p>
</div>
)}
{/* Linked providers */}
<h2 className="text-lg mb-3">Comptes liés</h2>
<div className="flex flex-col gap-2">
{PROVIDERS.map((provider) => {
const linked = linkedNames.has(provider);
const isLoading = actionLoading === provider;
return (
<div
key={provider}
className={`flex items-center justify-between px-3 py-2 rounded-lg border ${
linked
? "bg-[#1a2a1a] border-[#2a4a2a]"
: "bg-[#1a1a2a] border-[#2a2a4a]"
}`}
>
<span className="text-sm">
{EMOJIS[provider]} {provider.charAt(0).toUpperCase() + provider.slice(1)}
{linked && (
<span className="text-green-400 text-xs ml-2"> lié</span>
)}
</span>
{linked ? (
<button
className="btn-return text-xs! py-1! px-2.5!"
disabled={!canUnlink || isLoading}
onClick={() => handleUnlink(provider)}
type="button"
style={{ opacity: canUnlink ? 1 : 0.4 }}
>
{isLoading ? "..." : "Délier"}
</button>
) : (
<button
className="btn-return text-xs! py-1! px-2.5!"
disabled={isLoading}
onClick={() => handleLink(provider)}
type="button"
>
{isLoading ? "..." : "Lier"}
</button>
)}
</div>
);
})}
</div>
{/* Logout */}
<button
className="btn-return mt-6 w-full!"
onClick={logout}
type="button"
>
Déconnexion
</button>
</div>
</section>
);
}