diff --git a/app.py b/app.py index 0d5d02db..01b92936 100755 --- a/app.py +++ b/app.py @@ -168,6 +168,9 @@ from routes.verifyUser import ( verifyUserBlueprint, ) # Importing the blueprint for user verification route +from routes.returnHomeFeedData import ( + returnHomeFeedDataBlueprint, +) # Importing the blueprint for home page feed data endpoint route from utils.afterRequest import ( afterRequestLogger, ) # This function handles loggins of every request @@ -463,6 +466,9 @@ def afterRequest(response): app.register_blueprint( returnPostAnalyticsDataBlueprint ) # Registering the blueprint for the postAnalyticsData endpoint route +app.register_blueprint( + returnHomeFeedDataBlueprint +) # Registering the blueprint for the homeFeedData endpoint route # Check if the name of the module is the main module match __name__: diff --git a/modules.py b/modules.py index 3df5d6cb..b2c0dbdf 100644 --- a/modules.py +++ b/modules.py @@ -137,3 +137,6 @@ getAnalyticsPageOSGraphData, getAnalyticsPageTrafficGraphData, ) + +# Importing the getHomeFeedData for home page feed +from utils.getHomeFeedData import getHomeFeedData diff --git a/routes/category.py b/routes/category.py index be3ffb4e..5e30cf22 100755 --- a/routes/category.py +++ b/routes/category.py @@ -34,6 +34,10 @@ def category(category, by="timeStamp", sort="desc"): :param sort: The sorting order of the posts :return: A rendered template with the posts and the category as context """ + + # Original copy of by + _by = by + # List of available categories categories = [ "games", @@ -76,24 +80,6 @@ def category(category, by="timeStamp", sort="desc"): case False: abort(404) - Log.database( - f"Connecting to '{DB_POSTS_ROOT}' database" - ) # Log the database connection is started - - # Establishing a connection to the SQLite database - connection = sqlite3.connect(DB_POSTS_ROOT) - connection.set_trace_callback( - Log.database - ) # Set the trace callback for the connection - cursor = connection.cursor() - - # Executing SQL query to retrieve posts of the requested category and sorting them accordingly - cursor.execute( - f"""select * from posts where lower(category) = ? order by {by} {sort}""", - [(category.lower())], - ) - posts = cursor.fetchall() - # Modify the sorting name for better readability match by: case "timeStamp": @@ -120,8 +106,10 @@ def category(category, by="timeStamp", sort="desc"): # Rendering the HTML template with posts and category context return render_template( "category.html.jinja", - posts=posts, category=translations["categories"][category.lower()], sortName=sortName, source=f"/category/{category}", + sortBy=_by, + orderBy=sort, + categoryBy=category, ) diff --git a/routes/index.py b/routes/index.py index eae77211..5ac23e83 100755 --- a/routes/index.py +++ b/routes/index.py @@ -48,6 +48,9 @@ def index(by="hot", sort="desc"): The rendered template of the home page with sorted posts according to the provided sorting options. """ + # Original copy of by + _by = by + # Define valid options for sorting and filtering byOptions = ["timeStamp", "title", "views", "category", "lastEditTimeStamp", "hot"] sortOptions = ["asc", "desc"] @@ -60,31 +63,6 @@ def index(by="hot", sort="desc"): ) return redirect("/") - Log.database( - f"Connecting to '{DB_POSTS_ROOT}' database" - ) # Log the database connection is started - # Connect to the posts database - connection = sqlite3.connect(DB_POSTS_ROOT) - connection.set_trace_callback( - Log.database - ) # Set the trace callback for the connection - # Create a cursor object for executing queries - cursor = connection.cursor() - # Select all the columns from the posts table and order them by the specified field and sorting order - match by: - case "hot": # If the sorting field is "hot" - cursor.execute( - f"SELECT *, (views * 1 / log(1 + (strftime('%s', 'now') - timeStamp) / 3600 + 2)) AS hotScore FROM posts ORDER BY hotScore {sort}" - ) # Execute the query to sort by hotness - pass - case _: # For all other sorting fields - cursor.execute( - f"select * from posts order by {by} {sort}" - ) # Execute the query to sort by the specified field - - # Fetch all the results as a list of tuples - posts = cursor.fetchall() - # Modify the sorting name for better readability match by: case "timeStamp": @@ -110,5 +88,10 @@ def index(by="hot", sort="desc"): # Return the rendered template of the home page and pass the posts list and sorting name as keyword arguments return render_template( - "index.html.jinja", posts=posts, sortName=sortName, source="" + "index.html.jinja", + sortName=sortName, + source="", + sortBy=_by, + orderBy=sort, + categoryBy="all", ) diff --git a/routes/returnHomeFeedData.py b/routes/returnHomeFeedData.py new file mode 100644 index 00000000..862afde2 --- /dev/null +++ b/routes/returnHomeFeedData.py @@ -0,0 +1,69 @@ +# Import required modules and functions +from modules import ( + getHomeFeedData, + Blueprint, + make_response, + getSlugFromPostTitle, + url_for, + getProfilePicture, + request, +) + +returnHomeFeedDataBlueprint = Blueprint("returnHomeFeedData", __name__) + + +@returnHomeFeedDataBlueprint.route("/api/v1/homeFeedData") +def homeFeedData(): + """ + API endpoint to fetch home feed data. + Accepts query parameters: category, by, sort, limit, offset + """ + + # Get query parameters with default values if not provided + category = request.args.get("category", type=str, default="all") + by = request.args.get("by", type=str, default="hot") + sort = request.args.get("sort", type=str, default="desc") + limit = request.args.get("limit", type=int, default=6) + offset = request.args.get("offset", type=int, default=0) + + try: + # Fetch raw home feed data based on parameters + rawHomeFeedData = getHomeFeedData( + category=category, by=by, sort=sort, limit=limit, offset=offset + ) + + listOfHomeFeedData = [] + + # Process each post's raw data + for data in rawHomeFeedData: + homeFeedObj = {} + homeFeedObj["id"] = data[0] # Post ID + homeFeedObj["title"] = data[1] # Post Title + homeFeedObj["content"] = data[2] # Post Content + homeFeedObj["author"] = data[3] # Author Name + homeFeedObj["timeStamp"] = data[4] # Timestamp + homeFeedObj["category"] = data[5] # Post Category + homeFeedObj["urlID"] = data[6] # URL ID + + # Generate URL for the post's banner image + homeFeedObj["bannerImgSrc"] = url_for( + "returnPostBanner.returnPostBanner", postID=data[0] + ) + + # Get the author's profile picture + homeFeedObj["authorProfile"] = getProfilePicture(data[3]) + + # Generate URL for viewing the full post + homeFeedObj["postLink"] = url_for( + "post.post", slug=getSlugFromPostTitle(data[1]), urlID=data[6] + ) + + # Add the processed post data to the list + listOfHomeFeedData.append(homeFeedObj) + + # Return the list as a JSON payload with status 200 OK + return make_response({"payload": listOfHomeFeedData}, 200) + + except Exception as e: + # In case of any error, return a JSON error response with status 500 Internal Server Error + return make_response({"error": f"{e}"}, 500) diff --git a/routes/returnPostAnalyticsData.py b/routes/returnPostAnalyticsData.py index 65a56402..bda47385 100644 --- a/routes/returnPostAnalyticsData.py +++ b/routes/returnPostAnalyticsData.py @@ -212,4 +212,4 @@ def storeTimeSpendsDuraton() -> dict: case False: # Return error if analytics is disabled - return ({"message": "analytics is disabled by admin"}, 410) + return make_response({"message": "analytics is disabled by admin"}, 410) diff --git a/static/tailwindUI/js/homeFeed.js b/static/tailwindUI/js/homeFeed.js new file mode 100644 index 00000000..1cc79e1f --- /dev/null +++ b/static/tailwindUI/js/homeFeed.js @@ -0,0 +1,124 @@ +// Initialize variable for postCardMacro and postContainer +let postCardMacro; +let postContainer; + +// Limit +let limit = 6; +// Offset +let offset = 0; + +// Id of div elements and hidden input values +let homeSpinner = document.getElementById("homeSpinner"); +let loadMoreSpinner = document.getElementById("loadMoreSpinner"); +let currentCategory = document.getElementById("currentCategoryText"); +let loadMoreButtonDiv = document.getElementById("loadMoreButton"); +const sortBy = document.getElementById("currentSortText"); +const orderby = document.getElementById("currentOrderText"); + +// Categories and associated icons +const categoryList = { + 'Games': '', + 'History': '', + 'Science': '', + 'Code': '', + 'Technology': '', + 'Education': '', + 'Sports': '', + 'Foods': '', + 'Health': '', + 'Apps': '', + 'Movies': '', + 'Series': '', + 'Travel': '', + 'Books': '', + 'Music': '', + 'Nature': '', + 'Art': '', + 'Finance': '', + 'Business': '', + 'Web': '', + 'Other': '', + 'Default': '' +}; + +// Function to inject html post macro in document body +function initializePostCard() { + fetch(postCardMacroPath) + .then(res => res.text()) + .then(postCardHtml => { + // Insert the fetched template into a hidden container + const tempContainer = document.createElement('div'); + tempContainer.innerHTML = postCardHtml; + + // Register macro in DOM + document.body.appendChild(tempContainer); // or keep it detached + + /// Assign html element's id of content to variable + postCardMacro = document.getElementById("postCardMacro"); + postContainer = document.getElementById("postCardContainer"); + }); +} + + +// Function to fetch homeFeedData from backend +async function fetchHomeFeedData() { + try { + let connection = await fetch(`/api/v1/homeFeedData?category=${currentCategory.value}&by=${sortBy.value}&sort=${orderby.value}&limit=${limit}&offset=${offset}`); + let res = await connection.json(); + + if (connection.ok) { + let posts = res.payload; + + posts.forEach(post => { + const clone = postCardMacro.content.cloneNode(true); + clone.querySelector(".postTitle").innerText = post.title; + clone.querySelector(".postTitle").href = post.postLink; + clone.querySelector(".postContent").innerHTML = post.content; + clone.querySelector(".postBanner").src = post.bannerImgSrc; + clone.querySelector(".postAuthorPicture").src = post.authorProfile; + clone.querySelector(".postAuthor").innerText = post.author; + clone.querySelector(".postAuthor").href = `/user/${post.author}`; + clone.querySelector(".postCategory").innerHTML = categoryList[post.category] || categoryList["Default"]; + clone.querySelector(".postTimeStamp").innerText = post.timeStamp; + postContainer.appendChild(clone); + }); + + // Increase the offset with the value of limit + offset += limit; + + // Check if posts length is not less than limit + if (posts.length < limit) { + // Hide button + loadMoreButtonDiv.classList.add("hidden"); + } + } else { + // Print error on console + console.error(connection.status); + } + } catch (error) { + // Print error on console + console.error(error); + } +} + +async function loadMoreButton() { + // Show spinner + loadMoreSpinner.classList.remove("hidden"); + // Fetch homeFeed + await fetchHomeFeedData(); + // Hide spinner + loadMoreSpinner.classList.add("hidden"); +} + +// Call the function to load data +window.onload = async function () { + // Show spinner + homeSpinner.classList.remove("hidden"); + // Call initialize post card function to inject post macro in DOM + initializePostCard(); + + // Fetch initial homeFeed posts + await fetchHomeFeedData(); + // Hide spinner + homeSpinner.classList.add("hidden"); +} \ No newline at end of file diff --git a/static/tailwindUI/pureHtmlMacro/postCardMacro.html b/static/tailwindUI/pureHtmlMacro/postCardMacro.html new file mode 100644 index 00000000..d4457ace --- /dev/null +++ b/static/tailwindUI/pureHtmlMacro/postCardMacro.html @@ -0,0 +1,39 @@ + + + + \ No newline at end of file diff --git a/templates/tailwindUI/category.html.jinja b/templates/tailwindUI/category.html.jinja index bf35a781..80fbbbd8 100755 --- a/templates/tailwindUI/category.html.jinja +++ b/templates/tailwindUI/category.html.jinja @@ -17,18 +17,26 @@ {% from "components/sortMenu.html.jinja" import sortMenu %} -{{ sortMenu(sortName,source,translations) }} +{{ sortMenu(sortName,source,translations, sortBy, orderBy) }} -