feat(web): overhaul frontend Paper Dark, migration Tailwind v4 et composants magazine

This commit is contained in:
Michael committed 2026-02-04 16:22:59 +01:00
1 parent 8777d3b970
commit 8ed1782de6
16 files changed
+1998 -238

No files matched your search

+55 -29
View File
@@ -64,11 +64,8 @@ export function DashboardPage() {
const handleArticleClick = async (article: Article) => {
try {
// Fetch full content and sanitize
const fullArticle = await articlesApi.get(article.id);
setViewingArticle(fullArticle);
// Mark as read automatically
if (!article.is_read) {
toggleReadMutation.mutate({ id: article.id, is_read: false });
}
@@ -78,56 +75,85 @@ export function DashboardPage() {
};
return (
<div className="dashboard-layout">
<div className="flex h-screen bg-carbon overflow-hidden">
<Sidebar
onSelectFeed={setSelectedFeedId}
selectedFeedId={selectedFeedId}
/>
<div className="dashboard-content">
<header className="dashboard-header">
<h1>{selectedFeedId === 'favorites' ? 'Favoris' : 'Nouveautés'}</h1>
<div className="flex-1 flex flex-col min-w-0 bg-carbon overflow-hidden relative">
<header className="h-20 border-b border-carbon-dark/50 flex items-center justify-between px-8 bg-carbon/50 backdrop-blur-md z-10 shrink-0">
<div className="min-w-0">
<h1 className="text-gold text-2xl md:text-3xl font-serif italic truncate">
{selectedFeedId === 'favorites' ? 'Mes Favoris' : 'Nouveautés'}
</h1>
<p className="text-paper-muted text-[10px] uppercase tracking-widest font-bold">
{articles?.length || 0} ARTICLES À DÉCOUVRIR
</p>
</div>
<div className="header-actions">
<div className="flex items-center space-x-6 shrink-0">
{articles && articles.length > 0 && selectedFeedId !== 'favorites' && (
<button
className="mark-all-read-btn"
className="btn-secondary text-[10px] uppercase tracking-widest font-bold py-2 px-4 border-gold/30 hidden md:block"
onClick={() => markAllReadMutation.mutate()}
disabled={markAllReadMutation.isPending}
>
{markAllReadMutation.isPending ? 'Chargement...' : 'Tout marquer comme lu'}
{markAllReadMutation.isPending ? 'Patientez...' : 'Tout marquer lu'}
</button>
)}
<div className="user-info">
<span>{user?.email}</span>
<button onClick={handleLogout} className="logout-btn">
Déconnexion
</button>
<div className="flex items-center space-x-4 border-l border-carbon-dark/50 pl-6">
<div className="text-right hidden sm:block">
<p className="text-paper-white text-xs font-medium truncate max-w-[150px]">{user?.email}</p>
<button onClick={handleLogout} className="text-paper-muted hover:text-gold text-[10px] uppercase tracking-tighter transition-colors">
Déconnexion
</button>
</div>
<div className="w-10 h-10 rounded-full bg-gold/10 border border-gold/20 flex items-center justify-center text-gold font-serif text-lg shrink-0">
{user?.email?.[0].toUpperCase() || 'M'}
</div>
</div>
</div>
</header>
<main className="articles-main">
<main className="flex-1 overflow-y-auto px-4 md:px-8 py-10">
{isLoading ? (
<div className="loading-state">
<div className="spinner"></div>
<div className="flex flex-col items-center justify-center h-full space-y-4 opacity-50">
<div className="w-12 h-12 border-2 border-gold/20 border-t-gold rounded-full animate-spin" />
<p className="text-gold text-xs uppercase tracking-widest animate-pulse">Édition en cours...</p>
</div>
) : articles?.length === 0 ? (
<div className="empty-state">
<h2>🎉 Tout est lu !</h2>
<p>Revenez plus tard pour de nouveaux articles.</p>
<div className="flex flex-col items-center justify-center h-full text-center space-y-6 animate-in fade-in duration-1000">
<div className="text-6xl filter grayscale opacity-30">💆‍♂️</div>
<div className="max-w-md">
<h2 className="text-paper-white text-2xl font-serif mb-2">Immersion Totale</h2>
<p className="text-paper-muted font-reading text-lg">
Vous avez épuisé vos flux. Profitez de ce moment de silence.
</p>
</div>
<button
className="btn-secondary text-[10px] uppercase tracking-[0.2em] font-bold"
onClick={() => queryClient.invalidateQueries({ queryKey: ['articles'] })}
>
Actualiser
</button>
</div>
) : (
<div className="articles-grid">
{articles?.map((article: Article) => (
<ArticleCard
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-8">
{articles?.map((article: Article, index: number) => (
<div
key={article.id}
article={article}
onClick={handleArticleClick}
onToggleRead={(id, is_read) => toggleReadMutation.mutate({ id, is_read })}
onToggleFavorite={(id) => toggleFavoriteMutation.mutate({ id })}
/>
className={`${index === 0 ? 'md:col-span-2' : ''} animate-in fade-in slide-in-from-bottom-4 duration-500`}
style={{ animationDelay: `${index * 50}ms` }}
>
<ArticleCard
article={article}
onClick={handleArticleClick}
onToggleRead={(id, is_read) => toggleReadMutation.mutate({ id, is_read })}
onToggleFavorite={(id) => toggleFavoriteMutation.mutate({ id })}
/>
</div>
))}
</div>
)}
+40 -21
View File
@@ -1,7 +1,8 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useMutation } from '@tanstack/react-query';
import { authApi, ApiError } from '../api/auth';
import { authApi } from '../api/auth';
import { ApiError } from '../api/client';
import { useAuthStore } from '../stores/authStore';
export function LoginPage() {
@@ -21,7 +22,7 @@ export function LoginPage() {
if (err instanceof ApiError) {
setError(err.message);
} else {
setError('Login failed. Please try again.');
setError('Échec de la connexion. Veuillez réessayer.');
}
},
});
@@ -33,50 +34,68 @@ export function LoginPage() {
};
return (
<div className="login-container">
<div className="login-card">
<h1>FlowReader</h1>
<p className="subtitle">Connectez-vous à votre compte</p>
<div className="min-h-screen bg-carbon flex items-center justify-center p-6">
<div className="w-full max-w-md bg-carbon-light p-10 rounded-xl shadow-2xl border border-carbon-dark/50 animate-in fade-in zoom-in duration-500">
<div className="text-center mb-10">
<h1 className="text-gold text-4xl font-serif mb-2 italic tracking-tight">FlowReader</h1>
<p className="text-paper-muted text-[10px] uppercase tracking-[0.3em] font-bold">Magazine Personnel</p>
</div>
<form onSubmit={handleSubmit}>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-500/10 border border-red-500/50 text-red-500 text-xs p-3 rounded-md animate-shake">
{error}
</div>
)}
<div className="form-group">
<label htmlFor="email">Email</label>
<div className="space-y-2">
<label htmlFor="email" className="block text-paper-muted text-[10px] uppercase tracking-widest font-bold">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="vous@example.com"
className="w-full bg-carbon border border-carbon-dark text-paper-white px-4 py-3 rounded-md focus:border-gold focus:ring-1 focus:ring-gold outline-none transition-all placeholder:text-carbon-light"
placeholder="vous@exemple.com"
required
autoFocus
/>
</div>
<div className="form-group">
<label htmlFor="password">Mot de passe</label>
<div className="space-y-2">
<label htmlFor="password" title="password" className="block text-paper-muted text-[10px] uppercase tracking-widest font-bold">
Mot de passe
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-carbon border border-carbon-dark text-paper-white px-4 py-3 rounded-md focus:border-gold focus:ring-1 focus:ring-gold outline-none transition-all placeholder:text-carbon-light"
placeholder="••••••••"
required
minLength={8}
/>
</div>
<button type="submit" disabled={loginMutation.isPending}>
{loginMutation.isPending ? 'Connexion...' : 'Se connecter'}
<button
type="submit"
disabled={loginMutation.isPending}
className="w-full btn-primary py-4 text-xs uppercase tracking-[0.2em] font-bold mt-4 disabled:opacity-50 disabled:cursor-not-wait"
>
{loginMutation.isPending ? 'Authentification...' : 'Continuer'}
</button>
</form>
<p className="register-link">
Pas encore de compte ?{' '}
<a href="/register" onClick={(e) => { e.preventDefault(); navigate('/register'); }}>
S'inscrire
</a>
<p className="mt-8 text-center text-paper-muted text-xs">
Nouveau chez FlowReader ?{' '}
<button
className="text-gold hover:underline font-bold tracking-wide"
onClick={() => navigate('/register')}
>
Créer un compte
</button>
</p>
</div>
</div>
+43 -21
View File
@@ -1,7 +1,8 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useMutation } from '@tanstack/react-query';
import { authApi, ApiError } from '../api/auth';
import { authApi } from '../api/auth';
import { ApiError } from '../api/client';
export function RegisterPage() {
const navigate = useNavigate();
@@ -19,7 +20,7 @@ export function RegisterPage() {
if (err instanceof ApiError) {
setError(err.message);
} else {
setError('Registration failed. Please try again.');
setError('L\'inscription a échoué. Veuillez réessayer.');
}
},
});
@@ -37,63 +38,84 @@ export function RegisterPage() {
};
return (
<div className="login-container">
<div className="login-card">
<h1>FlowReader</h1>
<p className="subtitle">Créer un nouveau compte</p>
<div className="min-h-screen bg-carbon flex items-center justify-center p-6">
<div className="w-full max-w-md bg-carbon-light p-10 rounded-xl shadow-2xl border border-carbon-dark/50 animate-in fade-in zoom-in duration-500">
<div className="text-center mb-10">
<h1 className="text-gold text-4xl font-serif mb-2 italic tracking-tight">FlowReader</h1>
<p className="text-paper-muted text-[10px] uppercase tracking-[0.3em] font-bold">Rejoindre l'Édition</p>
</div>
<form onSubmit={handleSubmit}>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-500/10 border border-red-500/50 text-red-500 text-xs p-3 rounded-md animate-shake">
{error}
</div>
)}
<div className="form-group">
<label htmlFor="email">Email</label>
<div className="space-y-2">
<label htmlFor="email" className="block text-paper-muted text-[10px] uppercase tracking-widest font-bold">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="vous@example.com"
className="w-full bg-carbon border border-carbon-dark text-paper-white px-4 py-3 rounded-md focus:border-gold focus:ring-1 focus:ring-gold outline-none transition-all placeholder:text-carbon-light"
placeholder="vous@exemple.com"
required
autoFocus
/>
</div>
<div className="form-group">
<label htmlFor="password">Mot de passe</label>
<div className="space-y-2">
<label htmlFor="password" title='password' className="block text-paper-muted text-[10px] uppercase tracking-widest font-bold">
Mot de passe
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-carbon border border-carbon-dark text-paper-white px-4 py-3 rounded-md focus:border-gold focus:ring-1 focus:ring-gold outline-none transition-all placeholder:text-carbon-light"
placeholder="••••••••"
required
minLength={8}
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirmer le mot de passe</label>
<div className="space-y-2">
<label htmlFor="confirmPassword" title='confirm password' className="block text-paper-muted text-[10px] uppercase tracking-widest font-bold">
Confirmer le mot de passe
</label>
<input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full bg-carbon border border-carbon-dark text-paper-white px-4 py-3 rounded-md focus:border-gold focus:ring-1 focus:ring-gold outline-none transition-all placeholder:text-carbon-light"
placeholder="••••••••"
required
minLength={8}
/>
</div>
<button type="submit" disabled={registerMutation.isPending}>
<button
type="submit"
disabled={registerMutation.isPending}
className="w-full btn-primary py-4 text-xs uppercase tracking-[0.2em] font-bold mt-4 disabled:opacity-50 disabled:cursor-wait"
>
{registerMutation.isPending ? 'Création...' : 'Créer mon compte'}
</button>
</form>
<p className="register-link">
Déjà un compte ?{' '}
<a href="/login" onClick={(e) => { e.preventDefault(); navigate('/login'); }}>
<p className="mt-8 text-center text-paper-muted text-xs">
Déjà membre ?{' '}
<button
className="text-gold hover:underline font-bold tracking-wide"
onClick={() => navigate('/login')}
>
Se connecter
</a>
</button>
</p>
</div>
</div>