Add ids to tags; use pygments to color JSON

This commit is contained in:
Joakim Hellsén 2025-09-07 22:31:31 +02:00
commit 8f438aca2d
18 changed files with 365 additions and 211 deletions

View file

@ -17,6 +17,7 @@ dependencies = [
"platformdirs>=4.3.8",
"python-dotenv>=1.1.1",
"psycopg[binary]>=3.2.3",
"pygments>=2.19.2",
]
[dependency-groups]

View file

@ -3,15 +3,15 @@
Login
{% endblock title %}
{% block content %}
<h4>Login</h4>
<h4 id="page-title">Login</h4>
{% if form.errors %}
<ul>
<ul id="error-list">
{% for field, errors in form.errors.items %}
{% for error in errors %}<li>{{ error }}</li>{% endfor %}
{% endfor %}
</ul>
{% endif %}
<form method="post">
<form id="login-form" method="post">
{% csrf_token %}
<label for="{{ form.username.id_for_label }}">Username</label>
<input type="text"
@ -24,10 +24,10 @@
name="password"
id="{{ form.password.id_for_label }}"
required>
<button type="submit">Login</button>
<button id="login-button" type="submit">Login</button>
</form>
<p>
Don't have an account? <a href="{% url 'accounts:signup' %}">Sign up here</a>
Don't have an account? <a id="signup-link" href="{% url 'accounts:signup' %}">Sign up here</a>
</p>
<style>
.form-control {

View file

@ -3,9 +3,11 @@
{{ user.username }}
{% endblock title %}
{% block content %}
<h2>{{ user.username }}</h2>
<p>Joined {{ user.date_joined|date:"F d, Y" }}</p>
<table>
<h2 id="username">{{ user.username }}</h2>
<p>
Joined <time id="date-joined">{{ user.date_joined|date:"F d, Y" }}</time>
</p>
<table id="user-info-table">
<tr>
<td>
<strong>Date Joined:</strong>
@ -25,12 +27,12 @@
<td>{{ user.email|default:"Not provided" }}</td>
</tr>
</table>
<a href="{% url 'accounts:logout' %}">Logout</a>
<a id="logout-link" href="{% url 'accounts:logout' %}">Logout</a>
<h2>Will get notifications for these subscriptions:</h2>
<h3>Games</h3>
<ul>
<h3 id="games-subscriptions-header">Games</h3>
<ul id="games-subscriptions-list">
{% for item in games_with_inheritance %}
<li>
<li id="game-subscription-{{ item.game.id }}">
<a href="{% url 'twitch:game_detail' item.game.id %}">{{ item.game.display_name }}</a>
{% if item.is_inherited %}
<span style="font-size: 0.85em; color: #666; font-style: italic;">(inherited from {{ item.inherited_from }})</span>
@ -40,10 +42,10 @@
<li>You have no game subscriptions yet.</li>
{% endfor %}
</ul>
<h3>Organizations</h3>
<ul>
<h3 id="org-subscriptions-header">Organizations</h3>
<ul id="org-subscriptions-list">
{% for subscription in org_subscriptions %}
<li>
<li id="org-subscription-{{ subscription.organization_id }}">
<a href="{% url 'twitch:organization_detail' subscription.organization_id %}">{{ subscription.organization.name }}</a>
</li>
{% empty %}

View file

@ -3,15 +3,15 @@
Sign Up
{% endblock title %}
{% block content %}
<h4>Sign Up</h4>
<h4 id="page-title">Sign Up</h4>
{% if form.errors %}
<ul>
<ul id="error-list">
{% for field, errors in form.errors.items %}
{% for error in errors %}<li>{{ error }}</li>{% endfor %}
{% endfor %}
</ul>
{% endif %}
<form method="post">
<form id="signup-form" method="post">
{% csrf_token %}
<label for="{{ form.username.id_for_label }}">Username</label>
<input type="text"
@ -19,22 +19,22 @@
id="{{ form.username.id_for_label }}"
value="{{ form.username.value|default:'' }}"
required>
{% if form.username.help_text %}{{ form.username.help_text }}{% endif %}
{% if form.username.help_text %}<small id="username-help">{{ form.username.help_text }}</small>{% endif %}
<label for="{{ form.password1.id_for_label }}">Password</label>
<input type="password"
name="password1"
id="{{ form.password1.id_for_label }}"
required>
{% if form.password1.help_text %}{{ form.password1.help_text }}{% endif %}
{% if form.password1.help_text %}<small id="password1-help">{{ form.password1.help_text }}</small>{% endif %}
<label for="{{ form.password2.id_for_label }}">Confirm Password</label>
<input type="password"
name="password2"
id="{{ form.password2.id_for_label }}"
required>
{% if form.password2.help_text %}{{ form.password2.help_text }}{% endif %}
<button type="submit">Sign Up</button>
{% if form.password2.help_text %}<small id="password2-help">{{ form.password2.help_text }}</small>{% endif %}
<button id="signup-button" type="submit">Sign Up</button>
</form>
<p>
Already have an account? <a href="{% url 'accounts:login' %}">Login here</a>
Already have an account? <a id="login-link" href="{% url 'accounts:login' %}">Login here</a>
</p>
{% endblock content %}

View file

@ -39,7 +39,33 @@
</title>
<style>
html { color-scheme: light dark; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; line-height: 1.4; padding: 0 15px; font-size: 115%;}
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; line-height: 1.4; padding: 0; font-size: 115%; max-width: 75%; margin: 0 auto;}
@media (max-width: 900px) { body { max-width: 95%; } }
table { width: 100%; }
th, td { padding: 8px; text-align: left; vertical-align: middle; }
th {background-color: Canvas; color: CanvasText; font-weight: bold; }
tr:nth-child(even) { background-color: color-mix(in srgb, Canvas 95%, CanvasText 5%); }
td img { display: block; height: 160px; width: 160px; object-fit: cover; border-radius: 4px; }
@media (prefers-color-scheme: dark) {
.highlight { background: #0d1117; color: #E6EDF3; }
.highlight .p { color: #E6EDF3; }
.highlight .nt { color: #7EE787; }
.highlight .s2, .highlight .mi { color: #A5D6FF; }
.highlight .kc { color: #79C0FF; }
.highlight .w { color: #6E7681; }
}
@media (prefers-color-scheme: light) {
.highlight { background: #f6f8fa; color: #24292e; }
.highlight .p { color: #24292e; }
.highlight .nt { color: #005cc5; }
.highlight .s2, .highlight .mi { color: #032f62; }
.highlight .kc { color: #d73a49; }
.highlight .w { color: #6a737d; }
}
</style>
</head>
<body>
@ -47,17 +73,7 @@
<a href="{% url 'twitch:campaign_list' %}">Campaigns</a> |
<a href="{% url 'twitch:game_list' %}">Games</a> |
<a href="{% url 'twitch:org_list' %}">Organizations</a> |
<a href="{% url 'twitch:docs_rss' %}">RSS Docs</a> |
<form action="{% url 'twitch:search' %}"
method="get"
style="display: inline">
<input type="search"
name="q"
placeholder="Search..."
value="{{ request.GET.q }}">
<button type="submit">Search</button>
</form>
|
<a href="{% url 'twitch:docs_rss' %}">RSS</a> |
{% if user.is_authenticated %}
<a href="{% url 'twitch:debug' %}">Debug</a> |
{% if user.is_staff %}
@ -68,6 +84,16 @@
<a href="{% url 'accounts:login' %}">Login</a> |
<a href="{% url 'accounts:signup' %}">Sign Up</a>
{% endif %}
|
<form action="{% url 'twitch:search' %}"
method="get"
style="display: inline">
<input type="search"
name="q"
placeholder="Search..."
value="{{ request.GET.q }}">
<button type="submit">Search</button>
</form>
{% if messages %}
<ul>
{% for message in messages %}

View file

@ -4,96 +4,136 @@
{{ campaign.clean_name }}
{% endblock title %}
{% block content %}
<h1>
<a href="{% url 'twitch:game_detail' campaign.game.id %}">{{ campaign.game.display_name }}</a> - {{ campaign.clean_name }}
</h1>
{% if campaign.owner %}
<p>
<a href="{% url 'twitch:organization_detail' campaign.owner.id %}">{{ campaign.owner.name }}</a>
</p>
<!-- Campaign Title -->
{% if campaign.game %}
<h1 id="campaign-title">
<a href="{% url 'twitch:game_detail' campaign.game.id %}">{{ campaign.game.name }}</a> - {{ campaign.clean_name }}
</h1>
{% else %}
<p>Organization Unknown</p>
<h1 id="campaign-title">{{ campaign.clean_name }}</h1>
{% endif %}
{% if owner %}
<p id="campaign-owner">
<a href="{% url 'twitch:organization_detail' owner.id %}">{{ owner.name }}</a>
</p>
{% endif %}
<!-- Campaign image -->
{% if campaign.image_url %}
<img height="70"
width="70"
<img id="campaign-image"
height="160"
width="160"
src="{{ campaign.image_url }}"
alt="{{ campaign.name }}">
{% endif %}
<p>{{ campaign.description|linebreaksbr }}</p>
<p>
Start:
{{ campaign.start_at }}
</p>
<p>
End:
{{ campaign.end_at }}
</p>
<!-- Campaign description -->
<p id="campaign-description">{{ campaign.description|linebreaksbr }}</p>
<!-- Campaign end times -->
<div>
<time id="campaign-end-time"
datetime="{{ campaign.end_at|date:'c' }}"
title="{{ campaign.end_at|date:'DATETIME_FORMAT' }}">
<strong>Ends in</strong> {{ campaign.end_at|timeuntil }}
</time>
</div>
<!-- Campaign start times -->
<div>
<time id="campaign-start-time"
datetime="{{ campaign.start_at|date:'c' }}"
title="{{ campaign.start_at|date:'DATETIME_FORMAT' }}">
<strong>Started</strong> {{ campaign.start_at|timesince }} ago
</time>
</div>
<!-- Campaign added times -->
<div>
<time id="campaign-added-time"
datetime="{{ campaign.added_at|date:'c' }}"
title="{{ campaign.added_at|date:'DATETIME_FORMAT' }}">
<strong>Scraped</strong> {{ campaign.added_at|timesince }} ago
</time>
</div>
<!-- Campaign duration -->
<div>
<time id="campaign-duration"
datetime="{{ campaign.start_at|date:'c' }} to {{ campaign.end_at|date:'c' }}"
title="{{ campaign.start_at|date:'DATETIME_FORMAT' }} to {{ campaign.end_at|date:'DATETIME_FORMAT' }}">
<strong>Duration</strong> {{ campaign.start_at|timesince:campaign.end_at }} ago
</time>
</div>
<!-- Campaign Detail URL -->
{% if campaign.details_url %}
{# TODO: Archive this URL automatically #}
<p>
<a href="{{ campaign.details_url }}" target="_blank">Official Details</a>
<a id="campaign-details-url"
href="{{ campaign.details_url }}"
target="_blank">Official Details</a>
</p>
{% endif %}
<!-- Campaign Account Link URL -->
{% if campaign.account_link_url %}
{# TODO: Archive this URL automatically #}
<p>
<a href="{{ campaign.account_link_url }}" target="_blank">Connect Account</a>
<a id="campaign-account-link-url"
href="{{ campaign.account_link_url }}"
target="_blank">Connect Account</a>
</p>
{% endif %}
<h5>Campaign Info</h5>
{% if user.is_staff %}
<p>
{% if campaign.is_account_connected %}
Connected
{% else %}
Not Connected
{% endif %}
</p>
{% endif %}
{% if drops %}
<table>
<tr>
<th>Image</th>
<th>Name</th>
<th>Requirements</th>
<th>Availability</th>
</tr>
{% for drop in drops %}
<table id="drops-table" style="border-collapse: collapse; width: 100%;">
<thead>
<tr>
<td>
{% for benefit in drop.benefits.all %}
{% if benefit.image_asset_url %}
<img height="120"
width="120"
style="object-fit: cover"
src="{{ benefit.image_asset_url }}"
alt="{{ benefit.name }}">
{% else %}
<img height="120"
width="120"
style="object-fit: cover"
src="{% static 'images/placeholder.png' %}"
alt="No Image Available">
<th>Benefits</th>
<th>Drop Name</th>
<th>Requirements</th>
<th>Period</th>
</tr>
</thead>
<tbody>
{% for drop in drops %}
<tr id="drop-{{ drop.drop.id }}">
<td>
{% for benefit in drop.drop.benefits.all %}
{% if benefit.image_asset_url %}
<img height="160"
width="160"
style="object-fit: cover;
margin-right: 3px"
src="{{ benefit.image_asset_url }}"
alt="{{ benefit.name }}">
{% else %}
<img height="160"
width="160"
style="object-fit: cover;
margin-right: 3px"
src="{% static 'images/placeholder.png' %}"
alt="No Image Available">
{% endif %}
{% endfor %}
</td>
<td>{{ drop.drop.name }}</td>
<td>
{% if drop.drop.required_minutes_watched %}{{ drop.drop.required_minutes_watched }} minutes watched{% endif %}
{% if drop.drop.required_subs > 0 %}
{% if drop.drop.required_minutes_watched %}and{% endif %}
{{ drop.drop.required_subs }} subscriptions required
{% endif %}
{% endfor %}
</td>
<td>{{ drop.name }}</td>
<td>
{{ drop.required_minutes_watched }} minutes watched
{% if drop.required_subs > 0 %}and {{ drop.required_subs }} subscriptions required{% endif %}
</td>
<td>{{ drop.start_at }} - {{ drop.end_at }}</td>
</tr>
{% endfor %}
{% if not drops %}
<tr>
<td colspan="6">No drops found for this campaign.</td>
</tr>
{% endif %}
</td>
<td>
<div>
{% if drop.local_start %}{{ drop.local_start|date:"M j, Y H:i" }}{% endif %}
{% if drop.local_start and drop.local_end %}-{% endif %}
{% if drop.local_end %}{{ drop.local_end|date:"M j, Y H:i" }}{% endif %}
<small style="color: #666;">{{ drop.timezone_name }}</small>
</div>
<div style="font-size: 0.8em; color: #666;">{{ drop.countdown_text }}</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No drops available for this campaign.</p>
{% endif %}
<pre><code>{{ campaign_data }}</code></pre>
<!-- Campaign JSON -->
{{ campaign_data|safe }}
{% endblock content %}

View file

@ -6,10 +6,11 @@
{% block content %}
<main>
<header>
<h1>Drop Campaigns</h1>
<h1 id="page-title">Drop Campaigns</h1>
<p>Browse all available drop campaigns</p>
</header>
<form method="get"
<form id="filter-form"
method="get"
action="{% url 'twitch:campaign_list' %}"
style="margin-bottom: 2rem;
padding: 1rem;
@ -42,13 +43,14 @@
{% endfor %}
</select>
</div>
<button type="submit">Apply Filters</button>
<button id="apply-filters-button" type="submit">Apply Filters</button>
</div>
</form>
{% if campaigns %}
{% regroup campaigns by game as campaigns_by_game %}
{% for game_group in campaigns_by_game %}
<section style="margin-bottom: 3rem;">
<section id="game-group-{{ game_group.grouper.id }}"
style="margin-bottom: 3rem">
<div style="display: flex; gap: 1rem;">
<div style="flex-shrink: 0;">
{% if game_group.grouper.box_art_base_url %}
@ -76,13 +78,15 @@
{% comment %} Find this header section in your template {% endcomment %}
<header style="margin-bottom: 1rem;">
<h2 style="margin: 0 0 0.5rem 0;">
<a href="{% url 'twitch:game_detail' game_group.grouper.id %}"
<a id="game-link-{{ game_group.grouper.id }}"
href="{% url 'twitch:game_detail' game_group.grouper.id %}"
style="text-decoration: none">{{ game_group.grouper.display_name|default:game_group.grouper.name|default:game_group.grouper.slug|default:game_group.grouper.id }}</a>
</h2>
{% comment %} MODIFICATION: Check if the owner exists before creating the link {% endcomment %}
{% if game_group.grouper.owner %}
<p style="margin: 0;">
<a href="{% url 'twitch:organization_detail' game_group.grouper.owner.id %}"
<a id="org-link-{{ game_group.grouper.owner.id }}"
href="{% url 'twitch:organization_detail' game_group.grouper.owner.id %}"
style="text-decoration: none">{{ game_group.grouper.owner.name }}</a>
</p>
{% endif %}
@ -90,16 +94,19 @@
<div style="overflow-x: auto;">
<div style="display: flex; gap: 1rem; min-width: max-content;">
{% for campaign in game_group.list %}
<article style="display: flex;
<article id="campaign-{{ campaign.id }}"
style="display: flex;
flex-direction: column;
align-items: flex-start;
padding: 0.5rem;
flex-shrink: 0">
<div>
<a href="{% url 'twitch:campaign_detail' campaign.id %}"
<a id="campaign-link-{{ campaign.id }}"
href="{% url 'twitch:campaign_detail' campaign.id %}"
style="text-decoration: none">
{% if campaign.image_url %}
<img src="{{ campaign.image_url }}"
<img id="campaign-image-{{ campaign.id }}"
src="{{ campaign.image_url }}"
alt="Campaign artwork for {{ campaign.name }}"
width="120"
height="120"
@ -118,7 +125,9 @@
No Image
</div>
{% endif %}
<h4 style="margin: 0.5rem 0; text-align: left;">{{ campaign.clean_name }}</h4>
<h4 id="campaign-name-{{ campaign.id }}"
style="margin: 0.5rem 0;
text-align: left">{{ campaign.clean_name }}</h4>
</a>
<div style="font-size: 0.9rem;">
<time datetime="{{ campaign.start_at|date:'c' }}"
@ -133,11 +142,15 @@
</div>
<div style="margin-top: 0.5rem;">
{% if campaign.start_at <= now and campaign.end_at >= now %}
<span style="font-weight: 600; color: #28a745;">Active</span>
<span id="campaign-status-{{ campaign.id }}"
style="font-weight: 600;
color: #28a745">Active</span>
{% elif campaign.start_at > now %}
<span style="font-weight: 600;">Upcoming</span>
<span id="campaign-status-{{ campaign.id }}" style="font-weight: 600;">Upcoming</span>
{% else %}
<span style="font-weight: 600; color: #dc3545;">Expired</span>
<span id="campaign-status-{{ campaign.id }}"
style="font-weight: 600;
color: #dc3545">Expired</span>
{% endif %}
</div>
</div>
@ -162,7 +175,7 @@
{% endif %}
<!-- Pagination -->
{% if is_paginated %}
<nav style="margin-top: 3rem; text-align: center;">
<nav id="pagination" style="margin-top: 3rem; text-align: center;">
<div style="display: flex;
justify-content: center;
align-items: center;

View file

@ -5,14 +5,14 @@
{% endblock title %}
{% block content %}
<main>
<h1>Twitch Drops</h1>
<h1 id="page-title">Twitch Drops</h1>
<pre>
Drops are sorted alphabetically by organization and game. Click on a campaign or game title to see more details.
Hover over the end time to see the exact date and time.
</pre>
{% if campaigns_by_org_game %}
{% for org_id, org_data in campaigns_by_org_game.items %}
<section>
<section id="org-section-{{ org_id }}">
<h2>
{% if org_data.name %}
{{ org_data.name }}
@ -21,7 +21,7 @@ Hover over the end time to see the exact date and time.
{% endif %}
</h2>
{% for game_id, game_data in org_data.games.items %}
<article>
<article id="game-article-{{ game_id }}">
<header style="margin-bottom: 1rem;">
<h3 style="margin: 0 0 0.5rem 0;">
<a href="{% url 'twitch:game_detail' game_id %}">{{ game_data.name }}</a>
@ -38,7 +38,8 @@ Hover over the end time to see the exact date and time.
<div style="flex: 1; overflow-x: auto;">
<div style="display: flex; gap: 1rem; min-width: max-content;">
{% for campaign in game_data.campaigns %}
<article style="display: flex;
<article id="campaign-article-{{ campaign.id }}"
style="display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem;
@ -68,13 +69,13 @@ Hover over the end time to see the exact date and time.
text-align: left">
Started {{ campaign.start_at|timesince }} ago
</time>
<time datetime="{{ campaign.created_at|date:'c' }}"
title="{{ campaign.created_at|date:'DATETIME_FORMAT' }}"
<time datetime="{{ campaign.added_at|date:'c' }}"
title="{{ campaign.added_at|date:'DATETIME_FORMAT' }}"
style="font-size: 0.9rem;
color: #666;
display: block;
text-align: left">
Scraped {{ campaign.created_at|timesince }} ago
Scraped {{ campaign.added_at|timesince }} ago
</time>
<time datetime="{{ campaign.start_at|date:'c' }} to {{ campaign.end_at|date:'c' }}"
title="{{ campaign.start_at|date:'DATETIME_FORMAT' }} to {{ campaign.end_at|date:'DATETIME_FORMAT' }}"

View file

@ -3,14 +3,16 @@
Debug
{% endblock title %}
{% block content %}
<h1>Debug Data Integrity Report</h1>
<p>Generated at: {{ now }}</p>
<h1 id="page-title">Debug Data Integrity Report</h1>
<p>
Generated at: <time id="generation-time">{{ now }}</time>
</p>
<section>
<h2>Games Without an Assigned Owner ({{ games_without_owner|length }})</h2>
<h2 id="games-without-owner-header">Games Without an Assigned Owner ({{ games_without_owner|length }})</h2>
{% if games_without_owner %}
<ul>
<ul id="games-without-owner-list">
{% for game in games_without_owner %}
<li>
<li id="game-{{ game.id }}">
<a href="{% url 'twitch:game_detail' game.id %}">{{ game.display_name }}</a> (ID: {{ game.id }})
</li>
{% endfor %}
@ -20,11 +22,11 @@
{% endif %}
</section>
<section>
<h2>Campaigns With Broken Image URLs ({{ broken_image_campaigns|length }})</h2>
<h2 id="broken-image-campaigns-header">Campaigns With Broken Image URLs ({{ broken_image_campaigns|length }})</h2>
{% if broken_image_campaigns %}
<ul>
<ul id="broken-image-campaigns-list">
{% for c in broken_image_campaigns %}
<li>
<li id="campaign-{{ c.id }}">
<a href="{% url 'twitch:campaign_detail' c.id %}">{{ c.name }}</a>
(Game: <a href="{% url 'twitch:game_detail' c.game.id %}">{{ c.game.display_name }}</a>)
- URL: {{ c.image_url|default:'(empty)' }}
@ -36,14 +38,14 @@
{% endif %}
</section>
<section>
<h2>Benefits With Broken Image URLs ({{ broken_benefit_images|length }})</h2>
<h2 id="broken-benefit-images-header">Benefits With Broken Image URLs ({{ broken_benefit_images|length }})</h2>
{% if broken_benefit_images %}
<ul>
<ul id="broken-benefit-images-list">
{% for b in broken_benefit_images %}
{# A benefit is linked to a game via a drop and a campaign. #}
{# We use the 'with' tag to get the first drop for cleaner access. #}
{% with first_drop=b.drops.all.0 %}
<li>
<li id="benefit-{{ b.id }}">
{{ b.name }}
{# Check if the relationship path to the game exists #}
{% if first_drop and first_drop.campaign and first_drop.campaign.game %}
@ -61,11 +63,11 @@
{% endif %}
</section>
<section>
<h2>Active Campaigns Missing Image ({{ active_missing_image|length }})</h2>
<h2 id="active-missing-image-header">Active Campaigns Missing Image ({{ active_missing_image|length }})</h2>
{% if active_missing_image %}
<ul>
<ul id="active-missing-image-list">
{% for c in active_missing_image %}
<li>
<li id="campaign-{{ c.id }}">
<a href="{% url 'twitch:campaign_detail' c.id %}">{{ c.name }}</a>
(Game: <a href="{% url 'twitch:game_detail' c.game.id %}">{{ c.game.display_name }}</a>)
</li>
@ -76,11 +78,11 @@
{% endif %}
</section>
<section>
<h2>Time-Based Drops Without Benefits ({{ drops_without_benefits|length }})</h2>
<h2 id="drops-without-benefits-header">Time-Based Drops Without Benefits ({{ drops_without_benefits|length }})</h2>
{% if drops_without_benefits %}
<ul>
<ul id="drops-without-benefits-list">
{% for d in drops_without_benefits %}
<li>
<li id="drop-{{ d.id }}">
{{ d.name }}
(Campaign: <a href="{% url 'twitch:campaign_detail' d.campaign.id %}">{{ d.campaign.name }}</a>
in Game: <a href="{% url 'twitch:game_detail' d.campaign.game.id %}">{{ d.campaign.game.display_name }}</a>)
@ -92,11 +94,11 @@
{% endif %}
</section>
<section>
<h2>Campaigns With Invalid Dates ({{ invalid_date_campaigns|length }})</h2>
<h2 id="invalid-date-campaigns-header">Campaigns With Invalid Dates ({{ invalid_date_campaigns|length }})</h2>
{% if invalid_date_campaigns %}
<ul>
<ul id="invalid-date-campaigns-list">
{% for c in invalid_date_campaigns %}
<li>
<li id="campaign-{{ c.id }}">
<a href="{% url 'twitch:campaign_detail' c.id %}">{{ c.name }}</a>
(Game: <a href="{% url 'twitch:game_detail' c.game.id %}">{{ c.game.display_name }}</a>)
- Start: {{ c.start_at|default:'(none)' }} / End: {{ c.end_at|default:'(none)' }}
@ -108,9 +110,9 @@
{% endif %}
</section>
<section>
<h2>Duplicate Campaign Names Per Game ({{ duplicate_name_campaigns|length }})</h2>
<h2 id="duplicate-name-campaigns-header">Duplicate Campaign Names Per Game ({{ duplicate_name_campaigns|length }})</h2>
{% if duplicate_name_campaigns %}
<table>
<table id="duplicate-name-campaigns-table">
<thead>
<tr>
<th>Game</th>

View file

@ -5,13 +5,13 @@
{% endblock title %}
{% block content %}
<main>
<h1>RSS Feeds Documentation</h1>
<h1 id="page-title">RSS Feeds Documentation</h1>
<p>This page lists all available RSS feeds for TTVDrops.</p>
<section>
<h2>Available RSS Feeds</h2>
<ul>
<h2 id="available-feeds-header">Available RSS Feeds</h2>
<ul id="feeds-list">
{% for feed in feeds %}
<li>
<li id="feed-{{ forloop.counter }}">
<h3>{{ feed.title }}</h3>
<p>{{ feed.description }}</p>
<p>

View file

@ -3,12 +3,27 @@
{{ game.display_name }}
{% endblock title %}
{% block content %}
<h1>{{ game.display_name }}</h1>
<!-- Game Title -->
<h1 id="game-name">
{{ game.display_name }}
{% if game.display_name != game.name and game.name %}<small>({{ game.name }})</small>{% endif %}
</h1>
<!-- Game image -->
{% if game.box_art_url %}
<img id="game-image"
height="160"
width="160"
src="{{ game.box_art_url }}"
alt="{{ game.name }}">
{% endif %}
<!-- Game owner -->
{% if owner %}
<small><a href="{% url 'twitch:organization_detail' owner.id %}">{{ owner.name }}</a></small>
<small><a id="owner-link"
href="{% url 'twitch:organization_detail' owner.id %}">{{ owner.name }}</a></small>
{% endif %}
{% if user.is_authenticated %}
<form method="post"
<form id="notification-form"
method="post"
action="{% url 'twitch:subscribe_notifications' game_id=game.id %}">
{% csrf_token %}
<div>
@ -25,17 +40,17 @@
{% if subscription and subscription.notify_live %}checked{% endif %} />
<label for="live">🎮 Get notified when the drop is live and ready to be farmed.</label>
</div>
<button type="submit">Save notification preferences</button>
<button id="save-notifications-button" type="submit">Save notification preferences</button>
</form>
{% else %}
Login to subscribe!
<p id="login-prompt">Login to subscribe!</p>
{% endif %}
{% if active_campaigns %}
<h5>Active Campaigns</h5>
<table>
<h5 id="active-campaigns-header">Active Campaigns</h5>
<table id="active-campaigns-table">
<tbody>
{% for campaign in active_campaigns %}
<tr>
<tr id="campaign-row-{{ campaign.id }}">
<td>
<a href="{% url 'twitch:campaign_detail' campaign.id %}">{{ campaign.clean_name }}</a>
</td>
@ -48,11 +63,11 @@
</table>
{% endif %}
{% if upcoming_campaigns %}
<h5>Upcoming Campaigns</h5>
<table>
<h5 id="upcoming-campaigns-header">Upcoming Campaigns</h5>
<table id="upcoming-campaigns-table">
<tbody>
{% for campaign in upcoming_campaigns %}
<tr>
<tr id="campaign-row-{{ campaign.id }}">
<td>
<a href="{% url 'twitch:campaign_detail' campaign.id %}">{{ campaign.clean_name }}</a>
</td>
@ -65,11 +80,11 @@
</table>
{% endif %}
{% if expired_campaigns %}
<h5>Past Campaigns</h5>
<table>
<h5 id="expired-campaigns-header">Past Campaigns</h5>
<table id="expired-campaigns-table">
<tbody>
{% for campaign in expired_campaigns %}
<tr>
<tr id="campaign-row-{{ campaign.id }}">
<td>
<a href="{% url 'twitch:campaign_detail' campaign.id %}">{{ campaign.clean_name }}</a>
</td>
@ -82,7 +97,7 @@
</table>
{% endif %}
{% if not active_campaigns and not upcoming_campaigns and not expired_campaigns %}
No campaigns found for this game.
<p id="no-campaigns-message">No campaigns found for this game.</p>
{% endif %}
<pre><code>{{ game_data }}</code></pre>
{{ game_data|safe }}
{% endblock content %}

View file

@ -5,7 +5,7 @@
{% block content %}
<main>
<header>
<h1>All Games</h1>
<h1 id="page-title">All Games</h1>
<p>Browse all available games</p>
<p>
<a href="{% url 'twitch:game_list_simple' %}">List View</a>
@ -16,7 +16,8 @@
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
{% for organization, games in games_by_org.items %}
{% for item in games %}
<article style="padding: 0.25rem;
<article id="game-{{ item.game.id }}"
style="padding: 0.25rem;
border-radius: 8px;
flex: 1 1 160px;
text-align: center">

View file

@ -4,16 +4,16 @@
{% endblock title %}
{% block content %}
<main>
<h1>Games List</h1>
<h1 id="page-title">Games List</h1>
<p>
<a href="{% url 'twitch:game_list' %}">Grid View</a>
</p>
{% if games_by_org %}
{% for organization, games in games_by_org.items %}
<h2>{{ organization.name }}</h2>
<h2 id="org-{{ organization.id }}">{{ organization.name }}</h2>
<ul style="list-style: none; padding: 0; margin: 0;">
{% for item in games %}
<li>
<li id="game-{{ item.game.id }}">
<a href="{% url 'twitch:game_detail' item.game.id %}">{{ item.game.display_name }}</a>
</li>
{% endfor %}

View file

@ -3,16 +3,16 @@
Games
{% endblock title %}
{% block content %}
<h1>Organizations</h1>
<h1 id="page-title">Organizations</h1>
{% if orgs %}
<ul>
<ul id="org-list">
{% for organization in orgs %}
<li>
<li id="org-{{ organization.id }}">
<a href="{% url 'twitch:organization_detail' organization.id %}">{{ organization.name }}</a>
</li>
{% endfor %}
</ul>
{% else %}
No games found.
<p>No games found.</p>
{% endif %}
{% endblock content %}

View file

@ -3,9 +3,10 @@
{{ organization.name }}
{% endblock title %}
{% block content %}
<h1>{{ organization.name }}</h1>
<h1 id="org-name">{{ organization.name }}</h1>
{% if user.is_authenticated %}
<form method="post"
<form id="notification-form"
method="post"
action="{% url 'twitch:subscribe_org_notifications' org_id=organization.id %}">
{% csrf_token %}
<div>
@ -22,17 +23,17 @@
{% if subscription and subscription.notify_live %}checked{% endif %} />
<label for="live">🎮 Get notified when the drop is live and ready to be farmed.</label>
</div>
<button type="submit">Save preferences</button>
<button id="save-preferences-button" type="submit">Save preferences</button>
</form>
{% else %}
Login to subscribe!
<p id="login-prompt">Login to subscribe!</p>
{% endif %}
<ul>
<ul id="games-list">
{% for game in games %}
<li>
<li id="game-{{ game.id }}">
<a href="{% url 'twitch:game_detail' pk=game.id %}">{{ game }}</a>
</li>
{% endfor %}
</ul>
<pre><code>{{ org_data }}</code></pre>
<pre><code id="org-data">{{ org_data }}</code></pre>
{% endblock content %}

View file

@ -3,55 +3,55 @@
Search Results for "{{ query }}"
{% endblock title %}
{% block content %}
<div class="container">
<h1>Search Results for "{{ query }}"</h1>
<div class="container" id="search-results-container">
<h1 id="page-title">Search Results for "{{ query }}"</h1>
{% if not results.organizations and not results.games and not results.campaigns and not results.drops and not results.benefits %}
<p>No results found.</p>
<p id="no-results">No results found.</p>
{% else %}
{% if results.organizations %}
<h2>Organizations</h2>
<ul>
<h2 id="organizations-header">Organizations</h2>
<ul id="organizations-list">
{% for org in results.organizations %}
<li>
<li id="org-{{ org.pk }}">
<a href="{% url 'twitch:organization_detail' org.pk %}">{{ org.name }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% if results.games %}
<h2>Games</h2>
<ul>
<h2 id="games-header">Games</h2>
<ul id="games-list">
{% for game in results.games %}
<li>
<li id="game-{{ game.pk }}">
<a href="{% url 'twitch:game_detail' game.pk %}">{{ game.display_name }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% if results.campaigns %}
<h2>Campaigns</h2>
<ul>
<h2 id="campaigns-header">Campaigns</h2>
<ul id="campaigns-list">
{% for campaign in results.campaigns %}
<li>
<li id="campaign-{{ campaign.pk }}">
<a href="{% url 'twitch:campaign_detail' campaign.pk %}">{{ campaign.name }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% if results.drops %}
<h2>Drops</h2>
<ul>
<h2 id="drops-header">Drops</h2>
<ul id="drops-list">
{% for drop in results.drops %}
<li>
<li id="drop-{{ drop.id }}">
<a href="{% url 'twitch:campaign_detail' drop.campaign.pk %}#drop-{{ drop.id }}">{{ drop.name }}</a> (in {{ drop.campaign.name }})
</li>
{% endfor %}
</ul>
{% endif %}
{% if results.benefits %}
<h2>Benefits</h2>
<ul>
{% for benefit in results.benefits %}<li>{{ benefit.name }}</li>{% endfor %}
<h2 id="benefits-header">Benefits</h2>
<ul id="benefits-list">
{% for benefit in results.benefits %}<li id="benefit-{{ benefit.id }}">{{ benefit.name }}</li>{% endfor %}
</ul>
{% endif %}
{% endif %}

View file

@ -4,7 +4,7 @@ import datetime
import json
import logging
from collections import OrderedDict, defaultdict
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, Any
from django.contrib import messages
from django.contrib.auth.decorators import login_required
@ -18,6 +18,9 @@ from django.http.response import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.views.generic import DetailView, ListView
from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers.data import JsonLexer
from twitch.models import DropBenefit, DropCampaign, Game, NotificationSubscription, Organization, TimeBasedDrop
@ -185,7 +188,6 @@ class DropCampaignListView(ListView):
Returns:
dict: Context data.
"""
kwargs = cast("dict[str, Any]", kwargs)
context: dict[str, Any] = super().get_context_data(**kwargs)
context["games"] = Game.objects.all().order_by("display_name")
@ -198,6 +200,19 @@ class DropCampaignListView(ListView):
return context
def format_and_color_json(code: str) -> str:
"""Format and color a JSON string for HTML display.
Args:
code: The code string to format.
Returns:
str: The formatted code with HTML styles.
"""
formatted_code: str = json.dumps(code, indent=4)
return highlight(formatted_code, JsonLexer(), HtmlFormatter())
class DropCampaignDetailView(DetailView):
"""Detail view for a drop campaign."""
@ -221,7 +236,7 @@ class DropCampaignDetailView(DetailView):
return super().get_object(queryset=queryset)
def get_context_data(self, **kwargs: object) -> dict[str, Any]:
def get_context_data(self, **kwargs: object) -> dict[str, Any]: # noqa: PLR0914
"""Add additional context data.
Args:
@ -232,7 +247,9 @@ class DropCampaignDetailView(DetailView):
"""
context: dict[str, Any] = super().get_context_data(**kwargs)
campaign = context["campaign"]
drops = TimeBasedDrop.objects.filter(campaign=campaign).select_related("campaign").prefetch_related("benefits").order_by("required_minutes_watched")
drops: QuerySet[TimeBasedDrop, TimeBasedDrop] = (
TimeBasedDrop.objects.filter(campaign=campaign).select_related("campaign").prefetch_related("benefits").order_by("required_minutes_watched")
)
serialized_campaign = serialize(
"json",
@ -280,11 +297,46 @@ class DropCampaignDetailView(DetailView):
campaign_data[0]["fields"]["drops"] = drops_data
pretty_campaign_data = json.dumps(campaign_data[0], indent=4)
# Enhance drops with additional context data
enhanced_drops = []
now: datetime.datetime = timezone.now()
for drop in drops:
# Ensure benefits are loaded
benefits = list(drop.benefits.all())
context["now"] = timezone.now()
context["drops"] = drops
context["campaign_data"] = pretty_campaign_data
# Calculate countdown text
if drop.end_at and drop.end_at > now:
time_diff: datetime.timedelta = drop.end_at - now
days: int = time_diff.days
hours, remainder = divmod(time_diff.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
if days > 0:
countdown_text: str = f"{days}d {hours}h {minutes}m"
elif hours > 0:
countdown_text = f"{hours}h {minutes}m"
elif minutes > 0:
countdown_text = f"{minutes}m {seconds}s"
else:
countdown_text = f"{seconds}s"
elif drop.start_at and drop.start_at > now:
countdown_text = "Not started"
else:
countdown_text = "Expired"
enhanced_drop: dict[str, str | datetime.datetime | TimeBasedDrop] = {
"drop": drop,
"local_start": drop.start_at,
"local_end": drop.end_at,
"timezone_name": "UTC",
"countdown_text": countdown_text,
}
enhanced_drops.append(enhanced_drop)
context["now"] = now
context["drops"] = enhanced_drops
context["campaign_data"] = format_and_color_json(campaign_data[0])
context["owner"] = campaign.game.owner
return context
@ -432,8 +484,6 @@ class GameDetailView(DetailView):
campaigns_data = json.loads(serialized_campaigns)
game_data[0]["fields"]["campaigns"] = campaigns_data
pretty_game_data = json.dumps(game_data[0], indent=4)
context.update({
"active_campaigns": active_campaigns,
"upcoming_campaigns": upcoming_campaigns,
@ -441,7 +491,7 @@ class GameDetailView(DetailView):
"subscription": subscription,
"owner": game.owner,
"now": now,
"game_data": pretty_game_data,
"game_data": format_and_color_json(game_data[0]),
})
return context

2
uv.lock generated
View file

@ -599,6 +599,7 @@ dependencies = [
{ name = "orjson" },
{ name = "platformdirs" },
{ name = "psycopg", extra = ["binary"] },
{ name = "pygments" },
{ name = "python-dotenv" },
]
@ -622,6 +623,7 @@ requires-dist = [
{ name = "orjson", specifier = ">=3.11.1" },
{ name = "platformdirs", specifier = ">=4.3.8" },
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2.3" },
{ name = "pygments", specifier = ">=2.19.2" },
{ name = "python-dotenv", specifier = ">=1.1.1" },
]