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

View file

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

View file

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