From 9bd98dea09ca1941b1db3d7bddbba887abd5398c Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Tue, 4 Feb 2025 14:22:49 -0600 Subject: [PATCH 01/11] good! --- frontend/bun.lockb | Bin 0 -> 102455 bytes frontend/src/App.tsx | 107 ++++++++++++++++++++++++-- frontend/src/hooks/useWikiArticles.ts | 93 ++++++++++++++-------- 3 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 frontend/bun.lockb diff --git a/frontend/bun.lockb b/frontend/bun.lockb new file mode 100644 index 0000000000000000000000000000000000000000..08a563b205c24ad1b4052b60c3e6dc1a7275163e GIT binary patch literal 102455 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 aW1xeCJ6{^A+!a$Kn1L;GX7m4p|NMWgD^cSB literal 0 HcmV?d00001 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 65c03e7..52ecf5a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,15 +1,28 @@ import { useEffect, useRef, useCallback, useState } from 'react' import { WikiCard } from './components/WikiCard' import { useWikiArticles } from './hooks/useWikiArticles' -import { Loader2 } from 'lucide-react' +import { Loader2, MoreHorizontal } from 'lucide-react' import { Analytics } from "@vercel/analytics/react" import { LanguageSelector } from './components/LanguageSelector' function App() { + // Built-in topics to start with + const initialTopics = [ + { label: 'Random', value: '' }, + { label: 'Cats', value: 'Category:Cats' }, + { label: 'Music', value: 'Category:Music' }, + { label: 'History', value: 'Category:History' }, + ] + const [showAbout, setShowAbout] = useState(false) - const { articles, loading, fetchArticles } = useWikiArticles() - const observerTarget = useRef(null) + const [topics, setTopics] = useState(initialTopics) + const [showMenu, setShowMenu] = useState(false) + const [newCategory, setNewCategory] = useState('') + const [selectedTopic, setSelectedTopic] = useState('') + const { articles, loading, fetchArticles } = useWikiArticles(selectedTopic) + const observerTarget = useRef(null) + // Intersection Observer for infinite scrolling const handleObserver = useCallback( (entries: IntersectionObserverEntry[]) => { const [target] = entries @@ -33,9 +46,42 @@ function App() { return () => observer.disconnect() }, [handleObserver]) + // Whenever the topic changes, reset and fetch useEffect(() => { - fetchArticles() - }, []) + fetchArticles(true) + }, [selectedTopic]) + + // Toggle the top-right menu + const toggleMenu = () => { + setShowMenu((prev) => !prev) + } + + // When user chooses a topic + const handleTopicSelect = (topicValue: string) => { + setSelectedTopic(topicValue) + setShowMenu(false) + } + + // Adding a custom category + // Wikipedia categories often need "Category:Foo" – you could handle that automatically + const addNewCategory = () => { + if (!newCategory.trim()) return + const catValue = newCategory.startsWith('Category:') + ? newCategory.trim() + : `Category:${newCategory.trim()}` + + // Check if it already exists + if (topics.find((t) => t.value === catValue)) { + alert('Topic already exists!') + return + } + + setTopics((prev) => [ + ...prev, + { label: newCategory.trim(), value: catValue }, + ]) + setNewCategory('') + } return (
@@ -48,7 +94,7 @@ function App() {
-
+
+ + {/* Three-dots menu icon */} + + + {/* The sliding/fading menu for topics */} +
+
+ setNewCategory(e.target.value)} + placeholder="New Category" + className="w-full mb-2 px-2 py-1 text-sm text-black rounded" + /> + +
+
+
+ {topics.map((topic) => ( + + ))} +
+
{showAbout && ( diff --git a/frontend/src/hooks/useWikiArticles.ts b/frontend/src/hooks/useWikiArticles.ts index 5618795..c55a843 100644 --- a/frontend/src/hooks/useWikiArticles.ts +++ b/frontend/src/hooks/useWikiArticles.ts @@ -21,37 +21,53 @@ const preloadImage = (src: string): Promise => { }); }; -export function useWikiArticles() { +export function useWikiArticles(selectedTopic: string) { const [articles, setArticles] = useState([]); const [loading, setLoading] = useState(false); const [buffer, setBuffer] = useState([]); - const {currentLanguage} = useLocalization() + const { currentLanguage } = useLocalization(); - const fetchArticles = async (forBuffer = false) => { + const fetchArticles = async (reset = false) => { if (loading) return; setLoading(true); + try { + if (reset) { + setArticles([]); + setBuffer([]); + } + + let urlParams = { + action: "query", + format: "json", + prop: "extracts|pageimages", + exintro: "1", + exchars: "1000", + exlimit: "max", + explaintext: "1", + piprop: "thumbnail", + pithumbsize: "400", + origin: "*", + } as Record; + + if (!selectedTopic) { + urlParams.generator = "random"; + urlParams.grnnamespace = "0"; + urlParams.grnlimit = "20"; + } else { + urlParams.generator = "categorymembers"; + urlParams.gcmtitle = selectedTopic; + urlParams.gcmnamespace = "0"; + urlParams.gcmlimit = "20"; + } + const response = await fetch( - currentLanguage.api + - new URLSearchParams({ - action: "query", - format: "json", - generator: "random", - grnnamespace: "0", - prop: "extracts|pageimages", - grnlimit: "20", - exintro: "1", - exchars: "1000", - exlimit: "max", - explaintext: "1", - piprop: "thumbnail", - pithumbsize: "400", - origin: "*", - }) + currentLanguage.api + new URLSearchParams(urlParams) ); - const data = await response.json(); - const newArticles = Object.values(data.query.pages) + + const pages = data?.query?.pages || {}; + const newArticles = Object.values(pages) .map((page: any) => ({ title: page.title, extract: page.extract, @@ -66,27 +82,36 @@ export function useWikiArticles() { .map((article) => preloadImage(article.thumbnail!.source)) ); - if (forBuffer) { - setBuffer(newArticles); + setBuffer(newArticles); + + if (reset) { + setArticles(newArticles); } else { setArticles((prev) => [...prev, ...newArticles]); - fetchArticles(true); } + } catch (error) { console.error("Error fetching articles:", error); } setLoading(false); }; - const getMoreArticles = useCallback(() => { - if (buffer.length > 0) { - setArticles((prev) => [...prev, ...buffer]); - setBuffer([]); - fetchArticles(true); - } else { - fetchArticles(false); - } - }, [buffer]); + const getMoreArticles = useCallback( + (reset?: boolean) => { + if (buffer.length > 0 && !reset) { + setArticles((prev) => [...prev, ...buffer]); + setBuffer([]); + fetchArticles(false); + } else { + fetchArticles(Boolean(reset)); + } + }, + [buffer, fetchArticles] + ); - return { articles, loading, fetchArticles: getMoreArticles }; + return { + articles, + loading, + fetchArticles: getMoreArticles, + }; } From c979ef2db104a88fe3ee12a045a76f71df6921c6 Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Tue, 4 Feb 2025 14:25:40 -0600 Subject: [PATCH 02/11] save --- frontend/src/App.tsx | 9 ++++++--- frontend/src/components/LanguageSelector.tsx | 8 ++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 52ecf5a..a8b5698 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useCallback, useState } from 'react' import { WikiCard } from './components/WikiCard' import { useWikiArticles } from './hooks/useWikiArticles' -import { Loader2, MoreHorizontal } from 'lucide-react' +import { Loader2, MoreHorizontal, Info } from 'lucide-react' import { Analytics } from "@vercel/analytics/react" import { LanguageSelector } from './components/LanguageSelector' @@ -95,12 +95,15 @@ function App() {
+ {/* Icon button for "About" */} + {/* Three-dots menu icon */} diff --git a/frontend/src/components/LanguageSelector.tsx b/frontend/src/components/LanguageSelector.tsx index 03c34db..40172ab 100644 --- a/frontend/src/components/LanguageSelector.tsx +++ b/frontend/src/components/LanguageSelector.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useRef } from "react"; import { LANGUAGES } from "../languages"; import { useLocalization } from "../hooks/useLocalization"; +import { Globe2 } from "lucide-react"; export function LanguageSelector() { const [showDropdown, setShowDropdown] = useState(false); @@ -29,8 +30,11 @@ export function LanguageSelector() { onClick={() => setShowDropdown(!showDropdown)} ref={dropdownRef} > - {showDropdown && ( From d846784d31adb708f4f541f03195c50957cb0e2d Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Tue, 4 Feb 2025 14:30:28 -0600 Subject: [PATCH 03/11] save --- frontend/src/App.tsx | 37 +++++++++++++------- frontend/src/components/LanguageSelector.tsx | 27 ++++++++------ 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a8b5698..fa8b290 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -94,8 +94,7 @@ function App() {
-
- {/* Icon button for "About" */} +
- {/* The sliding/fading menu for topics */}
setNewCategory(e.target.value)} placeholder="New Category" - className="w-full mb-2 px-2 py-1 text-sm text-black rounded" + className="w-full mb-2 px-2 py-1 text-sm + bg-white/20 text-white placeholder-white/50 + rounded border border-white/20 + focus:outline-none focus:border-white/40" />
-
+
{topics.map((topic) => (
{showAbout && ( -
-
+
+

About WikiTok

-

+

A TikTok-style interface for exploring random Wikipedia articles.

diff --git a/frontend/src/components/LanguageSelector.tsx b/frontend/src/components/LanguageSelector.tsx index 40172ab..b3b947e 100644 --- a/frontend/src/components/LanguageSelector.tsx +++ b/frontend/src/components/LanguageSelector.tsx @@ -25,32 +25,39 @@ export function LanguageSelector() { }, []); return ( -

setShowDropdown(!showDropdown)} - ref={dropdownRef} - > +
- {showDropdown && ( -
+
+
{LANGUAGES.map((language) => ( ))}
- )} +
); } From 100c0a97e73f307c76fd6236a6870e9fe6a5ea78 Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Tue, 4 Feb 2025 14:34:53 -0600 Subject: [PATCH 04/11] save --- frontend/src/App.tsx | 67 +++++++++++++++----- frontend/src/components/LanguageSelector.tsx | 36 ++++------- 2 files changed, 65 insertions(+), 38 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fa8b290..675fe23 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,6 +5,9 @@ import { Loader2, MoreHorizontal, Info } from 'lucide-react' import { Analytics } from "@vercel/analytics/react" import { LanguageSelector } from './components/LanguageSelector' +// Type for different dialogs/popovers +type DialogType = 'none' | 'about' | 'language' | 'topics'; + function App() { // Built-in topics to start with const initialTopics = [ @@ -14,14 +17,41 @@ function App() { { label: 'History', value: 'Category:History' }, ] - const [showAbout, setShowAbout] = useState(false) + // State declarations + const [activeDialog, setActiveDialog] = useState('none') const [topics, setTopics] = useState(initialTopics) - const [showMenu, setShowMenu] = useState(false) - const [newCategory, setNewCategory] = useState('') const [selectedTopic, setSelectedTopic] = useState('') + const [newCategory, setNewCategory] = useState('') const { articles, loading, fetchArticles } = useWikiArticles(selectedTopic) const observerTarget = useRef(null) + // Handle escape key + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setActiveDialog('none') + } + } + + document.addEventListener('keydown', handleEscape) + return () => document.removeEventListener('keydown', handleEscape) + }, []) + + // Handle click outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as HTMLElement + if (!target.closest('[data-dialog]') && !target.closest('button')) { + setActiveDialog('none') + } + } + + if (activeDialog !== 'none') { + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + } + }, [activeDialog]) + // Intersection Observer for infinite scrolling const handleObserver = useCallback( (entries: IntersectionObserverEntry[]) => { @@ -51,15 +81,10 @@ function App() { fetchArticles(true) }, [selectedTopic]) - // Toggle the top-right menu - const toggleMenu = () => { - setShowMenu((prev) => !prev) - } - // When user chooses a topic const handleTopicSelect = (topicValue: string) => { setSelectedTopic(topicValue) - setShowMenu(false) + setActiveDialog('none') } // Adding a custom category @@ -96,31 +121,37 @@ function App() {
- + {/* Pass activeDialog state to LanguageSelector */} + setActiveDialog(activeDialog === 'language' ? 'none' : 'language')} + /> + {/* Topics Menu */}
@@ -167,11 +198,15 @@ function App() {
- {showAbout && ( -
+ {/* About Dialog */} + {activeDialog === 'about' && ( +
@@ -49,7 +38,10 @@ export function LanguageSelector() { {LANGUAGES.map((language) => (
+ {/* About */} - {/* Pass activeDialog state to LanguageSelector */} - setActiveDialog(activeDialog === 'language' ? 'none' : 'language')} + onToggle={() => + setActiveDialog(activeDialog === 'language' ? 'none' : 'language') + } /> + {/* Topics */} +
- {/* Topics Menu */} + {/* + TOPICS DIALOG + Use the backdrop-click approach rather than a global "click outside" effect. + */} + {activeDialog === 'topics' && (
setActiveDialog('none')} > -
- setNewCategory(e.target.value)} - placeholder="New Category" - className="w-full mb-2 px-2 py-1 text-sm - bg-white/20 text-white placeholder-white/50 - rounded border border-white/20 - focus:outline-none focus:border-white/40" - /> + {/* Inner container to stop propagation, so clicks here don't close the dialog */} +
e.stopPropagation()} + > + {/* "X" button for both mobile & desktop */} -
-
-
- {topics.map((topic) => ( + +
+ setNewCategory(e.target.value)} + placeholder="New Category" + className="w-full mb-2 px-2 py-1 text-sm + bg-white/20 text-white placeholder-white/50 + rounded border border-white/20 + focus:outline-none focus:border-white/40" + /> - ))} +
+ +
+ +
+ {topics.map((topic) => ( + + ))} +
-
+ )} - {/* About Dialog */} + {/* + ABOUT DIALOG + (Unchanged from your code, but you can do the same "backdrop & stopPropagation" approach if you like.) + */} {activeDialog === 'about' && (
-
+
)} + {/* Render Articles */} {articles.map((article) => ( ))} diff --git a/frontend/src/components/LanguageSelector.tsx b/frontend/src/components/LanguageSelector.tsx index ae04357..d49d03b 100644 --- a/frontend/src/components/LanguageSelector.tsx +++ b/frontend/src/components/LanguageSelector.tsx @@ -11,6 +11,9 @@ interface LanguageSelectorProps { export function LanguageSelector({ isOpen, onToggle }: LanguageSelectorProps) { const { setLanguage } = useLocalization(); const dropdownRef = useRef(null); + const isMobile = window.innerWidth <= 768; + + if (!isOpen) return null; return (
@@ -24,17 +27,31 @@ export function LanguageSelector({ isOpen, onToggle }: LanguageSelectorProps) {
-
+
e.stopPropagation()} + className={` + ${isMobile ? 'backdrop-blur-md bg-white/10 p-6 rounded-lg w-full max-w-md relative border border-white/20' : 'py-2 w-30'} + `} + > + {isMobile && ( + + )} {LANGUAGES.map((language) => ( -
+ {isOpen && (
e.stopPropagation()} + data-dialog="language" + onClick={onToggle} className={` - ${isMobile ? 'backdrop-blur-md bg-white/10 p-6 rounded-lg w-full max-w-md relative border border-white/20' : 'py-2 w-30'} + ${ + isMobile + ? "fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4" + : "absolute top-12 right-4 backdrop-blur-md bg-white/10 rounded-md shadow-lg p-4 w-48 border border-white/20 z-50" + } `} + style={{ zIndex: 9999 }} > - {isMobile && ( - - )} - {LANGUAGES.map((language) => ( - - ))} +
e.stopPropagation()} + className={` + ${ + isMobile + ? "backdrop-blur-md bg-white/10 p-6 rounded-lg w-full max-w-md relative border border-white/20" + : "" + } + `} + > + {isMobile && ( + + )} + {LANGUAGES.map((language) => ( + + ))} +
-
+ )}
); } From 03a309eaa3889a1c87dafe210f65dcaca02ad616 Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Tue, 4 Feb 2025 15:10:36 -0600 Subject: [PATCH 07/11] save --- frontend/src/App.tsx | 98 ++++++++++++++++---- frontend/src/components/LanguageSelector.tsx | 75 --------------- frontend/src/hooks/useWikiArticles.ts | 93 +++++++------------ 3 files changed, 114 insertions(+), 152 deletions(-) delete mode 100644 frontend/src/components/LanguageSelector.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e19d29e..00d5fa0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,9 +1,10 @@ import { useEffect, useRef, useCallback, useState } from 'react' import { WikiCard } from './components/WikiCard' import { useWikiArticles } from './hooks/useWikiArticles' -import { Loader2, MoreHorizontal, Info } from 'lucide-react' +import { Loader2, MoreHorizontal, Info, Globe2 } from 'lucide-react' import { Analytics } from "@vercel/analytics/react" -import { LanguageSelector } from './components/LanguageSelector' +import { useLocalization } from './hooks/useLocalization' +import { LANGUAGES } from './languages' // Type for different dialogs/popovers type DialogType = 'none' | 'about' | 'language' | 'topics'; @@ -27,6 +28,9 @@ function App() { const { articles, loading, fetchArticles } = useWikiArticles(selectedTopic) const observerTarget = useRef(null) + // For language selection + const { setLanguage } = useLocalization() + // Handle escape key to close any open dialog useEffect(() => { const handleEscape = (event: KeyboardEvent) => { @@ -42,11 +46,13 @@ function App() { const handleObserver = useCallback( (entries: IntersectionObserverEntry[]) => { const [target] = entries + // If any dialog is open, skip fetching to avoid layout shifts causing infinite fetches + if (activeDialog !== 'none') return if (target.isIntersecting && !loading) { fetchArticles() } }, - [loading, fetchArticles] + [activeDialog, loading, fetchArticles] ) useEffect(() => { @@ -63,7 +69,7 @@ function App() { // Whenever the topic changes, reset and fetch useEffect(() => { fetchArticles(true) - }, [selectedTopic]) + }, [selectedTopic, fetchArticles]) // When user chooses a topic const handleTopicSelect = (topicValue: string) => { @@ -114,13 +120,16 @@ function App() { - {/* Language Selector */} - + {/* Language (moved from separate file) */} + {/* Topics */}
+ {/* + LANGUAGE DIALOG + */} + {activeDialog === 'language' && ( +
setActiveDialog('none')} + className={` + ${ + isMobile + ? "fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4" + : "absolute top-12 right-4 backdrop-blur-md bg-white/10 rounded-md shadow-lg p-4 w-48 border border-white/20 z-50" + } + `} + style={{ zIndex: 9999 }} + > +
e.stopPropagation()} + className={` + ${ + isMobile + ? "backdrop-blur-md bg-white/10 p-6 rounded-lg w-full max-w-md relative border border-white/20" + : "" + } + `} + > + {isMobile && ( + + )} + {LANGUAGES.map((language) => ( + + ))} +
+
+ )} + {/* TOPICS DIALOG - Use the backdrop-click approach rather than a global "click outside" effect. */} {activeDialog === 'topics' && (
setActiveDialog('none')} className={` - ${isMobile - ? 'fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4' - : 'absolute top-12 right-4 backdrop-blur-md bg-white/10 rounded-md shadow-lg p-4 w-80 border border-white/20 z-50' + ${ + isMobile + ? 'fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4' + : 'absolute top-12 right-4 backdrop-blur-md bg-white/10 rounded-md shadow-lg p-4 w-80 border border-white/20 z-50' } `} >
e.stopPropagation()} className={` - ${isMobile - ? 'backdrop-blur-md bg-white/10 p-6 rounded-lg max-w-md relative border border-white/20' - : '' + ${ + isMobile + ? 'backdrop-blur-md bg-white/10 p-6 rounded-lg max-w-md relative border border-white/20' + : '' } `} > diff --git a/frontend/src/components/LanguageSelector.tsx b/frontend/src/components/LanguageSelector.tsx deleted file mode 100644 index df1214d..0000000 --- a/frontend/src/components/LanguageSelector.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useRef } from "react"; -import { LANGUAGES } from "../languages"; -import { useLocalization } from "../hooks/useLocalization"; -import { Globe2 } from "lucide-react"; - -interface LanguageSelectorProps { - isOpen: boolean; - onToggle: () => void; -} - -export function LanguageSelector({ isOpen, onToggle }: LanguageSelectorProps) { - const { setLanguage } = useLocalization(); - const dropdownRef = useRef(null); - const isMobile = window.innerWidth <= 768; - - return ( -
- - - {isOpen && ( -
-
e.stopPropagation()} - className={` - ${ - isMobile - ? "backdrop-blur-md bg-white/10 p-6 rounded-lg w-full max-w-md relative border border-white/20" - : "" - } - `} - > - {isMobile && ( - - )} - {LANGUAGES.map((language) => ( - - ))} -
-
- )} -
- ); -} diff --git a/frontend/src/hooks/useWikiArticles.ts b/frontend/src/hooks/useWikiArticles.ts index c55a843..5618795 100644 --- a/frontend/src/hooks/useWikiArticles.ts +++ b/frontend/src/hooks/useWikiArticles.ts @@ -21,53 +21,37 @@ const preloadImage = (src: string): Promise => { }); }; -export function useWikiArticles(selectedTopic: string) { +export function useWikiArticles() { const [articles, setArticles] = useState([]); const [loading, setLoading] = useState(false); const [buffer, setBuffer] = useState([]); - const { currentLanguage } = useLocalization(); + const {currentLanguage} = useLocalization() - const fetchArticles = async (reset = false) => { + const fetchArticles = async (forBuffer = false) => { if (loading) return; setLoading(true); - try { - if (reset) { - setArticles([]); - setBuffer([]); - } - - let urlParams = { - action: "query", - format: "json", - prop: "extracts|pageimages", - exintro: "1", - exchars: "1000", - exlimit: "max", - explaintext: "1", - piprop: "thumbnail", - pithumbsize: "400", - origin: "*", - } as Record; - - if (!selectedTopic) { - urlParams.generator = "random"; - urlParams.grnnamespace = "0"; - urlParams.grnlimit = "20"; - } else { - urlParams.generator = "categorymembers"; - urlParams.gcmtitle = selectedTopic; - urlParams.gcmnamespace = "0"; - urlParams.gcmlimit = "20"; - } - const response = await fetch( - currentLanguage.api + new URLSearchParams(urlParams) + currentLanguage.api + + new URLSearchParams({ + action: "query", + format: "json", + generator: "random", + grnnamespace: "0", + prop: "extracts|pageimages", + grnlimit: "20", + exintro: "1", + exchars: "1000", + exlimit: "max", + explaintext: "1", + piprop: "thumbnail", + pithumbsize: "400", + origin: "*", + }) ); - const data = await response.json(); - const pages = data?.query?.pages || {}; - const newArticles = Object.values(pages) + const data = await response.json(); + const newArticles = Object.values(data.query.pages) .map((page: any) => ({ title: page.title, extract: page.extract, @@ -82,36 +66,27 @@ export function useWikiArticles(selectedTopic: string) { .map((article) => preloadImage(article.thumbnail!.source)) ); - setBuffer(newArticles); - - if (reset) { - setArticles(newArticles); + if (forBuffer) { + setBuffer(newArticles); } else { setArticles((prev) => [...prev, ...newArticles]); + fetchArticles(true); } - } catch (error) { console.error("Error fetching articles:", error); } setLoading(false); }; - const getMoreArticles = useCallback( - (reset?: boolean) => { - if (buffer.length > 0 && !reset) { - setArticles((prev) => [...prev, ...buffer]); - setBuffer([]); - fetchArticles(false); - } else { - fetchArticles(Boolean(reset)); - } - }, - [buffer, fetchArticles] - ); + const getMoreArticles = useCallback(() => { + if (buffer.length > 0) { + setArticles((prev) => [...prev, ...buffer]); + setBuffer([]); + fetchArticles(true); + } else { + fetchArticles(false); + } + }, [buffer]); - return { - articles, - loading, - fetchArticles: getMoreArticles, - }; + return { articles, loading, fetchArticles: getMoreArticles }; } From d1522d4b881bcb14dfb3770522eb694b236e5244 Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Tue, 4 Feb 2025 15:18:59 -0600 Subject: [PATCH 08/11] sab --- frontend/src/App.tsx | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 00d5fa0..6f0a062 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -68,8 +68,9 @@ function App() { // Whenever the topic changes, reset and fetch useEffect(() => { - fetchArticles(true) - }, [selectedTopic, fetchArticles]) + fetchArticles() + }, [fetchArticles]) + // When user chooses a topic const handleTopicSelect = (topicValue: string) => { @@ -101,6 +102,7 @@ function App() {
{/* Language (moved from separate file) */} )} {LANGUAGES.map((language) => ( @@ -130,7 +128,6 @@ function App() { onClick={() => setActiveDialog(activeDialog === 'language' ? 'none' : 'language') } - className="p-1 bg-white/10 rounded-full hover:bg-white/20 transition-colors" aria-label="Change language" > @@ -143,7 +140,6 @@ function App() { onClick={() => setActiveDialog(activeDialog === 'topics' ? 'none' : 'topics') } - className="p-1 bg-white/10 rounded-full hover:bg-white/20 transition-colors" aria-label="Show topics menu" > @@ -157,21 +153,12 @@ function App() { {activeDialog === 'language' && (
setActiveDialog('none')} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - setActiveDialog('none') - } - }} - className={` - ${ - isMobile - ? "fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4" - : "absolute top-12 right-4 backdrop-blur-md bg-white/10 rounded-md shadow-lg p-4 w-48 border border-white/20 z-50" - } - `} + className={`${ + isMobile + ? "fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4" + : "absolute top-12 right-4 backdrop-blur-md bg-white/10 rounded-md shadow-lg p-4 w-48 border border-white/20 z-50" + }`} style={{ zIndex: 9999 }} >
- {isMobile && ( - - )} {LANGUAGES.map((language) => ( -
-

About WikiTok

A TikTok-style interface for exploring random Wikipedia articles. @@ -338,6 +298,7 @@ function App() { > @Aizkmusic + , modified by Jacob

Check out the code on{' '} From c8cc1b2b842591be9b6247bb6fbcdb9d96c8ce8d Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Tue, 4 Feb 2025 15:31:39 -0600 Subject: [PATCH 10/11] save --- frontend/src/App.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index eb4a0dd..cb33d49 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -66,10 +66,14 @@ function App() { return () => observer.disconnect() }, [handleObserver]) - // Whenever the topic changes, reset and fetch + // Whenever the topic changes, flush the articles and fetch useEffect(() => { - fetchArticles() - }, [fetchArticles]) + if (!selectedTopic) return + // This logs to the console whenever we reset with a new topic + console.log('Flushing the article cache for new topic:', selectedTopic) + + fetchArticles(true) + }, [selectedTopic, fetchArticles]) // When user chooses a topic const handleTopicSelect = (topicValue: string) => { @@ -213,7 +217,7 @@ function App() { `} onClick={(e) => e.stopPropagation()} > -

+
setNewCategory(e.target.value)} From f4ae767269710efa7ac9a6a1b8bcf6aed99b066f Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Tue, 4 Feb 2025 16:52:01 -0600 Subject: [PATCH 11/11] fixed i think --- frontend/src/App.tsx | 155 ++++++++++++-------- frontend/src/hooks/useWikiArticles.ts | 198 ++++++++++++++++++-------- 2 files changed, 233 insertions(+), 120 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cb33d49..e8b9c86 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,13 +6,12 @@ import { Analytics } from "@vercel/analytics/react" import { useLocalization } from './hooks/useLocalization' import { LANGUAGES } from './languages' -// Type for different dialogs/popovers type DialogType = 'none' | 'about' | 'language' | 'topics'; function App() { - const isMobile = window.innerWidth <= 768; // Adjust as you see fit + const isMobile = window.innerWidth <= 768; // Adjust if needed - // Built-in topics to start with + // Built-in topics const initialTopics = [ { label: 'Random', value: '' }, { label: 'Cats', value: 'Category:Cats' }, @@ -20,18 +19,26 @@ function App() { { label: 'History', value: 'Category:History' }, ] - // State declarations const [activeDialog, setActiveDialog] = useState('none') const [topics, setTopics] = useState(initialTopics) const [selectedTopic, setSelectedTopic] = useState('') const [newCategory, setNewCategory] = useState('') - const { articles, loading, fetchArticles } = useWikiArticles(selectedTopic) + const [currentIndex, setCurrentIndex] = useState(0) + + const { + articles, + loading, + getMoreArticles, + fetchArticles, + resetArticles + } = useWikiArticles(); + const observerTarget = useRef(null) // For language selection const { setLanguage } = useLocalization() - // Handle escape key to close any open dialog + // Close any open dialog on ESC useEffect(() => { const handleEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') { @@ -42,19 +49,44 @@ function App() { return () => document.removeEventListener('keydown', handleEscape) }, []) - // Intersection Observer for infinite scrolling + /** + * Whenever `selectedTopic` changes, reset articles + * and do an initial fetch for the new topic. + */ + useEffect(() => { + resetArticles(); + setCurrentIndex(0); + + // Fetch the first chunk of articles + fetchArticles(selectedTopic) + .then(() => { + // Optionally fetch next chunk into buffer + return fetchArticles(selectedTopic, true); + }) + .catch(console.error); + + // We intentionally omit fetchArticles from the dependencies + // to avoid re-creating the function and causing infinite loops + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedTopic]) + + /** + * Infinite scroll intersection observer + */ const handleObserver = useCallback( - (entries: IntersectionObserverEntry[]) => { + async (entries: IntersectionObserverEntry[]) => { const [target] = entries - // If any dialog is open, skip fetching to avoid layout shifts causing infinite fetches - if (activeDialog !== 'none') return + // If a dialog is open or already loading, skip + if (activeDialog !== 'none') return; if (target.isIntersecting && !loading) { - fetchArticles() + // get more articles from the buffer or from the server + await getMoreArticles(selectedTopic); } }, - [activeDialog, loading, fetchArticles] + [activeDialog, loading, selectedTopic, getMoreArticles] ) + // Attach observer useEffect(() => { const observer = new IntersectionObserver(handleObserver, { threshold: 0.1, @@ -66,29 +98,20 @@ function App() { return () => observer.disconnect() }, [handleObserver]) - // Whenever the topic changes, flush the articles and fetch - useEffect(() => { - if (!selectedTopic) return - // This logs to the console whenever we reset with a new topic - console.log('Flushing the article cache for new topic:', selectedTopic) - - fetchArticles(true) - }, [selectedTopic, fetchArticles]) - - // When user chooses a topic + // Handler for selecting a topic from the menu const handleTopicSelect = (topicValue: string) => { setSelectedTopic(topicValue) setActiveDialog('none') } - // Adding a custom category + // Handler for adding a custom category const addNewCategory = () => { if (!newCategory.trim()) return const catValue = newCategory.startsWith('Category:') ? newCategory.trim() : `Category:${newCategory.trim()}` - // Check if it already exists + // Avoid duplicates if (topics.find((t) => t.value === catValue)) { alert('Topic already exists!') return @@ -103,6 +126,7 @@ function App() { return (
+ {/* App Title */}
+ {/* Top-right buttons */}
{/* About */} - {/* Language (moved from separate file) */} + {/* Language */}
- {/* - LANGUAGE DIALOG - */} + {/* LANGUAGE DIALOG */} {activeDialog === 'language' && (
e.stopPropagation()} - className={` - ${ - isMobile - ? "backdrop-blur-md bg-white/10 p-6 rounded-lg w-full max-w-md relative border border-white/20" - : "" - } - `} + className={`${ + isMobile + ? "backdrop-blur-md bg-white/10 p-6 rounded-lg w-full max-w-md relative border border-white/20" + : "" + }`} > {LANGUAGES.map((language) => (