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 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")
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -3,29 +3,36 @@
|
||||||
{% 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 %}
|
||||||
|
<div class="contest-item">
|
||||||
<hr>
|
<hr>
|
||||||
<h2>{{ year[0] }}</h2>
|
<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>
|
<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>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
<hr>
|
<hr>
|
||||||
{{ pagination|safe }}
|
<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 %}
|
{% endblock %}
|
Loading…
Reference in a new issue