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 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/<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"],

View file

@ -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);

View file

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