From d79f617b4f505d0f55df1af1d16b5d4c84d291f9 Mon Sep 17 00:00:00 2001 From: Chris-1010 <122332721@umail.ucc.ie> Date: Sun, 16 Feb 2025 23:01:09 +0000 Subject: [PATCH] REFACTOR: Enhance Categories Page; REFACTOR: Update ListRow component; REFACTOR: Improve component structure and navigation; FEAT: Scroll fetching hook; REFACTOR: Add more testing_data to database to demonstrate updated ListRow component; --- frontend/src/App.tsx | 2 +- .../src/components/Checkout/CheckoutForm.tsx | 1 + frontend/src/components/Input/Button.tsx | 16 ++- frontend/src/components/Layout/ListItem.tsx | 54 +++++++++ frontend/src/components/Layout/ListRow.tsx | 113 +++++++----------- .../src/components/Settings/ThemeSetting.tsx | 4 +- frontend/src/hooks/fetchContentOnScroll.ts | 21 ++++ frontend/src/pages/AllCategoriesPage.tsx | 71 +++++++++++ frontend/src/pages/CategoriesPage.tsx | 53 -------- frontend/src/pages/CategoryPage.tsx | 3 +- frontend/src/pages/HomePage.tsx | 23 +++- web_server/database/app.db | Bin 159744 -> 159744 bytes web_server/database/testing_data.sql | 41 +++---- 13 files changed, 240 insertions(+), 162 deletions(-) create mode 100644 frontend/src/components/Layout/ListItem.tsx create mode 100644 frontend/src/hooks/fetchContentOnScroll.ts create mode 100644 frontend/src/pages/AllCategoriesPage.tsx delete mode 100644 frontend/src/pages/CategoriesPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8ff76ba..a4fc03e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -52,7 +52,7 @@ function App() { path="/category/:category_name" element={} > - }> + }> }> } /> } /> diff --git a/frontend/src/components/Checkout/CheckoutForm.tsx b/frontend/src/components/Checkout/CheckoutForm.tsx index 1b79a52..ebd10c1 100644 --- a/frontend/src/components/Checkout/CheckoutForm.tsx +++ b/frontend/src/components/Checkout/CheckoutForm.tsx @@ -53,6 +53,7 @@ export const Return: React.FC = () => { // Main CheckoutForm component interface CheckoutFormProps { + streamerID: number; onClose: () => void; } diff --git a/frontend/src/components/Input/Button.tsx b/frontend/src/components/Input/Button.tsx index 0190847..6c20fbb 100644 --- a/frontend/src/components/Input/Button.tsx +++ b/frontend/src/components/Input/Button.tsx @@ -32,21 +32,19 @@ export const ToggleButton: React.FC = ({ children = "Toggle", extraClasses = "", onClick, - toggled = false + toggled = false, }) => { toggled ? (extraClasses += " cursor-default bg-purple-600") : (extraClasses += " cursor-pointer hover:text-purple-600 hover:bg-black/80 hover:border-purple-500 hover:border-b-4 hover:border-l-4"); return ( - - - {children} - - + + {children} + ); }; diff --git a/frontend/src/components/Layout/ListItem.tsx b/frontend/src/components/Layout/ListItem.tsx new file mode 100644 index 0000000..0c15060 --- /dev/null +++ b/frontend/src/components/Layout/ListItem.tsx @@ -0,0 +1,54 @@ +import React from "react"; + +export interface ListItemProps { + type: "stream" | "category"; + id: number; + title: string; + streamer?: string; + streamCategory?: string; + viewers: number; + thumbnail?: string; + onItemClick?: () => void; +} + +const ListItem: React.FC = ({ + type, + title, + streamer, + streamCategory, + viewers, + thumbnail, + onItemClick, +}) => { + return ( + + + + {thumbnail ? ( + + ) : ( + + )} + + + {title} + {type === "stream" && {streamer}} + {type === "stream" && ( + {streamCategory} + )} + {viewers} viewers + + + + ); +}; + +export default ListItem; diff --git a/frontend/src/components/Layout/ListRow.tsx b/frontend/src/components/Layout/ListRow.tsx index ec3c94d..9346fe6 100644 --- a/frontend/src/components/Layout/ListRow.tsx +++ b/frontend/src/components/Layout/ListRow.tsx @@ -1,46 +1,44 @@ import React, { useRef } from "react"; -import { ArrowLeft as ArrowLeftIcon, ArrowRight as ArrowRightIcon } from "lucide-react"; -import "../../assets/styles/listRow.css" - -interface ListItemProps { - type: "stream" | "category"; - id: number; - title: string; - streamer?: string; - streamCategory?: string; - viewers: number; - thumbnail?: string; - onItemClick?: () => void; -} +import { + ArrowLeft as ArrowLeftIcon, + ArrowRight as ArrowRightIcon, +} from "lucide-react"; +import "../../assets/styles/listRow.css"; +import ListItem, { ListItemProps } from "./ListItem"; interface ListRowProps { - type: "stream" | "category"; - title: string; - description: string; + type: "stream" | "category" | "user"; + title?: string; + description?: string; items: ListItemProps[]; - extraClasses?: string; + wrap: boolean; onClick: (itemName: string) => void; + extraClasses?: string; + children?: React.ReactNode; } // Row of entries const ListRow: React.FC = ({ - title, - description, + title = "", + description = "", items, + wrap, onClick, extraClasses = "", + children, }) => { const slider = useRef(null); + const scrollAmount = window.innerWidth * 0.3; const slideRight = () => { - if (slider.current) { - slider.current.scrollBy({ left: +200, behavior: "smooth" }); + if (!wrap && slider.current) { + slider.current.scrollBy({ left: +scrollAmount, behavior: "smooth" }); } }; const slideLeft = () => { - if (slider.current) { - slider.current.scrollBy({ left: -200, behavior: "smooth" }); + if (!wrap && slider.current) { + slider.current.scrollBy({ left: -scrollAmount, behavior: "smooth" }); } }; @@ -54,13 +52,26 @@ const ListRow: React.FC = ({ - - + {!wrap && items.length > 3 && ( + <> + + + > + )} {items.map((item) => ( @@ -83,53 +94,11 @@ const ListRow: React.FC = ({ /> ))} - - - + + {children} ); }; -// Individual list entry component -export const ListItem: React.FC = ({ - type, - title, - streamer, - streamCategory, - viewers, - thumbnail, - onItemClick, -}) => { - return ( - - - - {thumbnail ? ( - - ) : ( - - )} - - - {title} - {type === "stream" && {streamer}} - {type === "stream" && ( - {streamCategory} - )} - {viewers} viewers - - - - ); -}; - -export default ListRow; +export default ListRow; \ No newline at end of file diff --git a/frontend/src/components/Settings/ThemeSetting.tsx b/frontend/src/components/Settings/ThemeSetting.tsx index 06cdc69..14f974b 100644 --- a/frontend/src/components/Settings/ThemeSetting.tsx +++ b/frontend/src/components/Settings/ThemeSetting.tsx @@ -12,7 +12,7 @@ const themeIcons = { const themes = ["light", "dark", "blue", "green", "orange"]; -const Theme: React.FC = () => { +const ThemeSetting: React.FC = () => { const { theme, setTheme } = useTheme(); const handleNextTheme = () => { @@ -33,7 +33,7 @@ const Theme: React.FC = () => { ); }; -export default Theme; +export default ThemeSetting; {/* ${isMode ? diff --git a/frontend/src/hooks/fetchContentOnScroll.ts b/frontend/src/hooks/fetchContentOnScroll.ts new file mode 100644 index 0000000..520dac3 --- /dev/null +++ b/frontend/src/hooks/fetchContentOnScroll.ts @@ -0,0 +1,21 @@ +import { useEffect } from "react"; + +export function fetchContentOnScroll(callback: () => void, hasMoreData: boolean) { + useEffect(() => { + const handleScroll = () => { + if (!hasMoreData) return; // Don't trigger scroll if no more data + const scrollPosition = window.innerHeight + document.documentElement.scrollTop; + const scrollHeight = document.documentElement.scrollHeight; + + if (scrollPosition >= scrollHeight * 0.9) { + callback(); // Trigger data fetching when 90% scroll is reached + } + }; + + window.addEventListener("scroll", handleScroll); + + return () => { + window.removeEventListener("scroll", handleScroll); // Cleanup on unmount + }; + }, [callback, hasMoreData]); +} \ No newline at end of file diff --git a/frontend/src/pages/AllCategoriesPage.tsx b/frontend/src/pages/AllCategoriesPage.tsx new file mode 100644 index 0000000..ed032be --- /dev/null +++ b/frontend/src/pages/AllCategoriesPage.tsx @@ -0,0 +1,71 @@ +import React, { useEffect } from "react"; +import Navbar from "../components/Navigation/Navbar"; +import { useNavigate } from "react-router-dom"; +import ListRow from "../components/Layout/ListRow"; +import { useCategories } from "../context/ContentContext"; + +const AllCategoriesPage: React.FC = () => { + const { categories, setCategories } = useCategories(); + const navigate = useNavigate(); + + useEffect(() => { + const fetchCategories = async () => { + try { + const response = await fetch("/api/categories/popular/8/0"); + if (!response.ok) { + throw new Error("Failed to fetch categories"); + } + const data = await response.json(); + + // Transform the data to match CategoryItem interface + const processedCategories = data.map((category: any) => ({ + type: "category" as const, + id: category.category_id, + title: category.category_name, + viewers: category.num_viewers, + thumbnail: `/images/thumbnails/categories/${category.category_name + .toLowerCase() + .replace(/ /g, "_")}.webp`, + })); + + setCategories(processedCategories); + } catch (error) { + console.error("Error fetching categories:", error); + } + }; + + fetchCategories(); + }, [setCategories]); + + if (!categories.length) { + return ( + + Loading... + + ); + } + + const handleCategoryClick = (categoryName: string) => { + console.log(categoryName); + navigate(`/category/${categoryName}`); + }; + + return ( + + + + + ); +}; + +export default AllCategoriesPage; diff --git a/frontend/src/pages/CategoriesPage.tsx b/frontend/src/pages/CategoriesPage.tsx deleted file mode 100644 index 8ca6ba0..0000000 --- a/frontend/src/pages/CategoriesPage.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { useState, useEffect } from "react"; -import Navbar from "../components/Layout/Navbar"; -import { useNavigate } from "react-router-dom"; - -const CategoriesPage: React.FC = () => { - const [noCategories, setNoCategories] = useState(0); - const [moreCategories, setMoreCategories] = useState(12); - const [isLoading, setIsLoading] = useState(true); - const navigate = useNavigate(); - - useEffect(() => { - const fetchCategories = async () => { - try { - const response = await fetch(`api/categories/popular/${moreCategories}`) - if (!response.ok) { - throw new Error("Failed to fetch categories"); - } - const data = await response.json(); - console.log(data); - } catch (error) { - console.error("Error fetching categories:", error); - } finally { - setIsLoading(false); - } - }; - - fetchCategories(); - }, [noCategories]); - - if (isLoading) { - return ( - - Loading... - - ); - } - - const handleCategoryClick = (category_name: string) => { - navigate(`/category${category_name}`); - }; - - return ( - - - Categories Page - - ); -}; - -export default CategoriesPage; diff --git a/frontend/src/pages/CategoryPage.tsx b/frontend/src/pages/CategoryPage.tsx index 393e587..69ef2b6 100644 --- a/frontend/src/pages/CategoryPage.tsx +++ b/frontend/src/pages/CategoryPage.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; import { useParams } from "react-router-dom"; -import Navbar from "../components/Layout/Navbar"; +import Navbar from "../components/Navigation/Navbar"; import ListRow from "../components/Layout/ListRow"; import { useNavigate } from "react-router-dom"; @@ -78,6 +78,7 @@ const CategoryPage: React.FC = () => { title={`${category_name} Streams`} description={`Live streams in the ${category_name} category`} items={streams} + wrap={true} onClick={handleStreamClick} extraClasses="bg-purple-950/60" /> diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 593329b..6dcb489 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,8 +1,9 @@ import React from "react"; -import Navbar from "../components/Layout/Navbar"; +import Navbar from "../components/Navigation/Navbar"; import ListRow from "../components/Layout/ListRow"; import { useNavigate } from "react-router-dom"; import { useStreams, useCategories } from "../context/ContentContext"; +import Button from "../components/Input/Button"; interface HomePageProps { variant?: "default" | "personalised"; @@ -33,7 +34,8 @@ const HomePage: React.FC = ({ variant = "default" }) => { = ({ variant = "default" }) => { : "Streamers that are currently live" } items={streams} + wrap={false} onClick={handleStreamClick} extraClasses="bg-red-950/60" - /> + > + navigate("/")}> + Show More . . . + + {/* If Personalised_HomePage, display Categories the logged-in user follows. Else, trending categories. */} = ({ variant = "default" }) => { : "Categories that have been 'popping off' lately" } items={categories} + wrap={false} onClick={handleCategoryClick} extraClasses="bg-green-950/60" - /> + > + navigate("/categories")} + > + Show More . . . + + ); }; diff --git a/web_server/database/app.db b/web_server/database/app.db index 05a9107263b5db5e90b8a80f8f7ce3bb0945e7e6..2dca23ab5a8e1cfd29f4f07e8aa5c41ea503cc99 100644 GIT binary patch delta 667 zcmYk4+iMd+6o==`?q;*eW@l533HFkWEmVs!mrXAeZK_Z!i1wim)jqYA(u+;9_Cjn? zXj8|&6l___iinX`d=ZOg!4(t)Q4mq7q6MK!@k##!@kJ*Uq|AJ0=KMHk<}h<+x24%_ zX{pk`h7hV}1Q{Gcd|~;J&cwr`8v;{+{AL#Id1Ne*aCwJN1wHR$izu%ma*H%7pOi^O z@U6+KwqIM6ZL2B+AlX9ABKQVCM&a6K9B@mgs?FQsE_Y{tW>D+OMoJp5DgtRhWQ#l` zl$;|CEImYGQNe{`I)-i|H$FL`og5h&9%D>SB;(1(XgnEBG-^q`H6CXZeH6?q>G`_- z;6l|pNIQq;vXTFxO)6Sd*M4Fzn4d4Y=M3?#>n5Hm535CmIXj|018y!) zO@;gATo!OI=G`H*VMf%qe8EKroVGS_1MU~C-T)Xpd+0TmQv_>M#Fs4j2y8hkUi=)K z`w7^_&UzTS)q~!3q@XhCOv3?Iavku69%|SdLJx2q^rH1H03GH2kakKb%+Rde lFV4W@b`Ui`W@5S7A`F#5oKPS9C^~)veiAdT2Jfl3?=KfIrmO$} delta 3553 zcmcguTWs4@81`|T*m0dYy^${2sLM9GUe>7zQ@5hE3#(VUOHw7-y0JkQlC-kaHX%v7 zO{4PC%tK;4tcsA~g-x2cY!jPUBr*gNFYIAFARZ9XB!tS_B+$eI5&{XqIVZN`G%cuz z(aJgJ{1^K_pTGa}IlpW_zihwkY2XNg=zy0CUL-+KPcHTNps}}S%!(}hFYuRW|0wku z-`1?O!g-@&cff(>RDxgPyXx=POOD?iZ*gj|-TBA?n4-M%YOo!QbNcu@6X}J!WsGuhBEMM)SY*OY2e4Nu5A8 zdyIR?rPWG8G zo(9z7q+%((?FAA5WXbXj;7N)gz%e!31d_H33$&}RPmQ*KX7!IIa00rBlzPn#rc1Zl zK-jsH?I3t?li=_2!}SA>A?7L*qVL)UsY{f{@~*{AULiMwn~q8DA#sd5!*O+k><|%L z@^57A@JWqNfKW+hpA$z$1FQi5H1}vU5DDrL_)*)8 z=Sp$-QuQKcqce$4oO#rL=!h5=wgtrTUc-%Is|+~)Syl`Tm%iEvmPsnVLuoAj>9`Gn zpH~`6_xr(F0Gvx#RZ`*A?{?e zEVZ5j53hKUeDpYHcN|?DxWZWgSm1-SiNX_xSN;k$V-&x&J z#=fF!yj`uWjJQlqW7T4&YcA@nN}_6agRX2meQ{(qPB(mpr11T}P*;whhTI;vS{^eYrOZC-oE(S+0- zTSLi8@+!QPk(^egV{&3{8fN^(MVe}|ly#L;+_?CBuv%A*z`rXViK`617Up2HzG8XR zoN()=>NVC@C8D~`%r%_GdBJ2cgi@|--o#@g64^{@Y1mi0Cq^W9QrWPcBdh!$pIO&? UWbG5o!fM*znrGJcoaCv$0An^UPXGV_ diff --git a/web_server/database/testing_data.sql b/web_server/database/testing_data.sql index 69887a7..ed83017 100644 --- a/web_server/database/testing_data.sql +++ b/web_server/database/testing_data.sql @@ -1,10 +1,10 @@ -- Sample Data for users INSERT INTO users (username, password, email, num_followers, stream_key, is_partnered, bio, is_live, current_stream_title, current_selected_category_id) VALUES -('GamerDude', 'password123', 'gamerdude@example.com', 500, '1234', 0, 'Streaming my gaming adventures!', 0, 'Epic Gaming Session', 1), +('GamerDude', 'password123', 'gamerdude@example.com', 500, '1234', 0, 'Streaming my gaming adventures!', 1, 'Game On!', 1), ('MusicLover', 'music4life', 'musiclover@example.com', 1200, '2345', 0, 'I share my favorite tunes.', 1, 'Live Music Jam', 2), ('ArtFan', 'artistic123', 'artfan@example.com', 300, '3456', 0, 'Exploring the world of art.', 1, 'Sketching Live', 3), ('EduGuru', 'learn123', 'eduguru@example.com', 800, '4567', 0, 'Teaching everything I know.', 1, 'Math Made Easy', 4), -('SportsStar', 'sports123', 'sportsstar@example.com', 2000, '5678', 0, 'Join me for live sports updates!', 0, 'Sports Highlights', 5); +('SportsStar', 'sports123', 'sportsstar@example.com', 2000, '5678', 0, 'Join me for live sports updates!', 1, 'Sports Highlights', 5); INSERT INTO users (username, password, email, num_followers, stream_key, is_partnered, bio) VALUES ('GamerDude2', 'password123', 'gamerdude3@gmail.com', 3200, '7890', 0, 'Streaming my gaming adventures!'), @@ -53,10 +53,12 @@ INSERT INTO categories (category_name) VALUES ('Sports'); -- Sample Data for streams -INSERT INTO streams (user_id, title, start_time, num_viewers, category_id) VALUES +INSERT INTO streams (user_id, title, start_time, num_viewers, category_id) VALUES +(1, 'Game on!', '2025-02-16 17:00:00', 5, 1), (2, 'Live Music Jam', '2025-01-25 20:00:00', 350, 2), (3, 'Sketching Live', '2025-01-24 15:00:00', 80, 3), -(4, 'Math Made Easy', '2025-01-23 10:00:00', 400, 4); +(4, 'Math Made Easy', '2025-01-23 10:00:00', 400, 4), +(5, 'Sports Highlights', '2025-02-15 23:00:00', 210, 5); -- Sample Data for vods INSERT INTO vods (user_id, title, datetime, category_id, length, views) VALUES @@ -93,21 +95,6 @@ INSERT INTO chat (stream_id, chatter_id, message) VALUES (1, 2, 'Woah, cannot believe that'); - -SELECT users.user_id, streams.title, streams.num_viewers, users.username -FROM streams JOIN users -ON streams.user_id = users.user_id -WHERE users.user_id IN -(SELECT followed_id FROM follows WHERE user_id = 1) -AND users.is_live = 1; - -SELECT categories.category_id, categories.category_name, SUM(streams.num_viewers) AS total_viewers -FROM streams -JOIN categories ON streams.category_id = categories.category_id -GROUP BY categories.category_name -ORDER BY SUM(streams.num_viewers) DESC -LIMIT 10; - INSERT INTO follows (user_id, followed_id, since) VALUES (7, 1, '2024-08-30'), (7, 2, '2024-08-30'), @@ -135,4 +122,18 @@ SELECT * FROM stream_tags; -- To see all tables in the database SELECT name FROM sqlite_master WHERE type='table'; -UPDATE users SET is_live = 0 WHERE user_id = 1; +-- UPDATE users SET is_live = 0 WHERE user_id = 1; + +SELECT users.user_id, streams.title, streams.num_viewers, users.username +FROM streams JOIN users +ON streams.user_id = users.user_id +WHERE users.user_id IN +(SELECT followed_id FROM follows WHERE user_id = 1) +AND users.is_live = 1; + +SELECT categories.category_id, categories.category_name, SUM(streams.num_viewers) AS total_viewers +FROM streams +JOIN categories ON streams.category_id = categories.category_id +GROUP BY categories.category_name +ORDER BY SUM(streams.num_viewers) DESC +LIMIT 10;
{streamer}
{streamCategory}
{viewers} viewers