feat(contests): revamp contest archive UX with pagination

Reworked contest archive retrieval and display to enhance user experience by implementing JSON API data fetch and pagination. Simplified archive layout with modern styling and improved error handling.

- Switched from BeautifulSoup to JSON API for data fetching
- Added pagination control with page limit and offset
- Refactored UI elements for a cleaner look in archives page
- Updated CSS for better visuals and responsiveness
This commit is contained in:
Kumi 2024-10-02 13:32:53 +02:00
parent 837473fd7a
commit 380041ec1b
Signed by: kumi
GPG key ID: ECBCC9082395383F
3 changed files with 133 additions and 183 deletions

View file

@ -3,50 +3,59 @@ from urllib.request import urlopen
from urllib.error import HTTPError
from ..utils.helpers import proxy
from bs4 import BeautifulSoup
import json
def init_contest_routes(app):
@app.route("/contest/archive/")
def route_contest_archive():
page = 1
if request.args.get("page") is not None:
page = request.args.get("page")
# Default pagination settings
limit = 10
page = request.args.get("page", default=1, type=int)
offset = (page - 1) * limit
try:
data = urlopen(f"https://www.instructables.com/contest/archive/?page={page}")
# Fetch data using urlopen
url = f"https://www.instructables.com/json-api/getClosedContests?limit={limit}&offset={offset}"
response = urlopen(url)
data = json.loads(response.read().decode())
except HTTPError as e:
abort(e.code)
except Exception as e:
abort(500) # Handle other exceptions like JSON decode errors
soup = BeautifulSoup(data.read().decode(), "html.parser")
main = soup.select("div#contest-archive-wrapper")[0]
contest_count = main.select("p.contest-count")[0].text
contests = data.get("contests", [])
full_list_size = data.get("fullListSize", 0)
contest_list = []
for index, year in enumerate(main.select("div.contest-archive-list h2")):
year_list = main.select(
"div.contest-archive-list div.contest-archive-list-year"
)[index]
year_name = year.text
month_list = []
for month in year_list.select("div.contest-archive-list-month"):
month_name = month.select("h3")[0].text
month_contest_list = []
for p in month.select("p"):
date = p.select("span")[0].text
link = p.select("a")[0].get("href")
title = p.select("a")[0].text
month_contest_list.append([date, link, title])
month_list.append([month_name, month_contest_list])
contest_list.append([year_name, month_list])
for contest in contests:
contest_details = {
"title": contest["title"],
"link": f"https://www.instructables.com/{contest['urlString']}",
"deadline": contest["deadline"],
"startDate": contest["startDate"],
"numEntries": contest["numEntries"],
"state": contest["state"],
"bannerUrl": contest["bannerUrlMedium"],
}
contest_list.append(contest_details)
pagination = main.select("nav.pagination ul.pagination")[0]
# Calculate total pages
total_pages = (full_list_size + limit - 1) // limit
# Create pagination
pagination = {
"current_page": page,
"total_pages": total_pages,
"has_prev": page > 1,
"has_next": page < total_pages,
"limit": limit
}
return render_template(
"archives.html",
title=f"Contest Archives (Page {page})",
page=page,
contest_count=contest_count,
pagination=pagination,
contest_list=contest_list,
)
@ -78,7 +87,9 @@ def init_contest_routes(app):
body.select("span.contest-entity-count")[0].text
entry_list = []
for entry in body.select("div.contest-entries-list div.contest-entries-list-ible"):
for entry in body.select(
"div.contest-entries-list div.contest-entries-list-ible"
):
link = entry.a["href"]
entry_img = proxy(entry.select("a noscript img")[0].get("src"))
entry_title = entry.select("a.ible-title")[0].text
@ -120,14 +131,16 @@ def init_contest_routes(app):
soup = BeautifulSoup(data.read().decode(), "html.parser")
contest_count = str(soup.select("p.contest-count")[0])
contest_count = "0"
contests = []
for contest in soup.select("div#cur-contests div.row-fluid div.contest-banner"):
link = contest.select("div.contest-banner-inner a")[0].get("href")
img = proxy(contest.select("div.contest-banner-inner a img")[0].get("src"))
alt = contest.select("div.contest-banner-inner a img")[0].get("alt")
deadline = contest.select("span.contest-meta-deadline")[0].get("data-deadline")
deadline = contest.select("span.contest-meta-deadline")[0].get(
"data-deadline"
)
prizes = contest.select("span.contest-meta-count")[0].text
entries = contest.select("span.contest-meta-count")[1].text
@ -150,7 +163,9 @@ def init_contest_routes(app):
featured_items = []
for featured_item in display.select("ul.featured-items li"):
item_link = featured_item.select("div.ible-thumb a")[0].get("href")
item_img = proxy(featured_item.select("div.ible-thumb a img")[0].get("src"))
item_img = proxy(
featured_item.select("div.ible-thumb a img")[0].get("src")
)
item_title = featured_item.select("a.title")[0].text
item_author = featured_item.select("a.author")[0].text
item_author_link = featured_item.select("a.author")[0].get("href")
@ -174,4 +189,4 @@ def init_contest_routes(app):
contest_count=contest_count,
contests=contests,
closed=closed,
)
)

View file

@ -130,144 +130,67 @@ blockquote {
}
}
.ibles {
display: inline-block;
vertical-align: top;
.contest-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.ible-small {
font-size: 0.7em;
font-weight: thin;
line-height: 1em;
}
.step-section {
background-color: var(--primary-bg);
.contest-item {
background-color: #fff;
padding: 20px;
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 2px 4px var(--shadow-color);
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.step-header h2 {
font-size: 2em;
color: var(--heading-color);
.contest-item img {
max-width: 100%;
height: auto;
border-radius: 4px;
margin-top: 10px;
}
.step-images img,
.step-videos video,
.step-iframes iframe {
border-radius: 8px;
box-shadow: 0 2px 4px var(--shadow-color);
}
.step-text {
font-size: 1.1em;
}
.reply-button,
.replies {
display: none;
}
.reply-button+label {
position: relative;
display: block;
cursor: pointer;
}
input.reply-button:checked+label+.replies {
.pagination {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
}
.member-list {
display: inline-block;
max-width: 200px;
vertical-align: top;
}
.ible-list-item {
display: inline-block;
max-width: 350px;
vertical-align: top;
margin-bottom: 2rem;
}
.contest-list-item {
display: inline-block;
max-width: 500px;
vertical-align: top;
margin-bottom: 2rem;
}
.archive-month-wrapper {
display: inline-block;
width: 30vw;
vertical-align: top;
}
.archive-month {
display: flex;
flex-direction: column;
gap: -10px;
margin-bottom: 1rem;
justify-content: space-between;
}
.archive {
margin-bottom: -20px;
}
ul.pagination {
display: flex;
justify-content: space-around;
padding: 0 33vw;
justify-content: center;
padding: 10px 0;
list-style-type: none;
}
.pagination-list {
display: flex;
align-items: center;
gap: 10px;
}
ul.pagination li.active a,
ul.pagination li.disabled a,
ul.pagination li.active a:hover,
ul.pagination li.disabled a:hover {
color: #bbc2cf;
.pagination-list li {
display: inline-block;
}
.pagination-list a {
color: var(--main-color);
text-decoration: none;
padding: 5px 10px;
border-radius: 4px;
border: 1px solid transparent;
transition: all 0.3s ease;
}
.closed-contest-contest {
object-fit: cover;
width: 33vw;
height: 15vw;
display: inline-block;
vertical-align: top;
padding: 0 10px;
.pagination-list a:hover {
border-color: var(--link-color);
color: var(--link-color);
}
.closed-contest-winner,
.closed-contest-winner-img {
width: 15vw;
display: inline-block;
vertical-align: top;
padding: 0 10px;
font-size: 0.8em;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
.pagination-list li.active a {
background-color: var(--link-color);
color: #fff;
border-color: var(--link-color);
}
.sitemap-group {
margin-top: 2em;
display: inline-block;
width: 30vw;
text-align: left;
vertical-align: top;
}
.sitemap-group h2 {
text-align: center;
.pagination-list li.disabled a {
color: #ccc;
cursor: not-allowed;
}
.container {
@ -280,7 +203,6 @@ header {
.go_here_link {
background-color: #4caf50;
/* Green */
border: none;
color: white;
padding: 15px 32px;
@ -299,10 +221,6 @@ header {
overflow-wrap: break-word;
}
.navbar-logo {
height: 64px;
}
.img-fluid {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
@ -322,7 +240,17 @@ iframe {
color: #6c757d !important;
}
.sitemap-group {
margin-top: 2em;
display: inline-block;
width: 30vw;
text-align: left;
vertical-align: top;
}
.sitemap-group h2 {
text-align: center;
}
.sitemap-group .card {
background-color: var(--primary-bg);

View file

@ -1,31 +1,38 @@
{% extends "base.html" %}
{% block content %}
<center>
<h1>Past Contests</h1>
<p>{{ contest_count }}</p>
<p><a href="/contest/">See running contests</a></p>
</center>
{% for year in contest_list %}
<div class="archive-year">
<hr>
<h2>{{ year[0] }}</h2>
<hr>
{% for month in year[1] %}
<div class="archive-month-wrapper">
<div class="archive-month">
<h3>{{ month[0] }}</h3>
{% for contest in month[1] %}
<div class="archive">
<p>{{ contest[0] }}<a href="{{ contest[1] }}">{{ contest[2] }}</a></p>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
<br>
</div>
{% endfor %}
<hr>
{{ pagination|safe }}
{% endblock %}
<center>
<h1>Past Contests</h1>
<p>Total Contests: {{ pagination.total_pages * pagination.limit }}</p>
<p><a href="/contest/">See running contests</a></p>
</center>
<div class="contest-list">
{% for contest in contest_list %}
<div class="contest-item">
<hr>
<h2>{{ contest.title }}</h2>
<p>
<a href="{{ contest.link }}">{{ contest.title }}</a><br>
Start Date: {{ contest.startDate }}<br>
Deadline: {{ contest.deadline }}<br>
Entries: {{ contest.numEntries }}<br>
Status: {{ contest.state }}<br>
</p>
<img src="{{ contest.bannerUrl }}" alt="{{ contest.title }} banner" style="max-width:100%;">
<hr>
</div>
{% endfor %}
</div>
<hr>
<div class="pagination">
<ul class="pagination-list">
{% if pagination.has_prev %}
<li><a href="?page={{ pagination.current_page - 1 }}">Previous</a></li>
{% endif %}
<li>Page {{ pagination.current_page }} of {{ pagination.total_pages }}</li>
{% if pagination.has_next %}
<li><a href="?page={{ pagination.current_page + 1 }}">Next</a></li>
{% endif %}
</ul>
</div>
{% endblock %}