Merge branch 'main' of https://github.com/john-david3/cs3305-team11
This commit is contained in:
@@ -6,6 +6,8 @@ import HomePage from "./pages/HomePage";
|
|||||||
import StreamerRoute from "./components/Stream/StreamerRoute";
|
import StreamerRoute from "./components/Stream/StreamerRoute";
|
||||||
import NotFoundPage from "./pages/NotFoundPage";
|
import NotFoundPage from "./pages/NotFoundPage";
|
||||||
import UserPage from "./pages/UserPage";
|
import UserPage from "./pages/UserPage";
|
||||||
|
import ForgotPasswordPage from "./pages/ForgotPasswordPage";
|
||||||
|
import CategoryPage from "./pages/CategoryPage";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
@@ -40,6 +42,8 @@ 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={<ForgotPasswordPage />}></Route>
|
||||||
|
<Route path="/category/:category_name" element={<CategoryPage />}></Route>
|
||||||
|
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
:root{
|
:root{
|
||||||
|
|
||||||
--sideNav-LightBG: white;
|
--sideBar-LightBG: white;
|
||||||
--sideNav-LightText: black;
|
--sideBar-LightText: black;
|
||||||
|
|
||||||
--sideNav-DarkBG: black;
|
--sideBar-DarkBG: black;
|
||||||
--sideNav-DarkText: white;
|
--sideBar-DarkText: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,16 +74,21 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||||||
{isLoggedIn && (
|
{isLoggedIn && (
|
||||||
<>
|
<>
|
||||||
<Button onClick={() => handleSideBar()}
|
<Button onClick={() => handleSideBar()}
|
||||||
extraClasses={`absolute ${
|
extraClasses={`absolute ${showSideBar ? `fixed top-[20px] left-[20px] p-2 text-[1.5rem] text-white hover:text-white
|
||||||
showSideBar ? `fixed top-[20px] left-[20px] p-2 text-[1.5rem] text-white hover:text-white
|
|
||||||
bg-black/30 hover:bg-purple-500/80 rounded-md border border-gray-300 hover:border-white h
|
bg-black/30 hover:bg-purple-500/80 rounded-md border border-gray-300 hover:border-white h
|
||||||
over:border-b-4 hover:border-l-4 active:border-b-2 active:border-l-2 transition-all ` :
|
over:border-b-4 hover:border-l-4 active:border-b-2 active:border-l-2 transition-all ` :
|
||||||
"top-[75px] left-[20px]"
|
"top-[75px] left-[20px]"
|
||||||
} transition-all duration-300 z-[99]`}
|
} transition-all duration-300 z-[99]`}
|
||||||
>
|
>
|
||||||
<SidebarIcon className="h-15 w-15 mr-1 z-[90]" />
|
<SidebarIcon className="h-15 w-15 mr-1 z-[90]" />
|
||||||
</Button>
|
</Button>
|
||||||
{showSideBar && <Sidebar scrollActiveSideBar={showSideBar} />}
|
<div
|
||||||
|
className={`fixed top-0 left-0 w-[250px] h-screen bg-[var(--sideBar-LightBG)] text-[var(--sideBar-LightText)] z-[90] overflow-y-auto scrollbar-hide
|
||||||
|
transition-transform transition-opacity duration-500 ease-in-out ${showSideBar ? "translate-x-0 opacity-100" : "-translate-x-full opacity-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { SunMoon as SunMoonIcon} from "lucide-react"
|
||||||
|
import Theme from "./Theme";
|
||||||
import "../../assets/styles/sidebar.css"
|
import "../../assets/styles/sidebar.css"
|
||||||
|
|
||||||
interface SideBarProps {
|
interface SideBarProps {
|
||||||
extraClasses?: string;
|
extraClasses?: string;
|
||||||
scrollActiveSideBar: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sidebar: React.FC<SideBarProps> = ( {scrollActiveSideBar}) => {
|
const Sidebar: React.FC<SideBarProps> = () => {
|
||||||
const [thisTheme, setThisTheme] = useState(false);
|
const [thisTheme, setThisTheme] = useState(false);
|
||||||
const [isCursorOnSidebar, setIsCursorOnSidebar] = useState(false);
|
const [isCursorOnSidebar, setIsCursorOnSidebar] = useState(false);
|
||||||
|
const [triggerAnimation, setTriggerAnimation] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sideBarScroll = () => {
|
const sideBarScroll = () => {
|
||||||
document.body.style.overflow = isCursorOnSidebar ? "hidden" : "unset";
|
document.body.style.overflow = isCursorOnSidebar ? "hidden" : "unset";
|
||||||
@@ -21,10 +22,22 @@ const Sidebar: React.FC<SideBarProps> = ( {scrollActiveSideBar}) => {
|
|||||||
};
|
};
|
||||||
}, [isCursorOnSidebar]);
|
}, [isCursorOnSidebar]);
|
||||||
|
|
||||||
return <div id="sidebar" className={"fixed top-0 left-0 bg-[var(--sideNav-LightBG)] w-[250px] text-sideBar-text p-4 z-[90] h-screen overflow-y-auto scrollbar-hide"}
|
const handleTheme = () => {
|
||||||
onMouseEnter={() => setIsCursorOnSidebar(true)}
|
setThisTheme(!thisTheme);
|
||||||
onMouseLeave={() => setIsCursorOnSidebar(false)}
|
setTriggerAnimation(false); // Reset animation
|
||||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
|
setTimeout(() => setTriggerAnimation(true), 0); // Re-trigger animation
|
||||||
|
};
|
||||||
|
|
||||||
|
return (<div id="sidebar"
|
||||||
|
key={triggerAnimation ? 'burn-in' : 'reset'}
|
||||||
|
className={`fixed top-0 left-0 w-[250px] ${thisTheme
|
||||||
|
? " bg-[var(--sideBar-LightBG)] text-[var(--sideBar-LightText)]"
|
||||||
|
: " bg-[var(--sideBar-DarkBG)] text-[var(--sideBar-DarkText)]"
|
||||||
|
} p-4 z-[90] h-screen overflow-y-auto pt-10
|
||||||
|
transition-transform duration-500 ease-in-out animate-burnIn`}
|
||||||
|
onMouseLeave={() => setIsCursorOnSidebar(false)}
|
||||||
|
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
|
||||||
|
<Theme onClick={handleTheme} />
|
||||||
<ul className="overflow-y-auto">
|
<ul className="overflow-y-auto">
|
||||||
<li>1</li>
|
<li>1</li>
|
||||||
<li>1</li>
|
<li>1</li>
|
||||||
@@ -98,7 +111,7 @@ const Sidebar: React.FC<SideBarProps> = ( {scrollActiveSideBar}) => {
|
|||||||
<li>1</li>
|
<li>1</li>
|
||||||
<li>1</li>
|
<li>1</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>;
|
</div>);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Sidebar;
|
export default Sidebar;
|
||||||
|
|||||||
22
frontend/src/components/Layout/Theme.tsx
Normal file
22
frontend/src/components/Layout/Theme.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { SunMoon as SunMoonIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ThemeProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Theme: React.FC<ThemeProps> = ({ onClick }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className="p-2 text-[1.5rem] flex items-center gap-2 text-white hover:text-purple-600 bg-black/30 hover:bg-black/80 rounded-md border border-gray-300 hover:border-purple-500 hover:border-b-4 hover:border-l-4 active:border-b-2 active:border-l-2 transition-all"
|
||||||
|
>
|
||||||
|
<SunMoonIcon
|
||||||
|
className={`transition-transform duration-300 ease-in-out`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Theme;
|
||||||
9
frontend/src/pages/CategoryPage.tsx
Normal file
9
frontend/src/pages/CategoryPage.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const CategoryPage = () => {
|
||||||
|
return (
|
||||||
|
<div>CategoryPage</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategoryPage
|
||||||
9
frontend/src/pages/ForgotPasswordPage.tsx
Normal file
9
frontend/src/pages/ForgotPasswordPage.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const ForgotPasswordPage = () => {
|
||||||
|
return (
|
||||||
|
<div>ForgotPasswordPage</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ForgotPasswordPage
|
||||||
@@ -17,6 +17,11 @@ const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
|
|||||||
navigate(`/${streamerName}`);
|
navigate(`/${streamerName}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCategoryClick = (categoryID: number, categoryName: string) => {
|
||||||
|
console.log(`Navigating to category ${categoryID}`);
|
||||||
|
navigate(`category/${categoryName}`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="home-page"
|
id="home-page"
|
||||||
@@ -54,7 +59,7 @@ const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
|
|||||||
: "Categories that have been 'popping off' lately"
|
: "Categories that have been 'popping off' lately"
|
||||||
}
|
}
|
||||||
items={featuredCategories}
|
items={featuredCategories}
|
||||||
onClick={() => {}} //TODO
|
onClick={handleCategoryClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ export default {
|
|||||||
moving_text_colour: "moving_text_colour 6s ease-in-out infinite alternate",
|
moving_text_colour: "moving_text_colour 6s ease-in-out infinite alternate",
|
||||||
moving_bg: 'moving_bg 200s linear infinite',
|
moving_bg: 'moving_bg 200s linear infinite',
|
||||||
'border-spin': 'border-spin linear infinite',
|
'border-spin': 'border-spin linear infinite',
|
||||||
floating: "floating 30s linear infinite"
|
floating: "floating 30s linear infinite",
|
||||||
|
burnIn: 'burnIn 1s ease-out',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
@@ -48,12 +49,13 @@ export default {
|
|||||||
'80%': { transform: 'translate(10px, -7px) rotateX(-2.5deg) rotateY(1.5deg)' }, /* Top-right tilt */
|
'80%': { transform: 'translate(10px, -7px) rotateX(-2.5deg) rotateY(1.5deg)' }, /* Top-right tilt */
|
||||||
'100%': { transform: 'translate(0px, -5px) rotateX(0deg) rotateY(0deg)' },
|
'100%': { transform: 'translate(0px, -5px) rotateX(0deg) rotateY(0deg)' },
|
||||||
},
|
},
|
||||||
},
|
|
||||||
|
|
||||||
colors: {
|
burnIn: {
|
||||||
"sideBar-bg": "var(--sideBar-LightBG)",
|
'0%' : { opacity: '0'},
|
||||||
"sideBar-text": "var(--sideBar-LightText)"
|
'50%' : { opacity: '0.8'},
|
||||||
}
|
'100%' : { opacity: '1'},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
require('tailwind-scrollbar-hide')
|
require('tailwind-scrollbar-hide')
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
from flask import Blueprint, session
|
|
||||||
from database.database import Database
|
|
||||||
|
|
||||||
import smtplib
|
import smtplib
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
@@ -10,9 +7,7 @@ from dotenv import load_dotenv
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
email_bp = Blueprint("email", __name__)
|
def send_email(email) -> None:
|
||||||
|
|
||||||
def send_email(username) -> None:
|
|
||||||
"""
|
"""
|
||||||
Send a verification email to the user.
|
Send a verification email to the user.
|
||||||
"""
|
"""
|
||||||
@@ -23,8 +18,6 @@ def send_email(username) -> None:
|
|||||||
SMTP_EMAIL = getenv("EMAIL")
|
SMTP_EMAIL = getenv("EMAIL")
|
||||||
SMTP_PASSWORD = getenv("EMAIL_PASSWORD")
|
SMTP_PASSWORD = getenv("EMAIL_PASSWORD")
|
||||||
|
|
||||||
user_email = get_user_email(username)
|
|
||||||
|
|
||||||
# Setup up the receiver details
|
# Setup up the receiver details
|
||||||
login_code = randrange(100000, 1000000)
|
login_code = randrange(100000, 1000000)
|
||||||
body = f"""
|
body = f"""
|
||||||
@@ -39,7 +32,7 @@ def send_email(username) -> None:
|
|||||||
msg = MIMEText(body, "html")
|
msg = MIMEText(body, "html")
|
||||||
msg["Subject"] = "Reset Gander Login"
|
msg["Subject"] = "Reset Gander Login"
|
||||||
msg["From"] = SMTP_EMAIL
|
msg["From"] = SMTP_EMAIL
|
||||||
msg["To"] = user_email
|
msg["To"] = email
|
||||||
|
|
||||||
# Send the email using smtplib
|
# Send the email using smtplib
|
||||||
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as smtp:
|
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as smtp:
|
||||||
@@ -54,19 +47,3 @@ def send_email(username) -> None:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error: ", e)
|
print("Error: ", e)
|
||||||
|
|
||||||
def get_user_email(username):
|
|
||||||
"""
|
|
||||||
Get the users email address.
|
|
||||||
"""
|
|
||||||
|
|
||||||
db = Database()
|
|
||||||
db.create_connection()
|
|
||||||
|
|
||||||
user_email = db.fetchone("""SELECT email
|
|
||||||
FROM users
|
|
||||||
WHERE username = ?;""",
|
|
||||||
(username,))
|
|
||||||
email = user_email["email"]
|
|
||||||
db.close_connection()
|
|
||||||
return email
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
from flask import Blueprint, jsonify, session
|
from flask import Blueprint, jsonify, session
|
||||||
from utils.user_utils import is_subscribed, is_following, subscription_expiration, verify_token, reset_password, get_user_id, unfollow
|
from utils.user_utils import is_subscribed, is_following, subscription_expiration, verify_token, reset_password, get_user_id, unfollow
|
||||||
from blueprints.utils import login_required
|
from blueprints.utils import login_required
|
||||||
from utils.user_utils import get_email
|
|
||||||
|
|
||||||
user_bp = Blueprint("user", __name__)
|
user_bp = Blueprint("user", __name__)
|
||||||
|
|
||||||
@@ -26,17 +25,25 @@ def user_following(user_id: int, subscribed_id: int):
|
|||||||
return jsonify({"following": False})
|
return jsonify({"following": False})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@user_bp.route('/unfollow/<int:username>')
|
@user_bp.route('/follow/<string:username>')
|
||||||
|
def follow(username):
|
||||||
|
"""
|
||||||
|
Follows a user
|
||||||
|
"""
|
||||||
|
user_id = session.get("user_id")
|
||||||
|
following_id = get_user_id(username)
|
||||||
|
follow(user_id, following_id)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@user_bp.route('/unfollow/<string:username>')
|
||||||
def user_unfollow(followed_username):
|
def user_unfollow(followed_username):
|
||||||
"""
|
"""
|
||||||
Unfollows a user
|
Unfollows a user
|
||||||
"""
|
"""
|
||||||
user_id = session.get("user_id")
|
user_id = session.get("user_id")
|
||||||
followed_id = get_user_id(followed_username)
|
followed_id = get_user_id(followed_username)
|
||||||
status = unfollow(user_id, followed_id)
|
unfollow(user_id, followed_id)
|
||||||
|
|
||||||
status = True if status else False
|
|
||||||
return jsonify({"status": status})
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@user_bp.route('/subscription_remaining/<int:streamer_id>')
|
@user_bp.route('/subscription_remaining/<int:streamer_id>')
|
||||||
@@ -58,16 +65,13 @@ def get_login_status():
|
|||||||
username = session.get("username")
|
username = session.get("username")
|
||||||
return jsonify({'status': username is not None, 'username': username})
|
return jsonify({'status': username is not None, 'username': username})
|
||||||
|
|
||||||
@user_bp.route('/forgot_password/', defaults={'email': None}, methods=['POST'])
|
|
||||||
@user_bp.route('/forgot_password/<string:email>', methods=['POST'])
|
@user_bp.route('/forgot_password/<string:email>', methods=['POST'])
|
||||||
def user_forgot_password(email):
|
def user_forgot_password(email):
|
||||||
"""
|
"""
|
||||||
Will send link to email to reset password by looking at the user_id within session to see whos password should be reset
|
Will send link to email to reset password by looking at the user_id within session to see whos password should be reset
|
||||||
Creates a super random number to be used a the link to reset password I guess a random number generator seeded with a secret
|
Creates a super random number to be used a the link to reset password I guess a random number generator seeded with a secret
|
||||||
"""
|
"""
|
||||||
user_id = session.get("user_id")
|
|
||||||
if user_id != None:
|
|
||||||
email = get_email(user_id)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@user_bp.route('/reset_password/<string:token>/<string:new_password>')
|
@user_bp.route('/reset_password/<string:token>/<string:new_password>')
|
||||||
|
|||||||
@@ -72,7 +72,24 @@ def is_following(user_id: int, followed_id: int) -> bool:
|
|||||||
""", (user_id, followed_id))
|
""", (user_id, followed_id))
|
||||||
return bool(result)
|
return bool(result)
|
||||||
|
|
||||||
def unfollow(user_id: int, followed_id: int) -> bool:
|
def follow(user_id: int, following_id: int):
|
||||||
|
"""
|
||||||
|
Follows following_id user from user_id user
|
||||||
|
"""
|
||||||
|
with Database() as db:
|
||||||
|
data = db.execute("""
|
||||||
|
SELECT * FROM follows
|
||||||
|
WHERE user_id = ?
|
||||||
|
AND followed_id = ?
|
||||||
|
""", (user_id, following_id))
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
db.execute("""
|
||||||
|
INSERT INTO follows (user_id, followed_id)
|
||||||
|
VALUES(?,?)
|
||||||
|
""", (user_id, following_id))
|
||||||
|
|
||||||
|
def unfollow(user_id: int, followed_id: int):
|
||||||
"""
|
"""
|
||||||
Unfollows follow_id user from user_id user
|
Unfollows follow_id user from user_id user
|
||||||
"""
|
"""
|
||||||
@@ -82,8 +99,7 @@ def unfollow(user_id: int, followed_id: int) -> bool:
|
|||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
AND followed_id = ?
|
AND followed_id = ?
|
||||||
""", (user_id, followed_id))
|
""", (user_id, followed_id))
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def subscription_expiration(user_id: int, subscribed_id: int) -> int:
|
def subscription_expiration(user_id: int, subscribed_id: int) -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user