Début du projet
This commit is contained in:
commit
e7d4e607f6
23 files changed
+4855
No files matched your search
@@ -0,0 +1,137 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tableau de bord - Administration</title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="admin-header">
|
||||
<div class="container">
|
||||
<h1>Administration - Veille Tech</h1>
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin<%= language !== 'all' ? '?language=' + encodeURIComponent(language) : '' %>" class="<%= status === 'all' ? 'active' : '' %>">Tous (<%= stats.total %>)</a>
|
||||
<a href="/admin?status=pending<%= language !== 'all' ? '&language=' + encodeURIComponent(language) : '' %>" class="<%= status === 'pending' ? 'active' : '' %>">En attente (<%= stats.pending %>)</a>
|
||||
<a href="/admin?status=published<%= language !== 'all' ? '&language=' + encodeURIComponent(language) : '' %>" class="<%= status === 'published' ? 'active' : '' %>">Publiés (<%= stats.published %>)</a>
|
||||
<a href="/admin?status=hidden<%= language !== 'all' ? '&language=' + encodeURIComponent(language) : '' %>" class="<%= status === 'hidden' ? 'active' : '' %>">Masqués (<%= stats.hidden %>)</a>
|
||||
<a href="/admin?status=rejected<%= language !== 'all' ? '&language=' + encodeURIComponent(language) : '' %>" class="<%= status === 'rejected' ? 'active' : '' %>">Rejetés (<%= stats.rejected %>)</a>
|
||||
<a href="/admin/sources">Sources</a>
|
||||
<a href="/admin/log">Journal</a>
|
||||
<form method="POST" action="/admin/collect" style="display:inline">
|
||||
<button type="submit" class="btn btn-collect">Collecter maintenant</button>
|
||||
</form>
|
||||
<a href="/admin/logout" class="logout">Déconnexion</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<% const langLabel = (l) => l === 'fr' ? 'Français' : l === 'en' ? 'Anglais' : (l || '').toUpperCase(); %>
|
||||
|
||||
<form method="GET" action="/admin" class="filters">
|
||||
<% if (status !== 'all') { %>
|
||||
<input type="hidden" name="status" value="<%= status %>">
|
||||
<% } %>
|
||||
<label for="language">Langue :</label>
|
||||
<select id="language" name="language" onchange="this.form.submit()">
|
||||
<option value="all" <%= language === 'all' ? 'selected' : '' %>>Toutes les langues</option>
|
||||
<% languages.forEach(l => { %>
|
||||
<option value="<%= l.lang %>" <%= language === l.lang ? 'selected' : '' %>><%= langLabel(l.lang) %> (<%= l.c %>)</option>
|
||||
<% }); %>
|
||||
</select>
|
||||
<noscript><button type="submit" class="btn btn-filter">Filtrer</button></noscript>
|
||||
</form>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">En attente: <strong><%= stats.pending %></strong></div>
|
||||
<div class="stat-card">Publiés: <strong><%= stats.published %></strong></div>
|
||||
<div class="stat-card">Masqués: <strong><%= stats.hidden %></strong></div>
|
||||
<div class="stat-card">Rejetés: <strong><%= stats.rejected %></strong></div>
|
||||
</div>
|
||||
|
||||
<% if (articles.length === 0) { %>
|
||||
<div class="empty">Aucun article dans cette catégorie.</div>
|
||||
<% } else { %>
|
||||
<div class="articles-admin">
|
||||
<% articles.forEach(article => { %>
|
||||
<div class="article-admin-card">
|
||||
<div class="article-admin-header">
|
||||
<div>
|
||||
<h3><%= article.title %></h3>
|
||||
<div class="article-meta">
|
||||
<span>Source: <%= article.source %></span> |
|
||||
<span>URL: <a href="<%= article.url %>" target="_blank"><%= article.url.substring(0, 60) %>...</a></span> |
|
||||
<span>Statut: <span class="badge badge-<%= article.status %>"><%= article.status %></span></span> |
|
||||
<span>Langue: <%= langLabel(article.language || article.source_language || 'fr') %></span> |
|
||||
<span>Collecté par: <%= article.collected_by %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (article.summary) { %>
|
||||
<p class="article-admin-summary"><%= article.summary %></p>
|
||||
<% } %>
|
||||
|
||||
<% if (article.status === 'pending') { %>
|
||||
<form method="POST" action="/admin/article/<%= article.id %>/validate" class="validate-form">
|
||||
<div class="form-row">
|
||||
<input type="text" name="categories" placeholder="Catégories (séparées par virgule)"
|
||||
value="<% try { const cats = JSON.parse(article.categories||'[]'); %><%= cats.join(', ') %><% } catch(e) {} %>">
|
||||
<input type="text" name="summary" placeholder="Résumé à afficher (optionnel)" value="<%= article.summary || '' %>">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-publish">Valider et publier</button>
|
||||
</form>
|
||||
<% } %>
|
||||
|
||||
<div class="article-admin-actions">
|
||||
<% if (article.status !== 'published') { %>
|
||||
<form method="POST" action="/admin/article/<%= article.id %>/publish" style="display:inline">
|
||||
<button type="submit" class="btn btn-success">Publier</button>
|
||||
</form>
|
||||
<% } %>
|
||||
<% if (article.status !== 'hidden') { %>
|
||||
<form method="POST" action="/admin/article/<%= article.id %>/hide" style="display:inline">
|
||||
<button type="submit" class="btn btn-warning">Masquer</button>
|
||||
</form>
|
||||
<% } %>
|
||||
<% if (article.status !== 'rejected') { %>
|
||||
<form method="POST" action="/admin/article/<%= article.id %>/reject" style="display:inline">
|
||||
<button type="submit" class="btn btn-danger">Rejeter</button>
|
||||
</form>
|
||||
<% } %>
|
||||
<form method="POST" action="/admin/article/<%= article.id %>/delete" style="display:inline" onsubmit="return confirm('Supprimer définitivement cet article ?')">
|
||||
<button type="submit" class="btn btn-danger">Supprimer</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<% } %>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Conserve la position de défilement lors des actions (publier, masquer, rejeter, supprimer)
|
||||
(function () {
|
||||
var storageKey = 'admin-scroll:' + location.pathname + location.search;
|
||||
|
||||
document.querySelectorAll('form').forEach(function (form) {
|
||||
if (form.method.toLowerCase() !== 'post') return;
|
||||
|
||||
form.addEventListener('submit', function (event) {
|
||||
if (event.defaultPrevented) return; // ex. suppression annulée
|
||||
sessionStorage.setItem(storageKey, String(window.scrollY));
|
||||
});
|
||||
});
|
||||
|
||||
var savedPosition = sessionStorage.getItem(storageKey);
|
||||
if (savedPosition === null) return;
|
||||
sessionStorage.removeItem(storageKey);
|
||||
|
||||
window.addEventListener('load', function () {
|
||||
window.scrollTo(0, parseInt(savedPosition, 10) || 0);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Journal d'activité - Administration</title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="admin-header">
|
||||
<div class="container">
|
||||
<h1>Administration - Veille Tech</h1>
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin">Tableau de bord</a>
|
||||
<a href="/admin/sources">Sources</a>
|
||||
<a href="/admin/log" class="active">Journal</a>
|
||||
<a href="/admin/logout" class="logout">Déconnexion</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<h2>Journal d'activité (100 dernières entrées)</h2>
|
||||
<table class="log-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Action</th>
|
||||
<th>Article ID</th>
|
||||
<th>Détails</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% logs.forEach(log => { %>
|
||||
<tr>
|
||||
<td><%= log.created_at %></td>
|
||||
<td><span class="badge badge-<%= log.action %>"><%= log.action %></span></td>
|
||||
<td><%= log.article_id ? log.article_id.substring(0, 8) + '...' : '-' %></td>
|
||||
<td><%= log.details || '-' %></td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Connexion - Administration</title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body class="login-page">
|
||||
<div class="login-container">
|
||||
<h1>Administration</h1>
|
||||
<h2>Veille Tech</h2>
|
||||
<% if (error) { %>
|
||||
<div class="error"><%= error %></div>
|
||||
<% } %>
|
||||
<form method="POST" action="/admin/login">
|
||||
<div class="form-group">
|
||||
<label for="username">Identifiant</label>
|
||||
<input type="text" id="username" name="username" required autocomplete="username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Mot de passe</label>
|
||||
<input type="password" id="password" name="password" required autocomplete="current-password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Se connecter</button>
|
||||
</form>
|
||||
<p class="back-link"><a href="/">← Retour au site</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sources - Administration</title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="admin-header">
|
||||
<div class="container">
|
||||
<h1>Administration - Veille Tech</h1>
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin">Tableau de bord</a>
|
||||
<a href="/admin/sources" class="active">Sources</a>
|
||||
<a href="/admin/log">Journal</a>
|
||||
<a href="/admin/logout" class="logout">Déconnexion</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<h2>Sources RSS configurées</h2>
|
||||
<p class="info">Les sources sont configurées dans <code>config/sources.json</code>. Vous pouvez les modifier directement dans ce fichier.</p>
|
||||
|
||||
<div class="sources-list">
|
||||
<% sources.forEach(source => { %>
|
||||
<div class="source-card">
|
||||
<h3><%= source.name %> <span class="badge <%= source.enabled ? 'badge-published' : 'badge-rejected' %>"><%= source.enabled ? 'Activé' : 'Désactivé' %></span></h3>
|
||||
<p><strong>ID:</strong> <%= source.id %></p>
|
||||
<p><strong>URL:</strong> <a href="<%= source.url %>" target="_blank"><%= source.url %></a></p>
|
||||
<p><strong>Type:</strong> <%= source.type %></p>
|
||||
<p><strong>Catégorie suggérée:</strong> <%= source.category_hint || '-' %></p>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Agrégateur d'Actualités Informatiques</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="container">
|
||||
<h1 class="logo">💻 Veille Tech</h1>
|
||||
<p class="subtitle">Agrégateur d'actualités informatiques</p>
|
||||
<% if (typeof languages !== 'undefined' && languages.length > 0) { %>
|
||||
<div class="lang-filter">
|
||||
<span>Langue:</span>
|
||||
<a href="/?sort=<%= sort %><%= selectedCategory ? '&category=' + encodeURIComponent(selectedCategory) : '' %>" class="<%= !selectedLang ? 'active' : '' %>">Toutes</a>
|
||||
<% languages.forEach(l => { %>
|
||||
<a href="/?lang=<%= encodeURIComponent(l) %>&sort=<%= sort %><%= selectedCategory ? '&category=' + encodeURIComponent(selectedCategory) : '' %>" class="<%= selectedLang === l ? 'active' : '' %>"><%= l === 'fr' ? 'Français' : l === 'en' ? 'English' : l.toUpperCase() %></a>
|
||||
<% }); %>
|
||||
</div>
|
||||
<% } %>
|
||||
<nav class="nav">
|
||||
<a href="/?sort=<%= sort %><%= selectedLang ? '&lang=' + encodeURIComponent(selectedLang) : '' %>" class="<%= !selectedCategory ? 'active' : '' %>">Tous</a>
|
||||
<% categories.forEach(cat => { %>
|
||||
<a href="/?category=<%= encodeURIComponent(cat) %>&sort=<%= sort %><%= selectedLang ? '&lang=' + encodeURIComponent(selectedLang) : '' %>" class="<%= selectedCategory === cat ? 'active' : '' %>"><%= cat %></a>
|
||||
<% }); %>
|
||||
</nav>
|
||||
<div class="sort">
|
||||
<span>Trier par date:</span>
|
||||
<a href="/?sort=desc<%= selectedCategory ? '&category=' + encodeURIComponent(selectedCategory) : '' %><%= selectedLang ? '&lang=' + encodeURIComponent(selectedLang) : '' %>" class="<%= sort === 'desc' ? 'active' : '' %>">Plus récent</a>
|
||||
<a href="/?sort=asc<%= selectedCategory ? '&category=' + encodeURIComponent(selectedCategory) : '' %><%= selectedLang ? '&lang=' + encodeURIComponent(selectedLang) : '' %>" class="<%= sort === 'asc' ? 'active' : '' %>">Plus ancien</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<% if (articles.length === 0) { %>
|
||||
<div class="empty">
|
||||
<p>Aucun article publié pour le moment.</p>
|
||||
<p class="small">Les articles en attente doivent être validés depuis l'<a href="/admin">interface d'administration</a>.</p>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="articles">
|
||||
<% articles.forEach(article => { %>
|
||||
<article class="article-card">
|
||||
<div class="article-header">
|
||||
<span class="article-source"><%= article.source || 'Source inconnue' %> <% if (article.language || article.source_language) { %><span class="lang-badge"><%= (article.language || article.source_language) === 'fr' ? 'FR' : (article.language || article.source_language).toUpperCase() %></span><% } %></span>
|
||||
<span class="article-date">
|
||||
<%= article.published_at ? new Date(article.published_at).toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' }) : '' %>
|
||||
</span>
|
||||
</div>
|
||||
<h2 class="article-title"><%= article.title %></h2>
|
||||
<% if (article.summary) { %>
|
||||
<p class="article-summary"><%= article.summary %></p>
|
||||
<% } %>
|
||||
<% if (article.categories) { %>
|
||||
<div class="article-categories">
|
||||
<% try {
|
||||
const cats = JSON.parse(article.categories);
|
||||
if (Array.isArray(cats)) {
|
||||
cats.forEach(cat => { %>
|
||||
<span class="category-tag"><%= cat %></span>
|
||||
<% });
|
||||
}
|
||||
} catch(e) {} %>
|
||||
</div>
|
||||
<% } %>
|
||||
<a href="<%= article.url %>" target="_blank" rel="noopener noreferrer" class="article-link">
|
||||
Lire l'article original ↗
|
||||
</a>
|
||||
</article>
|
||||
<% }); %>
|
||||
</div>
|
||||
<% } %>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p>Veille Tech - Agrégateur d'actualités informatiques</p>
|
||||
<p class="small"><a href="/admin">Administration</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||