mirror of
https://github.com/arthur-pbty/links.git
synced 2026-08-01 20:29:27 +02:00
Refactor code structure and remove redundant sections for improved readability and maintainability
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user