design: rework for a better DA and light/dark mode

This commit is contained in:
2026-04-28 07:54:07 +02:00
parent 490a364c00
commit f92f51273e
13 changed files with 252 additions and 81 deletions

View File

@ -4,9 +4,12 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0f172a" />
<meta name="description" content="Agrégateur de news financières avec résumés IA" />
<title>Tradarr</title>
<meta name="theme-color" content="#f7f4ef" />
<meta name="description" content="Votre journal financier augmenté par l'IA" />
<title>FinancIAl</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>

View File

@ -12,7 +12,7 @@ const items = [
export function MobileNav() {
return (
<nav className="fixed bottom-0 left-0 right-0 z-50 flex border-t bg-card md:hidden">
<nav className="fixed bottom-0 left-0 right-0 z-50 flex border-t bg-card md:hidden shadow-sm">
{items.map(({ to, icon: Icon, label }) => (
<NavLink
key={to}
@ -21,7 +21,7 @@ export function MobileNav() {
className={({ isActive }) =>
cn(
'flex flex-1 flex-col items-center gap-1 py-3 text-xs transition-colors',
isActive ? 'text-primary' : 'text-muted-foreground'
isActive ? 'text-primary font-medium' : 'text-muted-foreground'
)
}
>

View File

@ -1,5 +1,5 @@
import { NavLink } from 'react-router-dom'
import { LayoutDashboard, Newspaper, Star, Settings, Key, Cpu, Database, ClipboardList, Users, LogOut, TrendingUp, CalendarDays, FileText } from 'lucide-react'
import { LayoutDashboard, Newspaper, Star, Settings, Key, Cpu, Database, ClipboardList, Users, LogOut, CalendarDays, FileText } from 'lucide-react'
import { useAuth } from '@/lib/auth'
import { cn } from '@/lib/cn'
@ -27,14 +27,14 @@ function NavItem({ to, icon: Icon, label }: { to: string; icon: React.ElementTyp
end={to === '/'}
className={({ isActive }) =>
cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
isActive
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
? 'bg-primary/8 text-primary font-medium border-l-2 border-primary pl-[10px]'
: 'text-muted-foreground hover:bg-accent hover:text-foreground font-normal'
)
}
>
<Icon className="h-4 w-4" />
<Icon className="h-4 w-4 shrink-0" />
{label}
</NavLink>
)
@ -44,25 +44,32 @@ export function Sidebar() {
const { user, logout, isAdmin } = useAuth()
return (
<aside className="flex h-screen w-60 flex-col border-r bg-card px-3 py-4">
<aside className="flex h-screen w-60 flex-col border-r bg-card px-3 py-5">
{/* Logo */}
<div className="mb-6 flex items-center gap-2 px-3">
<TrendingUp className="h-6 w-6 text-primary" />
<span className="text-lg font-bold">Tradarr</span>
<div className="mb-7 px-3">
<span className="font-serif text-xl font-bold tracking-tight text-foreground">
Financ<span className="text-primary">IA</span>l
</span>
<p className="text-[10px] text-muted-foreground mt-0.5 tracking-wide uppercase">Journal financier IA</p>
</div>
{/* Séparateur */}
<div className="border-t mb-4" />
{/* Navigation principale */}
<nav className="flex flex-col gap-1">
<nav className="flex flex-col gap-0.5">
{navItems.map(item => <NavItem key={item.to} {...item} />)}
</nav>
{/* Section admin */}
{isAdmin && (
<>
<div className="mt-6 mb-2 px-3">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Administration</p>
<div className="mt-5 mb-2 px-3 flex items-center gap-2">
<div className="flex-1 border-t" />
<p className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">Admin</p>
<div className="flex-1 border-t" />
</div>
<nav className="flex flex-col gap-1">
<nav className="flex flex-col gap-0.5">
{adminItems.map(item => <NavItem key={item.to} {...item} />)}
</nav>
</>
@ -71,12 +78,12 @@ export function Sidebar() {
{/* User + logout */}
<div className="mt-auto border-t pt-4">
<div className="px-3 mb-2">
<p className="text-sm font-medium truncate">{user?.email}</p>
<p className="text-sm font-medium truncate text-foreground">{user?.email}</p>
<p className="text-xs text-muted-foreground capitalize">{user?.role}</p>
</div>
<button
onClick={logout}
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
>
<LogOut className="h-4 w-4" />
Déconnexion

View File

@ -3,16 +3,16 @@ import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/cn'
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors',
'inline-flex items-center rounded border px-2 py-0.5 text-xs font-medium transition-colors',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground',
secondary: 'border-transparent bg-secondary text-secondary-foreground',
destructive: 'border-transparent bg-destructive text-destructive-foreground',
outline: 'text-foreground',
bullish: 'border-transparent bg-bullish/20 text-bullish',
bearish: 'border-transparent bg-bearish/20 text-bearish',
destructive: 'border-transparent bg-destructive/10 text-destructive border-destructive/20',
outline: 'text-foreground border-border',
bullish: 'border-transparent bg-bullish/10 text-bullish',
bearish: 'border-transparent bg-bearish/10 text-bearish',
},
},
defaultVariants: { variant: 'default' },

View File

@ -16,9 +16,9 @@ const buttonVariants = cva(
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
sm: 'h-7 rounded px-2.5 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
icon: 'h-8 w-8',
},
},
defaultVariants: { variant: 'default', size: 'default' },

View File

@ -3,14 +3,14 @@ import { cn } from '@/lib/cn'
export const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)} {...props} />
<div ref={ref} className={cn('rounded-md border bg-card text-card-foreground shadow-sm', className)} {...props} />
)
)
Card.displayName = 'Card'
export const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-5', className)} {...props} />
)
)
CardHeader.displayName = 'CardHeader'
@ -24,14 +24,14 @@ CardTitle.displayName = 'CardTitle'
export const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
<div ref={ref} className={cn('p-5 pt-0', className)} {...props} />
)
)
CardContent.displayName = 'CardContent'
export const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
<div ref={ref} className={cn('flex items-center p-5 pt-0', className)} {...props} />
)
)
CardFooter.displayName = 'CardFooter'

View File

@ -4,24 +4,71 @@
@layer base {
:root {
--background: 222 47% 6%;
--foreground: 210 40% 95%;
--card: 222 47% 9%;
--card-foreground: 210 40% 95%;
--border: 217 33% 18%;
--input: 217 33% 18%;
--ring: 221 83% 53%;
--primary: 221 83% 53%;
--primary-foreground: 210 40% 98%;
--secondary: 217 33% 18%;
--secondary-foreground: 210 40% 95%;
--muted: 217 33% 14%;
--muted-foreground: 215 20% 55%;
--accent: 217 33% 18%;
--accent-foreground: 210 40% 95%;
--destructive: 0 84% 60%;
--destructive-foreground: 210 40% 98%;
--radius: 0.5rem;
/* ── Fond & texte ── */
--background: 36 28% 97%;
--foreground: 20 15% 9%;
/* ── Cartes ── */
--card: 0 0% 100%;
--card-foreground: 20 15% 9%;
/* ── Bordures & inputs ── */
--border: 30 18% 87%;
--input: 30 18% 87%;
--ring: 22 62% 28%;
/* ── Primaire : brun cognac ── */
--primary: 22 62% 28%;
--primary-foreground: 36 30% 96%;
/* ── Secondaire & muted ── */
--secondary: 36 20% 93%;
--secondary-foreground: 20 12% 25%;
--muted: 35 18% 92%;
--muted-foreground: 25 8% 48%;
/* ── Accent (hover) ── */
--accent: 35 18% 90%;
--accent-foreground: 20 15% 9%;
/* ── Destructif ── */
--destructive: 0 78% 52%;
--destructive-foreground: 0 0% 98%;
--radius: 0.375rem;
}
.dark {
/* ── Fond & texte ── */
--background: 22 18% 7%;
--foreground: 36 22% 91%;
/* ── Cartes ── */
--card: 22 15% 10%;
--card-foreground: 36 22% 91%;
/* ── Bordures & inputs ── */
--border: 25 14% 18%;
--input: 25 14% 18%;
--ring: 22 55% 48%;
/* ── Primaire : cognac clair (lisible sur fond sombre) ── */
--primary: 22 55% 50%;
--primary-foreground: 36 30% 96%;
/* ── Secondaire & muted ── */
--secondary: 22 13% 14%;
--secondary-foreground: 36 18% 78%;
--muted: 22 12% 13%;
--muted-foreground: 30 8% 52%;
/* ── Accent (hover) ── */
--accent: 22 13% 15%;
--accent-foreground: 36 22% 91%;
/* ── Destructif ── */
--destructive: 0 72% 55%;
--destructive-foreground: 0 0% 98%;
}
}
@ -33,10 +80,14 @@
}
}
/* Scrollbar style */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { @apply bg-muted; }
::-webkit-scrollbar-thumb { @apply bg-border rounded-full; }
.font-serif {
font-family: 'Playfair Display', Georgia, serif;
}
/* Scrollbar */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: hsl(var(--muted)); }
::-webkit-scrollbar-thumb { background: hsl(var(--border)); border-radius: 9999px; }
@layer utilities {
.scrollbar-none { scrollbar-width: none; }

View File

@ -0,0 +1,40 @@
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
type Theme = 'light' | 'dark'
interface ThemeCtx {
theme: Theme
setTheme: (t: Theme) => void
}
const ThemeContext = createContext<ThemeCtx | null>(null)
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => {
const saved = localStorage.getItem('theme') as Theme | null
if (saved === 'light' || saved === 'dark') return saved
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
})
useEffect(() => {
const root = document.documentElement
root.classList.toggle('dark', theme === 'dark')
localStorage.setItem('theme', theme)
}, [theme])
function setTheme(t: Theme) {
setThemeState(t)
}
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
return ctx
}

View File

@ -2,13 +2,16 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import { AuthProvider } from '@/lib/auth'
import { ThemeProvider } from '@/lib/theme'
import { router } from '@/lib/router'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ThemeProvider>
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
</ThemeProvider>
</StrictMode>
)

View File

@ -1,12 +1,10 @@
import { useState, type FormEvent } from 'react'
import { Navigate } from 'react-router-dom'
import { TrendingUp } from 'lucide-react'
import { useAuth } from '@/lib/auth'
import { authApi } from '@/api/auth'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
export function Login() {
const { token, login } = useAuth()
@ -33,31 +31,64 @@ export function Login() {
return (
<div className="flex min-h-screen items-center justify-center bg-background p-4">
<Card className="w-full max-w-sm">
<CardHeader className="text-center">
<div className="mb-2 flex justify-center">
<TrendingUp className="h-10 w-10 text-primary" />
{/* Motif de fond discret */}
<div className="absolute inset-0 opacity-[0.03] pointer-events-none"
style={{ backgroundImage: 'repeating-linear-gradient(0deg, hsl(20 15% 9%), hsl(20 15% 9%) 1px, transparent 1px, transparent 40px), repeating-linear-gradient(90deg, hsl(20 15% 9%), hsl(20 15% 9%) 1px, transparent 1px, transparent 40px)' }}
/>
<div className="relative w-full max-w-sm">
{/* En-tête */}
<div className="mb-8 text-center">
<h1 className="font-serif text-4xl font-bold text-foreground">
Financ<span className="text-primary">IA</span>l
</h1>
<p className="mt-2 text-sm text-muted-foreground tracking-wide">
Votre journal financier augmenté par l'IA
</p>
</div>
<CardTitle className="text-2xl">Tradarr</CardTitle>
<p className="text-sm text-muted-foreground">Votre assistant trading IA</p>
</CardHeader>
<CardContent>
{/* Carte formulaire */}
<div className="rounded-lg border bg-card shadow-sm p-6">
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-1">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" value={email} onChange={e => setEmail(e.target.value)} required autoComplete="email" />
<div className="space-y-1.5">
<Label htmlFor="email">Adresse email</Label>
<Input
id="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
required
autoComplete="email"
placeholder="vous@exemple.com"
/>
</div>
<div className="space-y-1">
<div className="space-y-1.5">
<Label htmlFor="password">Mot de passe</Label>
<Input id="password" type="password" value={password} onChange={e => setPassword(e.target.value)} required autoComplete="current-password" />
<Input
id="password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
required
autoComplete="current-password"
placeholder="••••••••"
/>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
<Button type="submit" className="w-full" disabled={loading}>
{error && (
<p className="text-sm text-destructive border border-destructive/20 bg-destructive/5 rounded px-3 py-2">
{error}
</p>
)}
<Button type="submit" className="w-full mt-2" disabled={loading}>
{loading ? 'Connexion' : 'Se connecter'}
</Button>
</form>
</CardContent>
</Card>
</div>
<p className="mt-6 text-center text-xs text-muted-foreground">
© {new Date().getFullYear()} FinancIAl Accès restreint
</p>
</div>
</div>
)
}

View File

@ -1,11 +1,12 @@
import { useState, useEffect } from 'react'
import { Save, RotateCcw } from 'lucide-react'
import { Save, RotateCcw, Sun, Moon } from 'lucide-react'
import { adminApi, type Setting } from '@/api/admin'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Spinner } from '@/components/ui/spinner'
import { useTheme } from '@/lib/theme'
const NUMERIC_SETTINGS: Record<string, { label: string; description: string }> = {
summary_max_articles: { label: 'Articles max par résumé', description: 'Nombre maximum d\'articles envoyés à l\'IA pour la passe 2 (résumé)' },
@ -20,6 +21,7 @@ const COMMON_TIMEZONES = [
]
export function AdminSettings() {
const { theme, setTheme } = useTheme()
const [settings, setSettings] = useState<Setting[]>([])
const [loading, setLoading] = useState(true)
const [values, setValues] = useState<Record<string, string>>({})
@ -90,6 +92,37 @@ export function AdminSettings() {
<p className="text-muted-foreground text-sm">Configuration globale du service</p>
</div>
{/* Apparence */}
<Card>
<CardHeader><CardTitle>Apparence</CardTitle></CardHeader>
<CardContent>
<div className="flex items-center gap-3">
<button
onClick={() => setTheme('light')}
className={`flex items-center gap-2 px-4 py-2 rounded-md border text-sm font-medium transition-colors ${
theme === 'light'
? 'bg-primary text-primary-foreground border-primary'
: 'bg-transparent text-muted-foreground border-border hover:bg-accent hover:text-foreground'
}`}
>
<Sun className="h-4 w-4" />
Clair
</button>
<button
onClick={() => setTheme('dark')}
className={`flex items-center gap-2 px-4 py-2 rounded-md border text-sm font-medium transition-colors ${
theme === 'dark'
? 'bg-primary text-primary-foreground border-primary'
: 'bg-transparent text-muted-foreground border-border hover:bg-accent hover:text-foreground'
}`}
>
<Moon className="h-4 w-4" />
Sombre
</button>
</div>
</CardContent>
</Card>
{/* Fuseau horaire */}
<Card>
<CardHeader><CardTitle>Fuseau horaire</CardTitle></CardHeader>

View File

@ -5,6 +5,9 @@ export default {
content: ['./index.html', './src/**/*.{ts,tsx}'],
theme: {
extend: {
fontFamily: {
serif: ['Playfair Display', 'Georgia', 'serif'],
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
@ -17,9 +20,9 @@ export default {
muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))' },
accent: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-foreground))' },
card: { DEFAULT: 'hsl(var(--card))', foreground: 'hsl(var(--card-foreground))' },
bullish: '#22c55e',
bearish: '#ef4444',
neutral: '#94a3b8',
bullish: '#15803d',
bearish: '#b91c1c',
neutral: '#78716c',
},
borderRadius: {
lg: 'var(--radius)',

View File

@ -1 +1 @@
{"root":["./src/main.tsx","./src/api/admin.ts","./src/api/articles.ts","./src/api/assets.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/reports.ts","./src/api/summaries.ts","./src/components/layout/AppLayout.tsx","./src/components/layout/MobileNav.tsx","./src/components/layout/Sidebar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/markdown.tsx","./src/components/ui/select.tsx","./src/components/ui/spinner.tsx","./src/lib/auth.tsx","./src/lib/cn.ts","./src/lib/router.tsx","./src/pages/Dashboard.tsx","./src/pages/Feed.tsx","./src/pages/Login.tsx","./src/pages/Reports.tsx","./src/pages/Watchlist.tsx","./src/pages/admin/AIProviders.tsx","./src/pages/admin/AdminLayout.tsx","./src/pages/admin/AdminSettings.tsx","./src/pages/admin/AdminUsers.tsx","./src/pages/admin/Credentials.tsx","./src/pages/admin/Jobs.tsx","./src/pages/admin/Schedule.tsx","./src/pages/admin/Sources.tsx"],"version":"5.9.3"}
{"root":["./src/main.tsx","./src/api/admin.ts","./src/api/articles.ts","./src/api/assets.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/reports.ts","./src/api/summaries.ts","./src/components/layout/AppLayout.tsx","./src/components/layout/MobileNav.tsx","./src/components/layout/Sidebar.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/markdown.tsx","./src/components/ui/select.tsx","./src/components/ui/spinner.tsx","./src/lib/auth.tsx","./src/lib/cn.ts","./src/lib/router.tsx","./src/lib/theme.tsx","./src/pages/Dashboard.tsx","./src/pages/Feed.tsx","./src/pages/Login.tsx","./src/pages/Reports.tsx","./src/pages/Watchlist.tsx","./src/pages/admin/AIProviders.tsx","./src/pages/admin/AdminLayout.tsx","./src/pages/admin/AdminSettings.tsx","./src/pages/admin/AdminUsers.tsx","./src/pages/admin/Credentials.tsx","./src/pages/admin/Jobs.tsx","./src/pages/admin/Schedule.tsx","./src/pages/admin/Sources.tsx"],"version":"5.9.3"}