final home desing

This commit is contained in:
JakeBreath
2026-08-05 17:40:33 -05:00
parent cf78128284
commit 344cb524e3
5 changed files with 186 additions and 24 deletions
+57
View File
@@ -8,6 +8,63 @@ from django.core.cache import cache
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from library.models import Project
class HomeSectionsTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(username='Owner', password='pw')
def _home(self):
session = self.client.session
session['authorized'] = True
session.save()
self.client.force_login(self.user)
return self.client.get(reverse('landing:home')).content.decode()
def test_empty_categories_skipped(self):
Project.objects.create(slug='only-mod', title='Only Mod', category='mod', owner=self.user)
html = self._home()
self.assertIn('>Mod</h2>', html)
self.assertNotIn('>Skin</h2>', html)
self.assertNotIn('>Guide</h2>', html)
def test_popular_ordered_by_rating(self):
Project.objects.create(slug='m0', title='Mod 0', category='mod', owner=self.user, rating_score=0)
Project.objects.create(slug='m2', title='Mod 2', category='mod', owner=self.user, rating_score=2)
Project.objects.create(slug='m1', title='Mod 1', category='mod', owner=self.user, rating_score=1)
html = self._home()
sections = html.split('class="home-cat-block"')[1:]
mod = next(s for s in sections if '>Mod</h2>' in s)
popular = mod.split('home-cat-divider')[0]
self.assertLess(popular.index('Mod 2'), popular.index('Mod 1'))
self.assertLess(popular.index('Mod 1'), popular.index('Mod 0'))
def test_columns_capped_at_six(self):
for i in range(8):
Project.objects.create(slug=f'm{i}', title=f'Mod {i}', category='mod', owner=self.user, rating_score=i)
html = self._home()
sections = html.split('class="home-cat-block"')[1:]
mod = next(s for s in sections if '>Mod</h2>' in s)
popular = mod.split('home-cat-divider')[0]
recent = mod.split('home-cat-divider')[1]
self.assertEqual(popular.count('class="pack-card-row"'), 6)
self.assertEqual(recent.count('class="pack-card-row"'), 6)
def test_recent_column_orders_by_creation(self):
Project.objects.create(slug='s0', title='High', category='skin', owner=self.user, rating_score=5)
Project.objects.create(slug='s1', title='Mid', category='skin', owner=self.user, rating_score=3)
Project.objects.create(slug='s2', title='Low', category='skin', owner=self.user, rating_score=0)
html = self._home()
sections = html.split('class="home-cat-block"')[1:]
skin = next(s for s in sections if '>Skin</h2>' in s)
popular = skin.split('home-cat-divider')[0]
recent = skin.split('home-cat-divider')[1]
# Popular leads with the top-rated pack.
self.assertLess(popular.index('High'), popular.index('Low'))
# Recent leads with the newest (created last -> 'Low').
self.assertLess(recent.index('Low'), recent.index('High'))
class ServerStatsTests(TestCase): class ServerStatsTests(TestCase):
def setUp(self): def setUp(self):
+28 -6
View File
@@ -76,16 +76,38 @@ def gate(request):
def home(request): def home(request):
latest_projects = (
Project.objects.select_related('owner', 'thumbnail')
.prefetch_related('tag_links__tag__category', 'versions')
.order_by('-created_at')[:4]
)
total_downloads = Version.objects.aggregate(total=Sum('downloads'))['total'] or 0 total_downloads = Version.objects.aggregate(total=Sum('downloads'))['total'] or 0
creators = ( creators = (
get_user_model().objects.filter(projects__isnull=False).distinct().count() get_user_model().objects.filter(projects__isnull=False).distinct().count()
) )
# Per-category Popular (by rating) + Recent columns, 6 packs each.
def _base_query(category):
return (
Project.objects.filter(category=category)
.select_related('owner', 'thumbnail')
.prefetch_related('versions')
.annotate(downloads_total=Sum('versions__downloads'))
)
home_sections = []
for value, label in Project.CATEGORY_CHOICES:
base = _base_query(value)
popular = list(base.order_by('-rating_score', '-created_at')[:6])
recent = list(base.order_by('-created_at')[:6])
if popular or recent:
home_sections.append({
'value': value,
'label': label,
'popular': popular,
'recent': recent,
})
# Two categories share each row (compact cards leave room for it).
home_rows = [
home_sections[i:i + 2] for i in range(0, len(home_sections), 2)
]
announcements = ( announcements = (
Announcement.objects.filter(published=True) Announcement.objects.filter(published=True)
.annotate(comments_count=Count('comments')) .annotate(comments_count=Count('comments'))
@@ -106,7 +128,7 @@ def home(request):
) )
context = { context = {
'latest_projects': latest_projects, 'home_rows': home_rows,
'stats': { 'stats': {
'creators': creators, 'creators': creators,
'packs': Project.objects.count(), 'packs': Project.objects.count(),
+48
View File
@@ -3890,3 +3890,51 @@ a.deletelink {
.draft-title { font-weight: 600; color: var(--md-sys-color-on-surface, #cdd6f4); text-decoration: none; } .draft-title { font-weight: 600; color: var(--md-sys-color-on-surface, #cdd6f4); text-decoration: none; }
.draft-meta { color: var(--md-sys-color-on-surface-variant, #a6adc8); font-size: 0.75rem; } .draft-meta { color: var(--md-sys-color-on-surface-variant, #a6adc8); font-size: 0.75rem; }
.draft-actions { display: flex; gap: 0.4rem; align-items: center; } .draft-actions { display: flex; gap: 0.4rem; align-items: center; }
/* Home per-category Popular/Recent sections */
.home-cat-row {
display: grid;
grid-template-columns: 1fr 1px 1fr;
gap: 24px;
align-items: start;
margin-bottom: 32px;
}
.home-cat-row-divider { background: var(--md-sys-color-outline-variant, #45475a); align-self: stretch; }
.home-cat-block { min-width: 0; }
.home-cat-block:only-child { grid-column: 1 / -1; }
.home-cat-cols {
display: grid;
grid-template-columns: 1fr 1px 1fr;
gap: 20px;
align-items: start;
}
.home-cat-divider { background: var(--md-sys-color-outline-variant, #45475a); align-self: stretch; }
.home-cat-head { margin: 0 0 0.6rem; font-size: 0.95rem; }
.home-cat-head i { color: var(--md-sys-color-primary); margin-right: 0.3rem; }
.pack-row-list { display: flex; flex-direction: column; gap: 0.6rem; }
@media (max-width: 900px) {
.home-cat-row { grid-template-columns: 1fr; }
.home-cat-row-divider { height: 1px; width: 100%; }
.home-cat-cols { grid-template-columns: 1fr; }
.home-cat-divider { height: 1px; width: 100%; }
}
.pack-card-row {
display: flex; align-items: center; gap: 0.75rem;
background: var(--md-sys-color-surface-variant, #45475a);
border: 1px solid var(--md-sys-color-outline-variant, #3a3f4d);
border-radius: 10px; padding: 0.5rem 0.7rem;
text-decoration: none; transition: border-color 0.15s;
}
.pack-card-row:hover { border-color: var(--md-sys-color-primary); }
.pack-card-row-thumb {
width: 56px; height: 56px; flex-shrink: 0; border-radius: 8px;
overflow: hidden; display: flex; align-items: center; justify-content: center;
background: var(--md-sys-color-surface, #1e1e2e);
}
.pack-card-row-thumb img { width: 100%; height: 100%; object-fit: cover; }
.pack-card-row-thumb .pack-card-thumb-icon { font-size: 1.3rem; color: var(--md-sys-color-on-surface-variant, #a6adc8); }
.pack-card-row-body { min-width: 0; flex: 1; }
.pack-card-row-body h4 { margin: 0; font-size: 0.9rem; color: var(--md-sys-color-on-surface, #cdd6f4); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pack-card-row-author { margin: 0.1rem 0 0; color: var(--md-sys-color-on-surface-variant, #a6adc8); font-size: 0.72rem; }
.pack-card-row-meta { display: flex; gap: 0.6rem; margin: 0.15rem 0 0; color: var(--md-sys-color-on-surface-variant, #a6adc8); font-size: 0.72rem; flex-wrap: wrap; }
.pack-card-row-meta span { white-space: nowrap; }
+30 -16
View File
@@ -78,26 +78,40 @@
</div> </div>
</div> </div>
<div class="home-section"> {% for row in home_rows %}
<div class="home-cat-row">
{% for section in row %}
<div class="home-cat-block">
<div class="home-section-header"> <div class="home-section-header">
<h2>Latest packs</h2> <h2>{{ section.label }}</h2>
<a href="{% url 'library:browse' %}" class="home-section-link">View all <i class="fas fa-arrow-right"></i></a> <a href="{% url 'library:browse' %}?category={{ section.value }}" class="home-section-link">View all <i class="fas fa-arrow-right"></i></a>
</div> </div>
<div class="home-cat-cols">
<div class="pack-grid"> <div class="home-cat-col">
{% for project in latest_projects %} <h3 class="home-cat-head"><i class="fas fa-fire"></i> Popular</h3>
{% include 'library/_project_card.html' %} <div class="pack-row-list">
{% for project in section.popular %}
{% include 'library/_project_card_row.html' %}
{% empty %} {% empty %}
<div class="pack-card pack-card-empty"> <p class="empty-hint">None yet.</p>
<div class="pack-card-thumb">
<i class="fas fa-box-open"></i>
</div>
<div class="pack-card-body">
<h3>No packs yet</h3>
<p class="pack-card-desc">Be the first to share a pack.</p>
</div>
</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="home-cat-divider" role="separator"></div>
<div class="home-cat-col">
<h3 class="home-cat-head"><i class="fas fa-clock"></i> Recent</h3>
<div class="pack-row-list">
{% for project in section.recent %}
{% include 'library/_project_card_row.html' %}
{% empty %}
<p class="empty-hint">None yet.</p>
{% endfor %}
</div>
</div>
</div>
</div>
{% if not forloop.last %}<div class="home-cat-row-divider" role="separator"></div>{% endif %}
{% endfor %}
</div>
{% endfor %}
{% endblock %} {% endblock %}
@@ -0,0 +1,21 @@
{% load static %}
<a href="{% url 'library:project_detail' project.slug %}" class="pack-card-row">
<div class="pack-card-row-thumb">
{% if project.thumbnail %}
<img src="{{ project.thumbnail_url }}" alt="">
{% else %}
<i class="fas fa-box-open pack-card-thumb-icon"></i>
{% endif %}
</div>
<div class="pack-card-row-body">
<h4>{{ project.title }}</h4>
<p class="pack-card-row-author"><i class="fas fa-user"></i> {{ project.owner.username }}</p>
{% with latest=project.versions.all|first %}
<p class="pack-card-row-meta">
<span><i class="fas fa-tag"></i> {{ latest.version_name|default:"no versions" }}</span>
<span><i class="fas fa-download"></i> {{ project.downloads_total|default:project.downloads }}</span>
<span><i class="fas fa-thumbs-up"></i> {{ project.rating_score }}</span>
</p>
{% endwith %}
</div>
</a>