Apply all biomejs fixes

This commit is contained in:
Antoine Bartuccio 2024-10-08 17:14:22 +02:00
parent 20bea62542
commit 7405241b82
25 changed files with 480 additions and 428 deletions

View File

@ -20,7 +20,10 @@
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"recommended": true "all": true
} }
},
"javascript": {
"globals": ["Alpine", "$", "jQuery", "gettext", "interpolate"]
} }
} }

View File

@ -1,5 +1,5 @@
$(document).ready(() => { $(document).ready(() => {
$("#poster_list #view").click((e) => { $("#poster_list #view").click(() => {
$("#view").removeClass("active"); $("#view").removeClass("active");
}); });

View File

@ -1,13 +1,11 @@
$(document).ready(() => { $(document).ready(() => {
transition_time = 1000; const transitionTime = 1000;
i = 0; let i = 0;
max = $("#slideshow .slide").length; const max = $("#slideshow .slide").length;
next_trigger = 0;
function enterFullscreen() { function enterFullscreen() {
element = document.getElementById("slideshow"); const element = document.getElementById("slideshow");
$(element).addClass("fullscreen"); $(element).addClass("fullscreen");
if (element.requestFullscreen) { if (element.requestFullscreen) {
element.requestFullscreen(); element.requestFullscreen();
@ -21,7 +19,7 @@ $(document).ready(() => {
} }
function exitFullscreen() { function exitFullscreen() {
element = document.getElementById("slideshow"); const element = document.getElementById("slideshow");
$(element).removeClass("fullscreen"); $(element).removeClass("fullscreen");
if (document.exitFullscreen) { if (document.exitFullscreen) {
document.exitFullscreen(); document.exitFullscreen();
@ -34,61 +32,59 @@ $(document).ready(() => {
} }
} }
function init_progress_bar() { function initProgressBar() {
$("#slideshow #progress_bar").css("transition", "none"); $("#slideshow #progress_bar").css("transition", "none");
$("#slideshow #progress_bar").removeClass("progress"); $("#slideshow #progress_bar").removeClass("progress");
$("#slideshow #progress_bar").addClass("init"); $("#slideshow #progress_bar").addClass("init");
} }
function start_progress_bar(display_time) { function startProgressBar(displayTime) {
$("#slideshow #progress_bar").removeClass("init"); $("#slideshow #progress_bar").removeClass("init");
$("#slideshow #progress_bar").addClass("progress"); $("#slideshow #progress_bar").addClass("progress");
$("#slideshow #progress_bar").css("transition", `width ${display_time}s linear`); $("#slideshow #progress_bar").css("transition", `width ${displayTime}s linear`);
} }
function next() { function next() {
init_progress_bar(); initProgressBar();
slide = $($("#slideshow .slide").get(i % max)); const slide = $($("#slideshow .slide").get(i % max));
slide.removeClass("center"); slide.removeClass("center");
slide.addClass("left"); slide.addClass("left");
next_slide = $($("#slideshow .slide").get((i + 1) % max)); const nextSlide = $($("#slideshow .slide").get((i + 1) % max));
next_slide.removeClass("right"); nextSlide.removeClass("right");
next_slide.addClass("center"); nextSlide.addClass("center");
display_time = next_slide.attr("display_time") || 2; const displayTime = nextSlide.attr("display_time") || 2;
$("#slideshow .bullet").removeClass("active"); $("#slideshow .bullet").removeClass("active");
bullet = $("#slideshow .bullet")[(i + 1) % max]; const bullet = $("#slideshow .bullet")[(i + 1) % max];
$(bullet).addClass("active"); $(bullet).addClass("active");
i = (i + 1) % max; i = (i + 1) % max;
setTimeout(() => { setTimeout(() => {
others_left = $("#slideshow .slide.left"); const othersLeft = $("#slideshow .slide.left");
others_left.removeClass("left"); othersLeft.removeClass("left");
others_left.addClass("right"); othersLeft.addClass("right");
start_progress_bar(display_time); startProgressBar(displayTime);
next_trigger = setTimeout(next, display_time * 1000); setTimeout(next, displayTime * 1000);
}, transition_time); }, transitionTime);
} }
display_time = $("#slideshow .center").attr("display_time"); const displayTime = $("#slideshow .center").attr("display_time");
init_progress_bar(); initProgressBar();
setTimeout(() => { setTimeout(() => {
if (max > 1) { if (max > 1) {
start_progress_bar(display_time); startProgressBar(displayTime);
setTimeout(next, display_time * 1000); setTimeout(next, displayTime * 1000);
} }
}, 10); }, 10);
$("#slideshow").click((e) => { $("#slideshow").click(() => {
if (!$("#slideshow").hasClass("fullscreen")) { if ($("#slideshow").hasClass("fullscreen")) {
console.log("Entering fullscreen ...");
enterFullscreen();
} else {
console.log("Exiting fullscreen ...");
exitFullscreen(); exitFullscreen();
} else {
enterFullscreen();
} }
}); });
@ -96,7 +92,6 @@ $(document).ready(() => {
if (e.keyCode === 27) { if (e.keyCode === 27) {
// escape key maps to keycode `27` // escape key maps to keycode `27`
e.preventDefault(); e.preventDefault();
console.log("Exiting fullscreen ...");
exitFullscreen(); exitFullscreen();
} }
}); });

View File

@ -27,7 +27,7 @@
<div id="posters"> <div id="posters">
{% if poster_list.count() == 0 %} {% if poster_list.count() == 0 %}
<div id="no-posters">{% trans %}No posters{% endtrans %}</div> <div id="no-posters">{% trans %}No posters{% endtrans %}</div>
{% else %} {% else %}

View File

@ -13,13 +13,11 @@ $(() => {
bottom: "5%", bottom: "5%",
}); });
target.css("height", "300px"); target.css("height", "300px");
console.log(target);
}, },
buttons: [ buttons: [
{ {
text: "Choose", text: "Choose",
click: function () { click: function () {
console.log($("#file_id"));
$(`input[name=${$(this).attr("name")}]`).attr( $(`input[name=${$(this).attr("name")}]`).attr(
"value", "value",
$("#file_id").attr("value"), $("#file_id").attr("value"),
@ -34,7 +32,6 @@ $(() => {
.button() .button()
.on("click", function () { .on("click", function () {
const popup = popups.filter(`[name=${$(this).attr("name")}]`); const popup = popups.filter(`[name=${$(this).attr("name")}]`);
console.log(popup);
popup.html( popup.html(
'<iframe src="/file/popup" width="100%" height="95%"></iframe><div id="file_id" value="null" />', '<iframe src="/file/popup" width="100%" height="95%"></iframe><div id="file_id" value="null" />',
); );
@ -45,6 +42,7 @@ $(() => {
}); });
}); });
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function createQuickNotif(msg) { function createQuickNotif(msg) {
const el = document.createElement("li"); const el = document.createElement("li");
el.textContent = msg; el.textContent = msg;
@ -52,6 +50,7 @@ function createQuickNotif(msg) {
document.getElementById("quick_notif").appendChild(el); document.getElementById("quick_notif").appendChild(el);
} }
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function deleteQuickNotifs() { function deleteQuickNotifs() {
const el = document.getElementById("quick_notif"); const el = document.getElementById("quick_notif");
while (el.firstChild) { while (el.firstChild) {
@ -59,7 +58,8 @@ function deleteQuickNotifs() {
} }
} }
function display_notif() { // biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function displayNotif() {
$("#header_notif").toggle().parent().toggleClass("white"); $("#header_notif").toggle().parent().toggleClass("white");
} }
@ -69,10 +69,13 @@ function display_notif() {
// Sadly, getting the cookie is not possible with CSRF_COOKIE_HTTPONLY or CSRF_USE_SESSIONS is True // Sadly, getting the cookie is not possible with CSRF_COOKIE_HTTPONLY or CSRF_USE_SESSIONS is True
// So, the true workaround is to get the token from the dom // So, the true workaround is to get the token from the dom
// https://docs.djangoproject.com/en/2.0/ref/csrf/#acquiring-the-token-if-csrf-use-sessions-is-true // https://docs.djangoproject.com/en/2.0/ref/csrf/#acquiring-the-token-if-csrf-use-sessions-is-true
// biome-ignore lint/style/useNamingConvention: can't find it used anywhere but I will not play with the devil
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function getCSRFToken() { function getCSRFToken() {
return $("[name=csrfmiddlewaretoken]").val(); return $("[name=csrfmiddlewaretoken]").val();
} }
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
const initialUrlParams = new URLSearchParams(window.location.search); const initialUrlParams = new URLSearchParams(window.location.search);
/** /**
@ -80,8 +83,11 @@ const initialUrlParams = new URLSearchParams(window.location.search);
* @enum {number} * @enum {number}
*/ */
const History = { const History = {
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
NONE: 0, NONE: 0,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
PUSH: 1, PUSH: 1,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
REPLACE: 2, REPLACE: 2,
}; };
@ -91,7 +97,8 @@ const History = {
* @param {History} action * @param {History} action
* @param {URL | null} url * @param {URL | null} url
*/ */
function update_query_string(key, value, action = History.REPLACE, url = null) { // biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function updateQueryString(key, value, action = History.REPLACE, url = null) {
let ret = url; let ret = url;
if (!ret) { if (!ret) {
ret = new URL(window.location.href); ret = new URL(window.location.href);
@ -125,24 +132,25 @@ function update_query_string(key, value, action = History.REPLACE, url = null) {
* @param {string} url The paginated endpoint to fetch * @param {string} url The paginated endpoint to fetch
* @return {Promise<Object[]>} * @return {Promise<Object[]>}
*/ */
async function fetch_paginated(url) { // biome-ignore lint/correctness/noUnusedVariables: used in other scripts
const max_per_page = 199; async function fetchPaginated(url) {
const paginated_url = new URL(url, document.location.origin); const maxPerPage = 199;
paginated_url.searchParams.set("page_size", max_per_page.toString()); const paginatedUrl = new URL(url, document.location.origin);
paginated_url.searchParams.set("page", "1"); paginatedUrl.searchParams.set("page_size", maxPerPage.toString());
paginatedUrl.searchParams.set("page", "1");
const first_page = await (await fetch(paginated_url)).json(); const firstPage = await (await fetch(paginatedUrl)).json();
const results = first_page.results; const results = firstPage.results;
const nb_pictures = first_page.count; const nbPictures = firstPage.count;
const nb_pages = Math.ceil(nb_pictures / max_per_page); const nbPages = Math.ceil(nbPictures / maxPerPage);
if (nb_pages > 1) { if (nbPages > 1) {
const promises = []; const promises = [];
for (let i = 2; i <= nb_pages; i++) { for (let i = 2; i <= nbPages; i++) {
paginated_url.searchParams.set("page", i.toString()); paginatedUrl.searchParams.set("page", i.toString());
promises.push( promises.push(
fetch(paginated_url).then((res) => res.json().then((json) => json.results)), fetch(paginatedUrl).then((res) => res.json().then((json) => json.results)),
); );
} }
results.push(...(await Promise.all(promises)).flat()); results.push(...(await Promise.all(promises)).flat());

View File

@ -15,7 +15,7 @@
* ]; * ];
* document.addEventListener("DOMContentLoaded", () => sithSelect2({ * document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"), * element: document.getElementById("select2-input"),
* data_source: local_data_source(data) * dataSource: localDataSource(data)
* })); * }));
* ``` * ```
* *
@ -29,7 +29,7 @@
* ]; * ];
* document.addEventListener("DOMContentLoaded", () => sithSelect2({ * document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"), * element: document.getElementById("select2-input"),
* data_source: local_data_source(data, { * dataSource: localDataSource(data, {
* excluded: () => data.filter((i) => i.text === "to exclude").map((i) => parseInt(i)) * excluded: () => data.filter((i) => i.text === "to exclude").map((i) => parseInt(i))
* }) * })
* })); * }));
@ -38,15 +38,15 @@
* # Remote data source * # Remote data source
* *
* Select2 with remote data sources are similar to those with local * Select2 with remote data sources are similar to those with local
* data, but with some more parameters, like `result_converter`, * data, but with some more parameters, like `resultConverter`,
* which takes a callback that must return a `Select2Object`. * which takes a callback that must return a `Select2Object`.
* *
* ```js * ```js
* document.addEventListener("DOMContentLoaded", () => sithSelect2({ * document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"), * element: document.getElementById("select2-input"),
* data_source: remote_data_source("/api/user/search", { * dataSource: remoteDataSource("/api/user/search", {
* excluded: () => [1, 2], // exclude users 1 and 2 from the search * excluded: () => [1, 2], // exclude users 1 and 2 from the search
* result_converter: (user) => Object({id: user.id, text: user.first_name}) * resultConverter: (user) => Object({id: user.id, text: user.firstName})
* }) * })
* })); * }));
* ``` * ```
@ -62,8 +62,8 @@
* ```js * ```js
* document.addEventListener("DOMContentLoaded", () => sithSelect2({ * document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"), * element: document.getElementById("select2-input"),
* data_source: remote_data_source("/api/user/search", { * dataSource: remoteDataSource("/api/user/search", {
* result_converter: (user) => Object({id: user.id, text: user.first_name}), * resultConverter: (user) => Object({id: user.id, text: user.firstName}),
* overrides: { * overrides: {
* delay: 500 * delay: 500
* } * }
@ -85,15 +85,15 @@
* *
* Sometimes, you would like to display an image besides * Sometimes, you would like to display an image besides
* the text on the select items. * the text on the select items.
* In this case, fill the `picture_getter` option : * In this case, fill the `pictureGetter` option :
* *
* ```js * ```js
* document.addEventListener("DOMContentLoaded", () => sithSelect2({ * document.addEventListener("DOMContentLoaded", () => sithSelect2({
* element: document.getElementById("select2-input"), * element: document.getElementById("select2-input"),
* data_source: remote_data_source("/api/user/search", { * dataSource: remoteDataSource("/api/user/search", {
* result_converter: (user) => Object({id: user.id, text: user.first_name}) * resultConverter: (user) => Object({id: user.id, text: user.firstName})
* }) * })
* picture_getter: (user) => user.profile_pict, * pictureGetter: (user) => user.profilePict,
* })); * }));
* ``` * ```
* *
@ -105,8 +105,8 @@
* <body> * <body>
* <div x-data="select2_test"> * <div x-data="select2_test">
* <select x-ref="search" x-ref="select"></select> * <select x-ref="search" x-ref="select"></select>
* <p x-text="current_selection.id"></p> * <p x-text="currentSelection.id"></p>
* <p x-text="current_selection.text"></p> * <p x-text="currentSelection.text"></p>
* </div> * </div>
* </body> * </body>
* *
@ -114,20 +114,20 @@
* document.addEventListener("alpine:init", () => { * document.addEventListener("alpine:init", () => {
* Alpine.data("select2_test", () => ({ * Alpine.data("select2_test", () => ({
* selector: undefined, * selector: undefined,
* current_select: {id: "", text: ""}, * currentSelect: {id: "", text: ""},
* *
* init() { * init() {
* this.selector = sithSelect2({ * this.selector = sithSelect2({
* element: $(this.$refs.select), * element: $(this.$refs.select),
* data_source: local_data_source( * dataSource: localDataSource(
* [{id: 1, text: "foo"}, {id: 2, text: "bar"}] * [{id: 1, text: "foo"}, {id: 2, text: "bar"}]
* ), * ),
* }); * });
* this.selector.on("select2:select", (event) => { * this.selector.on("select2:select", (event) => {
* // select2 => Alpine signals here * // select2 => Alpine signals here
* this.current_select = this.selector.select2("data") * this.currentSelect = this.selector.select2("data")
* }); * });
* this.$watch("current_selected" (value) => { * this.$watch("currentSelected" (value) => {
* // Alpine => select2 signals here * // Alpine => select2 signals here
* }); * });
* }, * },
@ -145,10 +145,10 @@
/** /**
* @typedef Select2Options * @typedef Select2Options
* @property {Element} element * @property {Element} element
* @property {Object} data_source * @property {Object} dataSource
* the data source, built with `local_data_source` or `remote_data_source` * the data source, built with `localDataSource` or `remoteDataSource`
* @property {number[]} excluded A list of ids to exclude from search * @property {number[]} excluded A list of ids to exclude from search
* @property {undefined | function(Object): string} picture_getter * @property {undefined | function(Object): string} pictureGetter
* A callback to get the picture field from the API response * A callback to get the picture field from the API response
* @property {Object | undefined} overrides * @property {Object | undefined} overrides
* Any other select2 parameter to apply on the config * Any other select2 parameter to apply on the config
@ -157,13 +157,14 @@
/** /**
* @param {Select2Options} options * @param {Select2Options} options
*/ */
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function sithSelect2(options) { function sithSelect2(options) {
const elem = $(options.element); const elem = $(options.element);
return elem.select2({ return elem.select2({
theme: elem[0].multiple ? "classic" : "default", theme: elem[0].multiple ? "classic" : "default",
minimumInputLength: 2, minimumInputLength: 2,
templateResult: select_item_builder(options.picture_getter), templateResult: selectItemBuilder(options.pictureGetter),
...options.data_source, ...options.dataSource,
...(options.overrides || {}), ...(options.overrides || {}),
}); });
} }
@ -179,7 +180,8 @@ function sithSelect2(options) {
* @param {Select2Object[]} source The array containing the data * @param {Select2Object[]} source The array containing the data
* @param {RemoteSourceOptions} options * @param {RemoteSourceOptions} options
*/ */
function local_data_source(source, options) { // biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function localDataSource(source, options) {
if (options.excluded) { if (options.excluded) {
const ids = options.excluded(); const ids = options.excluded();
return { data: source.filter((i) => !ids.includes(i.id)) }; return { data: source.filter((i) => !ids.includes(i.id)) };
@ -191,7 +193,7 @@ function local_data_source(source, options) {
* @typedef RemoteSourceOptions * @typedef RemoteSourceOptions
* @property {undefined | function(): number[]} excluded * @property {undefined | function(): number[]} excluded
* A callback to the ids to exclude from the search * A callback to the ids to exclude from the search
* @property {undefined | function(): Select2Object} result_converter * @property {undefined | function(): Select2Object} resultConverter
* A converter for a value coming from the remote api * A converter for a value coming from the remote api
* @property {undefined | Object} overrides * @property {undefined | Object} overrides
* Any other select2 parameter to apply on the config * Any other select2 parameter to apply on the config
@ -202,7 +204,9 @@ function local_data_source(source, options) {
* @param {string} source The url of the endpoint * @param {string} source The url of the endpoint
* @param {RemoteSourceOptions} options * @param {RemoteSourceOptions} options
*/ */
function remote_data_source(source, options) {
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function remoteDataSource(source, options) {
jQuery.ajaxSettings.traditional = true; jQuery.ajaxSettings.traditional = true;
const params = { const params = {
url: source, url: source,
@ -219,9 +223,9 @@ function remote_data_source(source, options) {
}; };
}, },
}; };
if (options.result_converter) { if (options.resultConverter) {
params.processResults = (data) => ({ params.processResults = (data) => ({
results: data.results.map(options.result_converter), results: data.results.map(options.resultConverter),
}); });
} }
if (options.overrides) { if (options.overrides) {
@ -230,7 +234,8 @@ function remote_data_source(source, options) {
return { ajax: params }; return { ajax: params };
} }
function item_formatter(user) { // biome-ignore lint/correctness/noUnusedVariables: used in other scripts
function itemFormatter(user) {
if (user.loading) { if (user.loading) {
return user.text; return user.text;
} }
@ -238,22 +243,22 @@ function item_formatter(user) {
/** /**
* Build a function to display the results * Build a function to display the results
* @param {null | function(Object):string} picture_getter * @param {null | function(Object):string} pictureGetter
* @return {function(string): jQuery|HTMLElement} * @return {function(string): jQuery|HTMLElement}
*/ */
function select_item_builder(picture_getter) { function selectItemBuilder(pictureGetter) {
return (item) => { return (item) => {
const picture = typeof picture_getter === "function" ? picture_getter(item) : null; const picture = typeof pictureGetter === "function" ? pictureGetter(item) : null;
const img_html = picture const imgHtml = picture
? `<img ? `<img
src="${picture_getter(item)}" src="${pictureGetter(item)}"
alt="${item.text}" alt="${item.text}"
onerror="this.src = '/static/core/img/unknown.jpg'" onerror="this.src = '/static/core/img/unknown.jpg'"
/>` />`
: ""; : "";
return $(`<div class="select-item"> return $(`<div class="select-item">
${img_html} ${imgHtml}
<span class="select-item-text">${item.text}</span> <span class="select-item-text">${item.text}</span>
</div>`); </div>`);
}; };

View File

@ -1,7 +1,7 @@
async function get_graph_data(url, godfathers_depth, godchildren_depth) { async function getGraphData(url, godfathersDepth, godchildrenDepth) {
const data = await ( const data = await (
await fetch( await fetch(
`${url}?godfathers_depth=${godfathers_depth}&godchildren_depth=${godchildren_depth}`, `${url}?godfathers_depth=${godfathersDepth}&godchildren_depth=${godchildrenDepth}`,
) )
).json(); ).json();
return [ return [
@ -16,7 +16,8 @@ async function get_graph_data(url, godfathers_depth, godchildren_depth) {
]; ];
} }
function create_graph(container, data, active_user_id) { function createGraph(container, data, activeUserId) {
// biome-ignore lint/correctness/noUndeclaredVariables: imported by user_godphaters_tree.jinja
const cy = cytoscape({ const cy = cytoscape({
boxSelectionEnabled: false, boxSelectionEnabled: false,
autounselectify: true, autounselectify: true,
@ -83,9 +84,9 @@ function create_graph(container, data, active_user_id) {
}, },
}, },
}); });
const active_user = cy.getElementById(active_user_id).style("shape", "rectangle"); const activeUser = cy.getElementById(activeUserId).style("shape", "rectangle");
/* Reset graph */ /* Reset graph */
const reset_graph = () => { const resetGraph = () => {
cy.elements((element) => { cy.elements((element) => {
if (element.hasClass("traversed")) { if (element.hasClass("traversed")) {
element.removeClass("traversed"); element.removeClass("traversed");
@ -96,10 +97,10 @@ function create_graph(container, data, active_user_id) {
}); });
}; };
const on_node_tap = (el) => { const onNodeTap = (el) => {
reset_graph(); resetGraph();
/* Create path on graph if selected isn't the targeted user */ /* Create path on graph if selected isn't the targeted user */
if (el === active_user) { if (el === activeUser) {
return; return;
} }
cy.elements((element) => { cy.elements((element) => {
@ -108,7 +109,7 @@ function create_graph(container, data, active_user_id) {
for (const traversed of cy.elements().aStar({ for (const traversed of cy.elements().aStar({
root: el, root: el,
goal: active_user, goal: activeUser,
}).path) { }).path) {
traversed.removeClass("not-traversed"); traversed.removeClass("not-traversed");
traversed.addClass("traversed"); traversed.addClass("traversed");
@ -116,14 +117,13 @@ function create_graph(container, data, active_user_id) {
}; };
cy.on("tap", "node", (tapped) => { cy.on("tap", "node", (tapped) => {
on_node_tap(tapped.target); onNodeTap(tapped.target);
}); });
cy.zoomingEnabled(false); cy.zoomingEnabled(false);
/* Add context menu */ /* Add context menu */
if (cy.cxtmenu === undefined) { if (cy.cxtmenu === undefined) {
console.error("ctxmenu isn't loaded, context menu won't be available on graphs"); throw new Error("ctxmenu isn't loaded, context menu won't be available on graphs");
return cy;
} }
cy.cxtmenu({ cy.cxtmenu({
selector: "node", selector: "node",
@ -139,14 +139,14 @@ function create_graph(container, data, active_user_id) {
{ {
content: '<span class="fa fa-mouse-pointer fa-2x"></span>', content: '<span class="fa fa-mouse-pointer fa-2x"></span>',
select: (el) => { select: (el) => {
on_node_tap(el); onNodeTap(el);
}, },
}, },
{ {
content: '<i class="fa fa-eraser fa-2x"></i>', content: '<i class="fa fa-eraser fa-2x"></i>',
select: (el) => { select: (_) => {
reset_graph(); resetGraph();
}, },
}, },
], ],
@ -155,73 +155,81 @@ function create_graph(container, data, active_user_id) {
return cy; return cy;
} }
/* global api_url, active_user, depth_min, depth_max */
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
/* /*
This needs some constants to be set before the document has been loaded This needs some constants to be set before the document has been loaded
api_url: base url for fetching the tree as a string apiUrl: base url for fetching the tree as a string
active_user: id of the user to fetch the tree from activeUser: id of the user to fetch the tree from
depth_min: minimum tree depth for godfathers and godchildren as an int depthMin: minimum tree depth for godfathers and godchildren as an int
depth_max: maximum tree depth for godfathers and godchildren as an int depthMax: maximum tree depth for godfathers and godchildren as an int
*/ */
const default_depth = 2; const defaultDepth = 2;
if ( if (
typeof api_url === "undefined" || // biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
typeof active_user === "undefined" || typeof apiUrl === "undefined" ||
typeof depth_min === "undefined" || // biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
typeof depth_max === "undefined" typeof activeUser === "undefined" ||
// biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
typeof depthMin === "undefined" ||
// biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
typeof depthMax === "undefined"
) { ) {
console.error( throw new Error(
"Some constants are not set before using the family_graph script, please look at the documentation", "Some constants are not set before using the family_graph script, please look at the documentation",
); );
return;
} }
function get_initial_depth(prop) { function getInitialDepth(prop) {
// biome-ignore lint/correctness/noUndeclaredVariables: defined by script.js
const value = Number.parseInt(initialUrlParams.get(prop)); const value = Number.parseInt(initialUrlParams.get(prop));
if (Number.isNaN(value) || value < depth_min || value > depth_max) { // biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
return default_depth; if (Number.isNaN(value) || value < depthMin || value > depthMax) {
return defaultDepth;
} }
return value; return value;
} }
Alpine.data("graph", () => ({ Alpine.data("graph", () => ({
loading: false, loading: false,
godfathers_depth: get_initial_depth("godfathers_depth"), godfathersDepth: getInitialDepth("godfathersDepth"),
godchildren_depth: get_initial_depth("godchildren_depth"), godchildrenDepth: getInitialDepth("godchildrenDepth"),
// biome-ignore lint/correctness/noUndeclaredVariables: defined by script.js
reverse: initialUrlParams.get("reverse")?.toLowerCase?.() === "true", reverse: initialUrlParams.get("reverse")?.toLowerCase?.() === "true",
graph: undefined, graph: undefined,
graph_data: {}, graphData: {},
async init() { async init() {
const delayed_fetch = Alpine.debounce(async () => { const delayedFetch = Alpine.debounce(async () => {
this.fetch_graph_data(); await this.fetchGraphData();
}, 100); }, 100);
for (const param of ["godfathers_depth", "godchildren_depth"]) { for (const param of ["godfathersDepth", "godchildrenDepth"]) {
this.$watch(param, async (value) => { this.$watch(param, async (value) => {
if (value < depth_min || value > depth_max) { // biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
if (value < depthMin || value > depthMax) {
return; return;
} }
update_query_string(param, value, History.REPLACE); // biome-ignore lint/correctness/noUndeclaredVariables: defined by script.js
delayed_fetch(); updateQueryString(param, value, History.REPLACE);
await delayedFetch();
}); });
} }
this.$watch("reverse", async (value) => { this.$watch("reverse", async (value) => {
update_query_string("reverse", value, History.REPLACE); // biome-ignore lint/correctness/noUndeclaredVariables: defined by script.js
this.reverse_graph(); updateQueryString("reverse", value, History.REPLACE);
await this.reverseGraph();
}); });
this.$watch("graph_data", async () => { this.$watch("graphData", async () => {
await this.generate_graph(); await this.generateGraph();
if (this.reverse) { if (this.reverse) {
await this.reverse_graph(); await this.reverseGraph();
} }
}); });
this.fetch_graph_data(); await this.fetchGraphData();
}, },
async screenshot() { screenshot() {
const link = document.createElement("a"); const link = document.createElement("a");
link.href = this.graph.jpg(); link.href = this.graph.jpg();
link.download = interpolate( link.download = interpolate(
@ -234,30 +242,32 @@ document.addEventListener("alpine:init", () => {
document.body.removeChild(link); document.body.removeChild(link);
}, },
async reset() { reset() {
this.reverse = false; this.reverse = false;
this.godfathers_depth = default_depth; this.godfathersDepth = defaultDepth;
this.godchildren_depth = default_depth; this.godchildrenDepth = defaultDepth;
}, },
async reverse_graph() { async reverseGraph() {
this.graph.elements((el) => { this.graph.elements((el) => {
el.position({ x: -el.position().x, y: -el.position().y }); el.position({ x: -el.position().x, y: -el.position().y });
}); });
this.graph.center(this.graph.elements()); this.graph.center(this.graph.elements());
}, },
async fetch_graph_data() { async fetchGraphData() {
this.graph_data = await get_graph_data( this.graphData = await getGraphData(
api_url, // biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
this.godfathers_depth, apiUrl,
this.godchildren_depth, this.godfathersDepth,
this.godchildrenDepth,
); );
}, },
async generate_graph() { async generateGraph() {
this.loading = true; this.loading = true;
this.graph = create_graph($(this.$refs.graph), this.graph_data, active_user); // biome-ignore lint/correctness/noUndeclaredVariables: defined by user_godfathers_tree.jinja
this.graph = await createGraph($(this.$refs.graph), this.graphData, activeUser);
this.loading = false; this.loading = false;
}, },
})); }));

View File

@ -1,23 +1,24 @@
function alpine_webcam_builder(default_picture, delete_url, can_delete_picture) { // biome-ignore lint/correctness/noUnusedVariables: used in user_edit.jinja
function alpineWebcamBuilder(defaultPicture, deleteUrl, canDeletePicture) {
return () => ({ return () => ({
can_edit_picture: false, canEditPicture: false,
loading: false, loading: false,
is_camera_enabled: false, isCameraEnabled: false,
is_camera_error: false, isCameraError: false,
picture: null, picture: null,
video: null, video: null,
picture_form: null, pictureForm: null,
init() { init() {
this.video = this.$refs.video; this.video = this.$refs.video;
this.picture_form = this.$refs.form.getElementsByTagName("input"); this.pictureForm = this.$refs.form.getElementsByTagName("input");
if (this.picture_form.length > 0) { if (this.pictureForm.length > 0) {
this.picture_form = this.picture_form[0]; this.pictureForm = this.pictureForm[0];
this.can_edit_picture = true; this.canEditPicture = true;
// Link the displayed element to the form input // Link the displayed element to the form input
this.picture_form.onchange = (event) => { this.pictureForm.onchange = (event) => {
const files = event.srcElement.files; const files = event.srcElement.files;
if (files.length > 0) { if (files.length > 0) {
this.picture = (window.URL || window.webkitURL).createObjectURL( this.picture = (window.URL || window.webkitURL).createObjectURL(
@ -30,45 +31,45 @@ function alpine_webcam_builder(default_picture, delete_url, can_delete_picture)
} }
}, },
get_picture() { getPicture() {
return this.picture || default_picture; return this.picture || defaultPicture;
}, },
delete_picture() { deletePicture() {
// Only remove currently displayed picture // Only remove currently displayed picture
if (this.picture) { if (this.picture) {
const list = new DataTransfer(); const list = new DataTransfer();
this.picture_form.files = list.files; this.pictureForm.files = list.files;
this.picture_form.dispatchEvent(new Event("change")); this.pictureForm.dispatchEvent(new Event("change"));
return; return;
} }
if (!can_delete_picture) { if (!canDeletePicture) {
return; return;
} }
// Remove user picture if correct rights are available // Remove user picture if correct rights are available
window.open(delete_url, "_self"); window.open(deleteUrl, "_self");
}, },
enable_camera() { enableCamera() {
this.picture = null; this.picture = null;
this.loading = true; this.loading = true;
this.is_camera_error = false; this.isCameraError = false;
navigator.mediaDevices navigator.mediaDevices
.getUserMedia({ video: true, audio: false }) .getUserMedia({ video: true, audio: false })
.then((stream) => { .then((stream) => {
this.loading = false; this.loading = false;
this.is_camera_enabled = true; this.isCameraEnabled = true;
this.video.srcObject = stream; this.video.srcObject = stream;
this.video.play(); this.video.play();
}) })
.catch((err) => { .catch((err) => {
this.is_camera_error = true; this.isCameraError = true;
this.loading = false; this.loading = false;
throw err; throw err;
}); });
}, },
take_picture() { takePicture() {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
@ -94,14 +95,14 @@ function alpine_webcam_builder(default_picture, delete_url, can_delete_picture)
const list = new DataTransfer(); const list = new DataTransfer();
list.items.add(file); list.items.add(file);
this.picture_form.files = list.files; this.pictureForm.files = list.files;
// No change event is triggered, we trigger it manually #} // No change event is triggered, we trigger it manually #}
this.picture_form.dispatchEvent(new Event("change")); this.pictureForm.dispatchEvent(new Event("change"));
}, "image/webp"); }, "image/webp");
canvas.remove(); canvas.remove();
this.is_camera_enabled = false; this.isCameraEnabled = false;
}, },
}); });
} }

View File

@ -2,6 +2,6 @@ import Alpine from "alpinejs";
window.Alpine = Alpine; window.Alpine = Alpine;
window.addEventListener("DOMContentLoaded", (event) => { window.addEventListener("DOMContentLoaded", () => {
Alpine.start(); Alpine.start();
}); });

View File

@ -1,6 +1,7 @@
// biome-ignore lint/correctness/noUndeclaredDependencies: shipped by easymde
import "codemirror/lib/codemirror.css"; import "codemirror/lib/codemirror.css";
import "easymde/src/css/easymde.css"; import "easymde/src/css/easymde.css";
import EasyMDE from "easymde"; import easyMde from "easymde";
// This scripts dependens on Alpine but it should be loaded on every page // This scripts dependens on Alpine but it should be loaded on every page
@ -9,13 +10,13 @@ import EasyMDE from "easymde";
* @param {HTMLTextAreaElement} textarea to use * @param {HTMLTextAreaElement} textarea to use
* @param {string} link to the markdown api * @param {string} link to the markdown api
**/ **/
function easymdeFactory(textarea, markdownApiURL) { function easymdeFactory(textarea, markdownApiUrl) {
const easymde = new EasyMDE({ const easymde = new easyMde({
element: textarea, element: textarea,
spellChecker: false, spellChecker: false,
autoDownloadFontAwesome: false, autoDownloadFontAwesome: false,
previewRender: Alpine.debounce(async (plainText, preview) => { previewRender: Alpine.debounce(async (plainText, preview) => {
const res = await fetch(markdownApiURL, { const res = await fetch(markdownApiUrl, {
method: "POST", method: "POST",
body: JSON.stringify({ text: plainText }), body: JSON.stringify({ text: plainText }),
}); });
@ -26,25 +27,25 @@ function easymdeFactory(textarea, markdownApiURL) {
toolbar: [ toolbar: [
{ {
name: "heading-smaller", name: "heading-smaller",
action: EasyMDE.toggleHeadingSmaller, action: easyMde.toggleHeadingSmaller,
className: "fa fa-header", className: "fa fa-header",
title: gettext("Heading"), title: gettext("Heading"),
}, },
{ {
name: "italic", name: "italic",
action: EasyMDE.toggleItalic, action: easyMde.toggleItalic,
className: "fa fa-italic", className: "fa fa-italic",
title: gettext("Italic"), title: gettext("Italic"),
}, },
{ {
name: "bold", name: "bold",
action: EasyMDE.toggleBold, action: easyMde.toggleBold,
className: "fa fa-bold", className: "fa fa-bold",
title: gettext("Bold"), title: gettext("Bold"),
}, },
{ {
name: "strikethrough", name: "strikethrough",
action: EasyMDE.toggleStrikethrough, action: easyMde.toggleStrikethrough,
className: "fa fa-strikethrough", className: "fa fa-strikethrough",
title: gettext("Strikethrough"), title: gettext("Strikethrough"),
}, },
@ -77,71 +78,71 @@ function easymdeFactory(textarea, markdownApiURL) {
}, },
{ {
name: "code", name: "code",
action: EasyMDE.toggleCodeBlock, action: easyMde.toggleCodeBlock,
className: "fa fa-code", className: "fa fa-code",
title: gettext("Code"), title: gettext("Code"),
}, },
"|", "|",
{ {
name: "quote", name: "quote",
action: EasyMDE.toggleBlockquote, action: easyMde.toggleBlockquote,
className: "fa fa-quote-left", className: "fa fa-quote-left",
title: gettext("Quote"), title: gettext("Quote"),
}, },
{ {
name: "unordered-list", name: "unordered-list",
action: EasyMDE.toggleUnorderedList, action: easyMde.toggleUnorderedList,
className: "fa fa-list-ul", className: "fa fa-list-ul",
title: gettext("Unordered list"), title: gettext("Unordered list"),
}, },
{ {
name: "ordered-list", name: "ordered-list",
action: EasyMDE.toggleOrderedList, action: easyMde.toggleOrderedList,
className: "fa fa-list-ol", className: "fa fa-list-ol",
title: gettext("Ordered list"), title: gettext("Ordered list"),
}, },
"|", "|",
{ {
name: "link", name: "link",
action: EasyMDE.drawLink, action: easyMde.drawLink,
className: "fa fa-link", className: "fa fa-link",
title: gettext("Insert link"), title: gettext("Insert link"),
}, },
{ {
name: "image", name: "image",
action: EasyMDE.drawImage, action: easyMde.drawImage,
className: "fa-regular fa-image", className: "fa-regular fa-image",
title: gettext("Insert image"), title: gettext("Insert image"),
}, },
{ {
name: "table", name: "table",
action: EasyMDE.drawTable, action: easyMde.drawTable,
className: "fa fa-table", className: "fa fa-table",
title: gettext("Insert table"), title: gettext("Insert table"),
}, },
"|", "|",
{ {
name: "clean-block", name: "clean-block",
action: EasyMDE.cleanBlock, action: easyMde.cleanBlock,
className: "fa fa-eraser fa-clean-block", className: "fa fa-eraser fa-clean-block",
title: gettext("Clean block"), title: gettext("Clean block"),
}, },
"|", "|",
{ {
name: "preview", name: "preview",
action: EasyMDE.togglePreview, action: easyMde.togglePreview,
className: "fa fa-eye no-disable", className: "fa fa-eye no-disable",
title: gettext("Toggle preview"), title: gettext("Toggle preview"),
}, },
{ {
name: "side-by-side", name: "side-by-side",
action: EasyMDE.toggleSideBySide, action: easyMde.toggleSideBySide,
className: "fa fa-columns no-disable no-mobile", className: "fa fa-columns no-disable no-mobile",
title: gettext("Toggle side by side"), title: gettext("Toggle side by side"),
}, },
{ {
name: "fullscreen", name: "fullscreen",
action: EasyMDE.toggleFullScreen, action: easyMde.toggleFullScreen,
className: "fa fa-expand no-mobile", className: "fa fa-expand no-mobile",
title: gettext("Toggle fullscreen"), title: gettext("Toggle fullscreen"),
}, },
@ -159,7 +160,7 @@ function easymdeFactory(textarea, markdownApiURL) {
const parentDiv = textarea.parentElement; const parentDiv = textarea.parentElement;
let submitPressed = false; let submitPressed = false;
function checkMarkdownInput(e) { function checkMarkdownInput() {
// an attribute is null if it does not exist, else a string // an attribute is null if it does not exist, else a string
const required = textarea.getAttribute("required") != null; const required = textarea.getAttribute("required") != null;
const length = textarea.value.trim().length; const length = textarea.value.trim().length;

View File

@ -112,7 +112,7 @@
></a> ></a>
</div> </div>
<div class="notification"> <div class="notification">
<a href="#" onclick="display_notif()"> <a href="#" onclick="displayNotif()">
<i class="fa-regular fa-bell"></i> <i class="fa-regular fa-bell"></i>
{% set notification_count = user.notifications.filter(viewed=False).count() %} {% set notification_count = user.notifications.filter(viewed=False).count() %}

View File

@ -15,36 +15,36 @@
{% macro profile_picture(field_name) %} {% macro profile_picture(field_name) %}
{% set this_picture = form.instance[field_name] %} {% set this_picture = form.instance[field_name] %}
<div class="profile-picture" x-data="camera_{{ field_name }}" > <div class="profile-picture" x-data="camera_{{ field_name }}" >
<div class="profile-picture-display" :aria-busy="loading" :class="{ 'camera-error': is_camera_error }"> <div class="profile-picture-display" :aria-busy="loading" :class="{ 'camera-error': isCameraError }">
<img <img
x-show="!is_camera_enabled && !is_camera_error" x-show="!isCameraEnabled && !isCameraError"
:src="get_picture()" :src="getPicture()"
alt="{%- trans -%}Profile{%- endtrans -%}" title="{%- trans -%}Profile{%- endtrans -%}" alt="{%- trans -%}Profile{%- endtrans -%}" title="{%- trans -%}Profile{%- endtrans -%}"
loading="lazy" loading="lazy"
/> />
<video <video
x-show="is_camera_enabled" x-show="isCameraEnabled"
x-ref="video" x-ref="video"
></video> ></video>
<i <i
x-show="is_camera_error" x-show="isCameraError"
x-cloak x-cloak
class="fa fa-eye-slash" class="fa fa-eye-slash"
></i> ></i>
</div> </div>
<div class="profile-picture-buttons" x-show="can_edit_picture"> <div class="profile-picture-buttons" x-show="canEditPicture">
<button <button
x-show="can_edit_picture && !is_camera_enabled" x-show="canEditPicture && !isCameraEnabled"
class="btn btn-blue" class="btn btn-blue"
@click.prevent="enable_camera()" @click.prevent="enableCamera()"
> >
<i class="fa fa-camera"></i> <i class="fa fa-camera"></i>
{% trans %}Enable camera{% endtrans %} {% trans %}Enable camera{% endtrans %}
</button> </button>
<button <button
x-show="is_camera_enabled" x-show="isCameraEnabled"
class="btn btn-blue" class="btn btn-blue"
@click.prevent="take_picture()" @click.prevent="takePicture()"
> >
<i class="fa fa-camera"></i> <i class="fa fa-camera"></i>
{% trans %}Take a picture{% endtrans %} {% trans %}Take a picture{% endtrans %}
@ -54,7 +54,7 @@
{%- if form[field_name] -%} {%- if form[field_name] -%}
<div> <div>
{{ form[field_name] }} {{ form[field_name] }}
<button class="btn btn-red" @click.prevent="delete_picture()" <button class="btn btn-red" @click.prevent="deletePicture()"
{%- if not (this_picture and this_picture.is_owned_by(user)) -%} {%- if not (this_picture and this_picture.is_owned_by(user)) -%}
:disabled="!picture" :disabled="!picture"
{%- endif -%} {%- endif -%}
@ -86,7 +86,7 @@
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data( Alpine.data(
"camera_{{ field_name }}", "camera_{{ field_name }}",
alpine_webcam_builder( alpineWebcamBuilder(
{{ default_picture }}, {{ default_picture }},
{{ delete_url }}, {{ delete_url }},
{{ (this_picture and this_picture.is_owned_by(user))|tojson }} {{ (this_picture and this_picture.is_owned_by(user))|tojson }}

View File

@ -30,21 +30,21 @@
</label> </label>
<span class="depth-choice"> <span class="depth-choice">
<button <button
@click="godfathers_depth--" @click="godfathersDepth--"
:disabled="godfathers_depth <= {{ depth_min }}" :disabled="godfathersDepth <= {{ depth_min }}"
><i class="fa fa-minus"></i></button> ><i class="fa fa-minus"></i></button>
<input <input
x-model="godfathers_depth" x-model="godfathersDepth"
x-ref="godfather_depth_input" x-ref="godfather_depth_input"
type="number" type="number"
name="godfathers_depth" name="godfathersDepth"
id="godfather-depth-input" id="godfather-depth-input"
min="{{ depth_min }}" min="{{ depth_min }}"
max="{{ depth_max }}" max="{{ depth_max }}"
/> />
<button <button
@click="godfathers_depth++" @click="godfathersDepth++"
:disabled="godfathers_depth >= {{ depth_max }}" :disabled="godfathersDepth >= {{ depth_max }}"
><i class="fa fa-plus" ><i class="fa fa-plus"
></i></button> ></i></button>
</span> </span>
@ -56,22 +56,22 @@
</label> </label>
<span class="depth-choice"> <span class="depth-choice">
<button <button
@click="godchildren_depth--" @click="godchildrenDepth--"
:disabled="godchildren_depth <= {{ depth_min }}" :disabled="godchildrenDepth <= {{ depth_min }}"
><i ><i
class="fa fa-minus" class="fa fa-minus"
></i></button> ></i></button>
<input <input
x-model="godchildren_depth" x-model="godchildrenDepth"
type="number" type="number"
name="godchildren_depth" name="godchildrenDepth"
id="godchild-depth-input" id="godchild-depth-input"
min="{{ depth_min }}" min="{{ depth_min }}"
max="{{ depth_max }}" max="{{ depth_max }}"
/> />
<button <button
@click="godchildren_depth++" @click="godchildrenDepth++"
:disabled="godchildren_depth >= {{ depth_max }}" :disabled="godchildrenDepth >= {{ depth_max }}"
><i class="fa fa-plus" ><i class="fa fa-plus"
></i></button> ></i></button>
</span> </span>
@ -96,10 +96,10 @@
</div> </div>
<script> <script>
const api_url = "{{ api_url }}"; const apiUrl = "{{ api_url }}";
const active_user = "{{ object.id }}" const activeUser = "{{ object.id }}"
const depth_min = {{ depth_min }}; const depthMin = {{ depth_min }};
const depth_max = {{ depth_max }}; const depthMax = {{ depth_max }};
</script> </script>
{% endblock %} {% endblock %}

View File

@ -21,14 +21,14 @@
{% if user.id == object.id %} {% if user.id == object.id %}
<div x-show="pictures.length > 0" x-cloak> <div x-show="pictures.length > 0" x-cloak>
<button <button
:disabled="is_downloading" :disabled="isDownloading"
class="btn btn-blue" class="btn btn-blue"
@click="download_zip()" @click="downloadZip()"
> >
<i class="fa fa-download"></i> <i class="fa fa-download"></i>
{% trans %}Download all my pictures{% endtrans %} {% trans %}Download all my pictures{% endtrans %}
</button> </button>
<progress x-ref="progress" x-show="is_downloading"></progress> <progress x-ref="progress" x-show="isDownloading"></progress>
</div> </div>
{% endif %} {% endif %}
@ -92,13 +92,13 @@
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("user_pictures", () => ({ Alpine.data("user_pictures", () => ({
is_downloading: false, isDownloading: false,
loading: true, loading: true,
pictures: [], pictures: [],
albums: {}, albums: {},
async init() { async init() {
this.pictures = await fetch_paginated("{{ url("api:pictures") }}" + "?users_identified={{ object.id }}"); this.pictures = await fetchPaginated("{{ url("api:pictures") }}" + "?users_identified={{ object.id }}");
this.albums = this.pictures.reduce((acc, picture) => { this.albums = this.pictures.reduce((acc, picture) => {
if (!acc[picture.album]){ if (!acc[picture.album]){
acc[picture.album] = []; acc[picture.album] = [];
@ -109,8 +109,8 @@
this.loading = false; this.loading = false;
}, },
async download_zip(){ async downloadZip(){
this.is_downloading = true; this.isDownloading = true;
const bar = this.$refs.progress; const bar = this.$refs.progress;
bar.value = 0; bar.value = 0;
bar.max = this.pictures.length; bar.max = this.pictures.length;
@ -124,16 +124,16 @@
const zipWriter = new zip.ZipWriter(await fileHandle.createWritable()); const zipWriter = new zip.ZipWriter(await fileHandle.createWritable());
await Promise.all(this.pictures.map(p => { await Promise.all(this.pictures.map(p => {
const img_name = p.album + "/IMG_" + p.date.replaceAll(/[:\-]/g, "_") + p.name.slice(p.name.lastIndexOf(".")); const imgName = p.album + "/IMG_" + p.date.replaceAll(/[:\-]/g, "_") + p.name.slice(p.name.lastIndexOf("."));
return zipWriter.add( return zipWriter.add(
img_name, imgName,
new zip.HttpReader(p.full_size_url), new zip.HttpReader(p.full_size_url),
{level: 9, lastModDate: new Date(p.date), onstart: () => bar.value += 1} {level: 9, lastModDate: new Date(p.date), onstart: () => bar.value += 1}
); );
})); }));
await zipWriter.close(); await zipWriter.close();
this.is_downloading = false; this.isDownloading = false;
} }
})) }))
}); });

View File

@ -1,9 +1,10 @@
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("counter", () => ({ Alpine.data("counter", () => ({
basket, // biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja
basket: sessionBasket,
errors: [], errors: [],
sum_basket() { sumBasket() {
if (!this.basket || Object.keys(this.basket).length === 0) { if (!this.basket || Object.keys(this.basket).length === 0) {
return 0; return 0;
} }
@ -14,23 +15,26 @@ document.addEventListener("alpine:init", () => {
return total / 100; return total / 100;
}, },
async handle_code(event) { async handleCode(event) {
const code = $(event.target).find("#code_field").val().toUpperCase(); const code = $(event.target).find("#code_field").val().toUpperCase();
if (["FIN", "ANN"].includes(code)) { if (["FIN", "ANN"].includes(code)) {
$(event.target).submit(); $(event.target).submit();
} else { } else {
await this.handle_action(event); await this.handleAction(event);
} }
}, },
async handle_action(event) { async handleAction(event) {
const payload = $(event.target).serialize(); const payload = $(event.target).serialize();
const request = new Request(click_api_url, { // biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja
const request = new Request(clickApiUrl, {
method: "POST", method: "POST",
body: payload, body: payload,
headers: { headers: {
// biome-ignore lint/style/useNamingConvention: this goes into http headers
Accept: "application/json", Accept: "application/json",
"X-CSRFToken": csrf_token, // biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja
"X-CSRFToken": csrfToken,
}, },
}); });
const response = await fetch(request); const response = await fetch(request);
@ -44,25 +48,27 @@ document.addEventListener("alpine:init", () => {
$(() => { $(() => {
/* Autocompletion in the code field */ /* Autocompletion in the code field */
const code_field = $("#code_field"); const codeField = $("#code_field");
let quantity = ""; let quantity = "";
code_field.autocomplete({ codeField.autocomplete({
select: (event, ui) => { select: (event, ui) => {
event.preventDefault(); event.preventDefault();
code_field.val(quantity + ui.item.value); codeField.val(quantity + ui.item.value);
}, },
focus: (event, ui) => { focus: (event, ui) => {
event.preventDefault(); event.preventDefault();
code_field.val(quantity + ui.item.value); codeField.val(quantity + ui.item.value);
}, },
source: (request, response) => { source: (request, response) => {
// biome-ignore lint/performance/useTopLevelRegex: performance impact is minimal
const res = /^(\d+x)?(.*)/i.exec(request.term); const res = /^(\d+x)?(.*)/i.exec(request.term);
quantity = res[1] || ""; quantity = res[1] || "";
const search = res[2]; const search = res[2];
const matcher = new RegExp($.ui.autocomplete.escapeRegex(search), "i"); const matcher = new RegExp($.ui.autocomplete.escapeRegex(search), "i");
response( response(
$.grep(products_autocomplete, (value) => { // biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja
$.grep(productsAutocomplete, (value) => {
return matcher.test(value.tags); return matcher.test(value.tags);
}), }),
); );
@ -76,5 +82,5 @@ $(() => {
}); });
$("#products").tabs(); $("#products").tabs();
code_field.focus(); codeField.focus();
}); });

View File

@ -59,7 +59,7 @@
{# Formulaire pour rechercher un produit en tapant son code dans une barre de recherche #} {# Formulaire pour rechercher un produit en tapant son code dans une barre de recherche #}
<form method="post" action="" <form method="post" action=""
class="code_form" @submit.prevent="handle_code"> class="code_form" @submit.prevent="handleCode">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="action" value="code"> <input type="hidden" name="action" value="code">
<label for="code_field"></label> <label for="code_field"></label>
@ -77,7 +77,7 @@
<template x-for="[id, item] in Object.entries(basket)" :key="id"> <template x-for="[id, item] in Object.entries(basket)" :key="id">
<div> <div>
<form method="post" action="" class="inline del_product_form" <form method="post" action="" class="inline del_product_form"
@submit.prevent="handle_action"> @submit.prevent="handleAction">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="action" value="del_product"> <input type="hidden" name="action" value="del_product">
<input type="hidden" name="product_id" :value="id"> <input type="hidden" name="product_id" :value="id">
@ -87,7 +87,7 @@
<span x-text="item['qty'] + item['bonus_qty']"></span> <span x-text="item['qty'] + item['bonus_qty']"></span>
<form method="post" action="" class="inline add_product_form" <form method="post" action="" class="inline add_product_form"
@submit.prevent="handle_action"> @submit.prevent="handleAction">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="action" value="add_product"> <input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" :value="id"> <input type="hidden" name="product_id" :value="id">
@ -104,7 +104,7 @@
</ul> </ul>
<p> <p>
<strong>Total: </strong> <strong>Total: </strong>
<strong x-text="sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 })"></strong> <strong x-text="sumBasket().toLocaleString(undefined, { minimumFractionDigits: 2 })"></strong>
<strong> €</strong> <strong> €</strong>
</p> </p>
@ -147,7 +147,7 @@
{% for p in categories[category] -%} {% for p in categories[category] -%}
<form method="post" <form method="post"
action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"
class="form_button add_product_form" @submit.prevent="handle_action"> class="form_button add_product_form" @submit.prevent="handleAction">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="action" value="add_product"> <input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" value="{{ p.id }}"> <input type="hidden" name="product_id" value="{{ p.id }}">
@ -171,9 +171,9 @@
{% block script %} {% block script %}
{{ super() }} {{ super() }}
<script> <script>
const csrf_token = "{{ csrf_token }}"; const csrfToken = "{{ csrf_token }}";
const click_api_url = "{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"; const clickApiUrl = "{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}";
const basket = {{ request.session["basket"]|tojson }}; const sessionBasket = {{ request.session["basket"]|tojson }};
const products = { const products = {
{%- for p in products -%} {%- for p in products -%}
{{ p.id }}: { {{ p.id }}: {
@ -183,7 +183,7 @@
}, },
{%- endfor -%} {%- endfor -%}
}; };
const products_autocomplete = [ const productsAutocomplete = [
{% for p in products -%} {% for p in products -%}
{ {
value: "{{ p.code }}", value: "{{ p.code }}",

View File

@ -14,6 +14,7 @@ const BASKET_ITEMS_COOKIE_NAME = "basket_items";
* @returns {string|null|undefined} the value of the cookie or null if it does not exist, undefined if not found * @returns {string|null|undefined} the value of the cookie or null if it does not exist, undefined if not found
*/ */
function getCookie(name) { function getCookie(name) {
// biome-ignore lint/style/useBlockStatements: <explanation>
if (!document.cookie || document.cookie.length === 0) return null; if (!document.cookie || document.cookie.length === 0) return null;
const found = document.cookie const found = document.cookie
@ -28,7 +29,7 @@ function getCookie(name) {
* Fetch the basket items from the associated cookie * Fetch the basket items from the associated cookie
* @returns {BasketItem[]|[]} the items in the basket * @returns {BasketItem[]|[]} the items in the basket
*/ */
function get_starting_items() { function getStartingItems() {
const cookie = getCookie(BASKET_ITEMS_COOKIE_NAME); const cookie = getCookie(BASKET_ITEMS_COOKIE_NAME);
if (!cookie) { if (!cookie) {
return []; return [];
@ -45,13 +46,13 @@ function get_starting_items() {
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("basket", () => ({ Alpine.data("basket", () => ({
items: get_starting_items(), items: getStartingItems(),
/** /**
* Get the total price of the basket * Get the total price of the basket
* @returns {number} The total price of the basket * @returns {number} The total price of the basket
*/ */
get_total() { getTotal() {
return this.items.reduce((acc, item) => acc + item.quantity * item.unit_price, 0); return this.items.reduce((acc, item) => acc + item.quantity * item.unit_price, 0);
}, },
@ -61,38 +62,40 @@ document.addEventListener("alpine:init", () => {
*/ */
add(item) { add(item) {
item.quantity++; item.quantity++;
this.set_cookies(); this.setCookies();
}, },
/** /**
* Remove 1 to the quantity of an item in the basket * Remove 1 to the quantity of an item in the basket
* @param {BasketItem} item_id * @param {BasketItem} item_id
*/ */
remove(item_id) { remove(itemId) {
const index = this.items.findIndex((e) => e.id === item_id); const index = this.items.findIndex((e) => e.id === itemId);
if (index < 0) return; if (index < 0) {
return;
}
this.items[index].quantity -= 1; this.items[index].quantity -= 1;
if (this.items[index].quantity === 0) { if (this.items[index].quantity === 0) {
this.items = this.items.filter((e) => e.id !== this.items[index].id); this.items = this.items.filter((e) => e.id !== this.items[index].id);
} }
this.set_cookies(); this.setCookies();
}, },
/** /**
* Remove all the items from the basket & cleans the catalog CSS classes * Remove all the items from the basket & cleans the catalog CSS classes
*/ */
clear_basket() { clearBasket() {
this.items = []; this.items = [];
this.set_cookies(); this.setCookies();
}, },
/** /**
* Set the cookie in the browser with the basket items * Set the cookie in the browser with the basket items
* ! the cookie survives an hour * ! the cookie survives an hour
*/ */
set_cookies() { setCookies() {
if (this.items.length === 0) { if (this.items.length === 0) {
document.cookie = `${BASKET_ITEMS_COOKIE_NAME}=;Max-Age=0`; document.cookie = `${BASKET_ITEMS_COOKIE_NAME}=;Max-Age=0`;
} else { } else {
@ -107,18 +110,19 @@ document.addEventListener("alpine:init", () => {
* @param {number} price The unit price of the product * @param {number} price The unit price of the product
* @returns {BasketItem} The created item * @returns {BasketItem} The created item
*/ */
create_item(id, name, price) { createItem(id, name, price) {
const new_item = { const newItem = {
id, id,
name, name,
quantity: 0, quantity: 0,
// biome-ignore lint/style/useNamingConvention: used by django backend
unit_price: price, unit_price: price,
}; };
this.items.push(new_item); this.items.push(newItem);
this.add(new_item); this.add(newItem);
return new_item; return newItem;
}, },
/** /**
@ -128,15 +132,15 @@ document.addEventListener("alpine:init", () => {
* @param {string} name The name of the product * @param {string} name The name of the product
* @param {number} price The unit price of the product * @param {number} price The unit price of the product
*/ */
add_from_catalog(id, name, price) { addFromCatalog(id, name, price) {
let item = this.items.find((e) => e.id === id); let item = this.items.find((e) => e.id === id);
// if the item is not in the basket, we create it // if the item is not in the basket, we create it
// else we add + 1 to it // else we add + 1 to it
if (!item) { if (item) {
item = this.create_item(id, name, price);
} else {
this.add(item); this.add(item);
} else {
item = this.createItem(id, name, price);
} }
}, },
})); }));

View File

@ -3,18 +3,23 @@
* @enum {number} * @enum {number}
*/ */
const BillingInfoReqState = { const BillingInfoReqState = {
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
SUCCESS: 1, SUCCESS: 1,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
FAILURE: 2, FAILURE: 2,
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
SENDING: 3, SENDING: 3,
}; };
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.store("billing_inputs", { Alpine.store("billing_inputs", {
data: et_data, // biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
data: etData,
async fill() { async fill() {
document.getElementById("bank-submit-button").disabled = true; document.getElementById("bank-submit-button").disabled = true;
const res = await fetch(et_data_url); // biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
const res = await fetch(etDataUrl);
if (res.ok) { if (res.ok) {
this.data = await res.json(); this.data = await res.json();
document.getElementById("bank-submit-button").disabled = false; document.getElementById("bank-submit-button").disabled = false;
@ -24,10 +29,10 @@ document.addEventListener("alpine:init", () => {
Alpine.data("billing_infos", () => ({ Alpine.data("billing_infos", () => ({
/** @type {BillingInfoReqState | null} */ /** @type {BillingInfoReqState | null} */
req_state: null, reqState: null,
async send_form() { async sendForm() {
this.req_state = BillingInfoReqState.SENDING; this.reqState = BillingInfoReqState.SENDING;
const form = document.getElementById("billing_info_form"); const form = document.getElementById("billing_info_form");
document.getElementById("bank-submit-button").disabled = true; document.getElementById("bank-submit-button").disabled = true;
const payload = Object.fromEntries( const payload = Object.fromEntries(
@ -35,11 +40,12 @@ document.addEventListener("alpine:init", () => {
.filter((elem) => elem.type !== "submit" && elem.value) .filter((elem) => elem.type !== "submit" && elem.value)
.map((elem) => [elem.name, elem.value]), .map((elem) => [elem.name, elem.value]),
); );
const res = await fetch(billing_info_url, { // biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
const res = await fetch(billingInfoUrl, {
method: "PUT", method: "PUT",
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
this.req_state = res.ok this.reqState = res.ok
? BillingInfoReqState.SUCCESS ? BillingInfoReqState.SUCCESS
: BillingInfoReqState.FAILURE; : BillingInfoReqState.FAILURE;
if (res.status === 422) { if (res.status === 422) {
@ -56,22 +62,24 @@ document.addEventListener("alpine:init", () => {
} }
}, },
get_alert_color() { getAlertColor() {
if (this.req_state === BillingInfoReqState.SUCCESS) { if (this.reqState === BillingInfoReqState.SUCCESS) {
return "green"; return "green";
} }
if (this.req_state === BillingInfoReqState.FAILURE) { if (this.reqState === BillingInfoReqState.FAILURE) {
return "red"; return "red";
} }
return ""; return "";
}, },
get_alert_message() { getAlertMessage() {
if (this.req_state === BillingInfoReqState.SUCCESS) { if (this.reqState === BillingInfoReqState.SUCCESS) {
return billing_info_success_message; // biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
return billingInfoSuccessMessage;
} }
if (this.req_state === BillingInfoReqState.FAILURE) { if (this.reqState === BillingInfoReqState.FAILURE) {
return billing_info_failure_message; // biome-ignore lint/correctness/noUndeclaredVariables: defined in eboutic_makecommand.jinja
return billingInfoFailureMessage;
} }
return ""; return "";
}, },

View File

@ -56,11 +56,11 @@
{# Total price #} {# Total price #}
<li style="margin-top: 20px"> <li style="margin-top: 20px">
<span class="item-name"><strong>{% trans %}Basket amount: {% endtrans %}</strong></span> <span class="item-name"><strong>{% trans %}Basket amount: {% endtrans %}</strong></span>
<span x-text="get_total().toFixed(2) + ' €'" class="item-price"></span> <span x-text="getTotal().toFixed(2) + ' €'" class="item-price"></span>
</li> </li>
</ul> </ul>
<div class="catalog-buttons"> <div class="catalog-buttons">
<button @click="clear_basket()" class="btn btn-grey"> <button @click="clearBasket()" class="btn btn-grey">
<i class="fa fa-trash"></i> <i class="fa fa-trash"></i>
{% trans %}Clear{% endtrans %} {% trans %}Clear{% endtrans %}
</button> </button>
@ -106,7 +106,7 @@
id="{{ p.id }}" id="{{ p.id }}"
class="product-button" class="product-button"
:class="{selected: items.some((i) => i.id === {{ p.id }})}" :class="{selected: items.some((i) => i.id === {{ p.id }})}"
@click='add_from_catalog({{ p.id }}, {{ p.name|tojson }}, {{ p.selling_price }})' @click='addFromCatalog({{ p.id }}, {{ p.name|tojson }}, {{ p.selling_price }})'
> >
{% if p.icon %} {% if p.icon %}
<img class="product-image" src="{{ p.icon.url }}" <img class="product-image" src="{{ p.icon.url }}"

View File

@ -56,7 +56,7 @@
<div <div
class="collapse" class="collapse"
:class="{'shadow': collapsed}" :class="{'shadow': collapsed}"
x-data="{collapsed: !billing_info_exist}" x-data="{collapsed: !billingInfoExist}"
x-cloak x-cloak
> >
<div class="collapse-header clickable" @click="collapsed = !collapsed"> <div class="collapse-header clickable" @click="collapsed = !collapsed">
@ -73,27 +73,27 @@
x-data="billing_infos" x-data="billing_infos"
x-show="collapsed" x-show="collapsed"
x-transition.scale.origin.top x-transition.scale.origin.top
@submit.prevent="await send_form()" @submit.prevent="await sendForm()"
> >
{% csrf_token %} {% csrf_token %}
{{ billing_form }} {{ billing_form }}
<br> <br>
<br> <br>
<div <div
x-show="[BillingInfoReqState.SUCCESS, BillingInfoReqState.FAILURE].includes(req_state)" x-show="[BillingInfoReqState.SUCCESS, BillingInfoReqState.FAILURE].includes(reqState)"
class="alert" class="alert"
:class="'alert-' + get_alert_color()" :class="'alert-' + getAlertColor()"
x-transition x-transition
> >
<div class="alert-main" x-text="get_alert_message()"></div> <div class="alert-main" x-text="getAlertMessage()"></div>
<div class="clickable" @click="req_state = null"> <div class="clickable" @click="reqState = null">
<i class="fa fa-close"></i> <i class="fa fa-close"></i>
</div> </div>
</div> </div>
<input <input
type="submit" class="btn btn-blue clickable" type="submit" class="btn btn-blue clickable"
value="{% trans %}Validate{% endtrans %}" value="{% trans %}Validate{% endtrans %}"
:disabled="req_state === BillingInfoReqState.SENDING" :disabled="reqState === BillingInfoReqState.SENDING"
> >
</form> </form>
</div> </div>
@ -141,16 +141,16 @@
{% block script %} {% block script %}
<script> <script>
const billing_info_url = '{{ url("api:put_billing_info", user_id=request.user.id) }}'; const billingInfoUrl = '{{ url("api:put_billing_info", user_id=request.user.id) }}';
const et_data_url = '{{ url("api:etransaction_data") }}'; const etDataUrl = '{{ url("api:etransaction_data") }}';
const billing_info_exist = {{ "true" if billing_infos else "false" }}; const billingInfoExist = {{ "true" if billing_infos else "false" }};
const billing_info_success_message = "{% trans %}Billing info registration success{% endtrans %}"; const billingInfoSuccessMessage = "{% trans %}Billing info registration success{% endtrans %}";
const billing_info_failure_message = "{% trans %}Billing info registration failure{% endtrans %}"; const billingInfoFailureMessage = "{% trans %}Billing info registration failure{% endtrans %}";
{% if billing_infos %} {% if billing_infos %}
const et_data = {{ billing_infos|safe }} const etData = {{ billing_infos|safe }}
{% else %} {% else %}
const et_data = {} const etData = {}
{% endif %} {% endif %}
</script> </script>
{{ super() }} {{ super() }}

View File

@ -125,14 +125,14 @@
then fetch the corresponding data from the API. then fetch the corresponding data from the API.
This data will then be displayed on the result part of the page. This data will then be displayed on the result part of the page.
#} #}
const page_default = 1; const pageDefault = 1;
const page_size_default = 100; const pageSizeDefault = 100;
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("uv_search", () => ({ Alpine.data("uv_search", () => ({
uvs: [], uvs: [],
loading: false, loading: false,
page: page_default, page: pageDefault,
page_size: page_size_default, pageSize: pageSizeDefault,
search: "", search: "",
department: [], department: [],
credit_type: [], credit_type: [],
@ -142,12 +142,12 @@
update: undefined, update: undefined,
async initialize_args() { async initializeArgs() {
let url = new URLSearchParams(window.location.search); let url = new URLSearchParams(window.location.search);
this.pushstate = History.REPLACE; this.pushstate = History.REPLACE;
this.page = parseInt(url.get("page")) || page_default;; this.page = parseInt(url.get("page")) || pageDefault;;
this.page_size = parseInt(url.get("page_size")) || page_size_default; this.pageSize = parseInt(url.get("pageSize")) || pageSizeDefault;
this.search = url.get("search") || ""; this.search = url.get("search") || "";
this.department = url.getAll("department"); this.department = url.getAll("department");
this.credit_type = url.getAll("credit_type"); this.credit_type = url.getAll("credit_type");
@ -164,18 +164,18 @@
this.update = Alpine.debounce(async () => { this.update = Alpine.debounce(async () => {
{# Create the whole url before changing everything all at once #} {# Create the whole url before changing everything all at once #}
let first = this.to_change.shift(); let first = this.to_change.shift();
let url = update_query_string(first.param, first.value, History.NONE); let url = updateQueryString(first.param, first.value, History.NONE);
this.to_change.forEach((value) => { this.to_change.forEach((value) => {
url = update_query_string(value.param, value.value, History.NONE, url); url = updateQueryString(value.param, value.value, History.NONE, url);
}) })
update_query_string(first.param, first.value, this.pushstate, url); updateQueryString(first.param, first.value, this.pushstate, url);
await this.fetch_data(); {# reload data on form change #} await this.fetchData(); {# reload data on form change #}
this.to_change = []; this.to_change = [];
this.pushstate = History.PUSH; this.pushstate = History.PUSH;
}, 50); }, 50);
let search_params = ["search", "department", "credit_type", "semester"]; let search_params = ["search", "department", "credit_type", "semester"];
let pagination_params = ["page", "page_size"]; let pagination_params = ["page", "pageSize"];
search_params.forEach((param) => { search_params.forEach((param) => {
this.$watch(param, async (value) => { this.$watch(param, async (value) => {
@ -184,8 +184,8 @@
return; return;
} }
{# Reset pagination on search #} {# Reset pagination on search #}
this.page = page_default; this.page = pageDefault;
this.page_size = page_size_default; this.pageSize = pageSizeDefault;
}); });
}); });
search_params.concat(pagination_params).forEach((param) => { search_params.concat(pagination_params).forEach((param) => {
@ -195,13 +195,13 @@
}); });
}); });
window.addEventListener("popstate", async (event) => { window.addEventListener("popstate", async (event) => {
await this.initialize_args(); await this.initializeArgs();
}); });
await this.initialize_args(); await this.initializeArgs();
}, },
async fetch_data() { async fetchData() {
this.loading = true; this.loading = true;
const url = "{{ url("api:fetch_uvs") }}" + window.location.search; const url = "{{ url("api:fetch_uvs") }}" + window.location.search;
this.uvs = await (await fetch(url)).json(); this.uvs = await (await fetch(url)).json();
@ -209,7 +209,7 @@
}, },
max_page() { max_page() {
return Math.ceil(this.uvs.count / this.page_size); return Math.ceil(this.uvs.count / this.pageSize);
} }
})) }))
}) })

View File

@ -10,8 +10,8 @@
*/ */
class PictureWithIdentifications { class PictureWithIdentifications {
identifications = null; identifications = null;
image_loading = false; imageLoading = false;
identifications_loading = false; identificationsLoading = false;
/** /**
* @param {Picture} picture * @param {Picture} picture
@ -23,7 +23,7 @@ class PictureWithIdentifications {
/** /**
* @param {Picture} picture * @param {Picture} picture
*/ */
static from_picture(picture) { static fromPicture(picture) {
return new PictureWithIdentifications(picture); return new PictureWithIdentifications(picture);
} }
@ -33,19 +33,19 @@ class PictureWithIdentifications {
* @param {?Object=} options * @param {?Object=} options
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async load_identifications(options) { async loadIdentifications(options) {
if (this.identifications_loading) { if (this.identificationsLoading) {
return; // The users are already being fetched. return; // The users are already being fetched.
} }
if (!!this.identifications && !options?.force_reload) { if (!!this.identifications && !options?.forceReload) {
// The users are already fetched // The users are already fetched
// and the user does not want to force the reload // and the user does not want to force the reload
return; return;
} }
this.identifications_loading = true; this.identificationsLoading = true;
const url = `/api/sas/picture/${this.id}/identified`; const url = `/api/sas/picture/${this.id}/identified`;
this.identifications = await (await fetch(url)).json(); this.identifications = await (await fetch(url)).json();
this.identifications_loading = false; this.identificationsLoading = false;
} }
/** /**
@ -56,12 +56,12 @@ class PictureWithIdentifications {
const img = new Image(); const img = new Image();
img.src = this.compressed_url; img.src = this.compressed_url;
if (!img.complete) { if (!img.complete) {
this.image_loading = true; this.imageLoading = true;
img.addEventListener("load", () => { img.addEventListener("load", () => {
this.image_loading = false; this.imageLoading = false;
}); });
} }
await this.load_identifications(); await this.loadIdentifications();
} }
} }
@ -78,13 +78,18 @@ document.addEventListener("alpine:init", () => {
* when loading the page at the beginning * when loading the page at the beginning
* @type PictureWithIdentifications * @type PictureWithIdentifications
**/ **/
current_picture: { currentPicture: {
// biome-ignore lint/style/useNamingConvention: json is snake_case
is_moderated: true, is_moderated: true,
id: null, id: null,
name: "", name: "",
// biome-ignore lint/style/useNamingConvention: json is snake_case
display_name: "", display_name: "",
// biome-ignore lint/style/useNamingConvention: json is snake_case
compressed_url: "", compressed_url: "",
// biome-ignore lint/style/useNamingConvention: json is snake_case
profile_url: "", profile_url: "",
// biome-ignore lint/style/useNamingConvention: json is snake_case
full_size_url: "", full_size_url: "",
owner: "", owner: "",
date: new Date(), date: new Date(),
@ -94,12 +99,12 @@ document.addEventListener("alpine:init", () => {
* The picture which will be displayed next if the user press the "next" button * The picture which will be displayed next if the user press the "next" button
* @type ?PictureWithIdentifications * @type ?PictureWithIdentifications
**/ **/
next_picture: null, nextPicture: null,
/** /**
* The picture which will be displayed next if the user press the "previous" button * The picture which will be displayed next if the user press the "previous" button
* @type ?PictureWithIdentifications * @type ?PictureWithIdentifications
**/ **/
previous_picture: null, previousPicture: null,
/** /**
* The select2 component used to identify users * The select2 component used to identify users
**/ **/
@ -111,7 +116,7 @@ document.addEventListener("alpine:init", () => {
* Error message when a moderation operation fails * Error message when a moderation operation fails
* @type string * @type string
**/ **/
moderation_error: "", moderationError: "",
/** /**
* Method of pushing new url to the browser history * Method of pushing new url to the browser history
* Used by popstate event and always reset to it's default value when used * Used by popstate event and always reset to it's default value when used
@ -120,114 +125,119 @@ document.addEventListener("alpine:init", () => {
pushstate: History.PUSH, pushstate: History.PUSH,
async init() { async init() {
this.pictures = (await fetch_paginated(picture_endpoint)).map( // biome-ignore lint/correctness/noUndeclaredVariables: Imported from script.js
PictureWithIdentifications.from_picture, this.pictures = (await fetchPaginated(pictureEndpoint)).map(
PictureWithIdentifications.fromPicture,
); );
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from script.js
this.selector = sithSelect2({ this.selector = sithSelect2({
element: $(this.$refs.search), element: $(this.$refs.search),
data_source: remote_data_source("/api/user/search", { // biome-ignore lint/correctness/noUndeclaredVariables: Imported from script.js
dataSource: remoteDataSource("/api/user/search", {
excluded: () => [ excluded: () => [
...(this.current_picture.identifications || []).map((i) => i.user.id), ...(this.currentPicture.identifications || []).map((i) => i.user.id),
], ],
result_converter: (obj) => Object({ ...obj, text: obj.display_name }), resultConverter: (obj) => new Object({ ...obj, text: obj.display_name }),
}), }),
picture_getter: (user) => user.profile_pict, pictureGetter: (user) => user.profile_pict,
}); });
this.current_picture = this.pictures.find((i) => i.id === first_picture_id); // biome-ignore lint/correctness/noUndeclaredVariables: Imported from picture.jinja
this.$watch("current_picture", (current, previous) => { this.currentPicture = this.pictures.find((i) => i.id === firstPictureId);
this.$watch("currentPicture", (current, previous) => {
if (current === previous) { if (current === previous) {
/* Avoid recursive updates */ /* Avoid recursive updates */
return; return;
} }
this.update_picture(); this.updatePicture();
}); });
window.addEventListener("popstate", async (event) => { window.addEventListener("popstate", async (event) => {
if (!event.state || event.state.sas_picture_id === undefined) { if (!event.state || event.state.sasPictureId === undefined) {
return; return;
} }
this.pushstate = History.REPLACE; this.pushstate = History.REPLACE;
this.current_picture = this.pictures.find( this.currentPicture = this.pictures.find(
(i) => i.id === Number.parseInt(event.state.sas_picture_id), (i) => i.id === Number.parseInt(event.state.sasPictureId),
); );
}); });
this.pushstate = History.REPLACE; /* Avoid first url push */ this.pushstate = History.REPLACE; /* Avoid first url push */
await this.update_picture(); await this.updatePicture();
}, },
/** /**
* Update the page. * Update the page.
* Called when the `current_picture` property changes. * Called when the `currentPicture` property changes.
* *
* The url is modified without reloading the page, * The url is modified without reloading the page,
* and the previous picture, the next picture and * and the previous picture, the next picture and
* the list of identified users are updated. * the list of identified users are updated.
*/ */
async update_picture() { async updatePicture() {
const update_args = [ const updateArgs = [
{ sas_picture_id: this.current_picture.id }, { sasPictureId: this.currentPicture.id },
"", "",
`/sas/picture/${this.current_picture.id}/`, `/sas/picture/${this.currentPicture.id}/`,
]; ];
if (this.pushstate === History.REPLACE) { if (this.pushstate === History.REPLACE) {
window.history.replaceState(...update_args); window.history.replaceState(...updateArgs);
this.pushstate = History.PUSH; this.pushstate = History.PUSH;
} else { } else {
window.history.pushState(...update_args); window.history.pushState(...updateArgs);
} }
this.moderation_error = ""; this.moderationError = "";
const index = this.pictures.indexOf(this.current_picture); const index = this.pictures.indexOf(this.currentPicture);
this.previous_picture = this.pictures[index - 1] || null; this.previousPicture = this.pictures[index - 1] || null;
this.next_picture = this.pictures[index + 1] || null; this.nextPicture = this.pictures[index + 1] || null;
await this.current_picture.load_identifications(); await this.currentPicture.loadIdentifications();
this.$refs.main_picture?.addEventListener("load", () => { this.$refs.mainPicture?.addEventListener("load", () => {
// once the current picture is loaded, // once the current picture is loaded,
// start preloading the next and previous pictures // start preloading the next and previous pictures
this.next_picture?.preload(); this.nextPicture?.preload();
this.previous_picture?.preload(); this.previousPicture?.preload();
}); });
}, },
async moderate_picture() { async moderatePicture() {
const res = await fetch(`/api/sas/picture/${this.current_picture.id}/moderate`, { const res = await fetch(`/api/sas/picture/${this.currentPicture.id}/moderate`, {
method: "PATCH", method: "PATCH",
}); });
if (!res.ok) { if (!res.ok) {
this.moderation_error = `${gettext("Couldn't moderate picture")} : ${res.statusText}`; this.moderationError = `${gettext("Couldn't moderate picture")} : ${res.statusText}`;
return; return;
} }
this.current_picture.is_moderated = true; this.currentPicture.is_moderated = true;
this.current_picture.asked_for_removal = false; this.currentPicture.askedForRemoval = false;
}, },
async delete_picture() { async deletePicture() {
const res = await fetch(`/api/sas/picture/${this.current_picture.id}`, { const res = await fetch(`/api/sas/picture/${this.currentPicture.id}`, {
method: "DELETE", method: "DELETE",
}); });
if (!res.ok) { if (!res.ok) {
this.moderation_error = `${gettext("Couldn't delete picture")} : ${res.statusText}`; this.moderationError = `${gettext("Couldn't delete picture")} : ${res.statusText}`;
return; return;
} }
this.pictures.splice(this.pictures.indexOf(this.current_picture), 1); this.pictures.splice(this.pictures.indexOf(this.currentPicture), 1);
if (this.pictures.length === 0) { if (this.pictures.length === 0) {
// The deleted picture was the only one in the list. // The deleted picture was the only one in the list.
// As the album is now empty, go back to the parent page // As the album is now empty, go back to the parent page
document.location.href = album_url; // biome-ignore lint/correctness/noUndeclaredVariables: imported from picture.jinja
document.location.href = albumUrl;
} }
this.current_picture = this.next_picture || this.previous_picture; this.currentPicture = this.nextPicture || this.previousPicture;
}, },
/** /**
* Send the identification request and update the list of identified users. * Send the identification request and update the list of identified users.
*/ */
async submit_identification() { async submitIdentification() {
const url = `/api/sas/picture/${this.current_picture.id}/identified`; const url = `/api/sas/picture/${this.currentPicture.id}/identified`;
await fetch(url, { await fetch(url, {
method: "PUT", method: "PUT",
body: JSON.stringify(this.selector.val().map((i) => Number.parseInt(i))), body: JSON.stringify(this.selector.val().map((i) => Number.parseInt(i))),
}); });
// refresh the identified users list // refresh the identified users list
await this.current_picture.load_identifications({ force_reload: true }); await this.currentPicture.loadIdentifications({ forceReload: true });
this.selector.empty().trigger("change"); this.selector.empty().trigger("change");
}, },
@ -236,23 +246,22 @@ document.addEventListener("alpine:init", () => {
* @param {PictureIdentification} identification * @param {PictureIdentification} identification
* @return {boolean} * @return {boolean}
*/ */
can_be_removed(identification) { canBeRemoved(identification) {
return user_is_sas_admin || identification.user.id === user_id; // biome-ignore lint/correctness/noUndeclaredVariables: imported from picture.jinja
return userIsSasAdmin || identification.user.id === userId;
}, },
/** /**
* Untag a user from the current picture * Untag a user from the current picture
* @param {PictureIdentification} identification * @param {PictureIdentification} identification
*/ */
async remove_identification(identification) { async removeIdentification(identification) {
const res = await fetch(`/api/sas/relation/${identification.id}`, { const res = await fetch(`/api/sas/relation/${identification.id}`, {
method: "DELETE", method: "DELETE",
}); });
if (res.ok && Array.isArray(this.current_picture.identifications)) { if (res.ok && Array.isArray(this.currentPicture.identifications)) {
this.current_picture.identifications = this.currentPicture.identifications =
this.current_picture.identifications.filter( this.currentPicture.identifications.filter((i) => i.id !== identification.id);
(i) => i.id !== identification.id,
);
} }
}, },
})); }));

View File

@ -83,7 +83,7 @@
</a> </a>
</template> </template>
</div> </div>
{{ paginate_alpine("page", "nb_pages()") }} {{ paginate_alpine("page", "nbPages()") }}
</div> </div>
{% if is_sas_admin %} {% if is_sas_admin %}
@ -116,14 +116,14 @@
loading: false, loading: false,
async init() { async init() {
await this.fetch_pictures(); await this.fetchPictures();
this.$watch("page", () => { this.$watch("page", () => {
update_query_string("page", updateQueryString("page",
this.page === 1 ? null : this.page, this.page === 1 ? null : this.page,
this.pushstate this.pushstate
); );
this.pushstate = History.PUSH; this.pushstate = History.PUSH;
this.fetch_pictures(); this.fetchPictures();
}); });
window.addEventListener("popstate", () => { window.addEventListener("popstate", () => {
@ -134,7 +134,7 @@
}); });
}, },
async fetch_pictures() { async fetchPictures() {
this.loading=true; this.loading=true;
const url = "{{ url("api:pictures") }}" const url = "{{ url("api:pictures") }}"
+"?album_id={{ album.id }}" +"?album_id={{ album.id }}"
@ -144,7 +144,7 @@
this.loading = false; this.loading = false;
}, },
nb_pages() { nbPages() {
return Math.ceil(this.pictures.count / {{ settings.SITH_SAS_IMAGES_PER_PAGE }}); return Math.ceil(this.pictures.count / {{ settings.SITH_SAS_IMAGES_PER_PAGE }});
} }
})) }))

View File

@ -17,21 +17,21 @@
{% block content %} {% block content %}
<main x-data="picture_viewer"> <main x-data="picture_viewer">
<code> <code>
<a href="{{ url('sas:main') }}">SAS</a> / {{ print_path(album) }} <span x-text="current_picture.name"></span> <a href="{{ url('sas:main') }}">SAS</a> / {{ print_path(album) }} <span x-text="currentPicture.name"></span>
</code> </code>
<br> <br>
<div class="title"> <div class="title">
<h3 x-text="current_picture.name"></h3> <h3 x-text="currentPicture.name"></h3>
<h4 x-text="`${pictures.indexOf(current_picture) + 1 } / ${pictures.length}`"></h4> <h4 x-text="`${pictures.indexOf(currentPicture) + 1 } / ${pictures.length}`"></h4>
</div> </div>
<br> <br>
<template x-if="!current_picture.is_moderated"> <template x-if="!currentPicture.is_moderated">
<div class="alert alert-red"> <div class="alert alert-red">
<div class="alert-main"> <div class="alert-main">
<template x-if="current_picture.asked_for_removal"> <template x-if="currentPicture.askedForRemoval">
<span class="important">{% trans %}Asked for removal{% endtrans %}</span> <span class="important">{% trans %}Asked for removal{% endtrans %}</span>
</template> </template>
<p> <p>
@ -43,14 +43,14 @@
</div> </div>
<div> <div>
<div> <div>
<button class="btn btn-blue" @click="moderate_picture()"> <button class="btn btn-blue" @click="moderatePicture()">
{% trans %}Moderate{% endtrans %} {% trans %}Moderate{% endtrans %}
</button> </button>
<button class="btn btn-red" @click.prevent="delete_picture()"> <button class="btn btn-red" @click.prevent="deletePicture()">
{% trans %}Delete{% endtrans %} {% trans %}Delete{% endtrans %}
</button> </button>
</div> </div>
<p x-show="!!moderation_error" x-text="moderation_error"></p> <p x-show="!!moderationError" x-text="moderationError"></p>
</div> </div>
</div> </div>
</template> </template>
@ -58,12 +58,12 @@
<div class="container" id="pict"> <div class="container" id="pict">
<div class="main"> <div class="main">
<div class="photo" :aria-busy="current_picture.image_loading"> <div class="photo" :aria-busy="currentPicture.imageLoading">
<img <img
:src="current_picture.compressed_url" :src="currentPicture.compressed_url"
:alt="current_picture.name" :alt="currentPicture.name"
id="main-picture" id="main-picture"
x-ref="main_picture" x-ref="mainPicture"
/> />
</div> </div>
@ -76,13 +76,13 @@
<span <span
x-text="Intl.DateTimeFormat( x-text="Intl.DateTimeFormat(
'{{ LANGUAGE_CODE }}', {dateStyle: 'long'} '{{ LANGUAGE_CODE }}', {dateStyle: 'long'}
).format(new Date(current_picture.date))" ).format(new Date(currentPicture.date))"
> >
</span> </span>
</div> </div>
<div> <div>
<span>{% trans %}Owner: {% endtrans %}</span> <span>{% trans %}Owner: {% endtrans %}</span>
<a :href="current_picture.owner.profile_url" x-text="current_picture.owner.display_name"></a> <a :href="currentPicture.owner.profile_url" x-text="currentPicture.owner.display_name"></a>
</div> </div>
</div> </div>
</div> </div>
@ -91,14 +91,14 @@
<h5>{% trans %}Tools{% endtrans %}</h5> <h5>{% trans %}Tools{% endtrans %}</h5>
<div> <div>
<div> <div>
<a class="text" :href="current_picture.full_size_url"> <a class="text" :href="currentPicture.full_size_url">
{% trans %}HD version{% endtrans %} {% trans %}HD version{% endtrans %}
</a> </a>
<br> <br>
<a class="text danger" href="?ask_removal">{% trans %}Ask for removal{% endtrans %}</a> <a class="text danger" href="?ask_removal">{% trans %}Ask for removal{% endtrans %}</a>
</div> </div>
<div class="buttons"> <div class="buttons">
<a class="button" :href="`/sas/picture/${current_picture.id}/edit/`"><i class="fa-regular fa-pen-to-square edit-action"></i></a> <a class="button" :href="`/sas/picture/${currentPicture.id}/edit/`"><i class="fa-regular fa-pen-to-square edit-action"></i></a>
<a class="button" href="?rotate_left"><i class="fa-solid fa-rotate-left"></i></a> <a class="button" href="?rotate_left"><i class="fa-solid fa-rotate-left"></i></a>
<a class="button" href="?rotate_right"><i class="fa-solid fa-rotate-right"></i></a> <a class="button" href="?rotate_right"><i class="fa-solid fa-rotate-right"></i></a>
</div> </div>
@ -110,23 +110,23 @@
<div class="subsection"> <div class="subsection">
<div class="navigation"> <div class="navigation">
<div id="prev" class="clickable"> <div id="prev" class="clickable">
<template x-if="previous_picture"> <template x-if="previousPicture">
<div <div
@keyup.left.window="current_picture = previous_picture" @keyup.left.window="currentPicture = previousPicture"
@click="current_picture = previous_picture" @click="currentPicture = previousPicture"
> >
<img :src="previous_picture.thumb_url" alt="{% trans %}Previous picture{% endtrans %}"/> <img :src="previousPicture.thumb_url" alt="{% trans %}Previous picture{% endtrans %}"/>
<div class="overlay">←</div> <div class="overlay">←</div>
</div> </div>
</template> </template>
</div> </div>
<div id="next" class="clickable"> <div id="next" class="clickable">
<template x-if="next_picture"> <template x-if="nextPicture">
<div <div
@keyup.right.window="current_picture = next_picture" @keyup.right.window="currentPicture = nextPicture"
@click="current_picture = next_picture" @click="currentPicture = nextPicture"
> >
<img :src="next_picture.thumb_url" alt="{% trans %}Previous picture{% endtrans %}"/> <img :src="nextPicture.thumb_url" alt="{% trans %}Previous picture{% endtrans %}"/>
<div class="overlay">→</div> <div class="overlay">→</div>
</div> </div>
</template> </template>
@ -136,14 +136,14 @@
<div class="tags"> <div class="tags">
<h5>{% trans %}People{% endtrans %}</h5> <h5>{% trans %}People{% endtrans %}</h5>
{% if user.was_subscribed %} {% if user.was_subscribed %}
<form @submit.prevent="submit_identification" x-show="!!selector"> <form @submit.prevent="submitIdentification" x-show="!!selector">
<select x-ref="search" multiple="multiple"></select> <select x-ref="search" multiple="multiple"></select>
<input type="submit" value="{% trans %}Go{% endtrans %}"/> <input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form> </form>
{% endif %} {% endif %}
<ul> <ul>
<template <template
x-for="identification in (current_picture.identifications || [])" x-for="identification in (currentPicture.identifications || [])"
:key="identification.id" :key="identification.id"
> >
<li> <li>
@ -151,12 +151,12 @@
<img class="profile-pic" :src="identification.user.profile_pict" alt="image de profil"/> <img class="profile-pic" :src="identification.user.profile_pict" alt="image de profil"/>
<span x-text="identification.user.display_name"></span> <span x-text="identification.user.display_name"></span>
</a> </a>
<template x-if="can_be_removed(identification)"> <template x-if="canBeRemoved(identification)">
<a class="delete clickable" @click="remove_identification(identification)"><i class="fa fa-times fa-xl delete-action"></i></a> <a class="delete clickable" @click="removeIdentification(identification)"><i class="fa fa-times fa-xl delete-action"></i></a>
</template> </template>
</li> </li>
</template> </template>
<template x-if="current_picture.identifications_loading"> <template x-if="currentPicture.identificationsLoading">
{# shadow element that exists only to put the loading wheel below {# shadow element that exists only to put the loading wheel below
the list of identified people #} the list of identified people #}
<li class="loader" aria-busy="true"></li> <li class="loader" aria-busy="true"></li>
@ -171,10 +171,10 @@
{% block script %} {% block script %}
{{ super() }} {{ super() }}
<script> <script>
const picture_endpoint = "{{ url("api:pictures") + "?album_id=" + album.id|string }}"; const pictureEndpoint = "{{ url("api:pictures") + "?album_id=" + album.id|string }}";
const album_url = "{{ album.get_absolute_url() }}"; const albumUrl = "{{ album.get_absolute_url() }}";
const first_picture_id = {{ picture.id }}; {# id of the first picture to show after page load #} const firstPictureId = {{ picture.id }}; {# id of the first picture to show after page load #}
const user_id = {{ user.id }}; const userId = {{ user.id }};
const user_is_sas_admin = {{ (user.is_root or user.is_in_group(pk = settings.SITH_GROUP_SAS_ADMIN_ID))|tojson }} const userIsSasAdmin = {{ (user.is_root or user.is_in_group(pk = settings.SITH_GROUP_SAS_ADMIN_ID))|tojson }}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,4 +1,5 @@
const glob = require("glob"); const glob = require("glob");
// biome-ignore lint/correctness/noNodejsModules: this is backend side
const path = require("node:path"); const path = require("node:path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
@ -26,6 +27,7 @@ module.exports = {
terserOptions: { terserOptions: {
mangle: true, mangle: true,
compress: { compress: {
// biome-ignore lint/style/useNamingConvention: this is how the underlying library wants it
drop_console: true, drop_console: true,
}, },
}, },