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:
parent
4bf5ac680d
commit
dbfcb24fbb
3 changed files with 194 additions and 151 deletions
|
@ -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"],
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
Loading…
Reference in a new issue