From 136063792c15b916acaf5328aab1d4d7427025ed Mon Sep 17 00:00:00 2001 From: haddadrm <121486289+haddadrm@users.noreply.github.com> Date: Tue, 25 Feb 2025 23:19:37 +0400 Subject: [PATCH] Discover Page Optimization Restructured the Discover page to prevent the entire page from refreshing when selecting categories or updating settings 1. Component Separation -Split the page into three main components: -DiscoverHeader: Contains the title, settings button, and category navigation -DiscoverContent: Contains the grid of articles with its own loading state -PreferencesModal: Manages the settings modal with temporary state 2. Optimized Rendering -Used React.memo for all components to prevent unnecessary re-renders -Each component only receives the props it needs -The header remains stable while only the content area updates 3. Improved Loading States 3.1. Added separate loading states: -Initial loading for the first page load -Content-only loading when changing categories or preferences -Loading spinners now only appear in the content area when changing categories 3.2. Better State Management -Main state is managed in the parent component -Modal uses temporary state that only updates the main state after saving -Clear separation of concerns between components These changes create a more polished user experience where the header and sidebar remain stable while only the content area refreshes when needed. The page now feels more responsive and app-like, rather than having the entire page refresh on every interaction --- src/routes/discover.ts | 7 - ui/app/discover/page.tsx | 667 ++++++++++++++++++++++----------------- 2 files changed, 386 insertions(+), 288 deletions(-) diff --git a/src/routes/discover.ts b/src/routes/discover.ts index dd6bb20..feef0de 100644 --- a/src/routes/discover.ts +++ b/src/routes/discover.ts @@ -45,13 +45,6 @@ const getSearchQueriesForCategory = (category: string): { site: string, keyword: { site: 'kotaku.com', keyword: 'gaming' }, { site: 'eurogamer.net', keyword: 'games' } ], - 'Weather': [ - { site: 'weather.com', keyword: 'forecast' }, - { site: 'accuweather.com', keyword: 'weather' }, - { site: 'wunderground.com', keyword: 'weather' }, - { site: 'noaa.gov', keyword: 'weather' }, - { site: 'weatherchannel.com', keyword: 'forecast' } - ], 'Entertainment': [ { site: 'variety.com', keyword: 'entertainment' }, { site: 'hollywoodreporter.com', keyword: 'entertainment' }, diff --git a/ui/app/discover/page.tsx b/ui/app/discover/page.tsx index dde698f..20d354f 100644 --- a/ui/app/discover/page.tsx +++ b/ui/app/discover/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { Search, Sliders, ChevronLeft, ChevronRight } from 'lucide-react'; -import { useEffect, useState, useRef } from 'react'; +import { useEffect, useState, useRef, memo } from 'react'; import Link from 'next/link'; import { toast } from 'sonner'; @@ -15,21 +15,20 @@ interface Discover { // List of available categories const categories = [ 'For You', 'AI', 'Technology', 'Current News', 'Sports', - 'Money', 'Gaming', 'Weather', 'Entertainment', 'Art and Culture', + 'Money', 'Gaming', 'Entertainment', 'Art and Culture', 'Science', 'Health', 'Travel' ]; -const Page = () => { - const [discover, setDiscover] = useState(null); - const [loading, setLoading] = useState(true); - const [activeCategory, setActiveCategory] = useState('For You'); - const [showPreferences, setShowPreferences] = useState(false); - const [userPreferences, setUserPreferences] = useState(['AI', 'Technology']); - const [preferredLanguages, setPreferredLanguages] = useState(['en']); // Default to English - - // Temporary state for the preferences modal - const [tempPreferences, setTempPreferences] = useState([]); - const [tempLanguages, setTempLanguages] = useState([]); +// Memoized header component that won't re-render when content changes +const DiscoverHeader = memo(({ + activeCategory, + setActiveCategory, + setShowPreferences +}: { + activeCategory: string; + setActiveCategory: (category: string) => void; + setShowPreferences: (show: boolean) => void; +}) => { const categoryContainerRef = useRef(null); // Function to scroll categories horizontally @@ -48,61 +47,87 @@ const Page = () => { }); }; - // Load user preferences on component mount - useEffect(() => { - const loadUserPreferences = async () => { - try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/discover/preferences`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); + return ( +
+
+
+ +

Discover

+
+ +
+ + {/* Category Navigation with Buttons */} +
+ + +
+
+ {categories.map((category) => ( + + ))} +
+
+ + +
+ +
+
+ ); +}); - if (res.ok) { - const data = await res.json(); - setUserPreferences(data.categories || ['AI', 'Technology']); - setPreferredLanguages(data.languages || ['en']); // Default to English if no languages are set - } - } catch (err: any) { - console.error('Error loading preferences:', err.message); - // Use default preferences if loading fails - } - }; +DiscoverHeader.displayName = 'DiscoverHeader'; - loadUserPreferences(); - }, []); - - // Save user preferences - const saveUserPreferences = async (preferences: string[], languages: string[]) => { - try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/discover/preferences`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - categories: preferences, - languages - }), - }); - - if (res.ok) { - toast.success('Preferences saved successfully'); - } else { - const data = await res.json(); - throw new Error(data.message); - } - } catch (err: any) { - console.error('Error saving preferences:', err.message); - toast.error('Error saving preferences'); - } - }; +// Memoized content component that handles its own loading state +const DiscoverContent = memo(({ + activeCategory, + userPreferences, + preferredLanguages +}: { + activeCategory: string; + userPreferences: string[]; + preferredLanguages: string[]; +}) => { + const [discover, setDiscover] = useState(null); + const [contentLoading, setContentLoading] = useState(true); // Fetch data based on active category, user preferences, and language useEffect(() => { const fetchData = async () => { - setLoading(true); + setContentLoading(true); try { let endpoint = `${process.env.NEXT_PUBLIC_API_URL}/discover`; let params = []; @@ -141,239 +166,319 @@ const Page = () => { console.error('Error fetching data:', err.message); toast.error('Error fetching data'); } finally { - setLoading(false); + setContentLoading(false); } }; fetchData(); }, [activeCategory, userPreferences, preferredLanguages]); - return loading ? ( -
- + if (contentLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ {discover && + discover.map((item, i) => ( + + {/* Using img tag instead of Next.js Image for external URLs */} + {item.title} +
+
+ {item.title.slice(0, 100)}... +
+

+ {item.content.slice(0, 100)}... +

+
+ + ))}
- ) : ( - <> -
-
-
-
- -

Discover

-
- -
- - {/* Category Navigation with Buttons */} -
- - -
-
- {categories.map((category) => ( - - ))} -
-
- - -
- -
-
+ ); +}); - {/* Personalization Modal */} - {/* Initialize temp preferences when modal opens */} - {showPreferences && (() => { - // Initialize temp preferences when modal opens - if (tempPreferences.length === 0) { - setTempPreferences([...userPreferences]); - } - if (tempLanguages.length === 0) { - setTempLanguages([...preferredLanguages]); - } - - return ( -
-
-

Personalize Your Feed

- -

Select categories you're interested in:

-
- {categories.filter(c => c !== 'For You').map((category) => ( - - ))} -
- -
-

Preferred Languages

-
- {[ - { code: 'en', name: 'English' }, - { code: 'ar', name: 'Arabic' }, - { code: 'zh', name: 'Chinese' }, - { code: 'fr', name: 'French' }, - { code: 'de', name: 'German' }, - { code: 'hi', name: 'Hindi' }, - { code: 'it', name: 'Italian' }, - { code: 'ja', name: 'Japanese' }, - { code: 'ko', name: 'Korean' }, - { code: 'pt', name: 'Portuguese' }, - { code: 'ru', name: 'Russian' }, - { code: 'es', name: 'Spanish' }, - ].map((language) => ( - - ))} -
-

- {tempLanguages.length === 0 - ? "No languages selected will show results in all languages" - : `Selected: ${tempLanguages.length} language(s)`} -

-
- -
- - -
-
-
- ); - })()} +DiscoverContent.displayName = 'DiscoverContent'; -
- {discover && - discover?.map((item, i) => ( - - {/* Using img tag instead of Next.js Image for external URLs */} - void; + userPreferences: string[]; + setUserPreferences: (prefs: string[]) => void; + preferredLanguages: string[]; + setPreferredLanguages: (langs: string[]) => void; + setActiveCategory: (category: string) => void; +}) => { + const [tempPreferences, setTempPreferences] = useState([]); + const [tempLanguages, setTempLanguages] = useState([]); + + // Initialize temp preferences when modal opens + useEffect(() => { + if (showPreferences) { + setTempPreferences([...userPreferences]); + setTempLanguages([...preferredLanguages]); + } + }, [showPreferences, userPreferences, preferredLanguages]); + + // Save user preferences + const saveUserPreferences = async (preferences: string[], languages: string[]) => { + try { + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/discover/preferences`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + categories: preferences, + languages + }), + }); + + if (res.ok) { + toast.success('Preferences saved successfully'); + } else { + const data = await res.json(); + throw new Error(data.message); + } + } catch (err: any) { + console.error('Error saving preferences:', err.message); + toast.error('Error saving preferences'); + } + }; + + if (!showPreferences) return null; + + return ( +
+
+

Personalize Your Feed

+ +

Select categories you're interested in:

+
+ {categories.filter(c => c !== 'For You').map((category) => ( + + ))} +
+ +
+

Preferred Languages

+
+ {[ + { code: 'en', name: 'English' }, + { code: 'ar', name: 'Arabic' }, + { code: 'zh', name: 'Chinese' }, + { code: 'fr', name: 'French' }, + { code: 'de', name: 'German' }, + { code: 'hi', name: 'Hindi' }, + { code: 'it', name: 'Italian' }, + { code: 'ja', name: 'Japanese' }, + { code: 'ko', name: 'Korean' }, + { code: 'pt', name: 'Portuguese' }, + { code: 'ru', name: 'Russian' }, + { code: 'es', name: 'Spanish' }, + ].map((language) => ( + ))} +
+

+ {tempLanguages.length === 0 + ? "No languages selected will show results in all languages" + : `Selected: ${tempLanguages.length} language(s)`} +

+
+ +
+ +
- +
+ ); +}); + +PreferencesModal.displayName = 'PreferencesModal'; + +// Main page component +const Page = () => { + // State for the entire page + const [activeCategory, setActiveCategory] = useState('For You'); + const [showPreferences, setShowPreferences] = useState(false); + const [userPreferences, setUserPreferences] = useState(['AI', 'Technology']); + const [preferredLanguages, setPreferredLanguages] = useState(['en']); // Default to English + const [initialLoading, setInitialLoading] = useState(true); + + // Load user preferences on component mount + useEffect(() => { + const loadUserPreferences = async () => { + try { + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/discover/preferences`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (res.ok) { + const data = await res.json(); + setUserPreferences(data.categories || ['AI', 'Technology']); + setPreferredLanguages(data.languages || ['en']); // Default to English if no languages are set + } + } catch (err: any) { + console.error('Error loading preferences:', err.message); + // Use default preferences if loading fails + } finally { + setInitialLoading(false); + } + }; + + loadUserPreferences(); + }, []); + + if (initialLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Static header that doesn't re-render when content changes */} + + + {/* Dynamic content that updates independently */} + + + {/* Preferences modal */} + +
); };