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:
parent
837473fd7a
commit
380041ec1b
3 changed files with 133 additions and 183 deletions
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 %}
|
||||
|
|
Loading…
Reference in a new issue