Parcourir la source

feat: Add feed update functionality

Adds functionality to update feed information, including automatic retrieval from RSS feeds, within the /feeds section.
gpt-engineer-app[bot] il y a 6 mois
Parent
commit
42a5925ee7

+ 42 - 0
src/hooks/useFeedUpdate.tsx

@@ -0,0 +1,42 @@
+
+import { useState } from 'react';
+import { supabase } from '@/integrations/supabase/client';
+import { toast } from 'sonner';
+
+export function useFeedUpdate() {
+  const [updating, setUpdating] = useState<string | null>(null);
+
+  const updateFeed = async (feedId: string, url: string) => {
+    try {
+      setUpdating(feedId);
+      
+      toast.info('Mise à jour du flux en cours...');
+
+      const { data, error } = await supabase.functions.invoke('update-feed', {
+        body: { feedId, url }
+      });
+
+      if (error) {
+        throw error;
+      }
+
+      if (data.success) {
+        toast.success('Flux mis à jour avec succès !');
+        return data.data;
+      } else {
+        throw new Error(data.error || 'Erreur lors de la mise à jour');
+      }
+    } catch (error) {
+      console.error('Error updating feed:', error);
+      toast.error('Erreur lors de la mise à jour du flux');
+      throw error;
+    } finally {
+      setUpdating(null);
+    }
+  };
+
+  return {
+    updateFeed,
+    updating
+  };
+}

+ 29 - 4
src/pages/FeedsManagement.tsx

@@ -1,6 +1,6 @@
-
 import { useState } from 'react';
 import { useFeeds } from '@/hooks/useFeeds';
+import { useFeedUpdate } from '@/hooks/useFeedUpdate';
 import { useAuth } from '@/hooks/useAuth';
 import { Feed } from '@/types/feed';
 import { Button } from '@/components/ui/button';
@@ -28,16 +28,28 @@ import {
   Clock,
   ArrowLeft,
   LogOut,
-  User
+  User,
+  RefreshCw
 } from 'lucide-react';
 import { Link } from 'react-router-dom';
 
 const FeedsManagement = () => {
-  const { feeds, loading, toggleFollow } = useFeeds();
+  const { feeds, loading, toggleFollow, refetch } = useFeeds();
+  const { updateFeed, updating } = useFeedUpdate();
   const { user, signOut } = useAuth();
   const [searchQuery, setSearchQuery] = useState('');
   const [selectedType, setSelectedType] = useState<string | null>(null);
 
+  const handleUpdateFeed = async (feed: Feed) => {
+    try {
+      await updateFeed(feed.id, feed.url);
+      // Refetch feeds to get updated data
+      await refetch();
+    } catch (error) {
+      // Error already handled in useFeedUpdate
+    }
+  };
+
   const getTypeIcon = (type: Feed['type']) => {
     switch (type) {
       case 'website': return Globe;
@@ -250,7 +262,7 @@ const FeedsManagement = () => {
             </Card>
           )}
 
-          {/* Liste des flux */}
+          {/* Liste des flux avec colonne Actions */}
           <Card>
             <CardHeader>
               <CardTitle>Flux disponibles</CardTitle>
@@ -269,6 +281,7 @@ const FeedsManagement = () => {
                       <TableHead>Articles</TableHead>
                       <TableHead>Dernière MAJ</TableHead>
                       {user && <TableHead>Suivi</TableHead>}
+                      <TableHead>Actions</TableHead>
                     </TableRow>
                   </TableHeader>
                   <TableBody>
@@ -327,6 +340,18 @@ const FeedsManagement = () => {
                               />
                             </TableCell>
                           )}
+                          <TableCell>
+                            <Button
+                              variant="outline"
+                              size="sm"
+                              onClick={() => handleUpdateFeed(feed)}
+                              disabled={updating === feed.id}
+                              className="gap-2"
+                            >
+                              <RefreshCw className={`h-4 w-4 ${updating === feed.id ? 'animate-spin' : ''}`} />
+                              {updating === feed.id ? 'Mise à jour...' : 'Actualiser'}
+                            </Button>
+                          </TableCell>
                         </TableRow>
                       );
                     })}

+ 126 - 0
supabase/functions/update-feed/index.ts

@@ -0,0 +1,126 @@
+
+import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
+
+const corsHeaders = {
+  'Access-Control-Allow-Origin': '*',
+  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
+}
+
+serve(async (req) => {
+  if (req.method === 'OPTIONS') {
+    return new Response(null, { headers: corsHeaders })
+  }
+
+  try {
+    const { feedId, url } = await req.json()
+
+    if (!feedId || !url) {
+      return new Response(
+        JSON.stringify({ error: 'Feed ID and URL are required' }),
+        { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // Initialize Supabase client
+    const supabase = createClient(
+      Deno.env.get('SUPABASE_URL') ?? '',
+      Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
+    )
+
+    console.log(`Fetching RSS feed from: ${url}`)
+
+    // Fetch RSS feed
+    const response = await fetch(url, {
+      headers: {
+        'User-Agent': 'Mozilla/5.0 (compatible; RSS Feed Updater/1.0)'
+      }
+    })
+
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`)
+    }
+
+    const rssText = await response.text()
+    console.log('RSS content fetched successfully')
+
+    // Parse RSS content to extract metadata
+    const titleMatch = rssText.match(/<title[^>]*>(.*?)<\/title>/i)
+    const descriptionMatch = rssText.match(/<description[^>]*>(.*?)<\/description>/i) ||
+                            rssText.match(/<subtitle[^>]*>(.*?)<\/subtitle>/i)
+    
+    // Count items in the feed
+    const itemMatches = rssText.match(/<item[^>]*>/gi) || rssText.match(/<entry[^>]*>/gi) || []
+    const articleCount = itemMatches.length
+
+    // Clean up extracted text (remove HTML tags and decode entities)
+    const cleanText = (text: string) => {
+      if (!text) return ''
+      return text
+        .replace(/<!\[CDATA\[(.*?)\]\]>/g, '$1')
+        .replace(/<[^>]+>/g, '')
+        .replace(/&lt;/g, '<')
+        .replace(/&gt;/g, '>')
+        .replace(/&amp;/g, '&')
+        .replace(/&quot;/g, '"')
+        .replace(/&#39;/g, "'")
+        .trim()
+    }
+
+    const extractedTitle = titleMatch ? cleanText(titleMatch[1]) : null
+    const extractedDescription = descriptionMatch ? cleanText(descriptionMatch[1]) : null
+
+    console.log(`Extracted data: title="${extractedTitle}", description="${extractedDescription}", articles=${articleCount}`)
+
+    // Update feed in database
+    const updateData: any = {
+      last_updated: new Date().toISOString(),
+      article_count: articleCount,
+      status: 'active'
+    }
+
+    // Only update title and description if we found them and they're different
+    if (extractedTitle) {
+      updateData.name = extractedTitle
+    }
+    if (extractedDescription) {
+      updateData.description = extractedDescription
+    }
+
+    const { data, error } = await supabase
+      .from('feeds')
+      .update(updateData)
+      .eq('id', feedId)
+      .select()
+
+    if (error) {
+      console.error('Database update error:', error)
+      return new Response(
+        JSON.stringify({ error: 'Failed to update feed in database' }),
+        { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    console.log('Feed updated successfully')
+
+    return new Response(
+      JSON.stringify({ 
+        success: true, 
+        data: data[0],
+        extracted: {
+          title: extractedTitle,
+          description: extractedDescription,
+          articleCount
+        }
+      }),
+      { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+    )
+
+  } catch (error) {
+    console.error('Error updating feed:', error)
+    return new Response(
+      JSON.stringify({ error: error.message }),
+      { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+    )
+  }
+})