Refactor code structure and remove redundant sections for improved readability and maintainability

This commit is contained in:
Puechberty Arthur
2026-02-23 03:57:05 +01:00
parent 5be8966d66
commit 6760a523e4
27 changed files with 2025 additions and 1087 deletions
+267
View File
@@ -0,0 +1,267 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
const initialForm = {
name: '',
url: '',
icon: '/github.png',
description: '',
category: 'Autres'
};
export default function AdminPanel() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [username, setUsername] = useState('admin');
const [password, setPassword] = useState('');
const [links, setLinks] = useState([]);
const [analytics, setAnalytics] = useState([]);
const [form, setForm] = useState(initialForm);
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
async function loadAdminData() {
const [linksResponse, analyticsResponse] = await Promise.all([
fetch('/api/links'),
fetch('/api/analytics')
]);
if (!linksResponse.ok || !analyticsResponse.ok) {
setError('Impossible de charger les données admin.');
return;
}
const linksData = await linksResponse.json();
const analyticsData = await analyticsResponse.json();
setLinks(linksData.links || []);
setAnalytics(analyticsData.analytics || []);
setError('');
}
useEffect(() => {
async function checkSession() {
const response = await fetch('/api/admin/session');
const data = await response.json();
if (data.authenticated) {
setIsAuthenticated(true);
await loadAdminData();
}
setLoading(false);
}
checkSession();
}, []);
async function handleLogin(event) {
event.preventDefault();
setError('');
const response = await fetch('/api/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
if (!response.ok) {
setError('Identifiant ou mot de passe invalide.');
return;
}
setIsAuthenticated(true);
setPassword('');
await loadAdminData();
}
async function handleLogout() {
await fetch('/api/admin/logout', { method: 'POST' });
setIsAuthenticated(false);
setLinks([]);
setAnalytics([]);
}
async function handleAddLink(event) {
event.preventDefault();
setError('');
const response = await fetch('/api/links', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form)
});
if (!response.ok) {
setError("Impossible d'ajouter le lien.");
return;
}
setForm(initialForm);
await loadAdminData();
}
async function handleDeleteLink(linkId) {
setError('');
const response = await fetch(`/api/links/${linkId}`, { method: 'DELETE' });
if (!response.ok) {
setError('Impossible de supprimer ce lien.');
return;
}
setError('');
await loadAdminData();
}
const totalClicks = useMemo(
() => analytics.reduce((sum, item) => sum + (item.clicks || 0), 0),
[analytics]
);
if (loading) {
return (
<main className="page">
<div className="container admin-container">
<p className="empty">Chargement...</p>
</div>
</main>
);
}
if (!isAuthenticated) {
return (
<main className="page">
<div className="container admin-container">
<h1 className="admin-title">Admin</h1>
<p className="bio">Accès protégé aux analytics et à la gestion des liens.</p>
<form className="admin-form" onSubmit={handleLogin}>
<input
className="search"
name="username"
autoComplete="username"
value={username}
onChange={(event) => setUsername(event.target.value)}
placeholder="Identifiant"
required
/>
<input
className="search"
name="password"
autoComplete="current-password"
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="Mot de passe"
required
/>
<button className="theme-toggle" type="submit">
Se connecter
</button>
</form>
{error ? <p className="admin-error">{error}</p> : null}
</div>
</main>
);
}
return (
<main className="page">
<div className="container admin-container">
<div className="admin-head">
<h1 className="admin-title">Menu Admin</h1>
<button className="theme-toggle" type="button" onClick={handleLogout}>
Déconnexion
</button>
</div>
<section className="admin-section">
<h2 className="admin-subtitle">Analytics</h2>
<p className="bio">Total clics: {totalClicks}</p>
<div className="admin-list">
{analytics.map((item) => (
<div key={item.id} className="admin-row">
<div>
<strong>{item.name}</strong>
<div className="link-description">{item.url}</div>
</div>
<div className="admin-count">{item.clicks} clics</div>
</div>
))}
</div>
</section>
<section className="admin-section">
<h2 className="admin-subtitle">Ajouter un lien</h2>
<form className="admin-form" onSubmit={handleAddLink}>
<input
className="search"
name="name"
value={form.name}
onChange={(event) => setForm({ ...form, name: event.target.value })}
placeholder="Nom"
required
/>
<input
className="search"
name="url"
value={form.url}
onChange={(event) => setForm({ ...form, url: event.target.value })}
placeholder="URL"
required
/>
<input
className="search"
name="icon"
value={form.icon}
onChange={(event) => setForm({ ...form, icon: event.target.value })}
placeholder="Icône (ex: /github.png)"
/>
<input
className="search"
name="description"
value={form.description}
onChange={(event) => setForm({ ...form, description: event.target.value })}
placeholder="Description"
/>
<input
className="search"
name="category"
value={form.category}
onChange={(event) => setForm({ ...form, category: event.target.value })}
placeholder="Catégorie"
/>
<button className="theme-toggle" type="submit">
Ajouter
</button>
</form>
</section>
<section className="admin-section">
<h2 className="admin-subtitle">Supprimer un lien</h2>
<div className="admin-list">
{links.map((link) => (
<div key={link.id} className="admin-row">
<div>
<strong>{link.name}</strong>
<div className="link-description">{link.url}</div>
</div>
<button
className="danger-btn"
type="button"
onClick={() => handleDeleteLink(link.id)}
>
Supprimer
</button>
</div>
))}
</div>
</section>
{error ? <p className="admin-error">{error}</p> : null}
</div>
</main>
);
}
+144
View File
@@ -0,0 +1,144 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
function normalize(value) {
return (value || '').toLowerCase().trim();
}
function getDomain(url) {
try {
return new URL(url).hostname.replace('www.', '');
} catch {
return '';
}
}
export default function LinksPage({ profile, socialLinks }) {
const [query, setQuery] = useState('');
const [activeCategory, setActiveCategory] = useState('Tous');
const [theme, setTheme] = useState('light');
const [links, setLinks] = useState(socialLinks || []);
useEffect(() => {
const storedTheme = localStorage.getItem('theme') || 'light';
setTheme(storedTheme);
document.documentElement.setAttribute('data-theme', storedTheme);
async function loadLinks() {
const response = await fetch('/api/links');
if (!response.ok) {
return;
}
const data = await response.json();
setLinks(data.links || []);
}
loadLinks();
}, []);
const categories = useMemo(() => {
const set = new Set((links || []).map((link) => link.category || 'Autres'));
return ['Tous', ...Array.from(set)];
}, [links]);
const filteredLinks = useMemo(() => {
return (links || []).filter((link) => {
const matchesCategory =
activeCategory === 'Tous' || (link.category || 'Autres') === activeCategory;
const target = normalize(`${link.name} ${link.description} ${link.url}`);
const matchesSearch = target.includes(normalize(query));
return matchesCategory && matchesSearch;
});
}, [links, activeCategory, query]);
function toggleTheme() {
const nextTheme = theme === 'light' ? 'dark' : 'light';
setTheme(nextTheme);
localStorage.setItem('theme', nextTheme);
document.documentElement.setAttribute('data-theme', nextTheme);
}
async function trackClick(linkId) {
await fetch('/api/analytics/click', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ linkId })
});
}
return (
<main className="page">
<div className="container">
<div className="header">
<button type="button" className="theme-toggle" onClick={toggleTheme}>
{theme === 'light' ? '🌙 Sombre' : '☀️ Clair'}
</button>
</div>
<section className="profile">
<img src={profile.avatar} alt="Avatar" className="avatar" />
<h1>{profile.name}</h1>
<p className="bio">{profile.bio}</p>
</section>
<section className="controls">
<input
className="search"
type="search"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Rechercher un lien..."
aria-label="Rechercher un lien"
/>
<div className="categories">
{categories.map((category) => (
<button
key={category}
type="button"
className={`chip ${activeCategory === category ? 'active' : ''}`}
onClick={() => setActiveCategory(category)}
>
{category}
</button>
))}
</div>
</section>
<section className="links">
{filteredLinks.length === 0 ? (
<p className="empty">Aucun lien ne correspond à ta recherche.</p>
) : (
filteredLinks.map((link) => (
<a
key={link.name}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="link-card"
onClick={() => trackClick(link.id)}
>
<img src={link.icon} alt={link.name} className="icon" />
<div className="link-info">
<p className="link-title">{link.name}</p>
<p className="link-description">{link.description}</p>
</div>
<div className="link-meta">
<div>{getDomain(link.url)}</div>
</div>
</a>
))
)}
</section>
<footer>
© {new Date().getFullYear()} {profile.name} - Tous droits réservés ·{' '}
<a href="/admin" className="admin-link">
Admin
</a>
</footer>
</div>
</main>
);
}