diff --git a/nonpacks/landing/tests.py b/nonpacks/landing/tests.py index 47b7494..4098a04 100644 --- a/nonpacks/landing/tests.py +++ b/nonpacks/landing/tests.py @@ -8,6 +8,63 @@ from django.core.cache import cache from django.test import TestCase 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', html) + self.assertNotIn('>Skin', html) + self.assertNotIn('>Guide', 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' 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' 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' 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): def setUp(self): diff --git a/nonpacks/landing/views.py b/nonpacks/landing/views.py index 09d216a..af59a28 100644 --- a/nonpacks/landing/views.py +++ b/nonpacks/landing/views.py @@ -76,16 +76,38 @@ def gate(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 creators = ( 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 = ( Announcement.objects.filter(published=True) .annotate(comments_count=Count('comments')) @@ -106,7 +128,7 @@ def home(request): ) context = { - 'latest_projects': latest_projects, + 'home_rows': home_rows, 'stats': { 'creators': creators, 'packs': Project.objects.count(), diff --git a/nonpacks/static/css/style.css b/nonpacks/static/css/style.css index aee87e3..2126eec 100644 --- a/nonpacks/static/css/style.css +++ b/nonpacks/static/css/style.css @@ -3890,3 +3890,51 @@ a.deletelink { .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-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; } diff --git a/nonpacks/templates/landing/home.html b/nonpacks/templates/landing/home.html index fa64057..aa74262 100644 --- a/nonpacks/templates/landing/home.html +++ b/nonpacks/templates/landing/home.html @@ -78,26 +78,40 @@ -
-
-

Latest packs

- View all -
- -
- {% for project in latest_projects %} - {% include 'library/_project_card.html' %} - {% empty %} -
-
- -
-
-

No packs yet

-

Be the first to share a pack.

+{% for row in home_rows %} +
+ {% for section in row %} +
+
+

{{ section.label }}

+ View all +
+
+
+

Popular

+
+ {% for project in section.popular %} + {% include 'library/_project_card_row.html' %} + {% empty %} +

None yet.

+ {% endfor %}
- {% endfor %} + +
+

Recent

+
+ {% for project in section.recent %} + {% include 'library/_project_card_row.html' %} + {% empty %} +

None yet.

+ {% endfor %} +
+
+
+ {% if not forloop.last %}{% endif %} + {% endfor %}
+{% endfor %} {% endblock %} diff --git a/nonpacks/templates/library/_project_card_row.html b/nonpacks/templates/library/_project_card_row.html new file mode 100644 index 0000000..64c0c50 --- /dev/null +++ b/nonpacks/templates/library/_project_card_row.html @@ -0,0 +1,21 @@ +{% load static %} + +
+ {% if project.thumbnail %} + + {% else %} + + {% endif %} +
+
+

{{ project.title }}

+

{{ project.owner.username }}

+ {% with latest=project.versions.all|first %} +

+ {{ latest.version_name|default:"no versions" }} + {{ project.downloads_total|default:project.downloads }} + {{ project.rating_score }} +

+ {% endwith %} +
+