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 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"],
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<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 entries %}
|
||||
{% 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 }}">
|
||||
|
|
Loading…
Reference in a new issue