145 lines
No EOL
4.8 KiB
TypeScript
145 lines
No EOL
4.8 KiB
TypeScript
import React from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { Rss, Globe, ToggleLeft, ToggleRight, ExternalLink } from 'lucide-react';
|
|
import { apiClient, Feed } from '../lib/api';
|
|
|
|
const Feeds: React.FC = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data: feeds, isLoading, error } = useQuery({
|
|
queryKey: ['feeds'],
|
|
queryFn: () => apiClient.getFeeds(),
|
|
});
|
|
|
|
const toggleMutation = useMutation({
|
|
mutationFn: (feedId: number) => apiClient.toggleFeed(feedId),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['feeds'] });
|
|
},
|
|
});
|
|
|
|
const handleToggle = (feedId: number) => {
|
|
toggleMutation.mutate(feedId);
|
|
};
|
|
|
|
const getTypeIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'rss':
|
|
return <Rss className="h-5 w-5 text-orange-500" />;
|
|
case 'website':
|
|
return <Globe className="h-5 w-5 text-blue-500" />;
|
|
default:
|
|
return <Rss className="h-5 w-5 text-gray-500" />;
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="p-6">
|
|
<div className="animate-pulse">
|
|
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{[...Array(6)].map((_, i) => (
|
|
<div key={i} className="bg-white p-6 rounded-lg shadow">
|
|
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
|
|
<div className="h-3 bg-gray-200 rounded w-1/2 mb-4"></div>
|
|
<div className="h-8 bg-gray-200 rounded w-20"></div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="p-6">
|
|
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
|
<p className="text-red-600">Failed to load feeds</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<div className="mb-8">
|
|
<h1 className="text-2xl font-bold text-gray-900">Feeds</h1>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
Manage your RSS feeds and website sources
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{feeds?.map((feed: Feed) => (
|
|
<div key={feed.id} className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="flex items-center">
|
|
{getTypeIcon(feed.type)}
|
|
<h3 className="ml-2 text-lg font-medium text-gray-900 truncate">
|
|
{feed.name}
|
|
</h3>
|
|
</div>
|
|
<button
|
|
onClick={() => handleToggle(feed.id)}
|
|
disabled={toggleMutation.isPending}
|
|
className="flex-shrink-0"
|
|
>
|
|
{feed.is_active ? (
|
|
<ToggleRight className="h-6 w-6 text-green-500" />
|
|
) : (
|
|
<ToggleLeft className="h-6 w-6 text-gray-300" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
|
|
{feed.description || 'No description provided'}
|
|
</p>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
feed.is_active
|
|
? 'bg-green-100 text-green-800'
|
|
: 'bg-gray-100 text-gray-800'
|
|
}`}>
|
|
{feed.is_active ? 'Active' : 'Inactive'}
|
|
</span>
|
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
{feed.type.toUpperCase()}
|
|
</span>
|
|
</div>
|
|
<a
|
|
href={feed.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="p-1 text-gray-400 hover:text-gray-600 rounded"
|
|
title="Visit feed URL"
|
|
>
|
|
<ExternalLink className="h-4 w-4" />
|
|
</a>
|
|
</div>
|
|
|
|
<div className="mt-4 text-xs text-gray-500">
|
|
Added {new Date(feed.created_at).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{feeds?.length === 0 && (
|
|
<div className="col-span-full text-center py-12">
|
|
<Rss className="mx-auto h-12 w-12 text-gray-400" />
|
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No feeds</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
Get started by adding your first feed.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Feeds; |