|
import React, { useState, useEffect } from 'react'; |
|
import { useDispatch, useSelector } from 'react-redux'; |
|
import { fetchSources, addSource, deleteSource, clearError } from '../store/reducers/sourcesSlice'; |
|
|
|
const Sources = () => { |
|
const dispatch = useDispatch(); |
|
const { items: sources, loading, error } = useSelector(state => state.sources); |
|
|
|
const [newSource, setNewSource] = useState(''); |
|
const [isAdding, setIsAdding] = useState(false); |
|
const [successMessage, setSuccessMessage] = useState(''); |
|
const [deletingSources, setDeletingSources] = useState(new Set()); |
|
const [validationError, setValidationError] = useState(''); |
|
|
|
useEffect(() => { |
|
dispatch(fetchSources()); |
|
dispatch(clearError()); |
|
}, [dispatch]); |
|
|
|
const validateInput = (input) => { |
|
|
|
try { |
|
const urlObj = new URL(input); |
|
return urlObj.protocol === 'http:' || urlObj.protocol === 'https:'; |
|
} catch { |
|
|
|
return typeof input === 'string' && input.trim().length > 0; |
|
} |
|
}; |
|
|
|
const handleAddSource = async (e) => { |
|
e.preventDefault(); |
|
|
|
if (!newSource.trim()) { |
|
setValidationError('Please enter a RSS feed URL or keyword'); |
|
return; |
|
} |
|
|
|
if (!validateInput(newSource)) { |
|
setValidationError('Please enter a valid URL (http:// or https://) or keyword'); |
|
return; |
|
} |
|
|
|
setValidationError(''); |
|
setIsAdding(true); |
|
|
|
try { |
|
await dispatch(addSource({ source: newSource })).unwrap(); |
|
setNewSource(''); |
|
setSuccessMessage('Source added successfully!'); |
|
|
|
setTimeout(() => setSuccessMessage(''), 3000); |
|
} catch (err) { |
|
console.error('Failed to add source:', err); |
|
setValidationError('Failed to add source. Please try again.'); |
|
} finally { |
|
setIsAdding(false); |
|
} |
|
}; |
|
|
|
const handleDeleteSource = async (sourceId) => { |
|
|
|
setDeletingSources(prev => new Set(prev).add(sourceId)); |
|
|
|
try { |
|
await dispatch(deleteSource(sourceId)).unwrap(); |
|
} catch (err) { |
|
console.error('Failed to delete source:', err); |
|
setValidationError('Failed to delete source. Please try again.'); |
|
} finally { |
|
|
|
setDeletingSources(prev => { |
|
const newSet = new Set(prev); |
|
newSet.delete(sourceId); |
|
return newSet; |
|
}); |
|
} |
|
}; |
|
|
|
|
|
const totalSources = sources.length; |
|
const updatedSources = sources.filter(source => source.last_update).length; |
|
const categories = [...new Set(sources.map(source => source.category || 'Uncategorized'))]; |
|
|
|
|
|
|
|
return ( |
|
<div className="sources-page min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-50 p-3 sm:p-4 lg:p-6"> |
|
<div className="max-w-7xl mx-auto"> |
|
{/* Header Section */} |
|
<div className="sources-header mb-6 sm:mb-8 lg:mb-10 animate-slide-up"> |
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-3 sm:mb-4 gap-4"> |
|
<div className="flex-1"> |
|
<h1 className="sources-title text-2xl sm:text-3xl lg:text-4xl font-bold bg-gradient-to-r from-gray-900 via-gray-800 to-gray-900 bg-clip-text text-transparent mb-1 sm:mb-2"> |
|
RSS Sources |
|
</h1> |
|
<p className="sources-subtitle text-base sm:text-lg text-gray-600 font-medium max-w-2xl sm:max-w-3xl"> |
|
Manage your RSS feed sources for intelligent content aggregation and curation |
|
</p> |
|
</div> |
|
<div className="hidden sm:block lg:block"> |
|
<div className="w-12 h-12 sm:w-16 sm:h-16 bg-gradient-to-br from-orange-500 to-red-600 rounded-2xl shadow-lg flex items-center justify-center"> |
|
<svg className="w-6 h-6 sm:w-8 sm:h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" /> |
|
</svg> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
{/* Stats Cards */} |
|
<div className="grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 mt-6 sm:mt-8"> |
|
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-3 sm:p-4 border border-gray-200/50 shadow-sm hover:shadow-md transition-all duration-300"> |
|
<div className="flex items-center justify-between"> |
|
<div> |
|
<p className="text-xs sm:text-sm font-medium text-gray-600">Total Sources</p> |
|
<p className="text-lg sm:text-2xl font-bold text-gray-900">{totalSources}</p> |
|
</div> |
|
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-orange-100 rounded-lg flex items-center justify-center"> |
|
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> |
|
</svg> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-3 sm:p-4 border border-gray-200/50 shadow-sm hover:shadow-md transition-all duration-300"> |
|
<div className="flex items-center justify-between"> |
|
<div> |
|
<p className="text-xs sm:text-sm font-medium text-gray-600">Updated</p> |
|
<p className="text-lg sm:text-2xl font-bold text-gray-900">{updatedSources}</p> |
|
</div> |
|
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-green-100 rounded-lg flex items-center justify-center"> |
|
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> |
|
</svg> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-3 sm:p-4 border border-gray-200/50 shadow-sm hover:shadow-md transition-all duration-300"> |
|
<div className="flex items-center justify-between"> |
|
<div> |
|
<p className="text-xs sm:text-sm font-medium text-gray-600">Categories</p> |
|
<p className="text-lg sm:text-2xl font-bold text-gray-900">{categories.length}</p> |
|
</div> |
|
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-blue-100 rounded-lg flex items-center justify-center"> |
|
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> |
|
</svg> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div className="bg-white/80 backdrop-blur-sm rounded-xl p-3 sm:p-4 border border-gray-200/50 shadow-sm hover:shadow-md transition-all duration-300"> |
|
<div className="flex items-center justify-between"> |
|
<div> |
|
<p className="text-xs sm:text-sm font-medium text-gray-600">Active</p> |
|
<p className="text-lg sm:text-2xl font-bold text-gray-900"> |
|
{Math.round((updatedSources / totalSources) * 100) || 0}% |
|
</p> |
|
</div> |
|
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-purple-100 rounded-lg flex items-center justify-center"> |
|
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /> |
|
</svg> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
{/* Error Display */} |
|
{error && ( |
|
<div className="mb-6 sm:mb-8 animate-fade-in"> |
|
<div className="bg-red-50 border border-red-200 rounded-xl p-3 sm:p-4 flex items-start space-x-3"> |
|
<div className="flex-shrink-0"> |
|
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-red-400" fill="currentColor" viewBox="0 0 20 20"> |
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" /> |
|
</svg> |
|
</div> |
|
<div className="flex-1"> |
|
<p className="text-xs sm:text-sm font-medium text-red-800">{error}</p> |
|
</div> |
|
</div> |
|
</div> |
|
)} |
|
|
|
{/* Validation Error */} |
|
{validationError && ( |
|
<div className="mb-6 sm:mb-8 animate-fade-in"> |
|
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-3 sm:p-4 flex items-start space-x-3"> |
|
<div className="flex-shrink-0"> |
|
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20"> |
|
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" /> |
|
</svg> |
|
</div> |
|
<div className="flex-1"> |
|
<p className="text-xs sm:text-sm font-medium text-yellow-800">{validationError}</p> |
|
</div> |
|
</div> |
|
</div> |
|
)} |
|
|
|
{/* Success Message */} |
|
{successMessage && ( |
|
<div className="mb-6 sm:mb-8 animate-fade-in"> |
|
<div className="bg-green-50 border border-green-200 rounded-xl p-3 sm:p-4 flex items-start space-x-3"> |
|
<div className="flex-shrink-0"> |
|
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20"> |
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" /> |
|
</svg> |
|
</div> |
|
<div className="flex-1"> |
|
<p className="text-xs sm:text-sm font-medium text-green-800">{successMessage}</p> |
|
</div> |
|
</div> |
|
</div> |
|
)} |
|
|
|
<div className="sources-content space-y-6 sm:space-y-8"> |
|
{/* Add Source Section */} |
|
<div className="add-source-section bg-white/90 backdrop-blur-sm rounded-2xl p-4 sm:p-6 shadow-lg border border-gray-200/30 hover:shadow-xl transition-all duration-300 animate-slide-up"> |
|
<div className="flex items-center justify-between mb-4 sm:mb-6"> |
|
<h2 className="section-title text-xl sm:text-2xl font-bold text-gray-900 flex items-center space-x-2 sm:space-x-3"> |
|
<div className="w-6 h-6 sm:w-8 sm:h-8 bg-gradient-to-br from-orange-500 to-red-600 rounded-lg flex items-center justify-center"> |
|
<svg className="w-3 h-3 sm:w-5 sm:h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> |
|
</svg> |
|
</div> |
|
<span className="text-sm sm:text-base">Add New RSS Source</span> |
|
</h2> |
|
</div> |
|
|
|
<form onSubmit={handleAddSource} className="add-source-form space-y-4 sm:space-y-6"> |
|
<div className="form-field"> |
|
<label className="form-label block text-xs sm:text-sm font-semibold text-gray-700 mb-2">RSS Feed URL or Keyword</label> |
|
<div className="relative"> |
|
<input |
|
type="text" |
|
className="form-input w-full px-3 sm:px-4 py-2.5 sm:py-3 pl-10 sm:pl-12 border border-gray-300 rounded-xl focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all duration-300 bg-white/80 backdrop-blur-sm shadow-sm hover:shadow-md touch-manipulation" |
|
placeholder="https://example.com/feed.xml or enter a keyword" |
|
value={newSource} |
|
onChange={(e) => { |
|
setNewSource(e.target.value); |
|
if (validationError) setValidationError(''); |
|
}} |
|
required |
|
aria-label="RSS feed URL or keyword" |
|
/> |
|
<div className="absolute inset-y-0 left-0 pl-3 sm:pl-4 flex items-center pointer-events-none"> |
|
<svg className="w-4 h-4 sm:w-5 sm:h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /> |
|
</svg> |
|
</div> |
|
</div> |
|
<p className="mt-1.5 sm:mt-2 text-xs sm:text-sm text-gray-500">Enter the complete URL of the RSS feed or a keyword to search for sources</p> |
|
</div> |
|
|
|
<div className="flex justify-center pt-1 sm:pt-2"> |
|
<button |
|
type="submit" |
|
className="btn btn-primary bg-gradient-to-r from-orange-500 to-red-600 text-white py-2.5 sm:py-3 px-6 sm:px-8 rounded-xl font-semibold hover:from-orange-600 hover:to-red-700 transition-all duration-300 shadow-lg hover:shadow-xl disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center space-x-2 touch-manipulation active:scale-95" |
|
disabled={isAdding} |
|
aria-busy={isAdding} |
|
> |
|
{isAdding ? ( |
|
<> |
|
<svg className="animate-spin w-4 h-4 sm:w-5 sm:h-5 text-white" fill="none" viewBox="0 0 24 24"> |
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> |
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> |
|
</svg> |
|
<span className="text-xs sm:text-sm">Adding...</span> |
|
</> |
|
) : ( |
|
<> |
|
<svg className="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> |
|
</svg> |
|
<span className="text-xs sm:text-sm">Add Source</span> |
|
</> |
|
)} |
|
</button> |
|
</div> |
|
</form> |
|
</div> |
|
|
|
{/* Sources List Section */} |
|
<div className="sources-list-section bg-white/90 backdrop-blur-sm rounded-2xl p-4 sm:p-6 shadow-lg border border-gray-200/30 hover:shadow-xl transition-all duration-300 animate-slide-up"> |
|
{loading && ( |
|
<div className="flex items-center justify-center mb-4"> |
|
<div className="animate-spin w-4 h-4 border-2 border-gray-300 border-t-gray-900 rounded-full mr-2"></div> |
|
<span className="text-sm text-gray-600">Updating sources...</span> |
|
</div> |
|
)} |
|
<div className="flex items-center justify-between mb-4 sm:mb-6"> |
|
<h2 className="section-title text-xl sm:text-2xl font-bold text-gray-900 flex items-center space-x-2 sm:space-x-3"> |
|
<div className="w-6 h-6 sm:w-8 sm:h-8 bg-gradient-to-br from-blue-500 to-cyan-600 rounded-lg flex items-center justify-center"> |
|
<svg className="w-3 h-3 sm:w-5 sm:h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> |
|
</svg> |
|
</div> |
|
<span className="text-sm sm:text-base">Your RSS Sources</span> |
|
</h2> |
|
<span className="bg-blue-100 text-blue-800 text-xs sm:text-sm font-medium px-2 sm:px-3 py-0.5 sm:py-1 rounded-full"> |
|
{totalSources} sources |
|
</span> |
|
</div> |
|
|
|
{sources.length === 0 ? ( |
|
<div className="text-center py-8 sm:py-12"> |
|
<div className="w-12 h-12 sm:w-16 sm:h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-3 sm:mb-4"> |
|
<svg className="w-6 h-6 sm:w-8 sm:h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> |
|
</svg> |
|
</div> |
|
<h3 className="text-base sm:text-lg font-semibold text-gray-900 mb-1 sm:mb-2">No RSS sources added yet</h3> |
|
<p className="text-xs sm:text-sm text-gray-600 mb-4 sm:mb-6">Add your first RSS feed source to start aggregating content</p> |
|
<div className="bg-gray-50 rounded-lg p-3 sm:p-4 text-left max-w-xs sm:max-w-md mx-auto"> |
|
<h4 className="font-medium text-gray-900 mb-2 text-sm sm:text-base">Popular RSS Sources:</h4> |
|
<ul className="text-xs sm:text-sm text-gray-600 space-y-1"> |
|
<li>• Tech News: https://techcrunch.com/feed/</li> |
|
<li>• Design Inspiration: https://www.smashingmagazine.com/feed/</li> |
|
<li>• Programming: https://stackoverflow.com/feeds</li> |
|
</ul> |
|
</div> |
|
</div> |
|
) : ( |
|
<div className="sources-list space-y-3 sm:space-y-4"> |
|
{sources |
|
.slice() |
|
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)) |
|
.map((source, index) => ( |
|
<div key={source.id} className="source-item group border border-gray-200 rounded-xl bg-gradient-to-r from-gray-50 to-white hover:from-gray-100 hover:to-white transition-all duration-300 hover:shadow-lg animate-fade-in" style={{animationDelay: `${index * 100}ms`}}> |
|
<div className="p-4 sm:p-6"> |
|
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4"> |
|
<div className="flex-1"> |
|
<div className="flex flex-col sm:flex-row sm:items-start space-y-3 sm:space-y-0 sm:space-x-4"> |
|
<div className="flex-shrink-0"> |
|
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-orange-100 rounded-lg flex items-center justify-center"> |
|
<svg className="w-4 h-4 sm:w-6 sm:h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> |
|
</svg> |
|
</div> |
|
</div> |
|
|
|
<div className="flex-1 min-w-0"> |
|
<div className="source-url text-gray-900 font-medium text-base sm:text-lg break-all mb-1 sm:mb-2"> |
|
{source.source} |
|
</div> |
|
|
|
<div className="flex flex-wrap items-center gap-2 sm:gap-4 text-xs sm:text-sm"> |
|
<div className="source-category"> |
|
<span className="text-gray-600">Category:</span> |
|
<span className="ml-1 sm:ml-2 px-1.5 py-0.5 sm:px-2 sm:py-1 bg-blue-100 text-blue-800 text-xs font-medium rounded-full"> |
|
{source.category || 'Uncategorized'} |
|
</span> |
|
</div> |
|
|
|
<div className="source-date"> |
|
<span className="text-gray-600">Added:</span> |
|
<span className="ml-1 sm:ml-2 text-gray-900 font-medium"> |
|
{new Date(source.created_at).toLocaleDateString()} |
|
</span> |
|
</div> |
|
|
|
{source.last_update && ( |
|
<div className="source-updated"> |
|
<span className="text-gray-600">Updated:</span> |
|
<span className="ml-1 sm:ml-2 text-green-600 font-medium"> |
|
{new Date(source.last_update).toLocaleDateString()} |
|
</span> |
|
</div> |
|
)} |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div className="source-actions flex-shrink-0 flex items-center space-x-2 sm:space-x-4"> |
|
{source.last_update ? ( |
|
<div className="flex items-center space-x-1 text-green-600"> |
|
<svg className="w-3 h-3 sm:w-4 sm:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> |
|
</svg> |
|
<span className="text-xs sm:text-sm font-medium">Active</span> |
|
</div> |
|
) : ( |
|
<div className="flex items-center space-x-1 text-gray-400"> |
|
<svg className="w-3 h-3 sm:w-4 sm:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> |
|
</svg> |
|
<span className="text-xs sm:text-sm font-medium">Never updated</span> |
|
</div> |
|
)} |
|
|
|
<button |
|
className="btn btn-secondary bg-gradient-to-r from-red-500 to-pink-600 text-white py-1.5 px-4 sm:py-2 sm:px-6 rounded-xl font-semibold hover:from-red-600 hover:to-pink-700 transition-all duration-300 shadow-md hover:shadow-lg disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center space-x-2 touch-manipulation active:scale-95" |
|
onClick={() => handleDeleteSource(source.id)} |
|
disabled={deletingSources.has(source.id)} |
|
> |
|
{deletingSources.has(source.id) ? ( |
|
<> |
|
<svg className="animate-spin w-3 h-3 sm:w-4 sm:h-4 text-white" fill="none" viewBox="0 0 24 24"> |
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> |
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> |
|
</svg> |
|
<span className="text-xs sm:text-sm">Deleting...</span> |
|
</> |
|
) : ( |
|
<> |
|
<svg className="w-3 h-3 sm:w-4 sm:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> |
|
</svg> |
|
<span className="text-xs sm:text-sm">Delete</span> |
|
</> |
|
)} |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
))} |
|
</div> |
|
)} |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
export default Sources; |