Merge branch 'main' of https://github.com/john-david3/cs3305-team11 into dev
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
99
web_server/blueprints/search_bar.py
Normal file
99
web_server/blueprints/search_bar.py
Normal 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})
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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.
@@ -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');
|
||||
114
web_server/database/text_search.sql
Normal file
114
web_server/database/text_search.sql
Normal 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;
|
||||
@@ -27,3 +27,4 @@ gevent-websocket
|
||||
flask-oauthlib==0.9.6
|
||||
celery==5.2.3
|
||||
redis==5.2.1
|
||||
python-dateutil
|
||||
@@ -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
|
||||
"""
|
||||
if not is_following(user_id, following_id):
|
||||
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(?,?)
|
||||
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
|
||||
"""
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user