mirror of
https://github.com/R0m1k3/FlowReader.git
synced 2026-10-14 10:49:47 +02:00
refonte complète du WebUI (Éditorial Botanique)
Refonte de toutes les vues en conservant l'identité magazine/vert nature, avec un système de design cohérent et accessible. - index.css : tokens sémantiques, échelle radius/ombres, classes réutilisables (btn-*, chip, surface-card, magazine-content), :focus-visible, prefers-reduced-motion, grain papier affiné - nouveaux composants AuthShell (auth split-screen) et MobileTopBar (navigation mobile, auparavant absente) - framer-motion sur toutes les vues ; AddFeedModal expose l'import OPML ; Sidebar ajoute déconnexion + identité utilisateur - corrections : text-gold cassé -> nature, arithmétique Date -> getTime(), suppression de App.css mort, ProtectedRoute en Tailwind, snapshot du deck Focus conforme aux règles React Frontend : npm run lint + build OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
2a8b5664c5
commit
10fdfdeccb
19 files changed
+1336
-1922
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { feedsApi } from '../api/feeds';
|
||||
|
||||
interface AddFeedModalProps {
|
||||
@@ -9,10 +10,11 @@ interface AddFeedModalProps {
|
||||
|
||||
export function AddFeedModal({ isOpen, onClose }: AddFeedModalProps) {
|
||||
const [url, setUrl] = useState('');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const addFeedMutation = useMutation({
|
||||
mutationFn: (url: string) => feedsApi.add({ url }),
|
||||
mutationFn: (u: string) => feedsApi.add({ url: u }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['feeds'] });
|
||||
setUrl('');
|
||||
@@ -20,68 +22,112 @@ export function AddFeedModal({ isOpen, onClose }: AddFeedModalProps) {
|
||||
},
|
||||
});
|
||||
|
||||
if (!isOpen) return null;
|
||||
const importMutation = useMutation({
|
||||
mutationFn: (file: File) => feedsApi.importOPML(file),
|
||||
onSuccess: (res) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['feeds'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['articles'] });
|
||||
window.alert(`${res.imported} flux importé(s), ${res.skipped} ignoré(s).`);
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
// Close on Escape
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
addFeedMutation.mutate(url);
|
||||
};
|
||||
|
||||
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) importMutation.mutate(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-carbon-dark/80 backdrop-blur-sm animate-in fade-in duration-300" onClick={onClose}>
|
||||
<div
|
||||
className="w-full max-w-md paper-panel p-8 rounded-2xl shadow-2xl border border-nature/10 animate-in zoom-in duration-300"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-serif text-nature italic">Nouveau Flux</h2>
|
||||
<button onClick={onClose} className="text-paper-muted hover:text-white transition-colors">
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-nature-dark/60 backdrop-blur-sm"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="add-feed-title"
|
||||
>
|
||||
<motion.div
|
||||
className="w-full max-w-md surface-card p-8"
|
||||
initial={{ opacity: 0, scale: 0.95, y: 12 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 12 }}
|
||||
transition={{ duration: 0.25, ease: [0.22, 1, 0.36, 1] }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h2 id="add-feed-title" className="text-2xl font-serif italic text-nature">Nouveau flux</h2>
|
||||
<button onClick={onClose} className="icon-btn" aria-label="Fermer">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-paper-muted text-xs mb-6 leading-relaxed">
|
||||
Entrez l'URL d'un flux RSS ou Atom. FlowReader récupérera automatiquement le titre et le contenu.
|
||||
</p>
|
||||
<p className="text-paper-muted text-sm mb-6 leading-relaxed font-reading">
|
||||
Entrez l'URL d'un flux RSS ou Atom. FlowReader récupérera le titre et le contenu automatiquement.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="block text-[10px] uppercase tracking-[0.2em] font-bold text-paper-muted">URL du Flux</label>
|
||||
<input
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
className="input-field w-full"
|
||||
placeholder="https://exemple.com/feed"
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="feed-url" className="block eyebrow text-paper-muted">URL du flux</label>
|
||||
<input
|
||||
id="feed-url"
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
className="input-field"
|
||||
placeholder="https://exemple.com/feed"
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{addFeedMutation.isError && (
|
||||
<p className="text-red-400 text-xs italic">Échec de l'ajout. Vérifiez l'URL du flux.</p>
|
||||
)}
|
||||
{addFeedMutation.isError && (
|
||||
<p className="text-danger text-xs" role="alert">Échec de l'ajout. Vérifiez l'URL du flux.</p>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end pt-4 gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="btn-ghost"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={url.length < 5 || addFeedMutation.isPending}
|
||||
className="btn-primary"
|
||||
>
|
||||
{addFeedMutation.isPending ? 'Ajout...' : 'S\'abonner'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button type="button" onClick={onClose} className="btn-ghost">Annuler</button>
|
||||
<button type="submit" disabled={url.length < 5 || addFeedMutation.isPending} className="btn-primary">
|
||||
{addFeedMutation.isPending ? 'Ajout…' : "S'abonner"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* OPML import */}
|
||||
<div className="mt-6 pt-5 border-t border-paper-muted/12">
|
||||
<input ref={fileInputRef} type="file" accept=".opml,.xml,text/xml" className="hidden" onChange={handleFile} />
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={importMutation.isPending}
|
||||
className="btn-ghost w-full justify-center"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
</svg>
|
||||
{importMutation.isPending ? 'Import en cours…' : 'Importer un fichier OPML'}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user