feat(frontend): Add search functionality to homepage

This commit is contained in:
Peifan Li
2025-03-12 23:30:21 -04:00
parent 22571be6c7
commit 02b07431ef
4 changed files with 136 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
{ {
"isDownloading": false, "isDownloading": false,
"title": "", "title": "",
"timestamp": 1741836072302 "timestamp": 1741836609758
} }

View File

@@ -363,6 +363,7 @@ function App() {
error: 'Failed to search. Please try again.' error: 'Failed to search. Please try again.'
}; };
} }
return { success: false, error: 'Search was cancelled' };
} finally { } finally {
// Only update loading state if the request wasn't aborted // Only update loading state if the request wasn't aborted
if (searchAbortController.current && !searchAbortController.current.signal.aborted) { if (searchAbortController.current && !searchAbortController.current.signal.aborted) {
@@ -652,6 +653,12 @@ function App() {
error={error} error={error}
onDeleteVideo={handleDeleteVideo} onDeleteVideo={handleDeleteVideo}
collections={collections} collections={collections}
isSearchMode={isSearchMode}
searchTerm={searchTerm}
localSearchResults={localSearchResults}
youtubeLoading={youtubeLoading}
searchResults={searchResults}
onDownload={handleDownloadFromSearch}
/> />
} }
/> />

View File

@@ -42,7 +42,7 @@ const Header = ({ onSubmit, onSearch, downloadingTitle, isDownloading }) => {
const searchResult = await onSearch(videoUrl); const searchResult = await onSearch(videoUrl);
if (searchResult.success) { if (searchResult.success) {
setVideoUrl(''); setVideoUrl('');
navigate('/'); // Navigate to home which will show search results navigate('/'); // Stay on homepage to show search results
} else { } else {
setError(searchResult.error); setError(searchResult.error);
} }
@@ -55,7 +55,7 @@ const Header = ({ onSubmit, onSearch, downloadingTitle, isDownloading }) => {
if (result.success) { if (result.success) {
setVideoUrl(''); setVideoUrl('');
// Stay on home page which will show search results // Stay on homepage to show search results
navigate('/'); navigate('/');
} else { } else {
setError(result.error); setError(result.error);

View File

@@ -2,15 +2,27 @@ import AuthorsList from '../components/AuthorsList';
import Collections from '../components/Collections'; import Collections from '../components/Collections';
import VideoCard from '../components/VideoCard'; import VideoCard from '../components/VideoCard';
const Home = ({ videos = [], loading, error, onDeleteVideo, collections = [] }) => { const Home = ({
videos = [],
loading,
error,
onDeleteVideo,
collections = [],
isSearchMode = false,
searchTerm = '',
localSearchResults = [],
youtubeLoading = false,
searchResults = [],
onDownload
}) => {
// Add default empty array to ensure videos is always an array // Add default empty array to ensure videos is always an array
const videoArray = Array.isArray(videos) ? videos : []; const videoArray = Array.isArray(videos) ? videos : [];
if (loading && videoArray.length === 0) { if (loading && videoArray.length === 0 && !isSearchMode) {
return <div className="loading">Loading videos...</div>; return <div className="loading">Loading videos...</div>;
} }
if (error && videoArray.length === 0) { if (error && videoArray.length === 0 && !isSearchMode) {
return <div className="error">{error}</div>; return <div className="error">{error}</div>;
} }
@@ -34,6 +46,101 @@ const Home = ({ videos = [], loading, error, onDeleteVideo, collections = [] })
}); });
}); });
// If in search mode, show search results
if (isSearchMode) {
const hasLocalResults = localSearchResults && localSearchResults.length > 0;
const hasYouTubeResults = searchResults && searchResults.length > 0;
return (
<div className="search-results">
<div className="search-header">
<h2>Search Results for "{searchTerm}"</h2>
</div>
{/* Local Video Results */}
<div className="search-results-section">
<h3 className="section-title">From Your Library</h3>
{hasLocalResults ? (
<div className="search-results-grid">
{localSearchResults.map((video) => (
<VideoCard
key={video.id}
video={video}
onDeleteVideo={onDeleteVideo}
showDeleteButton={true}
collections={collections}
/>
))}
</div>
) : (
<p className="no-results">No matching videos in your library.</p>
)}
</div>
{/* YouTube Search Results */}
<div className="search-results-section">
<h3 className="section-title">From YouTube</h3>
{youtubeLoading ? (
<div className="youtube-loading">
<div className="loading-spinner"></div>
<p>Loading YouTube results...</p>
</div>
) : hasYouTubeResults ? (
<div className="search-results-grid">
{searchResults.map((result) => (
<div key={result.id} className="search-result-card">
<div className="search-result-thumbnail">
{result.thumbnailUrl ? (
<img
src={result.thumbnailUrl}
alt={result.title}
onError={(e) => {
e.target.onerror = null;
e.target.src = 'https://via.placeholder.com/480x360?text=No+Thumbnail';
}}
/>
) : (
<div className="thumbnail-placeholder">No Thumbnail</div>
)}
</div>
<div className="search-result-info">
<h3 className="search-result-title">{result.title}</h3>
<p className="search-result-author">{result.author}</p>
<div className="search-result-meta">
{result.duration && (
<span className="search-result-duration">
{formatDuration(result.duration)}
</span>
)}
{result.viewCount && (
<span className="search-result-views">
{formatViewCount(result.viewCount)} views
</span>
)}
<span className={`source-badge ${result.source}`}>
{result.source}
</span>
</div>
<button
className="download-btn"
onClick={() => onDownload(result.sourceUrl, result.title)}
>
Download
</button>
</div>
</div>
))}
</div>
) : (
<p className="no-results">No YouTube results found.</p>
)}
</div>
</div>
);
}
// Regular home view (not in search mode)
return ( return (
<div className="home-container"> <div className="home-container">
{videoArray.length === 0 ? ( {videoArray.length === 0 ? (
@@ -69,4 +176,20 @@ const Home = ({ videos = [], loading, error, onDeleteVideo, collections = [] })
); );
}; };
// Helper function to format duration in seconds to MM:SS
const formatDuration = (seconds) => {
if (!seconds) return '';
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
};
// Helper function to format view count
const formatViewCount = (count) => {
if (!count) return '0';
if (count < 1000) return count.toString();
if (count < 1000000) return `${(count / 1000).toFixed(1)}K`;
return `${(count / 1000000).toFixed(1)}M`;
};
export default Home; export default Home;