mirror of
https://github.com/ae-utbm/sith.git
synced 2026-05-22 08:50:17 +00:00
Merge pull request #1385 from ae-utbm/localstorage
Automatic localstorage cleaning
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
import { exportToHtml } from "#core:utils/globals.ts";
|
|
||||||
import { newsDeleteNews, newsFetchNewsDates, newsPublishNews } from "#openapi";
|
import { newsDeleteNews, newsFetchNewsDates, newsPublishNews } from "#openapi";
|
||||||
|
|
||||||
// This will be used in jinja templates,
|
// This will be used in jinja templates,
|
||||||
@@ -13,7 +12,8 @@ const AlertState = {
|
|||||||
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
|
// biome-ignore lint/style/useNamingConvention: this feels more like an enum
|
||||||
DISPLAYED: 4, // When published at page generation
|
DISPLAYED: 4, // When published at page generation
|
||||||
};
|
};
|
||||||
exportToHtml("AlertState", AlertState);
|
// biome-ignore lint/style/useNamingConvention: it's an enum, PascalCase is better
|
||||||
|
Object.assign(window, { AlertState });
|
||||||
|
|
||||||
document.addEventListener("alpine:init", () => {
|
document.addEventListener("alpine:init", () => {
|
||||||
Alpine.data("moderationAlert", (newsId: number) => ({
|
Alpine.data("moderationAlert", (newsId: number) => ({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<title>{% trans %}Slideshow{% endtrans %}</title>
|
<title>{% trans %}Slideshow{% endtrans %}</title>
|
||||||
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
|
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
|
||||||
<link href="{{ static('css/slideshow.scss') }}" rel="stylesheet" type="text/css" />
|
<link href="{{ static('css/slideshow.scss') }}" rel="stylesheet" type="text/css" />
|
||||||
<script type="module" src="{{ static('bundled/alpine-index.ts') }}"></script>
|
<script type="module" src="{{ static('bundled/base-bundle-index.ts') }}"></script>
|
||||||
<script type="module" src="{{ static('bundled/com/slideshow-index.ts') }}"></script>
|
<script type="module" src="{{ static('bundled/com/slideshow-index.ts') }}"></script>
|
||||||
</head>
|
</head>
|
||||||
<body x-data="slideshow([
|
<body x-data="slideshow([
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import sort from "@alpinejs/sort";
|
|
||||||
import { Alpine } from "alpinejs";
|
|
||||||
import { limitedChoices } from "#core:alpine/limited-choices";
|
|
||||||
import {
|
|
||||||
type NotificationPlugin,
|
|
||||||
notificationsPlugin as notifications,
|
|
||||||
} from "#core:utils/notifications";
|
|
||||||
|
|
||||||
declare module "alpinejs" {
|
|
||||||
interface Magics<T> {
|
|
||||||
$notifications: NotificationPlugin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Alpine.plugin([sort, limitedChoices, notifications]);
|
|
||||||
// biome-ignore lint/style/useNamingConvention: it's how it's named
|
|
||||||
Object.assign(window, { Alpine });
|
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
|
||||||
Alpine.start();
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* File containing main functions and library re-exports
|
||||||
|
* that should be accessible throughout the whole website.
|
||||||
|
*
|
||||||
|
* The idea is to group all that shared code into a single bundle,
|
||||||
|
* for more efficient tree-shaking and gzip compression.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 { expireOldStorage } from "#core:core/localstorage";
|
||||||
|
import { default as navbar } from "#core:core/navbar";
|
||||||
|
import {
|
||||||
|
type NotificationPlugin,
|
||||||
|
notificationsPlugin as notifications,
|
||||||
|
} from "#core:utils/notifications";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alpine
|
||||||
|
*/
|
||||||
|
declare module "alpinejs" {
|
||||||
|
interface Magics<T> {
|
||||||
|
$notifications: NotificationPlugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Alpine.plugin([sort, limitedChoices, notifications]);
|
||||||
|
// 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
|
||||||
|
*/
|
||||||
|
expireOldStorage();
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* For more detailed infos on how to use this file,
|
||||||
|
* check /docs/tutorial/front/localstorage.md,
|
||||||
|
* or https://ae-utbm.github.io/sith/tutorial/front/localstorage/
|
||||||
|
*/
|
||||||
|
|
||||||
|
// increment this number when a breaking change is made with localStorage
|
||||||
|
const CURRENT_LOCALSTORAGE_VERSION = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove keys that are no longer used from localStorage
|
||||||
|
*/
|
||||||
|
export function expireOldStorage() {
|
||||||
|
const version = Number.parseInt(localStorage.getItem("version") ?? "0", 10);
|
||||||
|
if (version === CURRENT_LOCALSTORAGE_VERSION) {
|
||||||
|
// The cache schema is up-to-date. Nothing to do.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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_LOCALSTORAGE_VERSION.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VersionedStorageItem<T> {
|
||||||
|
version?: number;
|
||||||
|
val: T | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const versionedLocalStorage = {
|
||||||
|
...localStorage,
|
||||||
|
/**
|
||||||
|
* set this item in localStorage, alongside its version.
|
||||||
|
*
|
||||||
|
* Note: this expects an object, not a JSON string, because the parsing
|
||||||
|
* into JSON needs to be done inside the function.
|
||||||
|
*/
|
||||||
|
setItem<T>(key: string, value: T, { version }: { version: number }) {
|
||||||
|
localStorage.setItem(key, JSON.stringify({ version: version, val: value }));
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get the item linked with the given key and version from localStorage.
|
||||||
|
*
|
||||||
|
* Note: if the given key exists in localStorage but doesn't satisfy
|
||||||
|
* the given version, it will be cleared from cache.
|
||||||
|
*
|
||||||
|
* @return the object if found and with the good version, else null;
|
||||||
|
*/
|
||||||
|
getItem<T>(key: string, { version }: { version: number }): T | null {
|
||||||
|
const stored = localStorage.getItem(key);
|
||||||
|
if (!stored) {
|
||||||
|
// this key doesn't exist, return null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const obj: VersionedStorageItem<T> = JSON.parse(stored);
|
||||||
|
if (obj.version !== version || obj.val === undefined) {
|
||||||
|
// The version is wrong, return null and remove this item from cache
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return obj.val;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
import { exportToHtml } from "#core:utils/globals.ts";
|
function showMenu() {
|
||||||
|
|
||||||
exportToHtml("showMenu", () => {
|
|
||||||
const navbar = document.getElementById("navbar-content");
|
const navbar = document.getElementById("navbar-content");
|
||||||
const current = navbar.getAttribute("mobile-display");
|
const current = navbar.getAttribute("mobile-display");
|
||||||
navbar.setAttribute("mobile-display", current === "hidden" ? "revealed" : "hidden");
|
navbar.setAttribute("mobile-display", current === "hidden" ? "revealed" : "hidden");
|
||||||
});
|
}
|
||||||
|
|
||||||
document.addEventListener("alpine:init", () => {
|
function navbarInit() {
|
||||||
const menuItems = document.querySelectorAll(".navbar details[name='navbar'].menu");
|
const menuItems = document.querySelectorAll(".navbar details[name='navbar'].menu");
|
||||||
const isDesktop = () => {
|
const isDesktop = () => {
|
||||||
return window.innerWidth >= 500;
|
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();
|
|
||||||
@@ -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,6 +1,5 @@
|
|||||||
// biome-ignore lint/performance/noNamespaceImport: this is the recommended way from the documentation
|
// biome-ignore lint/performance/noNamespaceImport: this is the recommended way from the documentation
|
||||||
import * as Sentry from "@sentry/browser";
|
import * as Sentry from "@sentry/browser";
|
||||||
import { exportToHtml } from "#core:utils/globals.ts";
|
|
||||||
|
|
||||||
interface LoggedUser {
|
interface LoggedUser {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -13,7 +12,7 @@ interface SentryOptions {
|
|||||||
user?: LoggedUser;
|
user?: LoggedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
exportToHtml("loadSentryPopup", (options: SentryOptions) => {
|
const loadSentryPopup = (options: SentryOptions) => {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: options.dsn,
|
dsn: options.dsn,
|
||||||
});
|
});
|
||||||
@@ -21,4 +20,5 @@ exportToHtml("loadSentryPopup", (options: SentryOptions) => {
|
|||||||
eventId: options.eventId,
|
eventId: options.eventId,
|
||||||
...(options.user ?? {}),
|
...(options.user ?? {}),
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
Object.assign(window, { loadSentryPopup });
|
||||||
|
|||||||
@@ -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> {
|
interface StringifyOptions<T extends object> {
|
||||||
/** The columns to include in the resulting CSV. */
|
/** The columns to include in the resulting CSV. */
|
||||||
|
|||||||
@@ -5,17 +5,3 @@ declare global {
|
|||||||
const gettext: (text: string) => string;
|
const gettext: (text: string) => string;
|
||||||
const interpolate: <T>(fmt: string, args: string[] | T, isNamed?: boolean) => string;
|
const interpolate: <T>(fmt: string, args: string[] | T, isNamed?: boolean) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to export typescript functions to regular html and jinja files
|
|
||||||
* Without it, you either have to use the any keyword and suppress warnings or do a
|
|
||||||
* very painful type conversion workaround which is only here to please the linter
|
|
||||||
*
|
|
||||||
* This is only useful if you're using typescript, this is equivalent to doing
|
|
||||||
* window.yourFunction = yourFunction
|
|
||||||
**/
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: Avoid strange tricks to export functions
|
|
||||||
export function exportToHtml(name: string, func: any) {
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: Avoid strange tricks to export functions
|
|
||||||
(window as any)[name] = func;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -35,12 +35,9 @@
|
|||||||
<noscript><link rel="stylesheet" href="{{ static('bundled/fontawesome-index.css') }}"></noscript>
|
<noscript><link rel="stylesheet" href="{{ static('bundled/fontawesome-index.css') }}"></noscript>
|
||||||
|
|
||||||
<script src="{{ url('javascript-catalog') }}"></script>
|
<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/core/components/include-index.ts") }}"></script>
|
||||||
<script type="module" src="{{ static('bundled/alpine-index.ts') }}"></script>
|
<script type="module" src="{{ static("bundled/core/tooltips-index.ts") }}"></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>
|
|
||||||
|
|
||||||
{% block additional_css %}{% endblock %}
|
{% block additional_css %}{% endblock %}
|
||||||
{% block additional_js %}{% endblock %}
|
{% block additional_js %}{% endblock %}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { showSaveFilePicker } from "native-file-system-adapter";
|
import { showSaveFilePicker } from "native-file-system-adapter";
|
||||||
import type TomSelect from "tom-select";
|
import type TomSelect from "tom-select";
|
||||||
|
import type { NestedKeyOf } from "#core:types/nested-key";
|
||||||
import { paginated } from "#core:utils/api";
|
import { paginated } from "#core:utils/api";
|
||||||
import { csv } from "#core:utils/csv";
|
import { csv } from "#core:utils/csv";
|
||||||
import { getCurrentUrlParams, History, updateQueryString } from "#core:utils/history";
|
import { getCurrentUrlParams, History, updateQueryString } from "#core:utils/history";
|
||||||
import type { NestedKeyOf } from "#core:utils/types";
|
|
||||||
import {
|
import {
|
||||||
type ProductSchema,
|
type ProductSchema,
|
||||||
type ProductSearchProductsDetailedData,
|
type ProductSearchProductsDetailedData,
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
[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 fichier chaque fois qu'un élément du localStorage
|
||||||
|
cessera d'être utilisé.
|
||||||
|
Les modifications à apporter sont les suivantes :
|
||||||
|
|
||||||
|
- 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());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versionnage d'une clef
|
||||||
|
|
||||||
|
Dans le cas où une paire clef-valeur du localStorage subit un changement
|
||||||
|
dans son schéma de données, utilisez `versionedLocalStorage` :
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { versionedLocalStorage } from "#core:core/localstorage";
|
||||||
|
|
||||||
|
const foo = () => {
|
||||||
|
let obj = versionedLocalStorage.getItem("<key>", { version: 1 });
|
||||||
|
if (obj === null) {
|
||||||
|
obj = fetchMyObject();
|
||||||
|
versionedLocalStorage.setItem("<key>", obj, { version: 1 })
|
||||||
|
}
|
||||||
|
// Do something with obj...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!Warning
|
||||||
|
|
||||||
|
Il existe une différence d'usage entre `localStorage` et `versionedLocalStorage` :
|
||||||
|
les valeurs données à `localStorage` doivent être des strings (généralement
|
||||||
|
obtenus avec `JSON.stringify`), tandis que `versionedLocalStorage` utilise
|
||||||
|
directement des objets JS.
|
||||||
|
|
||||||
|
Cette différence résulte du fait que `versionedLocalStorage` doit légèrement
|
||||||
|
modifier les données pour y inclure la version, et gérer en interne
|
||||||
|
la conversion en JSON.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export {};
|
import { versionedLocalStorage } from "#core:core/localstorage";
|
||||||
|
|
||||||
interface BasketItem {
|
interface BasketItem {
|
||||||
priceId: number;
|
priceId: number;
|
||||||
@@ -7,8 +7,8 @@ interface BasketItem {
|
|||||||
unitPrice: number;
|
unitPrice: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// increment the key number if the data schema of the cached basket changes
|
const BASKET_CACHE_KEY = "basket";
|
||||||
const BASKET_CACHE_KEY = "basket1";
|
const BASKET_CACHE_VERSION = 1;
|
||||||
|
|
||||||
document.addEventListener("alpine:init", () => {
|
document.addEventListener("alpine:init", () => {
|
||||||
Alpine.data("basket", (lastPurchaseTime?: number) => ({
|
Alpine.data("basket", (lastPurchaseTime?: number) => ({
|
||||||
@@ -34,18 +34,16 @@ document.addEventListener("alpine:init", () => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
loadBasket(): BasketItem[] {
|
loadBasket(): BasketItem[] {
|
||||||
if (localStorage.getItem(BASKET_CACHE_KEY) === null) {
|
const cached = versionedLocalStorage.getItem<BasketItem[]>(BASKET_CACHE_KEY, {
|
||||||
return [];
|
version: BASKET_CACHE_VERSION,
|
||||||
}
|
});
|
||||||
try {
|
return cached ?? [];
|
||||||
return JSON.parse(localStorage.getItem(BASKET_CACHE_KEY));
|
|
||||||
} catch (_err) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
saveBasket() {
|
saveBasket() {
|
||||||
localStorage.setItem(BASKET_CACHE_KEY, JSON.stringify(this.basket));
|
versionedLocalStorage.setItem(BASKET_CACHE_KEY, this.basket, {
|
||||||
|
version: BASKET_CACHE_VERSION,
|
||||||
|
});
|
||||||
localStorage.setItem("basketTimestamp", Date.now().toString());
|
localStorage.setItem("basketTimestamp", Date.now().toString());
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ nav:
|
|||||||
- Gestion des permissions: tutorial/perms.md
|
- Gestion des permissions: tutorial/perms.md
|
||||||
- Gestion des groupes: tutorial/groups.md
|
- Gestion des groupes: tutorial/groups.md
|
||||||
- Les fragments: tutorial/fragments.md
|
- Les fragments: tutorial/fragments.md
|
||||||
|
- Frontend:
|
||||||
|
- localStorage: tutorial/front/localstorage.md
|
||||||
- API:
|
- API:
|
||||||
- Développement: tutorial/api/dev.md
|
- Développement: tutorial/api/dev.md
|
||||||
- Connexion à l'API: tutorial/api/connect.md
|
- Connexion à l'API: tutorial/api/connect.md
|
||||||
|
|||||||
Generated
+11
@@ -44,6 +44,7 @@
|
|||||||
"@biomejs/biome": "^2.4.15",
|
"@biomejs/biome": "^2.4.15",
|
||||||
"@hey-api/openapi-ts": "^0.94.5",
|
"@hey-api/openapi-ts": "^0.94.5",
|
||||||
"@types/alpinejs": "^3.13.11",
|
"@types/alpinejs": "^3.13.11",
|
||||||
|
"@types/alpinejs__sort": "^3.13.0",
|
||||||
"@types/cytoscape-cxtmenu": "^3.4.5",
|
"@types/cytoscape-cxtmenu": "^3.4.5",
|
||||||
"@types/cytoscape-klay": "^3.1.5",
|
"@types/cytoscape-klay": "^3.1.5",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
@@ -2499,6 +2500,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/codemirror": {
|
||||||
"version": "5.60.17",
|
"version": "5.60.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.17.tgz",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"@biomejs/biome": "^2.4.15",
|
"@biomejs/biome": "^2.4.15",
|
||||||
"@hey-api/openapi-ts": "^0.94.5",
|
"@hey-api/openapi-ts": "^0.94.5",
|
||||||
"@types/alpinejs": "^3.13.11",
|
"@types/alpinejs": "^3.13.11",
|
||||||
|
"@types/alpinejs__sort": "^3.13.0",
|
||||||
"@types/cytoscape-cxtmenu": "^3.4.5",
|
"@types/cytoscape-cxtmenu": "^3.4.5",
|
||||||
"@types/cytoscape-klay": "^3.1.5",
|
"@types/cytoscape-klay": "^3.1.5",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
|||||||
@@ -22,35 +22,35 @@ document.addEventListener("alpine:init", () => {
|
|||||||
albums: [] as Album[],
|
albums: [] as Album[],
|
||||||
|
|
||||||
async fetchPictures(): Promise<PictureSchema[]> {
|
async fetchPictures(): Promise<PictureSchema[]> {
|
||||||
const localStorageKey = `user${config.userId}Pictures`;
|
// Check the cache before hitting the API.
|
||||||
const localStorageInvalidationKey = `user${config.userId}PicturesNumber`;
|
const storageKey = "userPictures";
|
||||||
const lastCachedNumber = localStorage.getItem(localStorageInvalidationKey);
|
const cacheContent: { userId: number; pictures: PictureSchema[] }[] = JSON.parse(
|
||||||
|
sessionStorage.getItem(storageKey) || "[]",
|
||||||
|
);
|
||||||
|
const userPictures = cacheContent.find((obj) => obj.userId === config.userId);
|
||||||
if (
|
if (
|
||||||
lastCachedNumber !== null &&
|
userPictures !== undefined &&
|
||||||
Number.parseInt(lastCachedNumber, 10) === config.nbPictures
|
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, {
|
const pictures = await paginated(picturesFetchPictures, {
|
||||||
// biome-ignore lint/style/useNamingConvention: from python api
|
// biome-ignore lint/style/useNamingConvention: from python api
|
||||||
query: { users_identified: [config.userId] },
|
query: { users_identified: [config.userId] },
|
||||||
} as PicturesFetchPicturesData);
|
} as PicturesFetchPicturesData);
|
||||||
|
|
||||||
|
cacheContent.push({ userId: config.userId, pictures: pictures });
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(localStorageInvalidationKey, config.nbPictures.toString());
|
// cache only the pictures of the last 4 visited profiles
|
||||||
localStorage.setItem(localStorageKey, JSON.stringify(pictures));
|
sessionStorage.setItem(storageKey, JSON.stringify(cacheContent.slice(-4)));
|
||||||
} catch {
|
} catch {
|
||||||
// an exception is raised if the localstorage is entirely filled
|
// an exception is raised if the storage is entirely filled.
|
||||||
// so just delete all cached user pictures.
|
// To be as safe as possible, delete the cached pictures.
|
||||||
// A cache hit is not worth the page breaking.
|
// A cache hit is not worth the page breaking.
|
||||||
Object.keys(localStorage)
|
sessionStorage.removeItem(storageKey);
|
||||||
.filter(
|
|
||||||
(key) =>
|
|
||||||
key.startsWith("user") &&
|
|
||||||
(key.endsWith("Pictures") || key.endsWith("PicturesNumber")),
|
|
||||||
)
|
|
||||||
.forEach((key) => {
|
|
||||||
localStorage.removeItem(key);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return pictures;
|
return pictures;
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user