namespace MaBibli.Client.Services; /// /// Parenté des routes de l'application : à quel écran remonte la flèche de retour du bandeau. /// /// /// /// ⚠️ Ceci renverse la décision actée le 2026-08-20 (« le retour passe par l'historique du /// navigateur, jamais par une destination calculée »). Le motif d'alors reste vrai — un même /// écran s'atteint par plusieurs chemins — mais l'historique remonte aussi les /// allers-retours (filtre, ordre, édition) : on cliquait cinq fois sans quitter le même /// écran. Une remontée d'un cran de route répond à « où suis-je ? », qui est la question posée. /// /// /// La table est explicite, et non un découpage naïf de l'URL : toutes les routes n'ont pas /// la forme d'une arborescence (/souhaits/ordre remonte à /souhaits, mais /// /auteurs/{id}/bibliographie remonte à /auteurs — la fiche d'un auteur n'existe /// pas). Une route qui apparaît dans l'application doit apparaître ici. /// /// /// ⚠️ Le retour ne sort jamais de l'application : la fonction rend toujours un chemin /// interne, et la racine d'une branche rend la destination de menu correspondante — le catalogue /// en dernier ressort. C'est ce qui remplace le test de history.length qui vivait en /// JavaScript, et qui ne disait pas ce qu'on croyait dans une PWA standalone : la pile /// d'une session y contient aussi ce qui précède l'application. /// /// public static class RemonteeRoutes { /// Le catalogue est la racine : son parent est lui-même, on ne remonte pas plus haut. public const string Racine = "/"; /// /// Table de parenté, dans l'ordre de lecture. {id} représente un segment numérique. /// /// /// La première ligne dont le modèle correspond gagne : les modèles les plus longs sont donc /// écrits avant ceux dont ils sont un prolongement. /// private static readonly (string Modele, string Parent)[] Parents = [ // Fiche livre : l'édition retombe sur la consultation, la consultation sur le catalogue. ("/livres/{id}/edition", "/livres/{id}"), ("/livres/{id}", Racine), // Séries : les deux écrans de modification retombent sur la consultation de la série. ("/series/{id}/edition", "/series/{id}"), ("/series/{id}/ordre", "/series/{id}"), ("/series/{id}", "/series"), ("/series", Racine), // Envies : l'ordre, l'ajout et les deux écrans d'édition sont des écrans de la même // liste. ⚠️ « /souhaits/revues/{id}/edition » ne peut pas se confondre avec // « /souhaits/{id}/edition » : les modèles n'ont pas le même nombre de segments. ("/souhaits/revues/{id}/edition", "/souhaits"), ("/souhaits/{id}/edition", "/souhaits"), ("/souhaits/ordre", "/souhaits"), ("/souhaits/ajout", "/souhaits"), ("/souhaits", Racine), // Ajout d'un ouvrage : on y entre depuis le catalogue comme depuis les revues, mais le // catalogue est la destination de menu de ce qu'on y saisit. ("/ajout/manuel", Racine), ("/ajout/isbn", Racine), ("/ajout", Racine), // Revues. ⚠️ « /revues/ajout » est écrit AVANT « /revues/{id} » : « ajout » n'est pas un // identifiant, mais l'ordre de lecture est ce qui le garantit sans ambiguïté. ("/revues/ajout", "/revues"), ("/revues/{id}/edition", "/revues/{id}"), ("/revues/{id}", "/revues"), ("/revues", Racine), // La bibliographie remonte à la LISTE des auteurs : il n'existe pas de fiche auteur. ("/auteurs/{id}/bibliographie", "/auteurs"), ("/auteurs", Racine), ("/prets", Racine), // « À propos » est une annexe du menu : elle remonte au catalogue, comme toute racine de // branche. Elle n'a pas d'enfants, et n'en aura pas. ("/a-propos", Racine), ("/not-found", Racine), ]; /// /// Écran d'où l'on vient hiérarchiquement, à partir d'un chemin d'application. /// /// /// Chemin absolu ou relatif à la base, avec ou sans requête (?auteur=3) ni fragment. /// /// Un chemin interne, toujours ; jamais null, jamais une URL absolue. public static string Parent(string? chemin) { var segments = Segments(chemin); if (segments.Length == 0) { return Racine; } foreach (var (modele, parent) in Parents) { if (Correspond(Segments(modele), segments)) { return Rendre(parent, segments); } } // Route inconnue : on retombe sur la destination de menu de sa branche, plutôt que sur // un chemin deviné. Une route ajoutée sans sa ligne de table reste ainsi utilisable — // mais elle doit être ajoutée : c'est un repli, pas le mécanisme. return Parents.FirstOrDefault(p => p.Modele == $"/{segments[0]}").Parent is not null ? $"/{segments[0]}" : Racine; } /// Découpe en segments non vides, requête et fragment retirés. private static string[] Segments(string? chemin) { if (string.IsNullOrWhiteSpace(chemin)) { return []; } var utile = chemin.Split('?', '#')[0]; return utile.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } private static bool Correspond(string[] modele, string[] segments) { if (modele.Length != segments.Length) { return false; } for (var i = 0; i < modele.Length; i++) { var attendu = modele[i]; var ok = attendu == "{id}" ? int.TryParse(segments[i], out _) : string.Equals(attendu, segments[i], StringComparison.OrdinalIgnoreCase); if (!ok) { return false; } } return true; } /// Remplace {id} du parent par l'identifiant lu dans le chemin d'origine. private static string Rendre(string parent, string[] segments) { if (!parent.Contains("{id}", StringComparison.Ordinal)) { return parent; } var identifiant = segments.FirstOrDefault(s => int.TryParse(s, out _)); // Sans identifiant lisible, on ne fabrique pas une route bancale : la racine est sûre. return identifiant is null ? Racine : parent.Replace("{id}", identifiant, StringComparison.Ordinal); } }