Преглед изворни кода

Refactor: Update site with Django and Bootstrap

Update the site to a more modern version using Django and Bootstrap.
gpt-engineer-app[bot] пре 6 месеци
родитељ
комит
9ac5838b65
6 измењених фајлова са 528 додато и 6 уклоњено
  1. 74 0
      src/components/CategoryFilter.tsx
  2. 75 0
      src/components/Header.tsx
  3. 127 0
      src/components/NewsCard.tsx
  4. 64 0
      src/data/mockNews.ts
  5. 165 6
      src/pages/Index.tsx
  6. 23 0
      src/types/news.ts

+ 74 - 0
src/components/CategoryFilter.tsx

@@ -0,0 +1,74 @@
+
+import { NewsCategory } from '@/types/news';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { cn } from '@/lib/utils';
+import { 
+  Rss, 
+  Play, 
+  Gamepad2, 
+  Newspaper,
+  Filter
+} from 'lucide-react';
+
+interface CategoryFilterProps {
+  categories: NewsCategory[];
+  selectedCategory: string | null;
+  onCategoryChange: (categoryId: string | null) => void;
+  newsCount: number;
+}
+
+const iconMap = {
+  rss: Rss,
+  play: Play,
+  'gamepad-2': Gamepad2,
+  newspaper: Newspaper,
+};
+
+const CategoryFilter = ({ 
+  categories, 
+  selectedCategory, 
+  onCategoryChange,
+  newsCount 
+}: CategoryFilterProps) => {
+  return (
+    <div className="bg-card border rounded-lg p-6 space-y-4">
+      <div className="flex items-center gap-2">
+        <Filter className="h-5 w-5 text-muted-foreground" />
+        <h2 className="text-lg font-semibold">Filtrer par type de flux</h2>
+      </div>
+      
+      <div className="space-y-2">
+        <Button
+          variant={selectedCategory === null ? "default" : "outline"}
+          className="w-full justify-start gap-2"
+          onClick={() => onCategoryChange(null)}
+        >
+          <span>Toutes</span>
+          <Badge variant="secondary" className="ml-auto">
+            {newsCount}
+          </Badge>
+        </Button>
+        
+        {categories.map((category) => {
+          const IconComponent = iconMap[category.icon as keyof typeof iconMap];
+          const isSelected = selectedCategory === category.id;
+          
+          return (
+            <Button
+              key={category.id}
+              variant={isSelected ? "default" : "outline"}
+              className="w-full justify-start gap-2"
+              onClick={() => onCategoryChange(category.id)}
+            >
+              <IconComponent className="h-4 w-4" />
+              <span>{category.name}</span>
+            </Button>
+          );
+        })}
+      </div>
+    </div>
+  );
+};
+
+export default CategoryFilter;

+ 75 - 0
src/components/Header.tsx

@@ -0,0 +1,75 @@
+
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Badge } from '@/components/ui/badge';
+import { 
+  Search, 
+  Plus, 
+  Settings, 
+  User,
+  Rss
+} from 'lucide-react';
+
+interface HeaderProps {
+  searchQuery: string;
+  onSearchChange: (query: string) => void;
+  pinnedCount: number;
+}
+
+const Header = ({ searchQuery, onSearchChange, pinnedCount }: HeaderProps) => {
+  return (
+    <header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50">
+      <div className="container mx-auto px-4 py-4">
+        <div className="flex items-center justify-between gap-4">
+          <div className="flex items-center gap-3">
+            <div className="flex items-center gap-2">
+              <Rss className="h-6 w-6 text-primary" />
+              <h1 className="text-xl font-bold">Feeds.Duhaz.fr</h1>
+            </div>
+            <div className="hidden sm:flex items-center gap-2 text-sm text-muted-foreground">
+              <span>•</span>
+              <span>Liste des flux rss surveillé</span>
+            </div>
+          </div>
+          
+          <div className="flex items-center gap-3">
+            <div className="relative">
+              <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+              <Input
+                placeholder="Rechercher..."
+                value={searchQuery}
+                onChange={(e) => onSearchChange(e.target.value)}
+                className="pl-10 w-64"
+              />
+            </div>
+            
+            <Button variant="outline" size="sm" className="gap-2">
+              <Plus className="h-4 w-4" />
+              Ajouter un Flux
+            </Button>
+            
+            <Button variant="ghost" size="sm">
+              <Settings className="h-4 w-4" />
+            </Button>
+            
+            <Button variant="ghost" size="sm" className="gap-2">
+              <User className="h-4 w-4" />
+              <span className="hidden sm:inline">Mon profil</span>
+            </Button>
+          </div>
+        </div>
+        
+        <div className="flex items-center gap-4 mt-4">
+          <div className="flex items-center gap-2">
+            <span className="text-sm font-medium">Épinglé</span>
+            <Badge variant="secondary">
+              {pinnedCount > 0 ? `${pinnedCount}` : 'Aucun'}
+            </Badge>
+          </div>
+        </div>
+      </div>
+    </header>
+  );
+};
+
+export default Header;

+ 127 - 0
src/components/NewsCard.tsx

@@ -0,0 +1,127 @@
+
+import { NewsItem } from '@/types/news';
+import { Card, CardContent, CardHeader } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { 
+  Clock, 
+  Pin, 
+  ExternalLink,
+  Eye,
+  Trash2
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+interface NewsCardProps {
+  news: NewsItem;
+  onTogglePin: (id: string) => void;
+  onMarkAsRead: (id: string) => void;
+  onDelete: (id: string) => void;
+}
+
+const NewsCard = ({ news, onTogglePin, onMarkAsRead, onDelete }: NewsCardProps) => {
+  const getSourceColor = (category: string) => {
+    switch (category) {
+      case 'rss': return 'bg-blue-500/10 text-blue-700 border-blue-200';
+      case 'youtube': return 'bg-red-500/10 text-red-700 border-red-200';
+      case 'steam': return 'bg-gray-500/10 text-gray-700 border-gray-200';
+      case 'actualites': return 'bg-green-500/10 text-green-700 border-green-200';
+      default: return 'bg-muted text-muted-foreground';
+    }
+  };
+
+  return (
+    <Card className={cn(
+      "group hover:shadow-lg transition-all duration-300 border-l-4",
+      news.isPinned && "border-l-yellow-500",
+      news.isRead && "opacity-75",
+      !news.isRead && "border-l-primary"
+    )}>
+      <CardHeader className="space-y-3">
+        <div className="flex items-start justify-between gap-4">
+          <div className="flex-1 space-y-2">
+            <div className="flex items-center gap-2">
+              <Badge variant="outline" className={getSourceColor(news.category)}>
+                {news.source}
+              </Badge>
+              <div className="flex items-center gap-1 text-xs text-muted-foreground">
+                <Clock className="h-3 w-3" />
+                {news.readTime} min
+              </div>
+            </div>
+            
+            <h3 className={cn(
+              "font-semibold leading-tight group-hover:text-primary transition-colors",
+              news.isRead && "text-muted-foreground"
+            )}>
+              {news.title}
+            </h3>
+          </div>
+          
+          <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
+            <Button
+              variant="ghost"
+              size="sm"
+              onClick={() => onTogglePin(news.id)}
+              className={cn(
+                "h-8 w-8 p-0",
+                news.isPinned && "text-yellow-600"
+              )}
+            >
+              <Pin className={cn("h-4 w-4", news.isPinned && "fill-current")} />
+            </Button>
+            
+            <Button
+              variant="ghost"
+              size="sm"
+              onClick={() => onDelete(news.id)}
+              className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive"
+            >
+              <Trash2 className="h-4 w-4" />
+            </Button>
+          </div>
+        </div>
+      </CardHeader>
+      
+      <CardContent className="space-y-4">
+        <p className="text-sm text-muted-foreground leading-relaxed">
+          {news.description}
+        </p>
+        
+        <div className="flex items-center justify-between">
+          <span className="text-xs text-muted-foreground">
+            {new Date(news.publishedAt).toLocaleDateString('fr-FR', {
+              day: 'numeric',
+              month: 'long',
+              hour: '2-digit',
+              minute: '2-digit'
+            })}
+          </span>
+          
+          <div className="flex items-center gap-2">
+            {!news.isRead && (
+              <Button
+                variant="outline"
+                size="sm"
+                onClick={() => onMarkAsRead(news.id)}
+                className="gap-1"
+              >
+                <Eye className="h-3 w-3" />
+                Marquer lu
+              </Button>
+            )}
+            
+            {news.url && (
+              <Button variant="default" size="sm" className="gap-1">
+                <ExternalLink className="h-3 w-3" />
+                Lire
+              </Button>
+            )}
+          </div>
+        </div>
+      </CardContent>
+    </Card>
+  );
+};
+
+export default NewsCard;

+ 64 - 0
src/data/mockNews.ts

@@ -0,0 +1,64 @@
+
+import { NewsItem, NewsCategory } from '@/types/news';
+
+export const categories: NewsCategory[] = [
+  { id: '1', name: 'Flux Rss', type: 'rss', color: 'bg-blue-500', icon: 'rss' },
+  { id: '2', name: 'YouTube', type: 'youtube', color: 'bg-red-500', icon: 'play' },
+  { id: '3', name: 'Steam', type: 'steam', color: 'bg-gray-700', icon: 'gamepad-2' },
+  { id: '4', name: 'Actualités', type: 'actualites', color: 'bg-green-500', icon: 'newspaper' },
+];
+
+export const newsItems: NewsItem[] = [
+  {
+    id: '1',
+    title: "Google vient enfin d'améliorer deux fonctions essentielles pour les utilisateurs dans Google Messages",
+    description: "L'application Google Messages poursuit son évolution avec le remaniement de sa galerie et de l'interface de son appareil photo.",
+    content: "L'application Google Messages continue d'évoluer pour offrir une meilleure expérience utilisateur. Les dernières améliorations concernent principalement l'interface de la galerie photo et les fonctionnalités de l'appareil photo intégré.",
+    source: "Clubic - News",
+    category: 'rss',
+    publishedAt: "2024-06-06T10:00:00Z",
+    readTime: 3,
+    isPinned: false,
+    isRead: false,
+    imageUrl: "/placeholder.svg"
+  },
+  {
+    id: '2',
+    title: "Test Asus ROG Delta II : identité gamer et immersion complète pour ce casque sans fil",
+    description: "On le voit de plus en plus sur la tête des joueurs d'eSport pros et des streamers. L'Asus ROG Delta II mise sur une esthétique affirmée, sans négliger pour autant un son riche et immersif.",
+    content: "Le nouveau casque gaming d'Asus impressionne par sa qualité audio et son design soigné. Avec ses fonctionnalités avancées comme la charge rapide et le mode tri connecté, il se positionne parmi les références du marché.",
+    source: "Clubic - News", 
+    category: 'rss',
+    publishedAt: "2024-06-06T09:30:00Z",
+    readTime: 5,
+    isPinned: true,
+    isRead: false,
+    imageUrl: "/placeholder.svg"
+  },
+  {
+    id: '3',
+    title: "Vis mon job ! Des demandeurs d'emploi en immersion avec des conseillers clientèle d'une banque du Sud-Ouest",
+    description: "Cette opération est le fruit d'un partenariat entre France Travail et le club 'Les entreprises s'engagent en Charente'. Une initiative qui permet aux demandeurs d'emploi de découvrir les métiers directement avec des professionnels.",
+    content: "Une initiative originale qui permet aux demandeurs d'emploi de découvrir concrètement les métiers du conseil clientèle bancaire à travers des immersions professionnelles.",
+    source: "France3 - Actualités à la Une",
+    category: 'actualites',
+    publishedAt: "2024-06-06T08:45:00Z",
+    readTime: 4,
+    isPinned: false,
+    isRead: true,
+    imageUrl: "/placeholder.svg"
+  },
+  {
+    id: '4',
+    title: "L'Arme Laser qui Montre la Vraie Puissance des USA 🇺🇸⚡",
+    description: "Découverte des technologies laser militaires américaines et de leur impact sur la défense moderne.",
+    content: "Les États-Unis développent des systèmes d'armes laser révolutionnaires qui changent la donne en matière de défense militaire.",
+    source: "YouTube Tech",
+    category: 'youtube',
+    publishedAt: "2024-06-06T07:15:00Z",
+    readTime: 8,
+    isPinned: false,
+    isRead: false,
+    imageUrl: "/placeholder.svg"
+  }
+];

+ 165 - 6
src/pages/Index.tsx

@@ -1,12 +1,171 @@
-// Update this page (the content is just a fallback if you fail to update the page)
+
+import { useState, useMemo } from 'react';
+import { newsItems, categories } from '@/data/mockNews';
+import { NewsItem } from '@/types/news';
+import Header from '@/components/Header';
+import CategoryFilter from '@/components/CategoryFilter';
+import NewsCard from '@/components/NewsCard';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { RefreshCw, Filter } from 'lucide-react';
+import { toast } from 'sonner';
 
 
 const Index = () => {
 const Index = () => {
+  const [news, setNews] = useState<NewsItem[]>(newsItems);
+  const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
+  const [searchQuery, setSearchQuery] = useState('');
+  const [showFilters, setShowFilters] = useState(true);
+
+  const filteredNews = useMemo(() => {
+    let filtered = news;
+    
+    if (selectedCategory) {
+      const category = categories.find(c => c.id === selectedCategory);
+      if (category) {
+        filtered = filtered.filter(item => item.category === category.type);
+      }
+    }
+    
+    if (searchQuery) {
+      filtered = filtered.filter(item => 
+        item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
+        item.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
+        item.source.toLowerCase().includes(searchQuery.toLowerCase())
+      );
+    }
+    
+    return filtered.sort((a, b) => {
+      if (a.isPinned && !b.isPinned) return -1;
+      if (!a.isPinned && b.isPinned) return 1;
+      return new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime();
+    });
+  }, [news, selectedCategory, searchQuery]);
+
+  const pinnedCount = news.filter(item => item.isPinned).length;
+  const unreadCount = news.filter(item => !item.isRead).length;
+
+  const handleTogglePin = (id: string) => {
+    setNews(prev => prev.map(item => 
+      item.id === id ? { ...item, isPinned: !item.isPinned } : item
+    ));
+    toast.success("Article épinglé mis à jour");
+  };
+
+  const handleMarkAsRead = (id: string) => {
+    setNews(prev => prev.map(item => 
+      item.id === id ? { ...item, isRead: true } : item
+    ));
+    toast.success("Article marqué comme lu");
+  };
+
+  const handleDelete = (id: string) => {
+    setNews(prev => prev.filter(item => item.id !== id));
+    toast.success("Article supprimé");
+  };
+
+  const handleRefresh = () => {
+    toast.success("Flux actualisés");
+  };
+
   return (
   return (
-    <div className="min-h-screen flex items-center justify-center bg-background">
-      <div className="text-center">
-        <h1 className="text-4xl font-bold mb-4">Welcome to Your Blank App</h1>
-        <p className="text-xl text-muted-foreground">Start building your amazing project here!</p>
-      </div>
+    <div className="min-h-screen bg-background">
+      <Header 
+        searchQuery={searchQuery}
+        onSearchChange={setSearchQuery}
+        pinnedCount={pinnedCount}
+      />
+      
+      <main className="container mx-auto px-4 py-6">
+        <div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
+          {/* Sidebar */}
+          <div className={`lg:col-span-1 space-y-6 ${!showFilters && 'hidden lg:block'}`}>
+            <CategoryFilter
+              categories={categories}
+              selectedCategory={selectedCategory}
+              onCategoryChange={setSelectedCategory}
+              newsCount={news.length}
+            />
+            
+            <div className="bg-card border rounded-lg p-4 space-y-3">
+              <h3 className="font-semibold text-sm">Statistiques</h3>
+              <div className="space-y-2 text-sm">
+                <div className="flex justify-between">
+                  <span className="text-muted-foreground">Total articles</span>
+                  <Badge variant="outline">{news.length}</Badge>
+                </div>
+                <div className="flex justify-between">
+                  <span className="text-muted-foreground">Non lus</span>
+                  <Badge variant="default">{unreadCount}</Badge>
+                </div>
+                <div className="flex justify-between">
+                  <span className="text-muted-foreground">Épinglés</span>
+                  <Badge variant="secondary">{pinnedCount}</Badge>
+                </div>
+              </div>
+            </div>
+          </div>
+          
+          {/* Main content */}
+          <div className="lg:col-span-3 space-y-6">
+            <div className="flex items-center justify-between">
+              <div className="flex items-center gap-4">
+                <h2 className="text-2xl font-bold">
+                  {selectedCategory ? 
+                    categories.find(c => c.id === selectedCategory)?.name : 
+                    'Tous les flux'
+                  }
+                </h2>
+                <Badge variant="outline">
+                  {filteredNews.length} article{filteredNews.length !== 1 ? 's' : ''}
+                </Badge>
+              </div>
+              
+              <div className="flex items-center gap-2">
+                <Button
+                  variant="outline"
+                  size="sm"
+                  onClick={() => setShowFilters(!showFilters)}
+                  className="lg:hidden gap-2"
+                >
+                  <Filter className="h-4 w-4" />
+                  Filtres
+                </Button>
+                
+                <Button
+                  variant="outline"
+                  size="sm"
+                  onClick={handleRefresh}
+                  className="gap-2"
+                >
+                  <RefreshCw className="h-4 w-4" />
+                  Actualiser
+                </Button>
+              </div>
+            </div>
+            
+            {filteredNews.length === 0 ? (
+              <div className="text-center py-12">
+                <p className="text-muted-foreground text-lg">Aucun article trouvé</p>
+                <p className="text-sm text-muted-foreground mt-2">
+                  Essayez de modifier vos filtres ou votre recherche
+                </p>
+              </div>
+            ) : (
+              <div className="space-y-4">
+                {filteredNews.map((item) => (
+                  <NewsCard
+                    key={item.id}
+                    news={item}
+                    onTogglePin={handleTogglePin}
+                    onMarkAsRead={handleMarkAsRead}
+                    onDelete={handleDelete}
+                  />
+                ))}
+              </div>
+            )}
+          </div>
+        </div>
+      </main>
     </div>
     </div>
   );
   );
 };
 };

+ 23 - 0
src/types/news.ts

@@ -0,0 +1,23 @@
+
+export interface NewsItem {
+  id: string;
+  title: string;
+  description: string;
+  content: string;
+  source: string;
+  category: 'rss' | 'youtube' | 'steam' | 'actualites';
+  publishedAt: string;
+  readTime: number;
+  isPinned: boolean;
+  isRead: boolean;
+  url?: string;
+  imageUrl?: string;
+}
+
+export interface NewsCategory {
+  id: string;
+  name: string;
+  type: 'rss' | 'youtube' | 'steam' | 'actualites';
+  color: string;
+  icon: string;
+}