feat: Add Twitch Drops Tracker application with campaign management
- Implemented models for DropCampaign, Game, Organization, DropBenefit, TimeBasedDrop, and DropBenefitEdge. - Created views for listing and detailing drop campaigns. - Added templates for dashboard, campaign list, and campaign detail. - Developed management command to import drop campaigns from JSON files. - Configured admin interface for managing campaigns and related models. - Updated URL routing for the application. - Enhanced README with installation instructions and project structure.
This commit is contained in:
parent
0c7c1c3f30
commit
5c482c1729
15 changed files with 1145 additions and 10 deletions
150
templates/twitch/campaign_detail.html
Normal file
150
templates/twitch/campaign_detail.html
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ campaign.name }} - Twitch Drops Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'twitch:dashboard' %}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'twitch:campaign_list' %}">Campaigns</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ campaign.name }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<h1 class="mb-3">{{ campaign.name }}</h1>
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<span class="badge bg-primary">{{ campaign.game.display_name }}</span>
|
||||
{% if campaign.start_at <= now and campaign.end_at >= now %}
|
||||
{% if campaign.status == 'ACTIVE' %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark">{{ campaign.status|title }}</span>
|
||||
{% endif %}
|
||||
{% elif campaign.start_at > now %}
|
||||
<span class="badge bg-info text-dark">Upcoming</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Expired</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p>{{ campaign.description }}</p>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<p><strong><i class="far fa-calendar-alt me-2"></i>Start Date:</strong>
|
||||
{{ campaign.start_at|date:"F j, Y, g:i a" }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong><i class="far fa-calendar-alt me-2"></i>End Date:</strong>
|
||||
{{ campaign.end_at|date:"F j, Y, g:i a" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if campaign.details_url %}
|
||||
<p>
|
||||
<a href="{{ campaign.details_url }}" target="_blank" class="btn btn-outline-primary">
|
||||
<i class="fas fa-external-link-alt me-2"></i>Official Details
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if campaign.account_link_url %}
|
||||
<p>
|
||||
<a href="{{ campaign.account_link_url }}" target="_blank" class="btn btn-success">
|
||||
<i class="fas fa-link me-2"></i>Connect Account
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% if campaign.image_url %}
|
||||
<img src="{{ campaign.image_url }}" class="img-fluid rounded shadow-sm" alt="{{ campaign.name }}">
|
||||
{% else %}
|
||||
<div class="bg-light rounded shadow-sm p-4 text-center">
|
||||
<i class="fas fa-image fa-5x text-muted mb-3"></i>
|
||||
<p class="text-muted">No image available</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card mt-3 border-0 shadow-sm">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h5 class="mb-0"><i class="fas fa-info-circle me-2"></i>Campaign Info</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Owner:</strong> {{ campaign.owner.name }}</p>
|
||||
<p><strong>Status:</strong> {{ campaign.status }}</p>
|
||||
<p><strong>Account Connected:</strong> {% if campaign.is_account_connected %}<span
|
||||
class="text-success">Yes</span>{% else %}<span class="text-danger">No</span>{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-twitch">
|
||||
<h5 class="mb-0"><i class="fas fa-gift me-2"></i>Rewards</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if drops %}
|
||||
<div class="timeline-container">
|
||||
{% for drop in drops %}
|
||||
<div class="card mb-4 drop-item">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h4>{{ drop.name }}</h4>
|
||||
<p class="mb-2">
|
||||
<span class="badge bg-primary">{{ drop.required_minutes_watched }} minutes
|
||||
watched</span>
|
||||
{% if drop.required_subs > 0 %}
|
||||
<span class="badge bg-info">{{ drop.required_subs }} subscriptions
|
||||
required</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="mb-2">
|
||||
<small class="text-muted">
|
||||
<i class="far fa-clock me-1"></i>Available:
|
||||
{{ drop.start_at|date:"M d, Y" }} - {{ drop.end_at|date:"M d, Y" }}
|
||||
</small>
|
||||
</p>
|
||||
<div class="progress mb-3" style="height: 25px;">
|
||||
<div class="progress-bar bg-twitch" role="progressbar" style="width: 0%;"
|
||||
aria-valuenow="0" aria-valuemin="0"
|
||||
aria-valuemax="{{ drop.required_minutes_watched }}">
|
||||
0 / {{ drop.required_minutes_watched }} minutes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
{% for benefit in drop.benefits.all %}
|
||||
<div class="mb-2">
|
||||
{% if benefit.image_asset_url %}
|
||||
<img src="{{ benefit.image_asset_url }}" class="benefit-img mb-2"
|
||||
alt="{{ benefit.name }}">
|
||||
{% else %}
|
||||
<div class="bg-light rounded p-3 mb-2">
|
||||
<i class="fas fa-gift fa-3x text-muted"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="mb-0"><strong>{{ benefit.name }}</strong></p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>No drops found for this campaign.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
125
templates/twitch/campaign_list.html
Normal file
125
templates/twitch/campaign_list.html
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Drop Campaigns - Twitch Drops Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h1 class="mb-4"><i class="fas fa-gift me-2 twitch-color"></i>Drop Campaigns</h1>
|
||||
<p class="lead">Browse all Twitch drop campaigns.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h5 class="mb-0"><i class="fas fa-filter me-2"></i>Filter Campaigns</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="get" action="{% url 'twitch:campaign_list' %}" class="row g-3">
|
||||
<div class="col-md-5">
|
||||
<label for="game" class="form-label">Game</label>
|
||||
<select class="form-select" id="game" name="game">
|
||||
<option value="">All Games</option>
|
||||
{% for game in games %}
|
||||
<option value="{{ game.id }}" {% if selected_game == game.id %}selected{% endif %}>
|
||||
{{ game.display_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label for="status" class="form-label">Status</label>
|
||||
<select class="form-select" id="status" name="status">
|
||||
<option value="">All Statuses</option>
|
||||
{% for status in status_options %}
|
||||
<option value="{{ status }}" {% if selected_status == status %}selected{% endif %}>
|
||||
{{ status|title }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fas fa-search me-2"></i>Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-twitch">
|
||||
<h5 class="mb-0"><i class="fas fa-list me-2"></i>Campaign List</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if campaigns %}
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||
{% for campaign in campaigns %}
|
||||
<div class="col">
|
||||
{% if campaign.start_at <= now and campaign.end_at >= now %}
|
||||
{% if campaign.status == 'ACTIVE' %}
|
||||
<div class="card h-100 campaign-card campaign-active">
|
||||
{% else %}
|
||||
<div class="card h-100 campaign-card">
|
||||
{% endif %}
|
||||
{% elif campaign.start_at > now %}
|
||||
<div class="card h-100 campaign-card campaign-upcoming">
|
||||
{% else %}
|
||||
<div class="card h-100 campaign-card campaign-expired">
|
||||
{% endif %}
|
||||
|
||||
{% if campaign.image_url %}
|
||||
<img src="{{ campaign.image_url }}" class="card-img-top"
|
||||
alt="{{ campaign.name }}">
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ campaign.name }}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">{{ campaign.game.display_name }}
|
||||
</h6>
|
||||
<p class="card-text small">{{ campaign.description|truncatewords:20 }}</p>
|
||||
<p class="card-text">
|
||||
<small class="text-muted">
|
||||
<i
|
||||
class="far fa-calendar-alt me-1"></i>{{ campaign.start_at|date:"M d, Y" }}
|
||||
- {{ campaign.end_at|date:"M d, Y" }}
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
{% if campaign.start_at <= now and campaign.end_at >= now %}
|
||||
{% if campaign.status == 'ACTIVE' %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% else %}
|
||||
<span
|
||||
class="badge bg-warning text-dark">{{ campaign.status|title }}</span>
|
||||
{% endif %}
|
||||
{% elif campaign.start_at > now %}
|
||||
<span class="badge bg-info text-dark">Upcoming</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Expired</span>
|
||||
{% endif %}
|
||||
<a href="{% url 'twitch:campaign_detail' campaign.id %}"
|
||||
class="btn btn-sm btn-outline-primary">View Details</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>No campaigns found with the current filters.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
86
templates/twitch/dashboard.html
Normal file
86
templates/twitch/dashboard.html
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - Twitch Drops Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h1 class="mb-4"><i class="fas fa-tachometer-alt me-2 twitch-color"></i>Dashboard</h1>
|
||||
<p class="lead">Track your active Twitch drop campaigns and progress.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-twitch">
|
||||
<h5 class="mb-0"><i class="fas fa-fire me-2"></i>Active Campaigns</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if active_campaigns %}
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||
{% for campaign in active_campaigns %}
|
||||
<div class="col">
|
||||
<div class="card h-100 campaign-card campaign-active">
|
||||
{% if campaign.image_url %}
|
||||
<img src="{{ campaign.image_url }}" class="card-img-top" alt="{{ campaign.name }}">
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ campaign.name }}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">{{ campaign.game.display_name }}</h6>
|
||||
<p class="card-text small">{{ campaign.description|truncatewords:20 }}</p>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="badge bg-success">Active</span>
|
||||
<a href="{% url 'twitch:campaign_detail' campaign.id %}"
|
||||
class="btn btn-sm btn-outline-primary">View Details</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>No active campaigns at the moment.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h5 class="mb-0"><i class="fas fa-chart-bar me-2"></i>Quick Stats</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-md-4 mb-3 mb-md-0">
|
||||
<div class="p-3 border rounded">
|
||||
<h2 class="twitch-color">{{ active_campaigns.count }}</h2>
|
||||
<p class="mb-0 text-muted">Active Campaigns</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3 mb-md-0">
|
||||
<div class="p-3 border rounded">
|
||||
<h2 class="text-primary">{{ now|date:"F j, Y" }}</h2>
|
||||
<p class="mb-0 text-muted">Current Date</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="p-3 border rounded">
|
||||
<a href="{% url 'twitch:campaign_list' %}" class="btn btn-primary btn-lg w-100">
|
||||
<i class="fas fa-list me-2"></i>View All Campaigns
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue