This commit is contained in:
JustIceO7
2025-02-06 19:32:49 +00:00
12 changed files with 262 additions and 140 deletions

View File

@@ -48,7 +48,9 @@ const PasswordResetForm: React.FC<SubmitProps> = ({ onSubmit, token }) => {
newErrors[key as keyof ResetPasswordErrors] = "Confirm your password";
}
});
if (resetData.newPassword.length < 8) {
newErrors.newPasswordError = "Password must be at least 8 characters long";
}
if (resetData.newPassword !== resetData.confirmNewPassword) {
newErrors.confirmNewPasswordError = "Passwords do not match";
}
@@ -76,6 +78,8 @@ const PasswordResetForm: React.FC<SubmitProps> = ({ onSubmit, token }) => {
if (!response.ok) {
const data = await response.json();
throw new Error(data.message || "An error has occurred while resetting");
} else {
confirmPasswordReset();
}
} catch (error: any) {
console.error("Password reset error:", error.message);
@@ -84,7 +88,6 @@ const PasswordResetForm: React.FC<SubmitProps> = ({ onSubmit, token }) => {
general: error.message || "An unexpected error occurred.",
}));
confirmPasswordReset();
}
}
};

View File

@@ -15,6 +15,7 @@ interface ListRowProps {
title: string;
description: string;
items: ListItemProps[];
extraClasses?: string;
onClick: (itemId: number, itemName: string) => void;
}
@@ -43,10 +44,10 @@ const ListItem: React.FC<ListItemProps> = ({
<div className="absolute top-0 left-0 w-full h-full bg-gray-600" />
)}
</div>
<div className="p-3">
<div className="p-3 bg-white">
<h3 className="font-semibold text-lg">{title}</h3>
{type === "stream" && <p className="text-gray-400">{streamer}</p>}
<p className="text-sm text-gray-500">{viewers} viewers</p>
{type === "stream" && <p className="text-red-600">{streamer}</p>}
<p className="text-sm text-white">{viewers} viewers</p>
</div>
</div>
);
@@ -58,12 +59,13 @@ const ListRow: React.FC<ListRowProps> = ({
description,
items,
onClick,
extraClasses="",
}) => {
return (
<div className="flex flex-col space-y-4 py-6">
<div className={`flex flex-col space-y-4 py-6 px-5 mx-2 mt-5 rounded-md ${extraClasses}`}>
<div className="space-y-1">
<h2 className="text-2xl font-bold">{title}</h2>
<p className="text-gray-400">{description}</p>
<h2 className="text-2xl font-bold text-white">{title}</h2>
<p className="text-white">{description}</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{items.map((item) => (

View File

@@ -43,6 +43,7 @@ const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
}
items={featuredStreams}
onClick={handleStreamClick}
extraClasses="border border-gray-700 bg-[#FF7F50]/80"
/>
{/* If Personalised_HomePage, display Categories the logged-in user follows. Else, trending categories. */}
@@ -60,6 +61,8 @@ const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
}
items={featuredCategories}
onClick={handleCategoryClick}
extraClasses="border border-gray-700 bg-[#5AFF75]/80"
/>
</div>
);

View File

@@ -0,0 +1,99 @@
from flask import Blueprint, jsonify
from database.database import Database
search_bp = Blueprint("search", __name__)
@search_bp.route("/search/<str:query>", methods=["GET", "POST"])
def search_results(query: str):
"""
Return the most similar search results
This is the main route that displays a subsection of each search topic
"""
# Create the connection to the database
db = Database()
db.create_connection()
# Get the most accurate search results
# 3 categories
categories = db.fetchall("""
SELECT bm25(category_fts), rank, f.category_id, f.category_name
FROM categories AS c
INNER JOIN category_fts AS f ON c.category_id = f.category_id
WHERE category_fts MATCH ?
LIMIT 3;
""", (query,))
# 3 users
users = db.fetchall("""
SELECT bm25(user_fts), rank, f.user_id, f.username, f.is_live
FROM users u
INNER JOIN user_fts f ON u.user_id = f.user_id
WHERE user_fts MATCH ?
LIMIT 3;
""", (query,))
# 3 streams
streams = db.fetchall("""
SELECT bm25(stream_fts), rank, f.user_id, f.title, f.num_viewers, f.category_id
FROM streams s
INNER JOIN stream_fts f ON s.user_id = f.user_id
WHERE stream_fts MATCH ?
LIMIT 3;
""", (query,))
db.close_connection()
return jsonify({"categories": categories, "users": users, "streams": streams})
@search_bp.route("/search/categories/<str:query>", methods=["GET", "POST"])
def search_categories(query: str):
# Create the connection to the database
db = Database()
db.create_connection()
categories = db.fetchall("""
SELECT bm25(category_fts), rank, f.category_id, f.category_name
FROM categories AS c
INNER JOIN category_fts AS f ON c.category_id = f.category_id
WHERE category_fts MATCH ?;
""", (query,))
db.close_connection()
return jsonify({"categories": categories})
@search_bp.route("/search/users/<str:query>", methods=["GET", "POST"])
def search_users(query: str):
# Create the connection to the database
db = Database()
db.create_connection()
users = db.fetchall("""
SELECT bm25(user_fts), rank, f.user_id, f.username, f.is_live
FROM users u
INNER JOIN user_fts f ON u.user_id = f.user_id
WHERE user_fts MATCH ?;
""", (query,))
db.close_connection()
return jsonify({"users": users})
@search_bp.route("/search/streams/<str:query>", methods=["GET", "POST"])
def search_streams(query: str):
# Create the connection to the database
db = Database()
db.create_connection()
streams = db.fetchall("""
SELECT bm25(stream_fts), rank, f.user_id, f.title, f.num_viewers, f.category_id
FROM streams s
INNER JOIN stream_fts f ON s.user_id = f.user_id
WHERE stream_fts MATCH ?;
""", (query,))
db.close_connection()
return jsonify({"streams": streams})

View File

@@ -1,79 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Interface</title>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script>
// Global constants
const socket = io("http://127.0.0.1:5000"); // TODO: Change this
const stream_id = "{{ stream_id }}";
const chatter_id = "{{ chatter_id }}";
function add_to_chat(data){
const chat_container = document.getElementById("chat_container");
console.log(data)
// Add each chat message to the chat box, one by one
data.forEach((element) => {
const div = document.createElement("div");
div.textContent = `${element.time_sent} ${element.chatter_id}: ${element.message}`;
chat_container.appendChild(div);
})
chat_container.scrollTop = chat_container.scrollHeight; // keeps you at the bottom of the chat
}
socket.on("connect", () => {
console.log("Socket Connection established!");
})
// Wait for a new message to be sent
socket.on("new_message", (data) => {
console.log("New message");
add_to_chat([data]);
});
function loadPrevChat() {
const init_chat_logs = JSON.parse('{{ chat_history | tojson }}');
add_to_chat(init_chat_logs);
}
function sendChat(){
// Get the chat message sent by user, if none, don't
const chat_message = document.getElementById("messageInput").value.trim();
if (!chat_message) return;
document.getElementById("messageInput").value = ""; // clear the chat box
// Send the message using sockets
socket.emit("send_message", {
chatter_id: chatter_id,
stream_id: stream_id,
message: chat_message
});
}
window.addEventListener('DOMContentLoaded', () => {
socket.emit("join", {stream_id: stream_id});
loadPrevChat();
});
window.addEventListener("beforeunload", () => {
socket.emit("leave", {stream_id: stream_id});
});
</script>
</head>
<body>
<h1>Chat for Stream #{{ stream_id }}</h1>
<div id="chat_container" style="max-height: 400px; overflow-y: auto;"></div>
<input type="text" id="messageInput" placeholder="Type a message" />
<button onclick="sendChat()">Send</button>
</body>
</html>

View File

@@ -1,27 +0,0 @@
<html>
<body>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<video id="video" controls></video>
<script>
if(Hls.isSupported())
{
var video = document.getElementById('video');
var hls = new Hls();
hls.loadSource('{{ video_url }}');
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED,function()
{
video.play();
});
}
else if (video.canPlayType('application/vnd.apple.mpegurl'))
{
video.src = ' {{ video_url }}';
video.addEventListener('canplay',function()
{
video.play();
});
}
</script>
</body>
</html>

View File

@@ -56,23 +56,28 @@ def user_following(user_id: int, followed_id: int):
@login_required
@user_bp.route('/user/follow/<string:username>')
def follow(username):
def follow_user(username):
"""
Follows a user
"""
user_id = session.get("user_id")
following_id = get_user_id(username)
follow(user_id, following_id)
if follow(user_id, following_id):
return jsonify({"success": True,
"already_following": False})
return jsonify({"success": True,
"already_following": True})
@login_required
@user_bp.route('/user/unfollow/<string:username>')
def user_unfollow(followed_username):
def unfollow_user(username):
"""
Unfollows a user
"""
user_id = session.get("user_id")
followed_id = get_user_id(followed_username)
followed_id = get_user_id(username)
unfollow(user_id, followed_id)
return jsonify({"success": True})
@login_required
@user_bp.route('/user/following')
@@ -92,7 +97,10 @@ def get_login_status():
Returns whether the user is logged in or not
"""
username = session.get("username")
return jsonify({'status': username is not None, 'username': username})
user_id = session.get("user_id")
return jsonify({'status': username is not None,
'username': username,
'user_id': user_id})
@user_bp.route('/user/forgot_password/<string:email>', methods=['GET','POST'])
def user_forgot_password(email):

Binary file not shown.

View File

@@ -124,9 +124,7 @@ LIMIT 10;
INSERT INTO follows (user_id, followed_id, since) VALUES
(7, 1, '2024-08-30'),
(7, 2, '2024-08-30'),
(7, 3, '2024-08-30'),
(7, 4, '2024-08-30'),
(7, 5, '2024-08-30');
(7, 3, '2024-08-30');
INSERT INTO followed_categories (user_id, category_id) VALUES
(7, 1),
@@ -134,5 +132,5 @@ INSERT INTO followed_categories (user_id, category_id) VALUES
(7, 3);
INSERT INTO subscribes (user_id, subscribed_id, since, expires) VALUES
(7, 1, '2024-08-30', '2025-02-28'),
(7, 1, '2024-08-30', '2025-02-28 12:00:00'),
(7, 2, '2024-08-30', '2025-02-15');

View File

@@ -0,0 +1,114 @@
/* Full text search queries for categories */
DROP TABLE IF EXISTS category_fts;
CREATE VIRTUAL TABLE category_fts
USING fts5 (category_id, category_name);
INSERT INTO category_fts (category_id, category_name)
SELECT category_id, category_name
FROM categories;
-- Triggers that inserts new titles into category_fts
DROP TRIGGER IF EXISTS insert_category_fts;
CREATE TRIGGER insert_category_fts
AFTER INSERT ON categories
BEGIN
INSERT INTO category_fts(category_id, category_name)
VALUES (NEW.category_id, NEW.category_name);
END;
DROP TRIGGER IF EXISTS update_category_fts;
CREATE TRIGGER update_category_fts
AFTER UPDATE ON categories
BEGIN
UPDATE category_fts
SET
category_id = NEW.category_id,
category_name = NEW.category_name
WHERE category_id = NEW.category_id;
END;
DROP TRIGGER IF EXISTS delete_category_fts;
CREATE TRIGGER delete_category_fts
AFTER DELETE ON categories
BEGIN
DELETE FROM category_fts
WHERE category_id = OLD.category_id;
END;
/* Full text search queries for users */
DROP TABLE IF EXISTS user_fts;
CREATE VIRTUAL TABLE user_fts
USING fts5 (user_id, username, is_live);
INSERT INTO user_fts (user_id, username, is_live)
SELECT user_id, username, is_live
FROM users;
-- Triggers that inserts new titles into user_fts
DROP TRIGGER IF EXISTS insert_user_fts;
CREATE TRIGGER insert_user_fts
AFTER INSERT ON users
BEGIN
INSERT INTO user_fts(user_id, username, is_live)
VALUES (NEW.user_id, NEW.username, NEW.is_live);
END;
DROP TRIGGER IF EXISTS update_user_fts;
CREATE TRIGGER update_user_fts
AFTER UPDATE ON users
BEGIN
UPDATE user_fts
SET
user_id = NEW.user_id,
username = NEW.username,
is_live = NEW.is_live
WHERE user_id = NEW.user_id;
END;
DROP TRIGGER IF EXISTS delete_user_fts;
CREATE TRIGGER delete_user_fts
AFTER DELETE ON users
BEGIN
DELETE FROM user_fts
WHERE user_id = OLD.user_id;
END;
/* Full text search queries for users */
DROP TABLE IF EXISTS stream_fts;
CREATE VIRTUAL TABLE stream_fts
USING fts5 (user_id, title, num_viewers, category_id);
INSERT INTO stream_fts (user_id, title, num_viewers, category_id)
SELECT user_id, title, num_viewers, category_id
FROM streams;
-- Triggers that inserts new titles into stream_fts
DROP TRIGGER IF EXISTS insert_stream_fts;
CREATE TRIGGER insert_stream_fts
AFTER INSERT ON streams
BEGIN
INSERT INTO stream_fts(user_id, title, num_viewers, category_id)
VALUES (NEW.user_id, NEW.title, NEW.num_viewers, NEW.category_id);
END;
DROP TRIGGER IF EXISTS update_stream_fts;
CREATE TRIGGER update_stream_fts
AFTER UPDATE ON streams
BEGIN
UPDATE stream_fts
SET
user_id = NEW.user_id,
title = NEW.title,
num_viewers = NEW.num_viewers,
category_id = NEW.category_id
WHERE user_id = NEW.user_id;
END;
DROP TRIGGER IF EXISTS delete_stream_fts;
CREATE TRIGGER delete_stream_fts
AFTER DELETE ON streams
BEGIN
DELETE FROM stream_fts
WHERE user_id = OLD.user_id;
END;

View File

@@ -26,4 +26,5 @@ gevent>=22.10.2
gevent-websocket
flask-oauthlib==0.9.6
celery==5.2.3
redis==5.2.1
redis==5.2.1
python-dateutil

View File

@@ -4,6 +4,7 @@ from datetime import datetime
from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
from os import getenv
from werkzeug.security import generate_password_hash, check_password_hash
from dateutil import parser
from dotenv import load_dotenv
load_dotenv()
@@ -91,29 +92,28 @@ 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:
if not is_following(user_id, following_id):
with Database() as db:
db.execute("""
INSERT INTO follows (user_id, followed_id)
VALUES(?,?)
VALUES(?,?);
""", (user_id, following_id))
return True
return False
def unfollow(user_id: int, followed_id: int):
"""
Unfollows follow_id user from user_id user
"""
with Database() as db:
db.execute("""
DELETE FROM follows
WHERE user_id = ?
AND followed_id = ?
""", (user_id, followed_id))
if is_following(user_id, followed_id):
with Database() as db:
db.execute("""
DELETE FROM follows
WHERE user_id = ?
AND followed_id = ?
""", (user_id, followed_id))
return True
return False
def subscription_expiration(user_id: int, subscribed_id: int) -> int:
@@ -131,7 +131,7 @@ def subscription_expiration(user_id: int, subscribed_id: int) -> int:
if data:
expiration_date = data["expires"]
remaining_time = (expiration_date - datetime.now()).seconds
remaining_time = (parser.parse(expiration_date) - datetime.now()).seconds
return remaining_time
return 0