REFACTOR: HomePage and ResultsPage for improved data handling

This commit is contained in:
Chris-1010
2025-02-16 14:19:51 +00:00
parent 557aeb9091
commit bd091b079a
9 changed files with 239 additions and 185 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { AuthContext } from "./context/AuthContext"; import { AuthContext } from "./context/AuthContext";
import { StreamsProvider } from "./context/StreamsContext"; import { ContentProvider } from "./context/ContentContext";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import HomePage from "./pages/HomePage"; import HomePage from "./pages/HomePage";
import StreamerRoute from "./components/Stream/StreamerRoute"; import StreamerRoute from "./components/Stream/StreamerRoute";
@@ -8,8 +8,8 @@ import NotFoundPage from "./pages/NotFoundPage";
import UserPage from "./pages/UserPage"; import UserPage from "./pages/UserPage";
import ResetPasswordPage from "./pages/ResetPasswordPage"; import ResetPasswordPage from "./pages/ResetPasswordPage";
import CategoryPage from "./pages/CategoryPage"; import CategoryPage from "./pages/CategoryPage";
import CategoriesPage from "./pages/CategoriesPage"; import CategoriesPage from "./pages/AllCategoriesPage";
import FoundPage from "./pages/FoundPage"; import ResultsPage from "./pages/ResultsPage";
function App() { function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false); const [isLoggedIn, setIsLoggedIn] = useState(false);
@@ -32,7 +32,7 @@ function App() {
<AuthContext.Provider <AuthContext.Provider
value={{ isLoggedIn, username, setIsLoggedIn, setUsername }} value={{ isLoggedIn, username, setIsLoggedIn, setUsername }}
> >
<StreamsProvider> <ContentProvider>
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route <Route
@@ -44,15 +44,21 @@ function App() {
<Route path="/:streamerName" element={<StreamerRoute />} /> <Route path="/:streamerName" element={<StreamerRoute />} />
<Route path="/user/:username" element={<UserPage />} /> <Route path="/user/:username" element={<UserPage />} />
<Route path="/reset_password/:token" element={<ResetPasswordPage />}></Route> <Route
<Route path="/category/:category_name" element={<CategoryPage />}></Route> path="/reset_password/:token"
element={<ResetPasswordPage />}
></Route>
<Route
path="/category/:category_name"
element={<CategoryPage />}
></Route>
<Route path="/category" element={<CategoriesPage />}></Route> <Route path="/category" element={<CategoriesPage />}></Route>
<Route path="/results" element={<FoundPage />}></Route> <Route path="/results" element={<ResultsPage />}></Route>
<Route path="/404" element={<NotFoundPage />} /> <Route path="/404" element={<NotFoundPage />} />
<Route path="*" element={<Navigate to="/404" replace />} /> <Route path="*" element={<Navigate to="/404" replace />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</StreamsProvider> </ContentProvider>
</AuthContext.Provider> </AuthContext.Provider>
); );
} }

View File

@@ -1,5 +1,3 @@
import { useEffect } from "react";
export default function GoogleLogin() { export default function GoogleLogin() {
const handleLoginClick = (e: React.MouseEvent<HTMLButtonElement>) => { const handleLoginClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
@@ -15,7 +13,8 @@ export default function GoogleLogin() {
//w-full basis-[90%] (% size of original container) //w-full basis-[90%] (% size of original container)
className="flex w-full max-w-[19em] basis-[90%] flex-grow flex-shrink items-center justify-start bg-white text-gray-600 className="flex w-full max-w-[19em] basis-[90%] flex-grow flex-shrink items-center justify-start bg-white text-gray-600
font-semibold py-[0.15em] pl-[0.3em] pr-[0.6em] rounded shadow-md flex-grow flex-shrink font-semibold py-[0.15em] pl-[0.3em] pr-[0.6em] rounded shadow-md flex-grow flex-shrink
hover:bg-gray-100 active:bg-gray-200 sm:max-w-[18em] mx-[1em]"> hover:bg-gray-100 active:bg-gray-200 sm:max-w-[18em] mx-[1em]"
>
<img <img
src="../../../images/icons/google-icon.png" src="../../../images/icons/google-icon.png"
alt="Google logo" alt="Google logo"

View File

@@ -0,0 +1,135 @@
import { createContext, useContext, useState, useEffect } from "react";
import { useAuth } from "./AuthContext";
// Base interfaces
interface Item {
id: number;
title: string;
viewers: number;
thumbnail?: string;
}
interface StreamItem extends Item {
type: "stream";
streamer: string;
streamCategory: string;
}
interface CategoryItem extends Item {
type: "category";
}
interface UserItem extends Item {
type: "user";
username: string;
isLive: boolean;
}
// Context type
interface ContentContextType {
streams: StreamItem[];
categories: CategoryItem[];
users: UserItem[];
setStreams: (streams: StreamItem[]) => void;
setCategories: (categories: CategoryItem[]) => void;
setUsers: (users: UserItem[]) => void;
}
const ContentContext = createContext<ContentContextType | undefined>(undefined);
export function ContentProvider({ children }: { children: React.ReactNode }) {
const [streams, setStreams] = useState<StreamItem[]>([]);
const [categories, setCategories] = useState<CategoryItem[]>([]);
const [users, setUsers] = useState<UserItem[]>([]);
const { isLoggedIn } = useAuth();
useEffect(() => {
// Fetch streams
const streamsUrl = isLoggedIn
? "/api/streams/recommended"
: "/api/streams/popular/4";
fetch(streamsUrl)
.then((response) => response.json())
.then((data: any[]) => {
const processedStreams: StreamItem[] = data.map(stream => ({
type: "stream",
id: stream.user_id,
title: stream.title,
streamer: stream.username,
streamCategory: stream.category_name,
viewers: stream.num_viewers,
thumbnail: stream.thumbnail ||
`/images/thumbnails/categories/${stream.category_name.toLowerCase().replace(/ /g, "_")}.webp`
}));
setStreams(processedStreams);
});
// Fetch categories
const categoriesUrl = isLoggedIn
? "/api/categories/recommended"
: "/api/categories/popular/4";
fetch(categoriesUrl)
.then((response) => response.json())
.then((data: any[]) => {
const processedCategories: CategoryItem[] = data.map(category => ({
type: "category",
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);
});
}, [isLoggedIn]);
return (
<ContentContext.Provider
value={{
streams,
categories,
users,
setStreams,
setCategories,
setUsers,
}}
>
{children}
</ContentContext.Provider>
);
}
// Custom hooks for specific content types
export function useStreams() {
const context = useContext(ContentContext);
if (!context) {
throw new Error("useStreams must be used within a ContentProvider");
}
return { streams: context.streams, setStreams: context.setStreams };
}
export function useCategories() {
const context = useContext(ContentContext);
if (!context) {
throw new Error("useCategories must be used within a ContentProvider");
}
return { categories: context.categories, setCategories: context.setCategories };
}
export function useUsers() {
const context = useContext(ContentContext);
if (!context) {
throw new Error("useUsers must be used within a ContentProvider");
}
return { users: context.users, setUsers: context.setUsers };
}
// General hook for all content
export function useContent() {
const context = useContext(ContentContext);
if (!context) {
throw new Error("useContent must be used within a ContentProvider");
}
return context;
}

View File

@@ -1,101 +0,0 @@
import { createContext, useContext, useState, useEffect } from "react";
import { useAuth } from "./AuthContext";
interface Item {
id: number;
title: string;
viewers: number;
thumbnail?: string;
}
interface StreamItem extends Item {
type: "stream";
streamer: string;
streamCategory: string;
}
interface CategoryItem extends Item {
type: "category";
}
interface StreamsContextType {
featuredStreams: StreamItem[];
featuredCategories: CategoryItem[];
setFeaturedStreams: (streams: StreamItem[]) => void;
setFeaturedCategories: (categories: CategoryItem[]) => void;
}
const StreamsContext = createContext<StreamsContextType | undefined>(undefined);
export function StreamsProvider({ children }: { children: React.ReactNode }) {
const [featuredStreams, setFeaturedStreams] = useState<StreamItem[]>([]);
const [featuredCategories, setFeaturedCategories] = useState<CategoryItem[]>(
[],
);
const { isLoggedIn } = useAuth();
const fetch_url = isLoggedIn
? ["/api/streams/recommended", "/api/categories/recommended"]
: ["/api/streams/popular/4", "/api/categories/popular/4"];
useEffect(() => {
// Streams
fetch(fetch_url[0])
.then((response) => response.json())
.then((data: StreamItem[]) => {
const extractedData: StreamItem[] = data.map((stream: any) => ({
type: "stream",
id: stream.user_id,
title: stream.title,
streamer: stream.username,
streamCategory: stream.category_name,
viewers: stream.num_viewers,
thumbnail:
stream.thumbnail ||
`/images/thumbnails/categories/${stream.category_name
.toLowerCase()
.replace(/ /g, "_")}.webp`
}));
setFeaturedStreams(extractedData);
});
// Categories
fetch(fetch_url[1])
.then((response) => response.json())
.then((data: CategoryItem[]) => {
const extractedData: CategoryItem[] = data.map((category: any) => ({
type: "category",
id: category.category_id,
title: category.category_name,
viewers: category.num_viewers,
thumbnail: `/images/thumbnails/categories/${category.category_name
.toLowerCase()
.replace(/ /g, "_")}.webp`,
}));
console.log(extractedData);
setFeaturedCategories(extractedData);
});
}, []);
return (
<StreamsContext.Provider
value={{
featuredStreams,
featuredCategories,
setFeaturedStreams,
setFeaturedCategories,
}}
>
{children}
</StreamsContext.Provider>
);
}
export function useStreams() {
const context = useContext(StreamsContext);
if (context === undefined) {
throw new Error("useStreams must be used within a StreamsProvider");
}
return context;
}

View File

@@ -1,4 +1,4 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from "react"; import { createContext, useContext, useState, useEffect, ReactNode } from "react";
// Defines the Theme (Colour Theme) that would be shared/used // Defines the Theme (Colour Theme) that would be shared/used
interface ThemeContextType { interface ThemeContextType {

View File

@@ -1,60 +0,0 @@
import React from 'react'
import { useLocation, useNavigate } from "react-router-dom";
const FoundPage: React.FC = ({}) => {
const location = useLocation();
const navigate = useNavigate();
const { searchResults, query } = location.state || { searchResults: null, query: "" };
if (!searchResults) {
return (
<div className="p-4">
<h2 className="text-xl font-bold">No results found for "{query}"</h2>
<button onClick={() => navigate(-1)} className="mt-4 px-4 py-2 bg-purple-500 text-white rounded">
Go Back
</button>
</div>
);
}
return (
<div className="p-4">
<h2 className="text-xl font-bold mb-4">Search Results for "{query}"</h2>
<div>
<h3 className="text-lg font-semibold">Categories</h3>
<ul>
{searchResults.categories.map((category: any, index: number) => (
<li key={index} className="border p-2 rounded my-2">{category.category_name}</li>
))}
</ul>
</div>
<div>
<h3 className="text-lg font-semibold">Users</h3>
<ul>
{searchResults.users.map((user: any, index: number) => (
<li key={index} className="border p-2 rounded my-2">{user.username} {user.is_live ? "🔴" : ""}</li>
))}
</ul>
</div>
<div>
<h3 className="text-lg font-semibold">Streams</h3>
<ul>
{searchResults.streams.map((stream: any, index: number) => (
<li key={index} className="border p-2 rounded my-2">
{stream.title} - {stream.num_viewers} viewers
</li>
))}
</ul>
</div>
<button onClick={() => navigate(-1)} className="mt-4 px-4 py-2 bg-purple-500 text-white rounded">
Go Back
</button>
</div>
);
};
export default FoundPage

View File

@@ -2,14 +2,15 @@ import React from "react";
import Navbar from "../components/Layout/Navbar"; import Navbar from "../components/Layout/Navbar";
import ListRow from "../components/Layout/ListRow"; import ListRow from "../components/Layout/ListRow";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useStreams } from "../context/StreamsContext"; import { useStreams, useCategories } from "../context/ContentContext";
interface HomePageProps { interface HomePageProps {
variant?: "default" | "personalised"; variant?: "default" | "personalised";
} }
const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => { const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
const { featuredStreams, featuredCategories } = useStreams(); const { streams } = useStreams();
const { categories } = useCategories();
const navigate = useNavigate(); const navigate = useNavigate();
const handleStreamClick = (streamerName: string) => { const handleStreamClick = (streamerName: string) => {
@@ -32,14 +33,14 @@ const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
<ListRow <ListRow
type="stream" type="stream"
title={ title={
"Live Now" + (variant === "personalised" ? " - Recommended" : "") "Streams - Live Now" + (variant === "personalised" ? " - Recommended" : "")
} }
description={ description={
variant === "personalised" variant === "personalised"
? "We think you might like these streams - Streamers recommended for you" ? "We think you might like these streams - Streamers recommended for you"
: "Streamers that are currently live" : "Streamers that are currently live"
} }
items={featuredStreams} items={streams}
onClick={handleStreamClick} onClick={handleStreamClick}
extraClasses="bg-red-950/60" extraClasses="bg-red-950/60"
/> />
@@ -57,10 +58,9 @@ const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
? "Current streams from your followed categories" ? "Current streams from your followed categories"
: "Categories that have been 'popping off' lately" : "Categories that have been 'popping off' lately"
} }
items={featuredCategories} items={categories}
onClick={handleCategoryClick} onClick={handleCategoryClick}
extraClasses="bg-green-950/60" extraClasses="bg-green-950/60"
/> />
</div> </div>
); );

View File

@@ -0,0 +1,72 @@
import React from "react";
import { useLocation, useNavigate } from "react-router-dom";
const ResultsPage: React.FC = ({}) => {
const location = useLocation();
const navigate = useNavigate();
const { searchResults, query } = location.state || {
searchResults: null,
query: "",
};
if (!searchResults) {
return (
<div className="p-4">
<h2 className="text-xl font-bold">No results found for "{query}"</h2>
<button
onClick={() => navigate(-1)}
className="mt-4 px-4 py-2 bg-purple-500 text-white rounded"
>
Go Back
</button>
</div>
);
}
return (
<div className="p-4">
<h2 className="text-xl font-bold mb-4">Search Results for "{query}"</h2>
<div>
<h3 className="text-lg font-semibold">Categories</h3>
<ul>
{searchResults.categories.map((category: any, index: number) => (
<li key={index} className="border p-2 rounded my-2">
{category.category_name}
</li>
))}
</ul>
</div>
<div>
<h3 className="text-lg font-semibold">Users</h3>
<ul>
{searchResults.users.map((user: any, index: number) => (
<li key={index} className="border p-2 rounded my-2">
{user.username} {user.is_live ? "🔴" : ""}
</li>
))}
</ul>
</div>
<div>
<h3 className="text-lg font-semibold">Streams</h3>
<ul>
{searchResults.streams.map((stream: any, index: number) => (
<li key={index} className="border p-2 rounded my-2">
{stream.title} - {stream.num_viewers} viewers
</li>
))}
</ul>
</div>
<button
onClick={() => navigate(-1)}
className="mt-4 px-4 py-2 bg-purple-500 text-white rounded"
>
Go Back
</button>
</div>
);
};
export default ResultsPage;

View File

@@ -1,3 +1,4 @@
from os import getenv
from authlib.integrations.flask_client import OAuth, OAuthError from authlib.integrations.flask_client import OAuth, OAuthError
from flask import Blueprint, jsonify, session, redirect, request from flask import Blueprint, jsonify, session, redirect, request
from blueprints.user import get_session_info_email from blueprints.user import get_session_info_email
@@ -9,7 +10,6 @@ from random import randint
oauth_bp = Blueprint("oauth", __name__) oauth_bp = Blueprint("oauth", __name__)
google = None google = None
from os import getenv
load_dotenv() load_dotenv()
url_api = getenv("VITE_API_URL") url_api = getenv("VITE_API_URL")
url = getenv("HOMEPAGE_URL") url = getenv("HOMEPAGE_URL")
@@ -28,9 +28,10 @@ def init_oauth(app):
api_base_url='https://www.googleapis.com/oauth2/v1/', api_base_url='https://www.googleapis.com/oauth2/v1/',
userinfo_endpoint='https://openidconnect.googleapis.com/v1/userinfo', userinfo_endpoint='https://openidconnect.googleapis.com/v1/userinfo',
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
redirect_uri=f"{url_api}/google_auth" redirect_uri=f"{url}/api/google_auth"
) )
@oauth_bp.route('/login/google') @oauth_bp.route('/login/google')
def login_google(): def login_google():
""" """
@@ -41,10 +42,11 @@ def login_google():
session["origin"] = request.args.get("next") session["origin"] = request.args.get("next")
return google.authorize_redirect( return google.authorize_redirect(
f'{url_api}/google_auth', f'{url}/api/google_auth',
nonce=session['nonce'] nonce=session['nonce']
) )
@oauth_bp.route('/google_auth') @oauth_bp.route('/google_auth')
def google_auth(): def google_auth():
""" """
@@ -70,7 +72,8 @@ def google_auth():
with Database() as db: with Database() as db:
# Generates a new username for the user # Generates a new username for the user
for _ in range(1000000): for _ in range(1000000):
username = user.get("given_name") + str(randint(1, 1000000)) username = user.get("given_name") + \
str(randint(1, 1000000))
taken = db.fetchone(""" taken = db.fetchone("""
SELECT * FROM users SELECT * FROM users
WHERE username = ? WHERE username = ?
@@ -91,7 +94,7 @@ def google_auth():
) )
user_data = get_session_info_email(user_email) user_data = get_session_info_email(user_email)
origin = session.pop("origin", f"{url}") origin = session.pop("origin", f"{url.replace('/api', '')}")
session.clear() session.clear()
session["username"] = user_data["username"] session["username"] = user_data["username"]
session["user_id"] = user_data["user_id"] session["user_id"] = user_data["user_id"]