feat(contest): enhance contest data handling and UI

Refactored endpoint for contest details to improve data fetching by introducing a dedicated function for retrieving contest entries via API calls. Replaced static URL strings with dynamic `url_for` to enhance routing flexibility. Updated HTML template and stylesheet to improve layout and maintainability, shifting to a more modern and responsive design. Reduced CSS complexity by removing unnecessary styles. These changes enhance usability and prepare for future layout enhancements.
This commit is contained in:
Kumi 2024-10-04 07:06:26 +02:00
parent 4bf5ac680d
commit dbfcb24fbb
Signed by: kumi
GPG key ID: ECBCC9082395383F
3 changed files with 194 additions and 151 deletions

View file

@ -1,5 +1,5 @@
from flask import render_template, request, abort from flask import render_template, request, abort, url_for
from urllib.request import urlopen from urllib.request import urlopen, Request
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
@ -31,7 +31,7 @@ def init_contest_routes(app):
for contest in contests: for contest in contests:
contest_details = { contest_details = {
"title": contest["title"], "title": contest["title"],
"link": f"/{contest['urlString']}", "link": url_for("route_contest", contest=contest["urlString"]),
"deadline": contest["deadline"], "deadline": contest["deadline"],
"startDate": contest["startDate"], "startDate": contest["startDate"],
"numEntries": contest["numEntries"], "numEntries": contest["numEntries"],
@ -60,57 +60,74 @@ def init_contest_routes(app):
contest_list=contest_list, 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/<contest>/") @app.route("/contest/<contest>/")
def route_contest(contest): def route_contest(contest):
try: try:
data = urlopen(f"https://www.instructables.com/contest/{contest}/") 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: except HTTPError as e:
abort(e.code) 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 = [] entry_list = []
for entry in body.select( entries = get_entries(contest)
"div.contest-entries-list div.contest-entries-list-ible" for entry in entries:
): doc = entry["document"]
link = entry.a["href"] entry_details = {
entry_img = proxy(entry.select("a noscript img")[0].get("src")) "link": url_for("route_article", article=doc["urlString"]),
entry_title = entry.select("a.ible-title")[0].text "entry_img": doc["coverImageUrl"],
author = entry.select("div span.ible-author a")[0].text "entry_title": doc["title"],
author_link = entry.select("div span.ible-author a")[0].get("href") "author": doc["screenName"],
channel = entry.select("div span.ible-channel a")[0].text "author_link": url_for("route_member", member=doc["screenName"]),
channel_link = entry.select("div span.ible-channel a")[0].get("href") "channel": doc["channel"][0],
views = entry.select(".ible-views")[0].text "channel_link": f"/{doc['primaryClassification']}",
"views": doc.get("views", 0),
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,
} }
) entry_list.append(entry_details)
return render_template( return render_template(
"contest.html", "contest.html",
@ -139,7 +156,7 @@ def init_contest_routes(app):
contest_list = [] contest_list = []
for contest in contests: for contest in contests:
contest_details = { contest_details = {
"link": f"/{contest['urlString']}", "link": url_for("route_contest", contest=contest["urlString"]),
"img": proxy(contest["bannerUrlMedium"]), "img": proxy(contest["bannerUrlMedium"]),
"alt": contest["title"], "alt": contest["title"],
"title": contest["title"], "title": contest["title"],

View file

@ -6,9 +6,6 @@
--code-bg: #20232a; --code-bg: #20232a;
--code-color: #969ba6; --code-color: #969ba6;
--border-color: lightgrey; --border-color: lightgrey;
--background-color: #1e1e2e;
--card-background: #2b2e3b;
--text-color: #f0f0f0;
} }
body { body {
@ -20,7 +17,6 @@ body {
color: var(--main-color); color: var(--main-color);
padding: 0 10px; padding: 0 10px;
hyphens: auto; hyphens: auto;
background-color: var(--background-color);
} }
a { a {
@ -67,94 +63,92 @@ blockquote {
margin-right: 10px; 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 { .navbar-logo {
height: 64px; height: 64px;
} }
.img-fluid, .card {
video, background-color: #fff;
iframe { 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; 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);
} }
.text-muted { .contest-item img {
color: #6c757d !important; max-width: 100%;
height: auto;
border-radius: 4px;
margin-top: 10px;
} }
.pagination { .pagination {
@ -199,6 +193,14 @@ iframe {
cursor: not-allowed; cursor: not-allowed;
} }
.container {
margin-top: 20px !important;
}
header {
margin-bottom: 50px;
}
.go_here_link { .go_here_link {
background-color: #4caf50; background-color: #4caf50;
border: none; border: none;
@ -214,6 +216,30 @@ iframe {
color: black; 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 { .sitemap-group {
margin-top: 2em; margin-top: 2em;
display: inline-block; display: inline-block;
@ -227,7 +253,7 @@ iframe {
} }
.sitemap-group .card { .sitemap-group .card {
background-color: var(--background-color); 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 var(--shadow-color);

View file

@ -1,23 +1,23 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<center> <center>
<img src="{{ img }}" alt="{{ title }}" style="max-width:98vw;"> <h1>{{ title }}</h1>
<p>{{ entry_count }} Entries, {{ prizes }} Prizes</p> <img src="{{ img }}" alt="{{ title }}" style="max-width:98vw;">
<br> <p>{{ entry_count }} Entries, {{ prizes }} Prizes</p>
{{ info|safe }} <br>
<div class="ible-list"> {{ info|safe }}
{% for ible in entries %} <div class="ible-list">
<div class="ible-list-item"> {% for ible in entry_list %}
<a href="{{ ible.link }}" style="color:#bbc2cf;"> <div class="ible-list-item">
<img style="max-width:350px;" src="{{ ible.entry_img }}" alt="{{ ible.entry_title }}"> <a href="{{ ible.link }}" style="color:#bbc2cf;">
<p>{{ ible.entry_title }}</p> <img style="max-width:350px;" src="{{ ible.entry_img }}" alt="{{ ible.entry_title }}">
</a> <p>{{ ible.entry_title }}</p>
<p>by <a href="{{ ible.author_link }}">{{ ible.author }}</a> in <a href="{{ ible.channel_link }}">{{ ible.channel }}</a></p> </a>
<p>{{ ible.views }} Views</p> <p>by <a href="{{ ible.author_link }}">{{ ible.author }}</a> in <a href="{{ ible.channel_link }}">{{ ible.channel }}</a></p>
</div> <p>{{ ible.views }} Views</p>
{% endfor %} </div>
</div> {% endfor %}
</center> </div>
{% endblock %} </center>
{% endblock %}