Files
Priceflow/frontend/src/pages/Dashboard.jsx
T
Claude d6c39252f2 feat: Rebranding PriceFlow + OpenRouter + i18n FR + Docker
Modifications majeures:

1. Rebranding: Pricecious → PriceFlow
   - Mise à jour du titre, logo et noms de projet

2. Intégration OpenRouter
   - Ajout comme provider IA avec filtres (chat, vision, code, reasoning, free)
   - Récupération dynamique des modèles et coûts
   - Service et router dédiés (/api/openrouter/models)

3. Internationalisation (i18n)
   - Installation i18next + react-i18next
   - Application entièrement traduite en français
   - Fichier de traduction: fr.json

4. Configuration Docker
   - docker-compose.yml avec réseau nginx_default (external)
   - PostgreSQL sur port 5488:5432
   - Browserless sur port 3012:3000
   - init.sql pour initialisation de la base de données
   - .env.example avec variables documentées

5. Favicon
   - Documentation ajoutée dans index.html pour faciliter le remplacement
2025-11-22 09:22:39 +00:00

175 lines
6.7 KiB
React

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { Plus, RefreshCw, Search, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ItemCard } from '@/components/dashboard/ItemCard';
import { ItemModal } from '@/components/dashboard/ItemModal';
import { DeleteConfirmationModal } from '@/components/dashboard/DeleteConfirmationModal';
const API_URL = '/api';
export default function Dashboard() {
const { t } = useTranslation();
const [items, setItems] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [showAddModal, setShowAddModal] = useState(false);
const [editingItem, setEditingItem] = useState(null);
const [itemToDelete, setItemToDelete] = useState(null);
const [zoomedImage, setZoomedImage] = useState(null);
const refreshItems = async () => {
try {
const response = await axios.get(`${API_URL}/items`);
const sortedItems = response.data.sort((a, b) => a.id - b.id);
setItems(sortedItems);
} catch (error) {
console.error('Error fetching items:', error);
toast.error(t('toast.fetchError'));
}
};
useEffect(() => {
refreshItems();
const interval = setInterval(refreshItems, 10000);
return () => clearInterval(interval);
}, []);
const handleCheck = async (id) => {
try {
toast.info(t('toast.refreshStarted'));
await axios.post(`${API_URL}/items/${id}/check`);
toast.success(t('toast.refreshComplete'));
refreshItems();
} catch (error) {
console.error('Error triggering check:', error);
toast.error(t('toast.refreshError'));
}
};
const handleRefreshAll = async () => {
try {
toast.info(t('toast.refreshStarted'));
setItems(prevItems => prevItems.map(item => ({ ...item, is_refreshing: true })));
await axios.post(`${API_URL}/jobs/refresh-all`);
toast.success(t('toast.refreshComplete'));
} catch (error) {
console.error('Error triggering refresh all:', error);
toast.error(t('toast.refreshError'));
refreshItems();
}
};
const handleDelete = async () => {
if (!itemToDelete) return;
try {
await axios.delete(`${API_URL}/items/${itemToDelete.id}`);
toast.success(t('toast.itemDeleted'));
refreshItems();
setItemToDelete(null);
} catch (error) {
console.error('Error deleting item:', error);
toast.error(t('toast.itemError'));
}
};
const filteredItems = items.filter(item => {
const term = searchTerm.toLowerCase();
return (
item.name.toLowerCase().includes(term) ||
item.url.toLowerCase().includes(term) ||
(item.tags && item.tags.toLowerCase().includes(term)) ||
(item.description && item.description.toLowerCase().includes(term))
);
});
return (
<div className="space-y-6 animate-in fade-in duration-500">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h2 className="text-2xl font-bold tracking-tight">{t('dashboard.title')}</h2>
<p className="text-muted-foreground">{t('dashboard.description')}</p>
</div>
<div className="flex items-center gap-2">
<div className="relative w-full md:w-64">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t('dashboard.search')}
className="pl-8"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<Button variant="outline" onClick={handleRefreshAll}>
<RefreshCw className="mr-2 h-4 w-4" />
{t('dashboard.refreshAll')}
</Button>
<Button onClick={() => setShowAddModal(true)}>
<Plus className="mr-2 h-4 w-4" />
{t('dashboard.addItem')}
</Button>
</div>
</div>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{filteredItems.map(item => (
<ItemCard
key={item.id}
item={item}
onEdit={setEditingItem}
onDelete={setItemToDelete}
onCheck={handleCheck}
onZoom={setZoomedImage}
/>
))}
</div>
<ItemModal
open={showAddModal}
onClose={() => setShowAddModal(false)}
onSaved={refreshItems}
/>
<ItemModal
open={!!editingItem}
item={editingItem}
onClose={() => setEditingItem(null)}
onSaved={refreshItems}
/>
<DeleteConfirmationModal
open={!!itemToDelete}
item={itemToDelete}
onClose={() => setItemToDelete(null)}
onConfirm={handleDelete}
/>
{/* Image Zoom Modal */}
{zoomedImage && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 animate-in fade-in duration-200"
onClick={() => setZoomedImage(null)}
>
<div className="relative max-h-full max-w-full">
<img
src={zoomedImage}
alt="Zoomed screenshot"
className="max-h-[90vh] max-w-[90vw] rounded-lg shadow-2xl"
/>
<Button
size="icon"
variant="secondary"
className="absolute -right-4 -top-4 rounded-full shadow-lg"
onClick={() => setZoomedImage(null)}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
)}
</div>
);
}