feat(frontend): Add search functionality to homepage
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"isDownloading": false,
|
"isDownloading": false,
|
||||||
"title": "",
|
"title": "",
|
||||||
"timestamp": 1741836072302
|
"timestamp": 1741836609758
|
||||||
}
|
}
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user