rework club list page

This commit is contained in:
imperosol
2026-03-20 16:10:39 +01:00
parent ffa0b94408
commit 41eb7cde3e
7 changed files with 195 additions and 50 deletions

View File

@@ -6,9 +6,10 @@ from ninja_extra.pagination import PageNumberPaginationExtra
from ninja_extra.schemas import PaginatedResponseSchema from ninja_extra.schemas import PaginatedResponseSchema
from api.auth import ApiKeyAuth 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.models import Club, Membership
from club.schemas import ( from club.schemas import (
ClubProfileSchema,
ClubSchema, ClubSchema,
ClubSearchFilterSchema, ClubSearchFilterSchema,
SimpleClubSchema, SimpleClubSchema,
@@ -22,13 +23,21 @@ class ClubController(ControllerBase):
@route.get( @route.get(
"/search", "/search",
response=PaginatedResponseSchema[SimpleClubSchema], response=PaginatedResponseSchema[SimpleClubSchema],
auth=[ApiKeyAuth(), SessionAuth()],
permissions=[CanAccessLookup],
url_name="search_club", url_name="search_club",
) )
@paginate(PageNumberPaginationExtra, page_size=50) @paginate(PageNumberPaginationExtra, page_size=50)
def search_club(self, filters: Query[ClubSearchFilterSchema]): 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( @route.get(
"/{int:club_id}", "/{int:club_id}",

View File

@@ -30,7 +30,7 @@ class ClubProfileSchema(ModelSchema):
class Meta: class Meta:
model = Club model = Club
fields = ["id", "name", "logo"] fields = ["id", "name", "logo", "is_active", "short_description"]
url: str url: str

View File

@@ -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<ClubSearchClubData> = {
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 !== "");
},
}));
});

View File

@@ -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%;
}
}
}
}

View File

@@ -1,4 +1,5 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from "core/macros.jinja" import paginate_alpine %}
{% block title -%} {% block title -%}
{% trans %}Club list{% endtrans %} {% trans %}Club list{% endtrans %}
@@ -8,45 +9,76 @@
{% trans %}The list of all clubs existing at UTBM.{% endtrans %} {% trans %}The list of all clubs existing at UTBM.{% endtrans %}
{%- endblock %} {%- endblock %}
{% macro display_club(club) -%} {% block additional_js %}
<script type="module" src="{{ static("bundled/club/club-list-index.ts") }}"></script>
{% endblock %}
{% if club.is_active or user.is_root %} {% block additional_css %}
<link rel="stylesheet" href="{{ static("club/list.scss") }}">
<li><a href="{{ url('club:club_view', club_id=club.id) }}">{{ club.name }}</a> {% endblock %}
{% if not club.is_active %}
({% trans %}inactive{% endtrans %})
{% endif %}
{% if club.president %} - <a href="{{ url('core:user_profile', user_id=club.president.user.id) }}">{{ club.president.user }}</a>{% endif %}
{% if club.short_description %}<p>{{ club.short_description|markdown }}</p>{% endif %}
{% endif %}
{%- if club.children.all()|length != 0 %}
<ul>
{%- for c in club.children.order_by('name').prefetch_related("children") %}
{{ display_club(c) }}
{%- endfor %}
</ul>
{%- endif -%}
</li>
{%- endmacro %}
{% block content %} {% block content %}
{% if user.is_root %} <main x-data="clubList">
<p><a href="{{ url('club:club_new') }}">{% trans %}New club{% endtrans %}</a></p> <h3>{% trans %}Filters{% endtrans %}</h3>
{% endif %} <form id="club-list-filters">
{% if club_list %} <div class="row gap-4x">
<fieldset>
<label for="club-name-input">{% trans %}Name{% endtrans %}</label>
<input
id="club-name-input"
type="text"
name="club-name"
x-model.debounce.500ms="clubName"
/>
</fieldset>
<fieldset class="grow">
<legend>{% trans %}Club state{% endtrans %}</legend>
<div class="row">
<input type="radio" id="filter-active-clubs" x-model="clubStatus" value="active">
<label for="filter-active-clubs">{% trans %}Active{% endtrans %}</label>
</div>
<div class="row">
<input type="radio" id="filter-inactive-clubs" x-model="clubStatus" value="inactive">
<label for="filter-inactive-clubs">{% trans %}Inactive{% endtrans %}</label>
</div>
<div class="row">
<input type="radio" id="filter-all-clubs" x-model="clubStatus" value="both">
<label for="filter-all-clubs">{% trans %}All clubs{% endtrans %}</label>
</div>
</fieldset>
</div>
</form>
<h3>{% trans %}Club list{% endtrans %}</h3> <h3>{% trans %}Club list{% endtrans %}</h3>
<ul> {% if user.has_perm("club.add_club") %}
{%- for club in club_list %} <br>
{{ display_club(club) }} <a href="{{ url('club:club_new') }}" class="btn btn-blue">
{%- endfor %} <i class="fa fa-plus"></i> {% trans %}New club{% endtrans %}
</ul> </a>
{% else %} {% endif %}
{% trans %}There is no club in this website.{% endtrans %} <section class="aria-busy-grow" :aria-busy="loading" id="club-list">
{% endif %} <template x-for="club of clubs" :key="club.id">
<div class="card">
<a :href="club.url">
<template x-if="club.logo">
<img class="club-image" :src="club.logo" :alt="`logo ${club.name}`">
</template>
<template x-if="!club.logo">
<i class="fa-regular fa-image fa-4x club-image"></i>
</template>
</a>
<div class="content">
<a :href="club.url">
<h4 x-text="`${club.name} ${club.is_active ? '' : '({% trans %}inactive{% endtrans %})'}`"></h4>
</a>
<template x-for="paragraph of getParagraphs(club.short_description)">
<p x-text="paragraph"></p>
</template>
</div>
</div>
</template>
</section>
{{ paginate_alpine("currentPage", "nbPages") }}
</main>
{% endblock %} {% endblock %}

View File

@@ -42,7 +42,7 @@ from django.utils.functional import cached_property
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _ 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.detail import SingleObjectMixin
from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.edit import CreateView, DeleteView, UpdateView
@@ -180,15 +180,10 @@ class ClubTabsMixin(TabedViewMixin):
return tab_list return tab_list
class ClubListView(ListView): class ClubListView(TemplateView):
"""List the Clubs.""" """List the Clubs."""
model = Club
template_name = "club/club_list.jinja" 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): class ClubView(ClubTabsMixin, DetailView):

View File

@@ -5,9 +5,8 @@
<details name="navbar" class="menu"> <details name="navbar" class="menu">
<summary class="head">{% trans %}Associations & Clubs{% endtrans %}</summary> <summary class="head">{% trans %}Associations & Clubs{% endtrans %}</summary>
<ul class="content"> <ul class="content">
<li><a href="{{ url('core:page', page_name='ae') }}">{% trans %}AE{% endtrans %}</a></li> <li><a href="{{ url("core:page", page_name="ae") }}">{% trans %}AE{% endtrans %}</a></li>
<li><a href="{{ url('core:page', page_name='clubs') }}">{% trans %}AE's clubs{% endtrans %}</a></li> <li><a href="{{ url("club:club_list") }}">{% trans %}AE's clubs{% endtrans %}</a></li>
<li><a href="{{ url('core:page', page_name='utbm-associations') }}">{% trans %}Others UTBM's Associations{% endtrans %}</a></li>
</ul> </ul>
</details> </details>
<details name="navbar" class="menu"> <details name="navbar" class="menu">