Skip to content

Pagination for home page and category page #158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 homeFeddData endpoint route
Copy link
Preview

Copilot AI Apr 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment contains a spelling mistake: 'homeFeddData' should be corrected to 'homeFeedData'.

Suggested change
) # Registering the blueprint for the homeFeddData endpoint route
) # Registering the blueprint for the homeFeedData endpoint route

Copilot uses AI. Check for mistakes.


# Check if the name of the module is the main module
match __name__:
Expand Down
3 changes: 3 additions & 0 deletions modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,6 @@
getAnalyticsPageOSGraphData,
getAnalyticsPageTrafficGraphData,
)

# Importing the getHomeFeedData for home page feed
from utils.getHomeFeedData import getHomeFeedData
26 changes: 7 additions & 19 deletions routes/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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":
Expand All @@ -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,
)
35 changes: 9 additions & 26 deletions routes/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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":
Expand All @@ -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",
)
69 changes: 69 additions & 0 deletions routes/returnHomeFeedData.py
Original file line number Diff line number Diff line change
@@ -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="4")
Copy link
Preview

Copilot AI Apr 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value for 'limit' is set as a string instead of an integer; change it to default=4 to ensure correct type conversion.

Suggested change
limit = request.args.get("limit", type=int, default="4")
limit = request.args.get("limit", type=int, default=4)

Copilot uses AI. Check for mistakes.

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)
2 changes: 1 addition & 1 deletion routes/returnPostAnalyticsData.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
122 changes: 122 additions & 0 deletions static/tailwindUI/js/homeFeed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// 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': '<a href="/category/Games"><i class="ti ti-device-gamepad text-rose-500 hover:text-rose-600 duration-150"></i></a>',
'History': '<a href="/category/History"><i class="ti ti-books text-sky-500 hover:text-sky-600 duration-150"></i></a>',
'Science': '<a href="/category/Science"><i class="ti ti-square-root-2 text-emerald-500 hover:text-emerald-600 duration-150"></i></a>',
'Code': '<a href="/category/Code"><i class="ti ti-code text-indigo-500 hover:text-indigo-600 duration-150"></i></a>',
'Technology': '<a href="/category/Technology"><i class="ti ti-cpu text-slate-500 hover:text-slate-600 duration-150"></i></a>',
'Education': '<a href="/category/Education"><i class="ti ti-school text-blue-500 hover:text-blue-600 duration-150"></i></a>',
'Sports': '<a href="/category/Sports"><i class="ti ti-shirt-sport text-cyan-500 hover:text-cyan-600 duration-150"></i></a>',
'Foods': '<a href="/category/Foods"><i class="ti ti-meat text-lime-500 hover:text-lime-600 duration-150"></i></a>',
'Health': '<a href="/category/Health"><i class="ti ti-heartbeat text-red-500 hover:text-red-600 duration-150"></i></a>',
'Apps': '<a href="/category/Apps"><i class="ti ti-apps text-pink-500 hover:text-pink-600 duration-150"></i></a>',
'Movies': '<a href="/category/Movies"><i class="ti ti-movie text-teal-500 hover:text-teal-600 duration-150"></i></a>',
'Series': '<a href="/category/Series"><i class="ti ti-player-play text-yellow-500 hover:text-yellow-600 duration-150"></i></a>',
'Travel': '<a href="/category/Travel"><i class="ti ti-plane text-zinc-500 hover:text-zinc-600 duration-150"></i></a>',
'Books': '<a href="/category/Books"><i class="ti ti-book text-violet-500 hover:text-violet-600 duration-150"></i></a>',
'Music': '<a href="/category/Music"><i class="ti ti-music text-orange-500 hover:text-orange-600 duration-150"></i></a>',
'Nature': '<a href="/category/Nature"><i class="ti ti-trees text-emerald-500 hover:text-emerald-600 duration-150"></i></a>',
'Art': '<a href="/category/Art"><i class="ti ti-brush text-amber-500 hover:text-amber-600 duration-150"></i></a>',
'Finance': '<a href="/category/Finance"><i class="ti ti-coin text-green-500 hover:text-green-600 duration-150"></i></a>',
'Business': '<a href="/category/Business"><i class="ti ti-tie text-stone-500 hover:text-stone-600 duration-150"></i></a>',
'Web': '<a href="/category/Web"><i class="ti ti-world text-purple-500 hover:text-purple-600 duration-150"></i></a>',
'Other': '<a href="/category/Other"><i class="ti ti-dots text-gray-500 hover:text-gray-600 duration-150"></i></a>',
'Default': '<a href="/"><i class="ti ti-stack-2 text-neutral-500 hover:text-neutral-600 duration-150"></i></a>'
};

// Function to inject html post macro in document body
async function intializePostCard() {
Copy link
Preview

Copilot AI Apr 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function name 'intializePostCard' appears to be misspelled; consider renaming it to 'initializePostCard' for clarity.

Suggested change
async function intializePostCard() {
async function initializePostCard() {

Copilot uses AI. Check for mistakes.

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 dome
Copy link
Preview

Copilot AI Apr 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: 'dome' should be corrected to 'DOM'.

Suggested change
// Register macro in dome
// Register macro in DOM

Copilot uses AI. Check for mistakes.

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");
});
}
// Call initialize post card function to inject post macro
intializePostCard();

// 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) {
Comment on lines +69 to +70
Copy link
Preview

Copilot AI Apr 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable 'posts' is assigned without declaration, which could lead to unintended global scope. Consider declaring it with 'let' or 'const'.

Suggested change
if (connection.ok) {
let posts; // Declare posts locally

Copilot uses AI. Check for mistakes.

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");
// Fetch initial homeFeed
await fetchHomeFeedData();
// Hide spinner
homeSpinner.classList.add("hidden");
}
39 changes: 39 additions & 0 deletions static/tailwindUI/pureHtmlMacro/postCardMacro.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<template id="postCardMacro">
<!-- Macro to generate a post card with details from the given 'post' and 'authorProfilePicture' -->
<div
class="w-[17rem] group flex flex-col justify-between h-min-max mx-auto rounded-md overflow-hidden shadow-lg hover:scale-105 transition duration-150">
<!-- Post Image -->
<div class="overflow-hidden">
<img
class="postBanner h-48 group-hover:scale-110 transition duration-150 object-cover w-max rounded-t-md mx-auto select-none" />
</div>

<!-- Post Title -->
<div class="px-2 mt-1">
<a class="postTitle text-rose-500 text-lg hover:text-rose-500/75 duration-150 break-words font-medium"> </a>
</div>

<!-- Post Content -->
<div class="postContent text-base px-2 overflow-y-hidden h-[7.5rem]">
</div>

<!-- Post Author and Category -->
<div class="flex justify-between text-sm font-medium px-2 select-none">
<!-- Author Section -->
<div class="my-2 flex items-center w-fit gap-2">
<img class="postAuthorPicture block w-8 h-8"/>
<a class="postAuthor"></a>
</div>

<!-- Category Section -->
<div class="flex items-center">
<div class="postCategory mr-1 text-base">

</div>

<!-- Date -->
<p class="postTimeStamp date"></p>
</div>
</div>
</div>
</template>
Loading