From 41eb7cde3e7c92b475ff14efc3dc5382cdb5e031 Mon Sep 17 00:00:00 2001 From: imperosol Date: Fri, 20 Mar 2026 16:10:39 +0100 Subject: [PATCH] rework club list page --- club/api.py | 17 +++- club/schemas.py | 2 +- club/static/bundled/club/club-list-index.ts | 63 ++++++++++++ club/static/club/list.scss | 47 +++++++++ club/templates/club/club_list.jinja | 102 +++++++++++++------- club/views.py | 9 +- core/templates/core/base/navbar.jinja | 5 +- 7 files changed, 195 insertions(+), 50 deletions(-) create mode 100644 club/static/bundled/club/club-list-index.ts create mode 100644 club/static/club/list.scss diff --git a/club/api.py b/club/api.py index 3ed425bf..fe71d7bd 100644 --- a/club/api.py +++ b/club/api.py @@ -6,9 +6,10 @@ from ninja_extra.pagination import PageNumberPaginationExtra from ninja_extra.schemas import PaginatedResponseSchema from api.auth import ApiKeyAuth -from api.permissions import CanAccessLookup, CanView, HasPerm +from api.permissions import CanView, HasPerm from club.models import Club, Membership from club.schemas import ( + ClubProfileSchema, ClubSchema, ClubSearchFilterSchema, SimpleClubSchema, @@ -22,13 +23,21 @@ class ClubController(ControllerBase): @route.get( "/search", response=PaginatedResponseSchema[SimpleClubSchema], - auth=[ApiKeyAuth(), SessionAuth()], - permissions=[CanAccessLookup], url_name="search_club", ) @paginate(PageNumberPaginationExtra, page_size=50) def search_club(self, filters: Query[ClubSearchFilterSchema]): - return filters.filter(Club.objects.all()) + return filters.filter(Club.objects.order_by("name")).values() + + @route.get( + "/search-profile", + response=PaginatedResponseSchema[ClubProfileSchema], + url_name="search_club_profile", + ) + @paginate(PageNumberPaginationExtra, page_size=50) + def search_club_profile(self, filters: Query[ClubSearchFilterSchema]): + """Same as /api/club/search, but with more returned data""" + return filters.filter(Club.objects.order_by("name")) @route.get( "/{int:club_id}", diff --git a/club/schemas.py b/club/schemas.py index 08488c31..02622110 100644 --- a/club/schemas.py +++ b/club/schemas.py @@ -30,7 +30,7 @@ class ClubProfileSchema(ModelSchema): class Meta: model = Club - fields = ["id", "name", "logo"] + fields = ["id", "name", "logo", "is_active", "short_description"] url: str diff --git a/club/static/bundled/club/club-list-index.ts b/club/static/bundled/club/club-list-index.ts new file mode 100644 index 00000000..84adedda --- /dev/null +++ b/club/static/bundled/club/club-list-index.ts @@ -0,0 +1,63 @@ +import { History, updateQueryString } from "#core:utils/history"; +import { + type ClubProfileSchema, + type ClubSearchClubData, + clubSearchClubProfile, + type Options, +} from "#openapi"; + +type ClubStatus = "active" | "inactive" | "both"; +const PAGE_SIZE = 50; + +document.addEventListener("alpine:init", () => { + Alpine.data("clubList", () => ({ + clubName: "", + clubStatus: "active" as ClubStatus, + currentPage: 1, + nbPages: 1, + clubs: [] as ClubProfileSchema[], + loading: false, + + async init() { + const urlParams = new URLSearchParams(window.location.search); + this.clubName = urlParams.get("clubName") || ""; + this.clubStatus = urlParams.get("clubStatus") || "active"; + this.currentPage = urlParams.get("currentPage") || 1; + for (const param of ["clubName", "clubStatus", "currentPage"]) { + this.$watch(param, async (value: number | string) => { + updateQueryString(param, value.toString(), History.Replace); + await this.loadClubs(); + }); + } + await this.loadClubs(); + }, + async loadClubs() { + this.loading = true; + const searchParams: Options = { + query: { page: this.currentPage }, + }; + if (this.clubName) { + searchParams.query.search = this.clubName; + } + if (this.clubStatus === "active") { + searchParams.query.is_active = true; + } else if (this.clubStatus === "inactive") { + searchParams.query.is_active = false; + } + const res = await clubSearchClubProfile(searchParams); + this.nbPages = Math.ceil(res.data.count / PAGE_SIZE); + this.clubs = res.data.results; + this.loading = false; + }, + + getParagraphs(s: string) { + if (!s) { + return []; + } + return s + .split("\n") + .map((s) => s.trim()) + .filter((s) => s !== ""); + }, + })); +}); diff --git a/club/static/club/list.scss b/club/static/club/list.scss new file mode 100644 index 00000000..9fbf952f --- /dev/null +++ b/club/static/club/list.scss @@ -0,0 +1,47 @@ +#club-list { + display: flex; + flex-direction: column; + gap: 2em; + padding: 2em; + + .card { + display: block; + background-color: unset; + + .club-image { + float: left; + margin-right: 2rem; + margin-bottom: .5rem; + width: 150px; + height: 150px; + border-radius: 10%; + background-color: rgba(173, 173, 173, 0.2); + + @media screen and (max-width: 500px) { + width: 100px; + height: 100px; + } + } + + i.club-image { + display: flex; + flex-direction: column; + justify-content: center; + color: black; + } + + .content { + display: block; + text-align: justify; + + h4 { + margin-top: 0; + margin-right: .5rem; + } + + p { + font-size: 100%; + } + } + } +} diff --git a/club/templates/club/club_list.jinja b/club/templates/club/club_list.jinja index da0a54be..15b9a54d 100644 --- a/club/templates/club/club_list.jinja +++ b/club/templates/club/club_list.jinja @@ -1,4 +1,5 @@ {% extends "core/base.jinja" %} +{% from "core/macros.jinja" import paginate_alpine %} {% block title -%} {% trans %}Club list{% endtrans %} @@ -8,45 +9,76 @@ {% trans %}The list of all clubs existing at UTBM.{% endtrans %} {%- endblock %} -{% macro display_club(club) -%} +{% block additional_js %} + +{% endblock %} - {% if club.is_active or user.is_root %} - -
  • {{ club.name }} - - {% if not club.is_active %} - ({% trans %}inactive{% endtrans %}) - {% endif %} - - {% if club.president %} - {{ club.president.user }}{% endif %} - {% if club.short_description %}

    {{ club.short_description|markdown }}

    {% endif %} - - {% endif %} - - {%- if club.children.all()|length != 0 %} -
      - {%- for c in club.children.order_by('name').prefetch_related("children") %} - {{ display_club(c) }} - {%- endfor %} -
    - {%- endif -%} -
  • -{%- endmacro %} +{% block additional_css %} + +{% endblock %} {% block content %} - {% if user.is_root %} -

    {% trans %}New club{% endtrans %}

    - {% endif %} - {% if club_list %} +
    +

    {% trans %}Filters{% endtrans %}

    +
    +
    +
    + + +
    +
    + {% trans %}Club state{% endtrans %} +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +

    {% trans %}Club list{% endtrans %}

    -
      - {%- for club in club_list %} - {{ display_club(club) }} - {%- endfor %} -
    - {% else %} - {% trans %}There is no club in this website.{% endtrans %} - {% endif %} + {% if user.has_perm("club.add_club") %} +
    + + {% trans %}New club{% endtrans %} + + {% endif %} +
    + +
    + {{ paginate_alpine("currentPage", "nbPages") }} +
    {% endblock %} diff --git a/club/views.py b/club/views.py index 9077d0d7..2a3c8f87 100644 --- a/club/views.py +++ b/club/views.py @@ -42,7 +42,7 @@ from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import gettext from django.utils.translation import gettext_lazy as _ -from django.views.generic import DetailView, ListView, View +from django.views.generic import DetailView, TemplateView, View from django.views.generic.detail import SingleObjectMixin from django.views.generic.edit import CreateView, DeleteView, UpdateView @@ -180,15 +180,10 @@ class ClubTabsMixin(TabedViewMixin): return tab_list -class ClubListView(ListView): +class ClubListView(TemplateView): """List the Clubs.""" - model = Club template_name = "club/club_list.jinja" - queryset = ( - Club.objects.filter(parent=None).order_by("name").prefetch_related("children") - ) - context_object_name = "club_list" class ClubView(ClubTabsMixin, DetailView): diff --git a/core/templates/core/base/navbar.jinja b/core/templates/core/base/navbar.jinja index fd1e6ddc..64ca9721 100644 --- a/core/templates/core/base/navbar.jinja +++ b/core/templates/core/base/navbar.jinja @@ -5,9 +5,8 @@