Add BackgroundImage model and enhance card templates with dynamic backgrounds

This commit is contained in:
Ross
2025-12-04 22:16:33 +00:00
parent 33fbae9426
commit d299f7ffd3
12 changed files with 253 additions and 16 deletions
+58
View File
@@ -0,0 +1,58 @@
# OS
.DS_Store
Thumbs.db
desktop.ini
$RECYCLE.BIN/
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log
# Node
node_modules/
dist/
build/
out/
coverage/
.nyc_output/
# Environment
.env
.env.*.local
# IDEs and editors
.vscode/
.idea/
*.sublime-workspace
*.sublime-project
*.code-workspace
# Package managers
package-lock.json
.pnpm-store/
# (keep yarn.lock if you rely on it; remove above line to keep package-lock.json in repo)
# Python
__pycache__/
*.py[cod]
venv/
ENV/
env/
# Java / JVM
target/
*.class
*.jar
*.war
*.ear
*.iml
# Misc
*.sqlite
*.db
*.swp
*~
+8 -1
View File
@@ -1,8 +1,15 @@
from django.contrib import admin
from .models import Dinosaur
from .models import Dinosaur, BackgroundImage
@admin.register(Dinosaur)
class DinosaurAdmin(admin.ModelAdmin):
list_display = ('name', 'era', 'diet', 'speed', 'weight', 'intelligence')
search_fields = ('name', 'era', 'diet')
@admin.register(BackgroundImage)
class BackgroundImageAdmin(admin.ModelAdmin):
list_display = ('__str__', 'is_active', 'uploaded_at')
list_filter = ('is_active',)
search_fields = ('title',)
+26
View File
@@ -0,0 +1,26 @@
# Generated by Django 6.0 on 2025-12-04 22:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cards', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='BackgroundImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(blank=True, max_length=200)),
('image', models.ImageField(upload_to='backgrounds/')),
('is_active', models.BooleanField(default=False, help_text='Mark as the active background')),
('uploaded_at', models.DateTimeField(auto_now_add=True)),
],
options={
'ordering': ['-is_active', '-uploaded_at'],
},
),
]
+26
View File
@@ -33,3 +33,29 @@ class Dinosaur(models.Model):
def __str__(self):
return self.name
class BackgroundImage(models.Model):
"""Background images used behind card renders.
Admins can upload multiple backgrounds and mark one as active. Templates
will use BackgroundImage.get_active() to obtain the active background.
"""
title = models.CharField(max_length=200, blank=True)
image = models.ImageField(upload_to='backgrounds/')
is_active = models.BooleanField(default=False, help_text='Mark as the active background')
uploaded_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-is_active', '-uploaded_at']
def __str__(self):
return self.title or f"Background #{self.pk}"
@classmethod
def get_active(cls):
"""Return the active BackgroundImage if set, otherwise the most recent one or None."""
active = cls.objects.filter(is_active=True).order_by('-uploaded_at').first()
if active:
return active
return cls.objects.order_by('-uploaded_at').first()
+7 -1
View File
@@ -1,4 +1,5 @@
<article class="box card-item" id="card-{{ card.pk }}">
<article class="box card-item" id="card-{{ card.pk }}"
{% if background and background.image %}style="background-image: url('{{ background.image.url }}'); background-size: cover; background-position: center; position: relative;"{% endif %}>
<div class="media">
<div class="media-content">
<p class="title is-5">{{ card.name }}</p>
@@ -20,4 +21,9 @@
<button class="button is-small is-danger" hx-post="{% url 'cards:delete' card.pk %}" hx-confirm="Delete this card?" hx-swap="none">Delete</button>
</div>
</div>
{% if card.image %}
<figure class="image is-96x96" style="position:absolute; right:0.75rem; top:0.75rem; width:96px; height:96px; background:transparent;">
<img src="{{ card.image.url }}" alt="{{ card.name }}" style="width:96px; height:96px; object-fit:cover; border-radius:6px; box-shadow:0 2px 6px rgba(0,0,0,0.3);">
</figure>
{% endif %}
</article>
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="columns is-multiline">
{% for card in cards %}
<div class="column is-3-desktop is-4-tablet is-6-mobile">
{% include 'cards/_card_item.html' with card=card %}
{% include 'cards/_card_item.html' with card=card background=background %}
</div>
{% empty %}
<div class="notification is-warning">No dinosaurs yet — add one!</div>
+54 -2
View File
@@ -7,19 +7,71 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<script src="https://unpkg.com/htmx.org@1.9.5"></script>
<style>
/* Theme variables */
:root{
--bg:#ffffff;
--text:#0a0a0a;
--muted:#4a4a4a;
--card-bg:#ffffff;
--accent:#00d1b2;
}
[data-theme="dark"]{
--bg:#0b0f13;
--text:#e6eef3;
--muted:#9aa8b0;
--card-bg:#0f1620;
--accent:#79ffd4;
}
body{background:var(--bg); color:var(--text)}
.app-container{padding:1.5rem}
.card-item{min-height:140px}
.card-item{min-height:140px; background:var(--card-bg); color:var(--text)}
.highlight-new{animation: highlight 1.6s ease}
@keyframes highlight{0%{background:lavenderblush}100%{background:transparent}}
/* Theme toggle */
#theme-toggle{margin-left:0.5rem}
</style>
</head>
<body>
<section class="section">
<div class="container app-container">
<h1 class="title">TopTrumps — Dinosaurs</h1>
<div class="level">
<div class="level-left">
<h1 class="title">TopTrumps — Dinosaurs</h1>
</div>
<div class="level-right">
<div class="level-item">
<button id="theme-toggle" class="button is-small" aria-pressed="false">Toggle theme</button>
</div>
</div>
</div>
<div id="messages"></div>
{% block content %}{% endblock %}
</div>
</section>
</body>
<script>
(function(){
const root = document.documentElement;
const toggle = document.getElementById('theme-toggle');
function applyTheme(t){
if(t==='dark') root.setAttribute('data-theme','dark');
else root.removeAttribute('data-theme');
if(toggle) toggle.setAttribute('aria-pressed', t==='dark');
}
// initial: localStorage -> prefers-color-scheme -> light
const stored = localStorage.getItem('toptrumps-theme');
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
applyTheme(stored || (prefersDark ? 'dark' : 'light'));
if(toggle){
toggle.addEventListener('click', ()=>{
const current = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
const next = current === 'dark' ? 'light' : 'dark';
applyTheme(next);
localStorage.setItem('toptrumps-theme', next);
});
}
})();
</script>
</html>
+11 -7
View File
@@ -1,7 +1,7 @@
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponse, HttpResponseBadRequest
from django.template.loader import render_to_string
from .models import Dinosaur
from .models import Dinosaur, BackgroundImage
from .forms import DinosaurForm
@@ -11,18 +11,21 @@ def is_htmx(request):
def card_list(request):
cards = Dinosaur.objects.all()
background = BackgroundImage.get_active() if BackgroundImage.objects.exists() else None
if is_htmx(request):
return render(request, 'cards/_cards_list.html', {'cards': cards})
return render(request, 'cards/list.html', {'cards': cards})
return render(request, 'cards/_cards_list.html', {'cards': cards, 'background': background})
return render(request, 'cards/list.html', {'cards': cards, 'background': background})
def card_create(request):
background = BackgroundImage.get_active() if BackgroundImage.objects.exists() else None
if request.method == 'POST':
form = DinosaurForm(request.POST, request.FILES)
if form.is_valid():
card = form.save()
if is_htmx(request):
card_html = render_to_string('cards/_card_item.html', {'card': card}, request=request)
card_html = render_to_string('cards/_card_item.html', {'card': card, 'background': background}, request=request)
# Also return an out-of-band fragment to replace/clear the form container
form_html = render_to_string('cards/_form.html', {'form': DinosaurForm()}, request=request)
oob = f'<div id="htmx-form" hx-swap-oob="true">{form_html}</div>'
@@ -37,17 +40,18 @@ def card_create(request):
else:
form = DinosaurForm()
return render(request, 'cards/_form.html', {'form': form})
return render(request, 'cards/_form.html', {'form': form, 'background': background})
def card_edit(request, pk):
card = get_object_or_404(Dinosaur, pk=pk)
background = BackgroundImage.get_active() if BackgroundImage.objects.exists() else None
if request.method == 'POST':
form = DinosaurForm(request.POST, request.FILES, instance=card)
if form.is_valid():
card = form.save()
if is_htmx(request):
card_html = render_to_string('cards/_card_item.html', {'card': card}, request=request)
card_html = render_to_string('cards/_card_item.html', {'card': card, 'background': background}, request=request)
# Clear the form container via OOB so the edit form disappears after successful save
oob = '<div id="htmx-form" hx-swap-oob="true"></div>'
return HttpResponse(card_html + oob)
@@ -60,7 +64,7 @@ def card_edit(request, pk):
else:
form = DinosaurForm(instance=card)
return render(request, 'cards/_form.html', {'form': form, 'card': card})
return render(request, 'cards/_form.html', {'form': form, 'card': card, 'background': background})
def card_delete(request, pk):
BIN
View File
Binary file not shown.
+7 -1
View File
@@ -1,4 +1,5 @@
<article class="box card-item" id="card-{{ card.pk }}">
<article class="box card-item" id="card-{{ card.pk }}"
{% if background and background.image %}style="background-image: url('{{ background.image.url }}'); background-size: cover; background-position: center; position: relative;"{% endif %}>
<div class="media">
<div class="media-content">
<p class="title is-5">{{ card.name }}</p>
@@ -20,4 +21,9 @@
<button class="button is-small is-danger" hx-post="{% url 'cards:delete' card.pk %}" hx-confirm="Delete this card?" hx-swap="none">Delete</button>
</div>
</div>
{% if card.image %}
<figure class="image is-96x96" style="position:absolute; right:0.75rem; top:0.75rem; width:96px; height:96px; background:transparent;">
<img src="{{ card.image.url }}" alt="{{ card.name }}" style="width:96px; height:96px; object-fit:cover; border-radius:6px; box-shadow:0 2px 6px rgba(0,0,0,0.3);">
</figure>
{% endif %}
</article>
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="columns is-multiline">
{% for card in cards %}
<div class="column is-3-desktop is-4-tablet is-6-mobile">
{% include 'cards/_card_item.html' with card=card %}
{% include 'cards/_card_item.html' with card=card background=background %}
</div>
{% empty %}
<div class="notification is-warning">No dinosaurs yet — add one!</div>
+54 -2
View File
@@ -7,19 +7,71 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<script src="https://unpkg.com/htmx.org@1.9.5"></script>
<style>
/* Theme variables */
:root{
--bg:#ffffff;
--text:#0a0a0a;
--muted:#4a4a4a;
--card-bg:#ffffff;
--accent:#00d1b2;
}
[data-theme="dark"]{
--bg:#0b0f13;
--text:#e6eef3;
--muted:#9aa8b0;
--card-bg:#0f1620;
--accent:#79ffd4;
}
body{background:var(--bg); color:var(--text)}
.app-container{padding:1.5rem}
.card-item{min-height:140px}
.card-item{min-height:140px; background:var(--card-bg); color:var(--text)}
.highlight-new{animation: highlight 1.6s ease}
@keyframes highlight{0%{background:lavenderblush}100%{background:transparent}}
/* Theme toggle */
#theme-toggle{margin-left:0.5rem}
</style>
</head>
<body>
<section class="section">
<div class="container app-container">
<h1 class="title">TopTrumps — Dinosaurs</h1>
<div class="level">
<div class="level-left">
<h1 class="title">TopTrumps — Dinosaurs</h1>
</div>
<div class="level-right">
<div class="level-item">
<button id="theme-toggle" class="button is-small" aria-pressed="false">Toggle theme</button>
</div>
</div>
</div>
<div id="messages"></div>
{% block content %}{% endblock %}
</div>
</section>
</body>
<script>
(function(){
const root = document.documentElement;
const toggle = document.getElementById('theme-toggle');
function applyTheme(t){
if(t==='dark') root.setAttribute('data-theme','dark');
else root.removeAttribute('data-theme');
if(toggle) toggle.setAttribute('aria-pressed', t==='dark');
}
// initial: localStorage -> prefers-color-scheme -> light
const stored = localStorage.getItem('toptrumps-theme');
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
applyTheme(stored || (prefersDark ? 'dark' : 'light'));
if(toggle){
toggle.addEventListener('click', ()=>{
const current = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
const next = current === 'dark' ? 'light' : 'dark';
applyTheme(next);
localStorage.setItem('toptrumps-theme', next);
});
}
})();
</script>
</html>