@jjlmoya/utils-streaming 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +134 -0
  3. package/src/category/i18n/es.ts +134 -0
  4. package/src/category/i18n/fr.ts +134 -0
  5. package/src/category/index.ts +12 -0
  6. package/src/category/seo.astro +14 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +24 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +148 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/schemas_fulfillment.test.ts +23 -0
  21. package/src/tests/seo_length.test.ts +22 -0
  22. package/src/tests/title_quality.test.ts +55 -0
  23. package/src/tests/tool_validation.test.ts +17 -0
  24. package/src/tool/sorteo/bibliography.astro +14 -0
  25. package/src/tool/sorteo/component.astro +1319 -0
  26. package/src/tool/sorteo/engine.ts +143 -0
  27. package/src/tool/sorteo/i18n/en.ts +253 -0
  28. package/src/tool/sorteo/i18n/es.ts +253 -0
  29. package/src/tool/sorteo/i18n/fr.ts +253 -0
  30. package/src/tool/sorteo/index.ts +29 -0
  31. package/src/tool/sorteo/seo.astro +14 -0
  32. package/src/tool/sorteo/ui-manager.ts +81 -0
  33. package/src/tool/sorteo/ui.ts +30 -0
  34. package/src/tool/tebasCheck/bibliography.astro +14 -0
  35. package/src/tool/tebasCheck/component.astro +587 -0
  36. package/src/tool/tebasCheck/engine.ts +116 -0
  37. package/src/tool/tebasCheck/i18n/en.ts +254 -0
  38. package/src/tool/tebasCheck/i18n/es.ts +254 -0
  39. package/src/tool/tebasCheck/i18n/fr.ts +254 -0
  40. package/src/tool/tebasCheck/index.ts +29 -0
  41. package/src/tool/tebasCheck/ips.json +31 -0
  42. package/src/tool/tebasCheck/seo.astro +14 -0
  43. package/src/tool/tebasCheck/types.ts +45 -0
  44. package/src/tool/tebasCheck/ui-manager.ts +31 -0
  45. package/src/tools.ts +8 -0
  46. package/src/types.ts +71 -0
@@ -0,0 +1,253 @@
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { SorteoUI } from '../ui';
4
+
5
+ const slug = 'tirage-au-sort';
6
+ const title = 'Tirage au Sort de Noms pour Streaming';
7
+ const description =
8
+ 'Choisissez un gagnant au hasard dans une liste de noms. Outil de tirage au sort gratuit, rapide et visuel pour Twitch, YouTube et événements.';
9
+
10
+ const faqData = [
11
+ {
12
+ question: 'Ce tirage au sort est-il vraiment aléatoire ?',
13
+ answer:
14
+ 'Oui, nous utilisons l\'algorithme d\'aléa cryptographique du navigateur (Web Crypto API) pour garantir que chaque participant a exactement la même probabilité de gagner, sans biais ni manipulation.',
15
+ },
16
+ {
17
+ question: 'Puis-je utiliser ce tirage sur Twitch ou YouTube ?',
18
+ answer:
19
+ 'Absolument. En tant qu\'outil web, vous pouvez capturer la fenêtre avec OBS ou partager votre écran directement. Le design épuré et les animations sont conçus pour que le public voie le processus en toute transparence.',
20
+ },
21
+ {
22
+ question: 'Comment empêcher quelqu\'un de participer deux fois ?',
23
+ answer:
24
+ 'L\'outil dispose d\'une fonction de "nettoyage des doublons" automatique qui détecte les noms identiques ou avec de petites variations d\'espacement pour garantir que chaque personne réelle ne compte qu\'une seule fois.',
25
+ },
26
+ {
27
+ question: 'Puis-je tirer plusieurs gagnants à la fois ?',
28
+ answer:
29
+ 'Oui, vous pouvez configurer le nombre de gagnants souhaités avant de cliquer sur le bouton. L\'outil listera clairement les heureux élus pour que vous puissiez les mentionner dans votre flux en direct.',
30
+ },
31
+ {
32
+ question: 'Combien de noms puis-je ajouter à la liste ?',
33
+ answer:
34
+ 'Il n\'y a pas de limite stricte imposée par l\'outil. Nous avons optimisé le moteur pour gérer des listes de milliers de participants sans problèmes de performance, ce qui le rend idéal même pour les tirages massifs.',
35
+ },
36
+ {
37
+ question: 'Mes données ou la liste des participants sont-elles sauvegardées ?',
38
+ answer:
39
+ 'Non, jamais. Votre vie privée est notre priorité. Tout le processus de tirage au sort s\'exécute localement dans votre navigateur web. Les noms que vous saisissez ne sont jamais envoyés à nos serveurs ni stockés dans une base de données.',
40
+ },
41
+ ];
42
+
43
+ const howToData = [
44
+ {
45
+ name: 'Préparer la liste des participants',
46
+ text: 'Copiez la liste des noms depuis votre chat, Excel ou réseau social et collez-la dans la zone de texte.',
47
+ },
48
+ {
49
+ name: 'Configurer les options du tirage',
50
+ text: 'Choisissez le nombre de gagnants dont vous avez besoin et si vous souhaitez filtrer les doublons ou les noms vides.',
51
+ },
52
+ {
53
+ name: 'Lancer la main innocente',
54
+ text: 'Cliquez sur le bouton de tirage. Une animation visuelle maintiendra la tension avant de révéler le gagnant.',
55
+ },
56
+ {
57
+ name: 'Annoncer les résultats',
58
+ text: 'Copiez les noms des gagnants pour les partager sur vos réseaux sociaux ou votre chat de streaming.',
59
+ },
60
+ ];
61
+
62
+ const faqSchema: WithContext<FAQPage> = {
63
+ '@context': 'https://schema.org',
64
+ '@type': 'FAQPage',
65
+ mainEntity: faqData.map((item) => ({
66
+ '@type': 'Question',
67
+ name: item.question,
68
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
69
+ })),
70
+ };
71
+
72
+ const howToSchema: WithContext<HowTo> = {
73
+ '@context': 'https://schema.org',
74
+ '@type': 'HowTo',
75
+ name: title,
76
+ description,
77
+ step: howToData.map((step, i) => ({
78
+ '@type': 'HowToStep',
79
+ position: i + 1,
80
+ name: step.name,
81
+ text: step.text,
82
+ })),
83
+ };
84
+
85
+ const appSchema: WithContext<SoftwareApplication> = {
86
+ '@context': 'https://schema.org',
87
+ '@type': 'SoftwareApplication',
88
+ name: title,
89
+ description,
90
+ applicationCategory: 'UtilityApplication',
91
+ operatingSystem: 'All',
92
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
93
+ inLanguage: 'fr',
94
+ };
95
+
96
+ export const content: ToolLocaleContent<SorteoUI> = {
97
+ slug,
98
+ title,
99
+ description,
100
+ faqTitle: 'Questions Fréquemment Posées',
101
+ faq: faqData,
102
+ bibliographyTitle: 'Références Techniques',
103
+ bibliography: [
104
+ {
105
+ name: 'Web Crypto API: getRandomValues()',
106
+ url: 'https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues',
107
+ },
108
+ {
109
+ name: 'Fisher-Yates Shuffle Algorithm',
110
+ url: 'https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle',
111
+ },
112
+ ],
113
+ howTo: howToData,
114
+ schemas: [faqSchema as any, howToSchema as any, appSchema as any],
115
+ seo: [
116
+ {
117
+ type: 'title',
118
+ text: 'Tirage au Sort de Noms et Liste de Participants en Ligne',
119
+ level: 2,
120
+ },
121
+ {
122
+ type: 'paragraph',
123
+ html: 'Vous vous demandez comment faire un tirage au sort en ligne rapidement, en toute sécurité et en toute transparence ? Notre outil gratuit de <strong>Tirage de Noms</strong> est la solution ultime pour choisir un gagnant au hasard en quelques secondes. Conçu pour être simple, visuel et efficace, il est parfait pour tout scénario où vous avez besoin d\'une "main innocente" numérique.',
124
+ },
125
+ {
126
+ type: 'paragraph',
127
+ html: 'Que vous gériez un concours sur les réseaux sociaux, un tirage au sort massif sur votre chaîne de streaming ou que vous décidiez simplement qui sort les poubelles aujourd\'hui, notre sélecteur aléatoire garantit une impartialité totale grâce à des algorithmes cryptographiques modernes. <strong>Aucune manipulation, aucun biais, juste de la pureté aléatoire.</strong>'
128
+ },
129
+ {
130
+ type: 'title',
131
+ text: 'Cas d\'Usage',
132
+ level: 3,
133
+ },
134
+ {
135
+ type: 'comparative',
136
+ columns: 2,
137
+ items: [
138
+ {
139
+ title: 'Tirages sur les Réseaux Sociaux',
140
+ description: 'Idéal pour les concours Instagram, Twitter (X) ou Facebook. Copiez simplement les noms des commentaires et collez-les pour choisir un gagnant équitable. L\'outil nettoie automatiquement les doublons.',
141
+ },
142
+ {
143
+ title: 'Streaming sur Twitch / YouTube',
144
+ description: 'Grâce à notre Mode Studio avec des animations fluides et des sons intégrés, vous pouvez partager votre écran directement dans OBS et offrir un spectacle visuel passionnant à votre public tout en choisissant les gagnants en direct.',
145
+ },
146
+ {
147
+ title: 'Dynamiques de Classe et d\'Équipe',
148
+ description: 'Les enseignants et les chefs d\'équipe peuvent l\'utiliser pour former des groupes aléatoires, choisir qui expose en premier ou assigner des tâches au hasard en toute transparence et sans favoritisme.',
149
+ },
150
+ {
151
+ title: 'Secret Santa et Événements',
152
+ description: 'Simplifiez l\'organisation d\'événements familiaux, de tirages au sort de bureau ou de Secret Santa en choisissant des noms au hasard instantanément sans avoir besoin de papiers ni de logistique compliquée.',
153
+ },
154
+ ],
155
+ },
156
+ {
157
+ type: 'title',
158
+ text: 'Pourquoi notre outil est-il différent ?',
159
+ level: 3,
160
+ },
161
+ {
162
+ type: 'list',
163
+ items: [
164
+ '<strong>Cryptographie Réelle :</strong> Nous utilisons la Web Crypto API du navigateur (norme W3C) au lieu de générateurs pseudo-aléatoires faibles. Chaque tirage au sort est mathématiquement impartial.',
165
+ '<strong>Aucun Stockage :</strong> Vos données ne quittent jamais votre navigateur. Nous ne vendons pas de listes, nous ne vous profileurons pas, nous ne stockons rien. Pur traitement local.',
166
+ '<strong>Design Visuel :</strong> Le mode cinéma et les animations rendent chaque tirage au sort mémorable. Parfait pour la capture OBS ou la diffusion en direct.',
167
+ '<strong>Gestion des Doublons :</strong> Détecte automatiquement les noms répétés ou les variantes (espaces supplémentaires, majuscules, etc.) pour assurer que chaque personne réelle ne compte qu\'une seule fois.',
168
+ ],
169
+ },
170
+ {
171
+ type: 'title',
172
+ text: 'Comment utiliser le tirage au sort étape par étape',
173
+ level: 3,
174
+ },
175
+ {
176
+ type: 'list',
177
+ items: [
178
+ '<strong>Étape 1 - Entrez les participants :</strong> Collez votre liste de noms dans la zone de texte principale. L\'outil détecte automatiquement chaque saut de ligne comme un participant différent. Vous avez des doublons ? Aucun problème, l\'outil les nettoie.',
179
+ '<strong>Étape 2 - Personnalisez :</strong> Dans l\'onglet des paramètres, vous pouvez activer le compte à rebours pour générer de la tension, l\'effet de confettis pour célébrer, ou activer la "liste noire" pour exclure certains noms.',
180
+ '<strong>Étape 3 - Tirez au sort !</strong> Cliquez sur le bouton principal et notre moteur générera une sélection aléatoire sécurisée par cryptographie. Les gagnants seront affichés de manière claire et mémorable.',
181
+ ],
182
+ },
183
+ {
184
+ type: 'title',
185
+ text: 'Entrées Pondérées : Donner un Avantage à Certains Participants',
186
+ level: 3,
187
+ },
188
+ {
189
+ type: 'paragraph',
190
+ html: 'Vous souhaitez récompenser vos abonnés les plus fidèles ou donner plus de chances à certains participants ? Notre système d\'<strong>Entrées Pondérées</strong> est unique et vous permet d\'assigner un "poids" ou un multiplicateur à n\'importe quel nom sans avoir à l\'écrire plusieurs fois.',
191
+ },
192
+ {
193
+ type: 'tip',
194
+ title: 'Comment assigner des poids aux noms',
195
+ html: '<p>Utilisez un astérisque (*) ou un "x" suivi du nombre de participations. Exemples :</p><ul><li><strong>"Jean * 5"</strong> - Jean concourt comme s\'il était 5 personnes</li><li><strong>"Maria x 10"</strong> - Maria a 10 fois plus de chances</li><li><strong>"Pierre"</strong> - Sans symbole = 1 entrée normale</li></ul><p>C\'est parfait pour les tirages au sort où vous voulez donner un avantage aux abonnés VIP ou aux utilisateurs spéciaux.</p>',
196
+ },
197
+ {
198
+ type: 'title',
199
+ text: 'Confidentialité et Sécurité Totales',
200
+ level: 3,
201
+ },
202
+ {
203
+ type: 'paragraph',
204
+ html: 'Contrairement à d\'autres outils en ligne, <strong>nous ne stockons pas vos données</strong>. Tout le traitement des noms et l\'exécution du tirage au sort se font localement dans votre propre navigateur. Vos listes de participants ne voyagent jamais sur le réseau et ne sont enregistrées sur aucun serveur externe.',
205
+ },
206
+ {
207
+ type: 'paragraph',
208
+ html: '<strong>Qu\'est-ce que cela signifie ?</strong> Votre liste de participants vous appartient et vous seul. En fermant l\'onglet, elle disparaît. Pas de cookies de suivi, pas de profils utilisateur, pas de base de données centrale. Confidentialité maximale garantie pour vous et ceux qui participent à vos tirages au sort.',
209
+ },
210
+ {
211
+ type: 'title',
212
+ text: 'Transparence Mathématique',
213
+ level: 3,
214
+ },
215
+ {
216
+ type: 'paragraph',
217
+ html: 'Certains se poseront la question : "Et si vous manipuliez les résultats ?" La réponse est simple : <strong>nous ne pouvons pas</strong>. Le code du tirage au sort est déterministe et cryptographique. Pas de variables cachées, pas de "doigts sur la scène".',
218
+ },
219
+ {
220
+ type: 'paragraph',
221
+ html: 'Chaque gagnant est le résultat direct de l\'algorithme Fisher-Yates Shuffle appliqué à votre liste exacte, en utilisant l\'entropie cryptographique réelle. Si vous souhaitez auditer le processus, le code est disponible sur GitHub pour inspection publique.',
222
+ },
223
+ ],
224
+ ui: {
225
+ title: 'Tirage au Sort Aléatoire',
226
+ totalParticipants: 'Total Participants Uniques',
227
+ ready: 'PRÊT',
228
+ participants: 'Participants',
229
+ settings: 'Paramètres',
230
+ importFile: 'Importer Fichier',
231
+ clearAll: 'Tout effacer',
232
+ placeholder: 'Tapez ou collez les noms ici...\nJean Dupont\nMarie Martin\n@utilisateur_twitch',
233
+ onePerLine: '1 participant par ligne',
234
+ lines: 'lignes',
235
+ filters: 'Filtres',
236
+ allowDuplicates: 'Autoriser les Doublons',
237
+ winnerCount: 'Nombre de Gagnants',
238
+ autoRemove: 'Auto-Supprimer le Gagnant',
239
+ blacklist: 'Liste Noire (Exclure)',
240
+ blacklistPlaceholder: 'Noms interdits (un par ligne)...',
241
+ blacklistInfo: 'Ces utilisateurs ne participeront pas au tirage.',
242
+ sceneEffects: 'Effets de Scène',
243
+ countdown: 'Compte à Rebours (3s)',
244
+ confetti: 'Confettis de Victoire',
245
+ zenMode: 'Mode Ciné (Masquer Panel)',
246
+ waitingParticipants: 'En attente de participants...',
247
+ winner: 'GAGNANT',
248
+ reroll: 'Relancer le Tirage',
249
+ history: 'Historique de cette session',
250
+ noWinnersYet: 'Pas encore de gagnants...',
251
+ startGiveaway: 'Démarrer le Tirage',
252
+ },
253
+ };
@@ -0,0 +1,29 @@
1
+ import type { StreamingToolEntry, ToolLocaleContent, ToolDefinition } from '../../types';
2
+ import SorteoComponent from './component.astro';
3
+ import SorteoSEO from './seo.astro';
4
+ import SorteoBibliography from './bibliography.astro';
5
+ import type { SorteoUI } from './ui';
6
+
7
+ export type SorteoLocaleContent = ToolLocaleContent<SorteoUI>;
8
+
9
+ export const sorteo: StreamingToolEntry<SorteoUI> = {
10
+ id: 'sorteo',
11
+ icons: {
12
+ bg: 'mdi:ticket',
13
+ fg: 'mdi:account-group',
14
+ },
15
+ i18n: {
16
+ es: () => import('./i18n/es').then((m) => m.content),
17
+ en: () => import('./i18n/en').then((m) => m.content),
18
+ fr: () => import('./i18n/fr').then((m) => m.content),
19
+ },
20
+ };
21
+
22
+ export { SorteoComponent, SorteoSEO, SorteoBibliography };
23
+
24
+ export const SORTEO_TOOL: ToolDefinition = {
25
+ entry: sorteo,
26
+ Component: SorteoComponent,
27
+ SEOComponent: SorteoSEO,
28
+ BibliographyComponent: SorteoBibliography,
29
+ };
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { SORTEO_TOOL } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await SORTEO_TOOL.entry.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,81 @@
1
+ export function playBeep(audioContext: AudioContext | null, frequency = 440, duration = 0.1, type: OscillatorType = 'sine') {
2
+ if (!audioContext) {
3
+ return null;
4
+ }
5
+ const osc = audioContext.createOscillator();
6
+ const gain = audioContext.createGain();
7
+ osc.type = type;
8
+ osc.frequency.setValueAtTime(frequency, audioContext.currentTime);
9
+ gain.gain.setValueAtTime(0.1, audioContext.currentTime);
10
+ gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration);
11
+ osc.connect(gain);
12
+ gain.connect(audioContext.destination);
13
+ osc.start();
14
+ osc.stop(audioContext.currentTime + duration);
15
+ return audioContext;
16
+ }
17
+
18
+ export async function runCountdownSequence(
19
+ els: { stateCountdown: HTMLElement | null; countdownNumber: HTMLElement | null },
20
+ audioContext: AudioContext | null,
21
+ beepFn: typeof playBeep
22
+ ) {
23
+ if (!els.stateCountdown || !els.countdownNumber) {
24
+ return;
25
+ }
26
+ els.stateCountdown.classList.remove('hidden');
27
+ for (let i = 3; i > 0; i--) {
28
+ els.countdownNumber.textContent = i.toString();
29
+ els.countdownNumber.classList.remove('animate-ping-slow');
30
+ void els.countdownNumber.offsetWidth;
31
+ els.countdownNumber.classList.add('animate-ping-slow');
32
+ beepFn(audioContext, 600 + i * 100, 0.1, 'square');
33
+ await new Promise((r) => setTimeout(r, 1000));
34
+ }
35
+ els.stateCountdown.classList.add('hidden');
36
+ }
37
+
38
+ export function triggerConfetti(container: HTMLElement | null) {
39
+ if (!container) {
40
+ return;
41
+ }
42
+ const colors = ['#6366f1', '#a855f7', '#ec4899', '#fbbf24', '#34d399'];
43
+ for (let i = 0; i < 60; i++) {
44
+ const p = document.createElement('div');
45
+ p.className = 'sorteo-confetti-particle';
46
+ p.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)] || '';
47
+ const angle = Math.random() * Math.PI * 2;
48
+ const velocity = 100 + Math.random() * 300;
49
+ const tx = Math.cos(angle) * velocity;
50
+ const ty = Math.sin(angle) * velocity;
51
+ const rot = Math.random() * 360;
52
+ p.style.setProperty('--tx', `${tx}px`);
53
+ p.style.setProperty('--ty', `${ty}px`);
54
+ p.style.setProperty('--rot', `${rot}deg`);
55
+ container.appendChild(p);
56
+ setTimeout(() => p.remove(), 1500);
57
+ }
58
+ }
59
+
60
+ export function addToHistory(
61
+ historyList: HTMLElement | null,
62
+ names: string | string[],
63
+ noWinnersYetText: string
64
+ ) {
65
+ if (!historyList) {
66
+ return;
67
+ }
68
+
69
+ if (historyList.innerHTML.includes(noWinnersYetText)) {
70
+ historyList.innerHTML = '';
71
+ }
72
+
73
+ const list = Array.isArray(names) ? names : [names];
74
+
75
+ list.forEach((name) => {
76
+ const badge = document.createElement('div');
77
+ badge.className = 'sorteo-history-item';
78
+ badge.innerHTML = `<span class="sorteo-history-dot"></span>${name}`;
79
+ historyList.prepend(badge);
80
+ });
81
+ }
@@ -0,0 +1,30 @@
1
+ export interface SorteoUI {
2
+ [key: string]: string;
3
+ title: string;
4
+ totalParticipants: string;
5
+ ready: string;
6
+ participants: string;
7
+ settings: string;
8
+ importFile: string;
9
+ clearAll: string;
10
+ placeholder: string;
11
+ onePerLine: string;
12
+ lines: string;
13
+ filters: string;
14
+ allowDuplicates: string;
15
+ winnerCount: string;
16
+ autoRemove: string;
17
+ blacklist: string;
18
+ blacklistPlaceholder: string;
19
+ blacklistInfo: string;
20
+ sceneEffects: string;
21
+ countdown: string;
22
+ confetti: string;
23
+ zenMode: string;
24
+ waitingParticipants: string;
25
+ winner: string;
26
+ reroll: string;
27
+ history: string;
28
+ noWinnersYet: string;
29
+ startGiveaway: string;
30
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { tebasCheck } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await tebasCheck.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}