From 380041ec1b46ee2e7389022cc52e50389f32a7f5 Mon Sep 17 00:00:00 2001 From: Kumi Date: Wed, 2 Oct 2024 13:32:53 +0200 Subject: [PATCH] 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 --- src/structables/routes/contest.py | 79 ++++++----- src/structables/static/css/style.css | 174 +++++++----------------- src/structables/templates/archives.html | 63 +++++---- 3 files changed, 133 insertions(+), 183 deletions(-) diff --git a/src/structables/routes/contest.py b/src/structables/routes/contest.py index 48fef2f..3c88e13 100644 --- a/src/structables/routes/contest.py +++ b/src/structables/routes/contest.py @@ -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, - ) \ No newline at end of file + ) diff --git a/src/structables/static/css/style.css b/src/structables/static/css/style.css index 22d85b5..b3c1041 100644 --- a/src/structables/static/css/style.css +++ b/src/structables/static/css/style.css @@ -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); diff --git a/src/structables/templates/archives.html b/src/structables/templates/archives.html index 21d03eb..73da09b 100644 --- a/src/structables/templates/archives.html +++ b/src/structables/templates/archives.html @@ -1,31 +1,38 @@ {% extends "base.html" %} {% block content %} -
-

Past Contests

-

{{ contest_count }}

-

See running contests

-
- {% for year in contest_list %} -
-
-

{{ year[0] }}

-
- {% for month in year[1] %} -
-
-

{{ month[0] }}

- {% for contest in month[1] %} -
-

{{ contest[0] }}{{ contest[2] }}

-
- {% endfor %} -
-
- {% endfor %} -
-
- {% endfor %} -
- {{ pagination|safe }} -{% endblock %} \ No newline at end of file +
+

Past Contests

+

Total Contests: {{ pagination.total_pages * pagination.limit }}

+

See running contests

+
+
+ {% for contest in contest_list %} +
+
+

{{ contest.title }}

+

+ {{ contest.title }}
+ Start Date: {{ contest.startDate }}
+ Deadline: {{ contest.deadline }}
+ Entries: {{ contest.numEntries }}
+ Status: {{ contest.state }}
+

+ {{ contest.title }} banner +
+
+ {% endfor %} +
+
+ +{% endblock %}