diff --git a/src/structables/routes/contest.py b/src/structables/routes/contest.py index a60b88f..3c72da6 100644 --- a/src/structables/routes/contest.py +++ b/src/structables/routes/contest.py @@ -1,5 +1,5 @@ -from flask import render_template, request, abort -from urllib.request import urlopen +from flask import render_template, request, abort, url_for +from urllib.request import urlopen, Request from urllib.error import HTTPError from ..utils.helpers import proxy from bs4 import BeautifulSoup @@ -31,7 +31,7 @@ def init_contest_routes(app): for contest in contests: contest_details = { "title": contest["title"], - "link": f"/{contest['urlString']}", + "link": url_for("route_contest", contest=contest["urlString"]), "deadline": contest["deadline"], "startDate": contest["startDate"], "numEntries": contest["numEntries"], @@ -60,57 +60,74 @@ def init_contest_routes(app): contest_list=contest_list, ) + def get_entries(contest): + base_url = f"https://www.instructables.com/api_proxy/search/collections/projects/documents/search" + headers = {"x-typesense-api-key": app.typesense_api_key} + page, per_page = 1, 100 + all_entries = [] + + while True: + try: + url = f"{base_url}?q=*&filter_by=contestPath:{contest}&sort_by=contestEntryDate:desc&per_page={per_page}&page={page}" + request = Request(url, headers=headers) + response = urlopen(request) + data = json.loads(response.read().decode()) + except HTTPError as e: + abort(e.code) + + hits = data.get("hits", []) + if not hits: + break + + all_entries.extend(hits) + if len(hits) < per_page: + break + page += 1 + + return all_entries + @app.route("/contest//") def route_contest(contest): try: data = urlopen(f"https://www.instructables.com/contest/{contest}/") + html = data.read().decode() + soup = BeautifulSoup(html, "html.parser") + + title_tag = soup.find("h1") + title = title_tag.get_text() if title_tag else "Contest" + + img_tag = soup.find("img", alt=lambda x: x and "Banner" in x) + img = img_tag.get("src") if img_tag else "default.jpg" + + entry_count = len(get_entries(contest)) + prizes_items = soup.select("article") + prizes = len(prizes_items) if prizes_items else 0 + + overview_section = soup.find("section", id="overview") + info = ( + overview_section.decode_contents() + if overview_section + else "No Overview" + ) + except HTTPError as e: abort(e.code) - soup = BeautifulSoup(data.read().decode(), "html.parser") - - title = soup.select('meta[property="og:title"]')[0].get("content") - - body = soup.select("div#contest-wrapper")[0] - - img = proxy(body.select("div#contest-masthead img")[0].get("src")) - - entry_count = body.select("li.entries-nav-btn")[0].text.split(" ")[0] - prizes = body.select("li.prizes-nav-btn")[0].text.split(" ")[0] - - info = body.select("div.contest-body-column-left")[0] - info.select("div#site-announcements-page")[0].decompose() - info.select("h3")[0].decompose() - info.select("div#contest-body-nav")[0].decompose() - info = str(info).replace("https://www.instructables.com", "/") - - body.select("span.contest-entity-count")[0].text - entry_list = [] - 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 - author = entry.select("div span.ible-author a")[0].text - author_link = entry.select("div span.ible-author a")[0].get("href") - channel = entry.select("div span.ible-channel a")[0].text - channel_link = entry.select("div span.ible-channel a")[0].get("href") - views = entry.select(".ible-views")[0].text - - entry_list.append( - { - "link": link, - "entry_img": entry_img, - "entry_title": entry_title, - "author": author, - "author_link": author_link, - "channel": channel, - "channel_link": channel_link, - "views": views, + entries = get_entries(contest) + for entry in entries: + doc = entry["document"] + entry_details = { + "link": url_for("route_article", article=doc["urlString"]), + "entry_img": doc["coverImageUrl"], + "entry_title": doc["title"], + "author": doc["screenName"], + "author_link": url_for("route_member", member=doc["screenName"]), + "channel": doc["channel"][0], + "channel_link": f"/{doc['primaryClassification']}", + "views": doc.get("views", 0), } - ) + entry_list.append(entry_details) return render_template( "contest.html", @@ -139,7 +156,7 @@ def init_contest_routes(app): contest_list = [] for contest in contests: contest_details = { - "link": f"/{contest['urlString']}", + "link": url_for("route_contest", contest=contest["urlString"]), "img": proxy(contest["bannerUrlMedium"]), "alt": contest["title"], "title": contest["title"], diff --git a/src/structables/static/css/style.css b/src/structables/static/css/style.css index 86471cd..b3c1041 100644 --- a/src/structables/static/css/style.css +++ b/src/structables/static/css/style.css @@ -6,9 +6,6 @@ --code-bg: #20232a; --code-color: #969ba6; --border-color: lightgrey; - --background-color: #1e1e2e; - --card-background: #2b2e3b; - --text-color: #f0f0f0; } body { @@ -20,7 +17,6 @@ body { color: var(--main-color); padding: 0 10px; hyphens: auto; - background-color: var(--background-color); } a { @@ -67,94 +63,92 @@ blockquote { margin-right: 10px; } -.contest-list { - display: flex; - flex-wrap: wrap; - justify-content: space-around; - gap: 20px; - margin: 20px 0; -} - -.contest-list-item { - background-color: var(--card-background); - padding: 20px; - border: 1px solid var(--border-color); - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - max-width: 500px; - color: var(--text-color); - text-align: center; -} - -.contest-list-item img { - max-width: 100%; - height: auto; - border-radius: 4px; - margin-bottom: 10px; -} - -.closed-contests { - margin-top: 40px; -} - -.closed-contest { - margin-bottom: 30px; - text-align: center; -} - -.closed-contest-contest { - max-width: 100%; - height: auto; - border-radius: 8px; - margin-bottom: 15px; -} - -.closed-contest-winner { - display: inline-block; - max-width: 200px; - margin: 10px; - text-align: center; - color: var(--text-color); -} - -.closed-contest-winner-img { - max-width: 100%; - height: auto; - border-radius: 4px; - margin-bottom: 5px; -} - -.ible-small { - font-size: 0.9em; - color: #ccc; -} - -.container { - margin-top: 20px !important; -} - -header { - margin-bottom: 50px; -} - -.wrap { - word-wrap: break-word; - overflow-wrap: break-word; -} - .navbar-logo { height: 64px; } -.img-fluid, -video, -iframe { +.card { + background-color: #fff; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + overflow: hidden; + display: flex; + flex-direction: column; +} + +.card-body { + flex: 1; + padding: 1rem; +} + +.card-footer { + padding: 1rem; + background-color: #f8f9fa; + border-top: 1px solid #ddd; + margin-top: auto; +} + +.card-img-top { + max-width: 100%; + height: auto; + object-fit: cover; +} + +.navbar-collapse { + display: flex; + justify-content: space-between; +} + +.navbar-nav { + flex-direction: row; +} + +.nav-item { + margin-left: 1rem; +} + +@media (max-width: 768px) { + .navbar-nav { + flex-direction: column; + align-items: center; + } + + .nav-item { + margin-left: 0; + margin-bottom: 0.5rem; + } + + .navbar-collapse { + flex-direction: column; + align-items: center; + } + + .form-control { + width: 100%; + margin-bottom: 0.5rem; + } +} + +.contest-list { + display: flex; + flex-direction: column; + gap: 20px; +} + +.contest-item { + background-color: #fff; + padding: 20px; + border: 1px solid var(--border-color); border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } -.text-muted { - color: #6c757d !important; +.contest-item img { + max-width: 100%; + height: auto; + border-radius: 4px; + margin-top: 10px; } .pagination { @@ -199,6 +193,14 @@ iframe { cursor: not-allowed; } +.container { + margin-top: 20px !important; +} + +header { + margin-bottom: 50px; +} + .go_here_link { background-color: #4caf50; border: none; @@ -214,6 +216,30 @@ iframe { color: black; } +.wrap { + word-wrap: break-word; + overflow-wrap: break-word; +} + +.img-fluid { + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +video { + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +iframe { + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.text-muted { + color: #6c757d !important; +} + .sitemap-group { margin-top: 2em; display: inline-block; @@ -227,7 +253,7 @@ iframe { } .sitemap-group .card { - background-color: var(--background-color); + background-color: var(--primary-bg); border: 1px solid var(--border-color); border-radius: 8px; box-shadow: 0 2px 4px var(--shadow-color); diff --git a/src/structables/templates/contest.html b/src/structables/templates/contest.html index 95addd8..8ff9bb4 100644 --- a/src/structables/templates/contest.html +++ b/src/structables/templates/contest.html @@ -1,23 +1,23 @@ - {% extends "base.html" %} {% block content %} -
- {{ title }} -

{{ entry_count }} Entries, {{ prizes }} Prizes

-
- {{ info|safe }} -
- {% for ible in entries %} - - {% endfor %} -
-
-{% endblock %} \ No newline at end of file +
+

{{ title }}

+ {{ title }} +

{{ entry_count }} Entries, {{ prizes }} Prizes

+
+ {{ info|safe }} +
+ {% for ible in entry_list %} + + {% endfor %} +
+
+{% endblock %}