Compare commits

..

4 Commits

Author SHA1 Message Date
imperosol 35d4465d47 feat: automatic localstorage cleaning 2026-05-11 13:16:37 +02:00
imperosol 35aca2b3b2 refactor: move nested-key.d.ts 2026-05-11 11:49:45 +02:00
imperosol 88ddae7042 refactor: assemble main js files into a single bundle 2026-05-11 11:44:20 +02:00
imperosol 2f9a4c3d4f feat: use a queue in user pictures localstorage 2026-05-11 11:08:51 +02:00
17 changed files with 179 additions and 63 deletions
-12
View File
@@ -1,12 +0,0 @@
import sort from "@alpinejs/sort";
import Alpine from "alpinejs";
import { limitedChoices } from "#core:alpine/limited-choices.ts";
import { alpinePlugin as notificationPlugin } from "#core:utils/notifications.ts";
Alpine.plugin([sort, limitedChoices]);
Alpine.magic("notifications", notificationPlugin);
window.Alpine = Alpine;
window.addEventListener("DOMContentLoaded", () => {
Alpine.start();
});
+49
View File
@@ -0,0 +1,49 @@
import sort from "@alpinejs/sort";
import Alpine from "alpinejs";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import htmx from "htmx.org";
import { limitedChoices } from "#core:alpine/limited-choices";
import { cacheBuster } from "#core:core/cache";
import { default as navbar } from "#core:core/navbar";
import { alpinePlugin as notificationPlugin } from "#core:utils/notifications";
/**
* Alpine
*/
Alpine.plugin([sort, limitedChoices]);
Alpine.magic("notifications", notificationPlugin);
// biome-ignore lint/style/useNamingConvention: it's how it's named
Object.assign(window, { Alpine });
window.addEventListener("DOMContentLoaded", () => {
Alpine.start();
});
/**
* Polyfill for country flags (used for language choice)
*/
polyfillCountryFlagEmojis();
/**
* HTMX
*/
document.body.addEventListener("htmx:beforeRequest", (event: CustomEvent) => {
event.detail.target.ariaBusy = true;
});
document.body.addEventListener("htmx:beforeSwap", (event: CustomEvent) => {
event.detail.target.ariaBusy = null;
});
Object.assign(window, { htmx });
/**
* navbar
*/
navbar();
/**
* Script that clears the cache when the cache version changes
*/
cacheBuster();
+24
View File
@@ -0,0 +1,24 @@
// increment this number when a breaking change is made with localStorage
const CURRENT_CACHE_VERSION = 1;
export function cacheBuster() {
const version = Number.parseInt(localStorage.getItem("version") ?? "0", 10);
if (version === CURRENT_CACHE_VERSION) {
// The cache schema is up-to-date. Nothing to do.
return;
}
localStorage.removeItem("basket");
localStorage.removeItem("basket1");
// remove all storage items which key is in the form
// `userXXXPictures` or `userXXXPicturesNumber`
Object.keys(localStorage)
.filter(
(key) =>
key.startsWith("user") &&
(key.endsWith("Pictures") || key.endsWith("PicturesNumber")),
)
.forEach((key) => {
localStorage.removeItem(key);
});
localStorage.setItem("version", CURRENT_CACHE_VERSION.toString());
}
@@ -1,12 +1,10 @@
import { exportToHtml } from "#core:utils/globals.ts";
exportToHtml("showMenu", () => {
function showMenu() {
const navbar = document.getElementById("navbar-content");
const current = navbar.getAttribute("mobile-display");
navbar.setAttribute("mobile-display", current === "hidden" ? "revealed" : "hidden");
});
}
document.addEventListener("alpine:init", () => {
function navbarInit() {
const menuItems = document.querySelectorAll(".navbar details[name='navbar'].menu");
const isDesktop = () => {
return window.innerWidth >= 500;
@@ -33,4 +31,9 @@ document.addEventListener("alpine:init", () => {
}
});
}
});
}
export default () => {
Object.assign(document, { showMenu });
document.addEventListener("alpine:init", navbarInit);
};
@@ -1,3 +0,0 @@
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
polyfillCountryFlagEmojis();
-11
View File
@@ -1,11 +0,0 @@
import htmx from "htmx.org";
document.body.addEventListener("htmx:beforeRequest", (event) => {
event.detail.target.ariaBusy = true;
});
document.body.addEventListener("htmx:beforeSwap", (event) => {
event.detail.target.ariaBusy = null;
});
Object.assign(window, { htmx });
+1 -1
View File
@@ -1,4 +1,4 @@
import type { NestedKeyOf } from "#core:utils/types.ts";
import type { NestedKeyOf } from "#core:types/nested-key";
interface StringifyOptions<T extends object> {
/** The columns to include in the resulting CSV. */
+2 -5
View File
@@ -35,12 +35,9 @@
<noscript><link rel="stylesheet" href="{{ static('bundled/fontawesome-index.css') }}"></noscript>
<script src="{{ url('javascript-catalog') }}"></script>
<script type="module" src="{{ static("bundled/core/navbar-index.ts") }}"></script>
<script type="module" src="{{ static("bundled/base-bundle-index.ts") }}"></script>
<script type="module" src="{{ static("bundled/core/components/include-index.ts") }}"></script>
<script type="module" src="{{ static('bundled/alpine-index.js') }}"></script>
<script type="module" src="{{ static('bundled/htmx-index.js') }}"></script>
<script type="module" src="{{ static('bundled/country-flags-index.ts') }}"></script>
<script type="module" src="{{ static('bundled/core/tooltips-index.ts') }}"></script>
<script type="module" src="{{ static("bundled/core/tooltips-index.ts") }}"></script>
{% block additional_css %}{% endblock %}
{% block additional_js %}{% endblock %}
@@ -1,9 +1,9 @@
import { showSaveFilePicker } from "native-file-system-adapter";
import type TomSelect from "tom-select";
import type { NestedKeyOf } from "#core:types/nested-key";
import { paginated } from "#core:utils/api";
import { csv } from "#core:utils/csv";
import { getCurrentUrlParams, History, updateQueryString } from "#core:utils/history";
import type { NestedKeyOf } from "#core:utils/types";
import {
type ProductSchema,
type ProductSearchProductsDetailedData,
+56
View File
@@ -0,0 +1,56 @@
[Documentation du localStorage (mozilla)](https://developer.mozilla.org/fr/docs/Web/API/Window/localStorage)
## Utilité et limitations
Le `localStorage` est un cache géré directement par le navigateur.
Il permet de stocker des données directement chez le client.
Il s'agit donc d'un outil extrêmement puissant, qui permet d'éviter
beaucoup de requêtes au serveur, améliorant ainsi les temps de chargement.
Cependant, il y a deux limitations majeures à prendre en compte :
- le `localStorage` est entièrement géré par le client,
une fois le déploiement effectué, vous ne pouvez plus y toucher ;
vous devez donc être sûr de vous avant d'apporter des modifications
reposant sur le `localStorage`.
- la quantité de données stockable est limitée à 10Mo ;
une fois ce quota rempli, le navigateur lèvera une `QuotaExceededError`.
## Invalidation du `localStorage`
Pour résoudre le premier de ces deux problèmes, il y a un script permettant
d'annuler une partie du cache.
Ce dernier se trouve dans le fichier `core/static/bundled/core/cache.ts`.
Vous devrez modifier ce cache chaque fois que vous effectuerez
un changement de schéma, c'est-à-dire dans un des cas suivants :
- une des clefs du cache n'est plus utilisée
- la clef n'a pas changé, mais la manière dont les données attachées à cette clef
sont formées a été modifiée.
!!!Note
Si vous ne faites qu'ajouter des données, sans modifier ni supprimer
celles qui sont là, vous n'avez pas besoin d'invalider le cache.
Vous devez effectuer deux modifications dans ce fichier :
- incrémenter la version du cache
- ajouter une ligne permettant de retirer votre clef du cache
```ts hl_lines="2 11"
// increment this number when a breaking change is made with localStorage
const CURRENT_CACHE_VERSION = 2; // <-- changez cette ligne
export function cacheBuster() {
const version = Number.parseInt(localStorage.getItem("version") ?? "0", 10);
if (version === CURRENT_CACHE_VERSION) {
// The cache schema is up-to-date. Nothing to do.
return;
}
// ...
localStorage.removeItem("<clef>"); // <-- et rajoutez cette ligne
localStorage.setItem("version", CURRENT_CACHE_VERSION.toString());
}
```
@@ -7,8 +7,7 @@ interface BasketItem {
unitPrice: number;
}
// increment the key number if the data schema of the cached basket changes
const BASKET_CACHE_KEY = "basket1";
const BASKET_CACHE_KEY = "basket";
document.addEventListener("alpine:init", () => {
Alpine.data("basket", (lastPurchaseTime?: number) => ({
+2
View File
@@ -66,6 +66,8 @@ nav:
- Gestion des permissions: tutorial/perms.md
- Gestion des groupes: tutorial/groups.md
- Les fragments: tutorial/fragments.md
- Frontend:
- localStorage: tutorial/front/localstorage.md
- API:
- Développement: tutorial/api/dev.md
- Connexion à l'API: tutorial/api/connect.md
+11
View File
@@ -44,6 +44,7 @@
"@biomejs/biome": "^2.4.13",
"@hey-api/openapi-ts": "^0.94.5",
"@types/alpinejs": "^3.13.11",
"@types/alpinejs__sort": "^3.13.0",
"@types/cytoscape-cxtmenu": "^3.4.5",
"@types/cytoscape-klay": "^3.1.5",
"@types/js-cookie": "^3.0.6",
@@ -2481,6 +2482,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/alpinejs__sort": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/@types/alpinejs__sort/-/alpinejs__sort-3.13.0.tgz",
"integrity": "sha512-iR9vEy6e3yXbYAK45/hpulzlt8SSKTsvYUl/t5nuWjtbJPoGxzxUUqOm3egp83Gqtf//TyJnDCI4OTebAKDRAA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/alpinejs": "*"
}
},
"node_modules/@types/codemirror": {
"version": "5.60.17",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.17.tgz",
+1
View File
@@ -29,6 +29,7 @@
"@biomejs/biome": "^2.4.13",
"@hey-api/openapi-ts": "^0.94.5",
"@types/alpinejs": "^3.13.11",
"@types/alpinejs__sort": "^3.13.0",
"@types/cytoscape-cxtmenu": "^3.4.5",
"@types/cytoscape-klay": "^3.1.5",
"@types/js-cookie": "^3.0.6",
+19 -19
View File
@@ -22,35 +22,35 @@ document.addEventListener("alpine:init", () => {
albums: [] as Album[],
async fetchPictures(): Promise<PictureSchema[]> {
const localStorageKey = `user${config.userId}Pictures`;
const localStorageInvalidationKey = `user${config.userId}PicturesNumber`;
const lastCachedNumber = localStorage.getItem(localStorageInvalidationKey);
// Check the cache before hitting the API.
const localStorageKey = "userPictures";
const cacheContent: { userId: number; pictures: PictureSchema[] }[] = JSON.parse(
localStorage.getItem(localStorageKey) || "[]",
);
const userPictures = cacheContent.find((obj) => obj.userId === config.userId);
if (
lastCachedNumber !== null &&
Number.parseInt(lastCachedNumber, 10) === config.nbPictures
userPictures !== undefined &&
userPictures.pictures.length === config.nbPictures
) {
return JSON.parse(localStorage.getItem(localStorageKey));
// The cached value is considered valid
// if it contains the right amount of pictures.
// This amount is known because it is given in the template.
return userPictures.pictures;
}
const pictures = await paginated(picturesFetchPictures, {
// biome-ignore lint/style/useNamingConvention: from python api
query: { users_identified: [config.userId] },
} as PicturesFetchPicturesData);
cacheContent.push({ userId: config.userId, pictures: pictures });
try {
localStorage.setItem(localStorageInvalidationKey, config.nbPictures.toString());
localStorage.setItem(localStorageKey, JSON.stringify(pictures));
// cache only the pictures of the last 4 visited profiles
localStorage.setItem(localStorageKey, JSON.stringify(cacheContent.slice(-4)));
} catch {
// an exception is raised if the localstorage is entirely filled
// so just delete all cached user pictures.
// an exception is raised if the localstorage is entirely filled.
// To be as safe as possible, delete the cached pictures.
// A cache hit is not worth the page breaking.
Object.keys(localStorage)
.filter(
(key) =>
key.startsWith("user") &&
(key.endsWith("Pictures") || key.endsWith("PicturesNumber")),
)
.forEach((key) => {
localStorage.removeItem(key);
});
localStorage.removeItem(localStorageKey);
}
return pictures;
},
Generated
+3 -3
View File
@@ -2424,11 +2424,11 @@ wheels = [
[[package]]
name = "urllib3"
version = "2.7.0"
version = "2.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
]
[[package]]