@ -1,4 +1,5 @@
import { useEffect , useState } from "react" ;
import React from "react" ;
interface FeedItem {
id : string ;
@ -18,12 +19,15 @@ interface FeedListProps {
categoryFilter? : string ;
}
export default function FeedList ( { searchTerm = "" , categoryFilter = "" } : FeedListProps = { } ) {
export default function FeedList ( {
searchTerm = "" ,
categoryFilter = "" ,
} : FeedListProps = { } ) {
const [ feeds , setFeeds ] = useState < FeedItem [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ error , setError ] = useState < string | null > ( null ) ;
const [ sortBy , setSortBy ] = useState < ' date' | ' title' > ( ' date' ) ;
const [ sortOrder , setSortOrder ] = useState < ' asc' | ' desc' > ( ' desc' ) ;
const [ sortBy , setSortBy ] = useState < " date" | " title" > ( " date" ) ;
const [ sortOrder , setSortOrder ] = useState < " asc" | " desc" > ( " desc" ) ;
useEffect ( ( ) = > {
fetchFeeds ( ) ;
@ -47,43 +51,48 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
} ;
const filteredAndSortedFeeds = feeds
. filter ( feed = > {
const matchesSearch = ! searchTerm ||
. filter ( ( feed ) = > {
const matchesSearch =
! searchTerm ||
feed . title . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
feed . contentSnippet ? . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
feed . source ? . title ? . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ;
const matchesCategory = ! categoryFilter || feed . category === categoryFilter ;
const matchesCategory =
! categoryFilter || feed . category === categoryFilter ;
return matchesSearch && matchesCategory ;
} )
. sort ( ( a , b ) = > {
const multiplier = sortOrder === ' asc' ? 1 : - 1 ;
if ( sortBy === ' date' ) {
return ( new Date ( a . pubDate ) . getTime ( ) - new Date ( b . pubDate ) . getTime ( ) ) * multiplier ;
const multiplier = sortOrder === " asc" ? 1 : - 1 ;
if ( sortBy === " date" ) {
return (
( new Date ( a . pubDate ) . getTime ( ) - new Date ( b . pubDate ) . getTime ( ) ) *
multiplier
) ;
} else {
return a . title . localeCompare ( b . title ) * multiplier ;
}
} ) ;
const handleSort = ( field : ' date' | ' title' ) = > {
const handleSort = ( field : " date" | " title" ) = > {
if ( sortBy === field ) {
setSortOrder ( sortOrder === ' asc' ? ' desc' : ' asc' ) ;
setSortOrder ( sortOrder === " asc" ? " desc" : " asc" ) ;
} else {
setSortBy ( field ) ;
setSortOrder ( ' desc' ) ;
setSortOrder ( " desc" ) ;
}
} ;
const formatDate = ( dateString : string ) = > {
try {
return new Date ( dateString ) . toLocaleDateString ( ' ja-JP' , {
year : ' numeric' ,
month : ' short' ,
day : ' numeric' ,
hour : ' 2-digit' ,
minute : ' 2-digit'
return new Date ( dateString ) . toLocaleDateString ( " ja-JP" , {
year : " numeric" ,
month : " short" ,
day : " numeric" ,
hour : " 2-digit" ,
minute : " 2-digit" ,
} ) ;
} catch {
return dateString ;
@ -94,7 +103,10 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
return (
< div className = "space-y-6" >
{ [ . . . Array ( 5 ) ] . map ( ( _ , i ) = > (
< div key = { i } className = "glass-effect rounded-3xl p-6 border border-white/20 animate-pulse" >
< div
key = { i }
className = "glass-effect rounded-3xl p-6 border border-white/20 animate-pulse"
>
< div className = "flex items-start space-x-4" >
< div className = "w-16 h-16 bg-slate-200 rounded-2xl" > < / div >
< div className = "flex-1 space-y-3" >
@ -115,15 +127,14 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
< div className = "glass-effect rounded-3xl p-8 border border-red-200 bg-red-50" >
< div className = "flex items-center space-x-4" >
< div className = "w-12 h-12 rounded-2xl bg-red-100 flex items-center justify-center" >
⚠ ️
⚠ ️
< / div >
< div >
< h3 className = "text-lg font-bold text-red-800" > エ ラ ー が 発 生 し ま し た < / h3 >
< h3 className = "text-lg font-bold text-red-800" >
エ ラ ー が 発 生 し ま し た
< / h3 >
< p className = "text-red-700" > { error } < / p >
< button
onClick = { fetchFeeds }
className = "mt-3 btn-primary text-sm"
>
< button onClick = { fetchFeeds } className = "mt-3 btn-primary text-sm" >
再 読 み 込 み
< / button >
< / div >
@ -137,36 +148,34 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
{ /* Sort Controls */ }
< div className = "glass-effect rounded-2xl p-4 border border-white/20" >
< div className = "flex items-center space-x-4" >
< span className = "text-sm font-semibold text-slate-700" > 並 び 替 え : < / span >
< span className = "text-sm font-semibold text-slate-700" >
並 び 替 え :
< / span >
< div className = "flex space-x-2" >
< button
onClick = { ( ) = > handleSort ( ' date' ) }
onClick = { ( ) = > handleSort ( " date" ) }
className = { ` flex items-center space-x-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 ${
sortBy === ' date'
? ' bg-blue-100 text-blue-800 border border-blue-200'
: ' text-slate-600 hover:text-slate-800 hover:bg-slate-100'
sortBy === " date"
? " bg-blue-100 text-blue-800 border border-blue-200"
: " text-slate-600 hover:text-slate-800 hover:bg-slate-100"
} ` }
>
< span > 日 時 < / span >
{ sortBy === ' date' && (
< span >
{ sortOrder === 'asc' ? '↑' : '↓' }
< / span >
{ sortBy === " date" && (
< span > { sortOrder === "asc" ? "↑" : "↓" } < / span >
) }
< / button >
< button
onClick = { ( ) = > handleSort ( ' title' ) }
onClick = { ( ) = > handleSort ( " title" ) }
className = { ` flex items-center space-x-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 ${
sortBy === ' title'
? ' bg-blue-100 text-blue-800 border border-blue-200'
: ' text-slate-600 hover:text-slate-800 hover:bg-slate-100'
sortBy === " title"
? " bg-blue-100 text-blue-800 border border-blue-200"
: " text-slate-600 hover:text-slate-800 hover:bg-slate-100"
} ` }
>
< span > タ イ ト ル < / span >
{ sortBy === ' title' && (
< span >
{ sortOrder === 'asc' ? '↑' : '↓' }
< / span >
{ sortBy === " title" && (
< span > { sortOrder === "asc" ? "↑" : "↓" } < / span >
) }
< / button >
< / div >
@ -179,17 +188,23 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
{ /* Feed Cards */ }
{ filteredAndSortedFeeds . length === 0 ? (
< div className = "text-center py-20" >
< div className = "w-24 h-24 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg" style = { { background : 'linear-gradient(135deg, #e2e8f0, #cbd5e1)' } } >
< span role = "img" aria-hidden = "true" className = "text-4xl" > 📰 < / span >
< div
className = "w-24 h-24 rounded-3xl flex items-center justify-center mx-auto mb-6 shadow-lg"
style = { { background : "linear-gradient(135deg, #e2e8f0, #cbd5e1)" } }
>
< span role = "img" aria-hidden = "true" className = "text-4xl" >
📰
< / span >
< / div >
< h3 className = "text-xl font-bold text-slate-700 mb-2" >
{ searchTerm || categoryFilter ? '検索結果がありません' : 'フィードがありません' }
{ searchTerm || categoryFilter
? "検索結果がありません"
: "フィードがありません" }
< / h3 >
< p className = "text-slate-500 max-w-md mx-auto" >
{ searchTerm || categoryFilter
? ' 別のキーワードやカテゴリで検索してみてください'
: ' RSSフィードを追加してバッチ処理を実行してください'
}
{ searchTerm || categoryFilter
? " 別のキーワードやカテゴリで検索してみてください"
: " RSSフィードを追加してバッチ処理を実行してください" }
< / p >
< / div >
) : (
@ -199,18 +214,23 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
key = { feed . id }
className = "group glass-effect rounded-3xl border border-white/20 hover:border-white/40 hover:shadow-2xl transition-all duration-300 overflow-hidden"
style = { {
animationDelay : ` ${ index * 0.05 } s `
animationDelay : ` ${ index * 0.05 } s ` ,
} }
>
< div className = "p-6" >
< div className = "flex items-start space-x-5" >
{ /* Article Icon */ }
< div className = "flex-shrink-0" >
< div className = "w-16 h-16 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-all duration-300 group-hover:scale-110" style = { { background : 'var(--gradient-primary)' } } >
< span role = "img" aria-hidden = "true" className = "text-2xl" > 📰 < / span >
< div
className = "w-16 h-16 rounded-2xl flex items-center justify-center shadow-lg group-hover:shadow-xl transition-all duration-300 group-hover:scale-110"
style = { { background : "var(--gradient-primary)" } }
>
< span role = "img" aria-hidden = "true" className = "text-2xl" >
📰
< / span >
< / div >
< / div >
{ /* Article Content */ }
< div className = "flex-1 min-w-0" >
{ /* Header */ }
@ -219,17 +239,19 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
< h3 className = "text-lg font-bold text-slate-800 line-clamp-2 group-hover:text-slate-900 transition-colors duration-200" >
{ feed . title }
< / h3 >
{ /* Meta Info */ }
< div className = "flex items-center space-x-3 mt-2 text-sm text-slate-600" >
{ feed . source ? . title && (
< span className = "font-medium" > { feed . source . title } < / span >
< span className = "font-medium" >
{ feed . source . title }
< / span >
) }
< span className = "text-slate-400" > • < / span >
< span > { formatDate ( feed . pubDate ) } < / span >
< / div >
< / div >
{ /* Category Badge */ }
{ feed . category && (
< span className = "ml-4 inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 border border-blue-200" >
@ -237,14 +259,14 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
< / span >
) }
< / div >
{ /* Content Snippet */ }
{ feed . contentSnippet && (
< p className = "text-slate-600 leading-relaxed line-clamp-3 mb-4" >
{ feed . contentSnippet }
< / p >
) }
{ /* Actions */ }
< div className = "flex items-center justify-between" >
< a
@ -255,10 +277,8 @@ export default function FeedList({ searchTerm = "", categoryFilter = "" }: FeedL
>
元 記 事 を 読 む →
< / a >
< div className = "text-xs text-slate-400" >
# { index + 1 }
< / div >
< div className = "text-xs text-slate-400" > # { index + 1 } < / div >
< / div >
< / div >
< / div >