diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3559c41..8ff76ba 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; 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 HomePage from "./pages/HomePage"; import StreamerRoute from "./components/Stream/StreamerRoute"; @@ -8,8 +8,8 @@ import NotFoundPage from "./pages/NotFoundPage"; import UserPage from "./pages/UserPage"; import ResetPasswordPage from "./pages/ResetPasswordPage"; import CategoryPage from "./pages/CategoryPage"; -import CategoriesPage from "./pages/CategoriesPage"; -import FoundPage from "./pages/FoundPage"; +import CategoriesPage from "./pages/AllCategoriesPage"; +import ResultsPage from "./pages/ResultsPage"; function App() { const [isLoggedIn, setIsLoggedIn] = useState(false); @@ -32,7 +32,7 @@ function App() { - + } /> } /> - }> - }> + } + > + } + > }> - }> + }> } /> } /> - + ); } diff --git a/frontend/src/components/Auth/OAuth.tsx b/frontend/src/components/Auth/OAuth.tsx index a9c71d8..a86d2fe 100644 --- a/frontend/src/components/Auth/OAuth.tsx +++ b/frontend/src/components/Auth/OAuth.tsx @@ -1,5 +1,3 @@ -import { useEffect } from "react"; - export default function GoogleLogin() { const handleLoginClick = (e: React.MouseEvent) => { e.preventDefault(); @@ -15,7 +13,8 @@ export default function GoogleLogin() { //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 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]" + > Google logo void; + setCategories: (categories: CategoryItem[]) => void; + setUsers: (users: UserItem[]) => void; +} + +const ContentContext = createContext(undefined); + +export function ContentProvider({ children }: { children: React.ReactNode }) { + const [streams, setStreams] = useState([]); + const [categories, setCategories] = useState([]); + const [users, setUsers] = useState([]); + 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 ( + + {children} + + ); +} + +// 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; +} diff --git a/frontend/src/context/StreamsContext.tsx b/frontend/src/context/StreamsContext.tsx deleted file mode 100644 index c2ac55d..0000000 --- a/frontend/src/context/StreamsContext.tsx +++ /dev/null @@ -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(undefined); - -export function StreamsProvider({ children }: { children: React.ReactNode }) { - const [featuredStreams, setFeaturedStreams] = useState([]); - const [featuredCategories, setFeaturedCategories] = useState( - [], - ); - 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 ( - - {children} - - ); -} - -export function useStreams() { - const context = useContext(StreamsContext); - if (context === undefined) { - throw new Error("useStreams must be used within a StreamsProvider"); - } - return context; -} diff --git a/frontend/src/context/ThemeContext.tsx b/frontend/src/context/ThemeContext.tsx index e7709ac..36649aa 100644 --- a/frontend/src/context/ThemeContext.tsx +++ b/frontend/src/context/ThemeContext.tsx @@ -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 interface ThemeContextType { diff --git a/frontend/src/pages/FoundPage.tsx b/frontend/src/pages/FoundPage.tsx deleted file mode 100644 index 5025275..0000000 --- a/frontend/src/pages/FoundPage.tsx +++ /dev/null @@ -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 ( -
-

No results found for "{query}"

- -
- ); - } - - return ( -
-

Search Results for "{query}"

- -
-

Categories

-
    - {searchResults.categories.map((category: any, index: number) => ( -
  • {category.category_name}
  • - ))} -
-
- -
-

Users

-
    - {searchResults.users.map((user: any, index: number) => ( -
  • {user.username} {user.is_live ? "🔴" : ""}
  • - ))} -
-
- -
-

Streams

-
    - {searchResults.streams.map((stream: any, index: number) => ( -
  • - {stream.title} - {stream.num_viewers} viewers -
  • - ))} -
-
- - -
- ); - }; - - -export default FoundPage diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index d85e606..593329b 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -2,14 +2,15 @@ import React from "react"; import Navbar from "../components/Layout/Navbar"; import ListRow from "../components/Layout/ListRow"; import { useNavigate } from "react-router-dom"; -import { useStreams } from "../context/StreamsContext"; +import { useStreams, useCategories } from "../context/ContentContext"; interface HomePageProps { variant?: "default" | "personalised"; } const HomePage: React.FC = ({ variant = "default" }) => { - const { featuredStreams, featuredCategories } = useStreams(); + const { streams } = useStreams(); + const { categories } = useCategories(); const navigate = useNavigate(); const handleStreamClick = (streamerName: string) => { @@ -32,14 +33,14 @@ const HomePage: React.FC = ({ variant = "default" }) => { @@ -57,10 +58,9 @@ const HomePage: React.FC = ({ variant = "default" }) => { ? "Current streams from your followed categories" : "Categories that have been 'popping off' lately" } - items={featuredCategories} + items={categories} onClick={handleCategoryClick} extraClasses="bg-green-950/60" - /> ); diff --git a/frontend/src/pages/ResultsPage.tsx b/frontend/src/pages/ResultsPage.tsx new file mode 100644 index 0000000..7320f3c --- /dev/null +++ b/frontend/src/pages/ResultsPage.tsx @@ -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 ( +
+

No results found for "{query}"

+ +
+ ); + } + + return ( +
+

Search Results for "{query}"

+ +
+

Categories

+
    + {searchResults.categories.map((category: any, index: number) => ( +
  • + {category.category_name} +
  • + ))} +
+
+ +
+

Users

+
    + {searchResults.users.map((user: any, index: number) => ( +
  • + {user.username} {user.is_live ? "🔴" : ""} +
  • + ))} +
+
+ +
+

Streams

+
    + {searchResults.streams.map((stream: any, index: number) => ( +
  • + {stream.title} - {stream.num_viewers} viewers +
  • + ))} +
+
+ + +
+ ); +}; + +export default ResultsPage; diff --git a/web_server/blueprints/oauth.py b/web_server/blueprints/oauth.py index e32ffc3..8d5846e 100644 --- a/web_server/blueprints/oauth.py +++ b/web_server/blueprints/oauth.py @@ -1,3 +1,4 @@ +from os import getenv from authlib.integrations.flask_client import OAuth, OAuthError from flask import Blueprint, jsonify, session, redirect, request from blueprints.user import get_session_info_email @@ -9,7 +10,6 @@ from random import randint oauth_bp = Blueprint("oauth", __name__) google = None -from os import getenv load_dotenv() url_api = getenv("VITE_API_URL") url = getenv("HOMEPAGE_URL") @@ -28,9 +28,10 @@ def init_oauth(app): api_base_url='https://www.googleapis.com/oauth2/v1/', userinfo_endpoint='https://openidconnect.googleapis.com/v1/userinfo', 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') def login_google(): """ @@ -41,10 +42,11 @@ def login_google(): session["origin"] = request.args.get("next") return google.authorize_redirect( - f'{url_api}/google_auth', + f'{url}/api/google_auth', nonce=session['nonce'] ) + @oauth_bp.route('/google_auth') def google_auth(): """ @@ -70,12 +72,13 @@ def google_auth(): with Database() as db: # Generates a new username for the user 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(""" SELECT * FROM users WHERE username = ? """, (username,)) - + if not taken: break @@ -91,7 +94,7 @@ def google_auth(): ) 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["username"] = user_data["username"] session["user_id"] = user_data["user_id"]