Эх сурвалжийг харах

Refactor: Split AddFeedModal into smaller components

Split AddFeedModal.tsx into smaller, more manageable components to improve readability and maintainability.
gpt-engineer-app[bot] 5 сар өмнө
parent
commit
87610fcd6c

+ 11 - 294
src/components/AddFeedModal.tsx

@@ -1,8 +1,6 @@
 
 import { useState } from 'react';
 import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Textarea } from '@/components/ui/textarea';
 import {
   Dialog,
   DialogContent,
@@ -11,23 +9,10 @@ import {
   DialogHeader,
   DialogTitle,
 } from '@/components/ui/dialog';
-import {
-  Form,
-  FormControl,
-  FormField,
-  FormItem,
-  FormLabel,
-  FormMessage,
-} from '@/components/ui/form';
-import { useForm } from 'react-hook-form';
-import { 
-  Globe, 
-  Rss, 
-  Play, 
-  Gamepad2,
-  Newspaper 
-} from 'lucide-react';
+import { Rss } from 'lucide-react';
 import { NewsCategory } from '@/types/news';
+import FeedTypeSelector from './FeedTypeSelector';
+import FeedForm from './FeedForm';
 
 interface AddFeedModalProps {
   isOpen: boolean;
@@ -36,137 +21,11 @@ interface AddFeedModalProps {
   categories: NewsCategory[];
 }
 
-interface FeedFormData {
-  name: string;
-  url: string;
-  category: string;
-  description?: string;
-}
-
-const feedTypeOptions = [
-  { value: 'website', label: "D'un site web", icon: Globe, color: 'bg-blue-500' },
-  { value: 'rss-auto', label: "D'un flux RSS (automatique)", icon: Rss, color: 'bg-orange-500' },
-  { value: 'rss-manual', label: "D'un flux RSS (manuel)", icon: Rss, color: 'bg-yellow-500' },
-  { value: 'youtube', label: "D'une chaîne YouTube", icon: Play, color: 'bg-red-500' },
-  { value: 'steam', label: "D'un Jeu présent sur Steam", icon: Gamepad2, color: 'bg-gray-700' },
-];
-
-// Function to extract YouTube channel ID from various URL formats
-const extractYouTubeChannelId = (url: string): string | null => {
-  const patterns = [
-    // Channel ID format: https://www.youtube.com/channel/UCxxxxxx
-    /youtube\.com\/channel\/([a-zA-Z0-9_-]+)/,
-    // Handle format: https://www.youtube.com/c/ChannelName
-    /youtube\.com\/c\/([a-zA-Z0-9_-]+)/,
-    // User format: https://www.youtube.com/user/username
-    /youtube\.com\/user\/([a-zA-Z0-9_-]+)/,
-    // Custom URL format: https://www.youtube.com/@channelname
-    /youtube\.com\/@([a-zA-Z0-9_-]+)/,
-  ];
-
-  for (const pattern of patterns) {
-    const match = url.match(pattern);
-    if (match) {
-      return match[1];
-    }
-  }
-  return null;
-};
-
-// Function to convert YouTube channel URL to RSS feed URL
-const convertYouTubeToRSS = (url: string): string => {
-  // If it's already an RSS feed URL, return as is
-  if (url.includes('feeds/videos.xml')) {
-    return url;
-  }
-
-  const channelId = extractYouTubeChannelId(url);
-  
-  if (channelId) {
-    // For channel ID format, we can directly create the RSS URL
-    if (url.includes('/channel/')) {
-      return `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`;
-    }
-    
-    // For other formats (@username, /c/, /user/), we need to note that
-    // the RSS conversion might need the actual channel ID
-    // For now, we'll try with the extracted identifier
-    return `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`;
-  }
-  
-  // If we can't extract the channel ID, return the original URL
-  return url;
-};
-
-// Function to fetch YouTube channel name from page metadata
-const fetchYouTubeChannelName = async (url: string): Promise<string | null> => {
-  try {
-    // Use a CORS proxy to fetch the YouTube page
-    const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`;
-    const response = await fetch(proxyUrl);
-    const data = await response.json();
-    
-    if (data.contents) {
-      const html = data.contents;
-      
-      // Try to extract channel name from various meta tags
-      const metaPatterns = [
-        /<meta property="og:title" content="([^"]+)"/,
-        /<meta name="twitter:title" content="([^"]+)"/,
-        /<title>([^<]+)<\/title>/,
-        /<meta property="og:site_name" content="([^"]+)"/
-      ];
-      
-      for (const pattern of metaPatterns) {
-        const match = html.match(pattern);
-        if (match && match[1]) {
-          let title = match[1].trim();
-          // Clean up the title (remove " - YouTube" suffix if present)
-          title = title.replace(/ - YouTube$/, '');
-          if (title && title !== 'YouTube') {
-            return title;
-          }
-        }
-      }
-    }
-  } catch (error) {
-    console.error('Error fetching YouTube channel name:', error);
-  }
-  
-  return null;
-};
-
 const AddFeedModal = ({ isOpen, onClose, onAddFeed, categories }: AddFeedModalProps) => {
   const [selectedType, setSelectedType] = useState<string>('');
-  const [isLoadingChannelName, setIsLoadingChannelName] = useState(false);
-  
-  const form = useForm<FeedFormData>({
-    defaultValues: {
-      name: '',
-      url: '',
-      category: '',
-      description: '',
-    },
-  });
 
-  const handleSubmit = (data: FeedFormData) => {
-    let processedUrl = data.url;
-    
-    // If it's a YouTube feed, convert the URL to RSS format
-    if (selectedType === 'youtube') {
-      processedUrl = convertYouTubeToRSS(data.url);
-      console.log('YouTube URL converted:', { original: data.url, converted: processedUrl });
-    }
-    
-    const feedData = {
-      ...data,
-      url: processedUrl,
-      type: selectedType,
-      id: Date.now().toString(), // Simple ID generation
-    };
-    
+  const handleSubmit = (feedData: any) => {
     onAddFeed(feedData);
-    form.reset();
     setSelectedType('');
     onClose();
   };
@@ -176,49 +35,10 @@ const AddFeedModal = ({ isOpen, onClose, onAddFeed, categories }: AddFeedModalPr
   };
 
   const handleCancel = () => {
-    form.reset();
     setSelectedType('');
     onClose();
   };
 
-  const handleUrlChange = async (url: string) => {
-    form.setValue('url', url);
-    
-    // If it's a YouTube URL and we don't have a name yet, try to fetch it
-    if (selectedType === 'youtube' && url && !form.getValues('name')) {
-      const isYouTubeUrl = url.includes('youtube.com');
-      if (isYouTubeUrl) {
-        setIsLoadingChannelName(true);
-        const channelName = await fetchYouTubeChannelName(url);
-        if (channelName) {
-          form.setValue('name', channelName);
-        }
-        setIsLoadingChannelName(false);
-      }
-    }
-  };
-
-  const selectedTypeOption = feedTypeOptions.find(option => option.value === selectedType);
-
-  const getUrlPlaceholder = () => {
-    switch (selectedType) {
-      case 'youtube':
-        return 'https://www.youtube.com/@channelname ou https://www.youtube.com/channel/UCxxxxx';
-      case 'rss-auto':
-      case 'rss-manual':
-        return 'https://example.com/feed.xml';
-      default:
-        return 'https://...';
-    }
-  };
-
-  const getUrlHelperText = () => {
-    if (selectedType === 'youtube') {
-      return 'Collez le lien de la chaîne YouTube (nom détecté automatiquement)';
-    }
-    return null;
-  };
-
   return (
     <Dialog open={isOpen} onOpenChange={onClose}>
       <DialogContent className="sm:max-w-md">
@@ -233,117 +53,14 @@ const AddFeedModal = ({ isOpen, onClose, onAddFeed, categories }: AddFeedModalPr
         </DialogHeader>
 
         {!selectedType ? (
-          <div className="space-y-3">
-            {feedTypeOptions.map((option) => {
-              const IconComponent = option.icon;
-              return (
-                <Button
-                  key={option.value}
-                  variant="outline"
-                  className="w-full justify-start gap-3 h-auto p-4"
-                  onClick={() => handleTypeSelect(option.value)}
-                >
-                  <div className={`p-2 rounded ${option.color} text-white`}>
-                    <IconComponent className="h-4 w-4" />
-                  </div>
-                  <span>{option.label}</span>
-                </Button>
-              );
-            })}
-          </div>
+          <FeedTypeSelector onTypeSelect={handleTypeSelect} />
         ) : (
-          <Form {...form}>
-            <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
-              <div className="flex items-center gap-2 p-3 bg-muted rounded-lg">
-                {selectedTypeOption && (
-                  <>
-                    <div className={`p-2 rounded ${selectedTypeOption.color} text-white`}>
-                      <selectedTypeOption.icon className="h-4 w-4" />
-                    </div>
-                    <span className="font-medium">{selectedTypeOption.label}</span>
-                  </>
-                )}
-              </div>
-
-              <FormField
-                control={form.control}
-                name="name"
-                render={({ field }) => (
-                  <FormItem>
-                    <FormLabel>
-                      Nom du flux
-                      {isLoadingChannelName && (
-                        <span className="text-xs text-muted-foreground ml-2">
-                          (détection automatique...)
-                        </span>
-                      )}
-                    </FormLabel>
-                    <FormControl>
-                      <Input 
-                        placeholder="Nom du flux..." 
-                        {...field} 
-                        required
-                        disabled={isLoadingChannelName}
-                      />
-                    </FormControl>
-                    <FormMessage />
-                  </FormItem>
-                )}
-              />
-
-              <FormField
-                control={form.control}
-                name="url"
-                render={({ field }) => (
-                  <FormItem>
-                    <FormLabel>URL</FormLabel>
-                    <FormControl>
-                      <Input 
-                        placeholder={getUrlPlaceholder()}
-                        type="url"
-                        {...field} 
-                        onChange={(e) => handleUrlChange(e.target.value)}
-                        required
-                      />
-                    </FormControl>
-                    {getUrlHelperText() && (
-                      <p className="text-xs text-muted-foreground mt-1">
-                        {getUrlHelperText()}
-                      </p>
-                    )}
-                    <FormMessage />
-                  </FormItem>
-                )}
-              />
-
-              <FormField
-                control={form.control}
-                name="description"
-                render={({ field }) => (
-                  <FormItem>
-                    <FormLabel>Description (optionnel)</FormLabel>
-                    <FormControl>
-                      <Textarea 
-                        placeholder="Description du flux..."
-                        rows={3}
-                        {...field}
-                      />
-                    </FormControl>
-                    <FormMessage />
-                  </FormItem>
-                )}
-              />
-
-              <DialogFooter className="gap-2">
-                <Button type="button" variant="outline" onClick={handleCancel}>
-                  Annuler
-                </Button>
-                <Button type="submit" disabled={isLoadingChannelName}>
-                  Ajouter le flux
-                </Button>
-              </DialogFooter>
-            </form>
-          </Form>
+          <FeedForm 
+            selectedType={selectedType}
+            onSubmit={handleSubmit}
+            onCancel={handleCancel}
+            categories={categories}
+          />
         )}
 
         {!selectedType && (

+ 199 - 0
src/components/FeedForm.tsx

@@ -0,0 +1,199 @@
+
+import { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+import {
+  Form,
+  FormControl,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from '@/components/ui/form';
+import { DialogFooter } from '@/components/ui/dialog';
+import { convertYouTubeToRSS, fetchYouTubeChannelName } from '@/utils/youtube';
+import { feedTypeOptions } from './FeedTypeOptions';
+import { NewsCategory } from '@/types/news';
+
+interface FeedFormData {
+  name: string;
+  url: string;
+  category: string;
+  description?: string;
+}
+
+interface FeedFormProps {
+  selectedType: string;
+  onSubmit: (feedData: any) => void;
+  onCancel: () => void;
+  categories: NewsCategory[];
+}
+
+const FeedForm = ({ selectedType, onSubmit, onCancel, categories }: FeedFormProps) => {
+  const [isLoadingChannelName, setIsLoadingChannelName] = useState(false);
+  
+  const form = useForm<FeedFormData>({
+    defaultValues: {
+      name: '',
+      url: '',
+      category: '',
+      description: '',
+    },
+  });
+
+  const handleSubmit = (data: FeedFormData) => {
+    let processedUrl = data.url;
+    
+    // If it's a YouTube feed, convert the URL to RSS format
+    if (selectedType === 'youtube') {
+      processedUrl = convertYouTubeToRSS(data.url);
+      console.log('YouTube URL converted:', { original: data.url, converted: processedUrl });
+    }
+    
+    const feedData = {
+      ...data,
+      url: processedUrl,
+      type: selectedType,
+      id: Date.now().toString(), // Simple ID generation
+    };
+    
+    onSubmit(feedData);
+  };
+
+  const handleUrlChange = async (url: string) => {
+    form.setValue('url', url);
+    
+    // If it's a YouTube URL and we don't have a name yet, try to fetch it
+    if (selectedType === 'youtube' && url && !form.getValues('name')) {
+      const isYouTubeUrl = url.includes('youtube.com');
+      if (isYouTubeUrl) {
+        setIsLoadingChannelName(true);
+        const channelName = await fetchYouTubeChannelName(url);
+        if (channelName) {
+          form.setValue('name', channelName);
+        }
+        setIsLoadingChannelName(false);
+      }
+    }
+  };
+
+  const selectedTypeOption = feedTypeOptions.find(option => option.value === selectedType);
+
+  const getUrlPlaceholder = () => {
+    switch (selectedType) {
+      case 'youtube':
+        return 'https://www.youtube.com/@channelname ou https://www.youtube.com/channel/UCxxxxx';
+      case 'rss-auto':
+      case 'rss-manual':
+        return 'https://example.com/feed.xml';
+      default:
+        return 'https://...';
+    }
+  };
+
+  const getUrlHelperText = () => {
+    if (selectedType === 'youtube') {
+      return 'Collez le lien de la chaîne YouTube (nom détecté automatiquement)';
+    }
+    return null;
+  };
+
+  return (
+    <Form {...form}>
+      <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
+        <div className="flex items-center gap-2 p-3 bg-muted rounded-lg">
+          {selectedTypeOption && (
+            <>
+              <div className={`p-2 rounded ${selectedTypeOption.color} text-white`}>
+                <selectedTypeOption.icon className="h-4 w-4" />
+              </div>
+              <span className="font-medium">{selectedTypeOption.label}</span>
+            </>
+          )}
+        </div>
+
+        <FormField
+          control={form.control}
+          name="name"
+          render={({ field }) => (
+            <FormItem>
+              <FormLabel>
+                Nom du flux
+                {isLoadingChannelName && (
+                  <span className="text-xs text-muted-foreground ml-2">
+                    (détection automatique...)
+                  </span>
+                )}
+              </FormLabel>
+              <FormControl>
+                <Input 
+                  placeholder="Nom du flux..." 
+                  {...field} 
+                  required
+                  disabled={isLoadingChannelName}
+                />
+              </FormControl>
+              <FormMessage />
+            </FormItem>
+          )}
+        />
+
+        <FormField
+          control={form.control}
+          name="url"
+          render={({ field }) => (
+            <FormItem>
+              <FormLabel>URL</FormLabel>
+              <FormControl>
+                <Input 
+                  placeholder={getUrlPlaceholder()}
+                  type="url"
+                  {...field} 
+                  onChange={(e) => handleUrlChange(e.target.value)}
+                  required
+                />
+              </FormControl>
+              {getUrlHelperText() && (
+                <p className="text-xs text-muted-foreground mt-1">
+                  {getUrlHelperText()}
+                </p>
+              )}
+              <FormMessage />
+            </FormItem>
+          )}
+        />
+
+        <FormField
+          control={form.control}
+          name="description"
+          render={({ field }) => (
+            <FormItem>
+              <FormLabel>Description (optionnel)</FormLabel>
+              <FormControl>
+                <Textarea 
+                  placeholder="Description du flux..."
+                  rows={3}
+                  {...field}
+                />
+              </FormControl>
+              <FormMessage />
+            </FormItem>
+          )}
+        />
+
+        <DialogFooter className="gap-2">
+          <Button type="button" variant="outline" onClick={onCancel}>
+            Annuler
+          </Button>
+          <Button type="submit" disabled={isLoadingChannelName}>
+            Ajouter le flux
+          </Button>
+        </DialogFooter>
+      </form>
+    </Form>
+  );
+};
+
+export default FeedForm;

+ 10 - 0
src/components/FeedTypeOptions.tsx

@@ -0,0 +1,10 @@
+
+import { Globe, Rss, Play, Gamepad2 } from 'lucide-react';
+
+export const feedTypeOptions = [
+  { value: 'website', label: "D'un site web", icon: Globe, color: 'bg-blue-500' },
+  { value: 'rss-auto', label: "D'un flux RSS (automatique)", icon: Rss, color: 'bg-orange-500' },
+  { value: 'rss-manual', label: "D'un flux RSS (manuel)", icon: Rss, color: 'bg-yellow-500' },
+  { value: 'youtube', label: "D'une chaîne YouTube", icon: Play, color: 'bg-red-500' },
+  { value: 'steam', label: "D'un Jeu présent sur Steam", icon: Gamepad2, color: 'bg-gray-700' },
+];

+ 32 - 0
src/components/FeedTypeSelector.tsx

@@ -0,0 +1,32 @@
+
+import { Button } from '@/components/ui/button';
+import { feedTypeOptions } from './FeedTypeOptions';
+
+interface FeedTypeSelectorProps {
+  onTypeSelect: (type: string) => void;
+}
+
+const FeedTypeSelector = ({ onTypeSelect }: FeedTypeSelectorProps) => {
+  return (
+    <div className="space-y-3">
+      {feedTypeOptions.map((option) => {
+        const IconComponent = option.icon;
+        return (
+          <Button
+            key={option.value}
+            variant="outline"
+            className="w-full justify-start gap-3 h-auto p-4"
+            onClick={() => onTypeSelect(option.value)}
+          >
+            <div className={`p-2 rounded ${option.color} text-white`}>
+              <IconComponent className="h-4 w-4" />
+            </div>
+            <span>{option.label}</span>
+          </Button>
+        );
+      })}
+    </div>
+  );
+};
+
+export default FeedTypeSelector;

+ 85 - 0
src/utils/youtube.ts

@@ -0,0 +1,85 @@
+
+// Function to extract YouTube channel ID from various URL formats
+export const extractYouTubeChannelId = (url: string): string | null => {
+  const patterns = [
+    // Channel ID format: https://www.youtube.com/channel/UCxxxxxx
+    /youtube\.com\/channel\/([a-zA-Z0-9_-]+)/,
+    // Handle format: https://www.youtube.com/c/ChannelName
+    /youtube\.com\/c\/([a-zA-Z0-9_-]+)/,
+    // User format: https://www.youtube.com/user/username
+    /youtube\.com\/user\/([a-zA-Z0-9_-]+)/,
+    // Custom URL format: https://www.youtube.com/@channelname
+    /youtube\.com\/@([a-zA-Z0-9_-]+)/,
+  ];
+
+  for (const pattern of patterns) {
+    const match = url.match(pattern);
+    if (match) {
+      return match[1];
+    }
+  }
+  return null;
+};
+
+// Function to convert YouTube channel URL to RSS feed URL
+export const convertYouTubeToRSS = (url: string): string => {
+  // If it's already an RSS feed URL, return as is
+  if (url.includes('feeds/videos.xml')) {
+    return url;
+  }
+
+  const channelId = extractYouTubeChannelId(url);
+  
+  if (channelId) {
+    // For channel ID format, we can directly create the RSS URL
+    if (url.includes('/channel/')) {
+      return `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`;
+    }
+    
+    // For other formats (@username, /c/, /user/), we need to note that
+    // the RSS conversion might need the actual channel ID
+    // For now, we'll try with the extracted identifier
+    return `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`;
+  }
+  
+  // If we can't extract the channel ID, return the original URL
+  return url;
+};
+
+// Function to fetch YouTube channel name from page metadata
+export const fetchYouTubeChannelName = async (url: string): Promise<string | null> => {
+  try {
+    // Use a CORS proxy to fetch the YouTube page
+    const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`;
+    const response = await fetch(proxyUrl);
+    const data = await response.json();
+    
+    if (data.contents) {
+      const html = data.contents;
+      
+      // Try to extract channel name from various meta tags
+      const metaPatterns = [
+        /<meta property="og:title" content="([^"]+)"/,
+        /<meta name="twitter:title" content="([^"]+)"/,
+        /<title>([^<]+)<\/title>/,
+        /<meta property="og:site_name" content="([^"]+)"/
+      ];
+      
+      for (const pattern of metaPatterns) {
+        const match = html.match(pattern);
+        if (match && match[1]) {
+          let title = match[1].trim();
+          // Clean up the title (remove " - YouTube" suffix if present)
+          title = title.replace(/ - YouTube$/, '');
+          if (title && title !== 'YouTube') {
+            return title;
+          }
+        }
+      }
+    }
+  } catch (error) {
+    console.error('Error fetching YouTube channel name:', error);
+  }
+  
+  return null;
+};