@jjlmoya/utils-sports 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 (79) hide show
  1. package/package.json +62 -0
  2. package/src/category/i18n/en.ts +108 -0
  3. package/src/category/i18n/es.ts +108 -0
  4. package/src/category/i18n/fr.ts +95 -0
  5. package/src/category/index.ts +21 -0
  6. package/src/category/seo.astro +15 -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 +55 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -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/gymTracker/bibliography.astro +15 -0
  25. package/src/tool/gymTracker/component.astro +835 -0
  26. package/src/tool/gymTracker/exercises.ts +28 -0
  27. package/src/tool/gymTracker/i18n/en.ts +225 -0
  28. package/src/tool/gymTracker/i18n/es.ts +225 -0
  29. package/src/tool/gymTracker/i18n/fr.ts +225 -0
  30. package/src/tool/gymTracker/index.ts +34 -0
  31. package/src/tool/gymTracker/logic.ts +169 -0
  32. package/src/tool/gymTracker/seo.astro +15 -0
  33. package/src/tool/gymTracker/storage.ts +43 -0
  34. package/src/tool/gymTracker/timer.ts +126 -0
  35. package/src/tool/gymTracker/types.ts +11 -0
  36. package/src/tool/gymTracker/ui-utils.ts +59 -0
  37. package/src/tool/gymTracker/ui.ts +27 -0
  38. package/src/tool/reactionTester/bibliography.astro +2 -0
  39. package/src/tool/reactionTester/component.astro +1074 -0
  40. package/src/tool/reactionTester/i18n/en.ts +144 -0
  41. package/src/tool/reactionTester/i18n/es.ts +144 -0
  42. package/src/tool/reactionTester/i18n/fr.ts +144 -0
  43. package/src/tool/reactionTester/index.ts +34 -0
  44. package/src/tool/reactionTester/seo.astro +12 -0
  45. package/src/tool/reactionTester/ui.ts +43 -0
  46. package/src/tool/scoreKeeper/bibliography.astro +14 -0
  47. package/src/tool/scoreKeeper/component.astro +858 -0
  48. package/src/tool/scoreKeeper/i18n/en.ts +207 -0
  49. package/src/tool/scoreKeeper/i18n/es.ts +207 -0
  50. package/src/tool/scoreKeeper/i18n/fr.ts +207 -0
  51. package/src/tool/scoreKeeper/index.ts +35 -0
  52. package/src/tool/scoreKeeper/logic.ts +275 -0
  53. package/src/tool/scoreKeeper/seo.astro +15 -0
  54. package/src/tool/scoreKeeper/sports.ts +70 -0
  55. package/src/tool/scoreKeeper/ui.ts +19 -0
  56. package/src/tool/tournamentBracket/bibliography.astro +10 -0
  57. package/src/tool/tournamentBracket/component.astro +1092 -0
  58. package/src/tool/tournamentBracket/i18n/en.ts +160 -0
  59. package/src/tool/tournamentBracket/i18n/es.ts +178 -0
  60. package/src/tool/tournamentBracket/i18n/fr.ts +160 -0
  61. package/src/tool/tournamentBracket/index.ts +34 -0
  62. package/src/tool/tournamentBracket/logic/active.controller.ts +106 -0
  63. package/src/tool/tournamentBracket/logic/generator.ts +71 -0
  64. package/src/tool/tournamentBracket/logic/manager.ts +165 -0
  65. package/src/tool/tournamentBracket/logic/setup.controller.ts +84 -0
  66. package/src/tool/tournamentBracket/logic/sharing.ts +81 -0
  67. package/src/tool/tournamentBracket/logic/storage.ts +56 -0
  68. package/src/tool/tournamentBracket/models.ts +34 -0
  69. package/src/tool/tournamentBracket/seo.astro +12 -0
  70. package/src/tool/tournamentBracket/tournament.controller.ts +65 -0
  71. package/src/tool/tournamentBracket/tournament.renderer.ts +45 -0
  72. package/src/tool/tournamentBracket/ui/bracket-desktop.ts +143 -0
  73. package/src/tool/tournamentBracket/ui/bracket-mobile.ts +82 -0
  74. package/src/tool/tournamentBracket/ui/mediator.ts +96 -0
  75. package/src/tool/tournamentBracket/ui/navigator.ts +84 -0
  76. package/src/tool/tournamentBracket/ui/setup.ts +120 -0
  77. package/src/tool/tournamentBracket/ui.ts +42 -0
  78. package/src/tools.ts +13 -0
  79. package/src/types.ts +72 -0
@@ -0,0 +1,160 @@
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { TournamentBracketUI } from '../ui';
4
+
5
+ const slug = 'tournament';
6
+ const title = 'Free Online Bracket Generator and Tournament Organizer';
7
+ const description = 'Organize tournaments and create single-elimination brackets for free with no registration. Perfect for FIFA, Padel, eSports and board games. 100% Mobile Friendly.';
8
+
9
+ const ui: TournamentBracketUI = {
10
+ tournamentInProgress: 'Tournament in Progress',
11
+ nextMatch: 'Next Match',
12
+ share: 'Share',
13
+ backNew: 'Back / New',
14
+ back: 'Back',
15
+ newTournament: 'New Tournament',
16
+ setupSubtitle: 'Set up and generate your competition bracket.',
17
+ tournamentNameLabel: 'Tournament Name',
18
+ tournamentNamePlaceholder: 'E.g. Summer Tournament',
19
+ addPlayersLabel: 'Add Participants',
20
+ addPlayerPlaceholder: 'Name... or several separated by commas',
21
+ playersLabel: 'Players',
22
+ clearAll: 'Clear All',
23
+ emptyList: 'The list is empty',
24
+ howItWorks: 'How does it work?',
25
+ howItWorksText: 'Add participants, give it a name and generate. The system will automatically create matchups and handle "Byes" (direct passes) if there is an odd number.',
26
+ historyLabel: 'History',
27
+ noHistory: 'No tournaments saved',
28
+ noOldTournaments: 'No previous tournaments',
29
+ generateBtn: 'Generate Bracket',
30
+ shuffleLabel: 'Randomize matchups',
31
+ scoreLabel: 'Enable Scores (Optional)',
32
+ dragHint: 'Drag to move',
33
+ roundFinal: 'Final',
34
+ roundSemifinal: 'Semifinals',
35
+ roundQuarter: 'Quarterfinals',
36
+ roundPrefix: 'Round',
37
+ byeLabel: 'Bye',
38
+ waiting: 'Waiting...',
39
+ emptyRound: 'Empty round',
40
+ confirmClearPlayers: 'Clear the entire player list?',
41
+ alertMinPlayers: 'You need at least 2 players.',
42
+ alertLoadFailed: 'Could not load the tournament.',
43
+ confirmDeleteTournament: 'Permanently delete this tournament from history?',
44
+ toastShareLimit: 'Only tournaments with up to 32 players can be shared',
45
+ toastShareError: 'Error generating the link',
46
+ toastShareCopied: 'Link copied to clipboard',
47
+ toastShareFailed: 'Could not copy. URL:',
48
+ toastFinished: 'Tournament Finished!',
49
+ defaultName: 'Tournament',
50
+ };
51
+
52
+ const faqData = [
53
+ { question: 'How does single elimination work?', answer: 'It is a competition system where the participant who loses a match is automatically eliminated from the tournament. Winners advance to the next round (round of 16, quarterfinals, semifinals) until only two remain for the grand final.' },
54
+ { question: 'What happens if I have an odd number of players?', answer: 'Our tool automatically handles "BYEs". Some players will advance directly to the second round without playing in the first so the bracket always ends in powers of two (2, 4, 8, 16...).' },
55
+ { question: 'Can I save and share the tournament bracket?', answer: 'Yes, you can share the bracket through a unique instantly generated link. As a registration-free tool, data is kept in your browser while the tab is open.' },
56
+ { question: 'Does it work for eSports tournaments like FIFA or LoL?', answer: 'Absolutely. It is designed to be fast and visual, ideal for managing quick console, PC game or even board and card game tournaments.' },
57
+ { question: 'Is creating tournaments free?', answer: 'Yes, completely free and without restrictions. No premium plans, participant limits, watermarks or intrusive ads. Everything works offline in your browser.' },
58
+ { question: 'Is my data deleted if I close the browser?', answer: 'No. We use LocalStorage to automatically save all your tournaments on your device. You can close the tab, shut down the computer and come back days later: your tournament will still be there. The full history is also persistent.' },
59
+ { question: 'How does the "Next Match" button work?', answer: 'The system automatically detects the next ready matchup (both participants confirmed) but without a result yet. Pressing "Next Match" jumps the view directly to that match.' },
60
+ ];
61
+
62
+ const howTo = [
63
+ { name: 'Enter participants', text: 'Write the names of the players or teams that will take part in the competition.' },
64
+ { name: 'Generate the bracket', text: 'Click the generate button. The system will automatically create the matchups and necessary rounds.' },
65
+ { name: 'Update results', text: 'Click on the winning participant of each match so they automatically advance to the next stage of the bracket.' },
66
+ { name: 'Finish', text: 'Once the tournament is complete, the final champion is shown.' },
67
+ ];
68
+
69
+ const seo = [
70
+ {
71
+ type: 'title' as const,
72
+ text: 'Free Online Bracket Generator and Tournament Organizer',
73
+ level: 2 as const,
74
+ },
75
+ {
76
+ type: 'paragraph' as const,
77
+ html: 'Manage your sports, video game or board game competitions with the most complete, free and registration-free tournament organizer. Create visual and interactive single-elimination brackets in seconds, with an <strong>integrated scoring system</strong>, automatic history and smart match navigation. Everything works offline, directly in your browser.',
78
+ },
79
+ {
80
+ type: 'title' as const,
81
+ text: 'How to Create a Single Elimination Tournament?',
82
+ level: 2 as const,
83
+ },
84
+ {
85
+ type: 'paragraph' as const,
86
+ html: '<strong>Name your tournament</strong>, add participants (one by one or paste a comma-separated list), randomize matchups if you want, generate the bracket, manage results by tapping the winner of each match and use the "Next Match" button to navigate between unresolved matches.',
87
+ },
88
+ {
89
+ type: 'title' as const,
90
+ text: 'Advanced Features',
91
+ level: 2 as const,
92
+ },
93
+ {
94
+ type: 'list' as const,
95
+ items: [
96
+ '<strong>Bulk entry:</strong> Add multiple participants at once separated by commas.',
97
+ '<strong>Exact scores:</strong> Scoring system with results like 3-1 or 21-19.',
98
+ '<strong>Smart navigation:</strong> "Next Match" button jumps to the next pending matchup.',
99
+ '<strong>Draggable bracket:</strong> Desktop view with free scroll for large tournaments.',
100
+ '<strong>Persistent history:</strong> All tournaments saved automatically in your browser.',
101
+ '<strong>Auto Walkovers:</strong> Byes and direct passes resolved without manual input.',
102
+ '<strong>Share by URL:</strong> Generate a compressed link to send the bracket to anyone.',
103
+ ],
104
+ },
105
+ {
106
+ type: 'title' as const,
107
+ text: 'Perfect for Any Competition',
108
+ level: 2 as const,
109
+ },
110
+ {
111
+ type: 'comparative' as const,
112
+ columns: 3 as const,
113
+ items: [
114
+ {
115
+ title: 'Video Games & eSports',
116
+ description: 'Perfect for FIFA, FC25, Valorant, League of Legends, Street Fighter, Tekken, Super Smash Bros or Rocket League.',
117
+ icon: 'mdi:controller-classic',
118
+ points: ['Fast matchups', 'No team limit', 'Instantly shareable'],
119
+ },
120
+ {
121
+ title: 'Sports & Racket Sports',
122
+ description: 'Manage Padel, Tennis, Ping Pong, Badminton, Futsal or 3x3 Basketball brackets.',
123
+ icon: 'mdi:trophy-outline',
124
+ points: ['Integrated scores', 'Optimized mobile view', 'No paper needed'],
125
+ },
126
+ {
127
+ title: 'Board & Card Games',
128
+ description: 'Organize Magic: The Gathering, Pokémon TCG, Yu-Gi-Oh!, Catan, Chess or Dominoes tournaments.',
129
+ icon: 'mdi:cards-playing-outline',
130
+ points: ['Up to 64 players', 'Round history', 'Bye management'],
131
+ },
132
+ ],
133
+ },
134
+ {
135
+ type: 'title' as const,
136
+ text: 'What are "Byes" or Direct Passes?',
137
+ level: 2 as const,
138
+ },
139
+ {
140
+ type: 'paragraph' as const,
141
+ html: 'In an ideal single-elimination tournament the number of participants must be a power of 2 (4, 8, 16, 32...). When you have an odd or non-power-of-2 number —for example 7, 10 or 13 players—, the system automatically assigns <strong>"Byes"</strong> in the first round. A "Bye" means a participant advances directly to the next phase without playing. Our algorithm calculates and assigns these passes fairly and automatically.',
142
+ },
143
+ {
144
+ type: 'title' as const,
145
+ text: 'Instant, Free and No Sign-up Required',
146
+ level: 2 as const,
147
+ },
148
+ {
149
+ type: 'paragraph' as const,
150
+ html: 'Zero friction. No accounts, no installations, no waiting. Add participants and generate your tournament instantly. Everything is automatically saved in your browser via <strong>LocalStorage</strong>: close the tab, shut down the computer and come back days later. Your tournament and full history will still be there.',
151
+ },
152
+ ];
153
+
154
+ const schemas: [WithContext<FAQPage>, WithContext<HowTo>, WithContext<SoftwareApplication>] = [
155
+ { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faqData.map((f) => ({ '@type': 'Question', name: f.question, acceptedAnswer: { '@type': 'Answer', text: f.answer } })) },
156
+ { '@context': 'https://schema.org', '@type': 'HowTo', name: title, description, step: howTo.map((s) => ({ '@type': 'HowToStep', name: s.name, text: s.text })) },
157
+ { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'SportsApplication', operatingSystem: 'Web', offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' } },
158
+ ];
159
+
160
+ export const content: ToolLocaleContent<TournamentBracketUI> = { slug, title, description, ui, seo, faqTitle: 'Frequently Asked Questions', faq: faqData, bibliography: [], howTo, schemas };
@@ -0,0 +1,178 @@
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { TournamentBracketUI } from '../ui';
4
+
5
+ const slug = 'torneo';
6
+ const title = 'Generador de Brackets y Torneos Online Gratis';
7
+ const description = 'Organiza torneos y crea brackets de eliminación directa gratis y sin registro. Ideal para FIFA, Pádel, eSports y juegos de mesa. 100% Mobile Friendly.';
8
+
9
+ const ui: TournamentBracketUI = {
10
+ tournamentInProgress: 'Torneo en Curso',
11
+ nextMatch: 'Siguiente Partido',
12
+ share: 'Compartir',
13
+ backNew: 'Volver / Nuevo',
14
+ back: 'Volver',
15
+ newTournament: 'Nuevo Torneo',
16
+ setupSubtitle: 'Configura y genera tu cuadro de competición.',
17
+ tournamentNameLabel: 'Nombre del Torneo',
18
+ tournamentNamePlaceholder: 'Ej. Torneo de Verano',
19
+ addPlayersLabel: 'Añadir Participantes',
20
+ addPlayerPlaceholder: 'Nombre... o varios separados por comas',
21
+ playersLabel: 'Jugadores',
22
+ clearAll: 'Borrar Todo',
23
+ emptyList: 'La lista está vacía',
24
+ howItWorks: '¿Cómo funciona?',
25
+ howItWorksText: 'Añade participantes, ponle nombre y genera. El sistema creará automáticamente los cruces y gestionará los "Byes" (pases directos) si son impares.',
26
+ historyLabel: 'Historial',
27
+ noHistory: 'No hay torneos guardados',
28
+ noOldTournaments: 'No hay torneos anteriores',
29
+ generateBtn: 'Generar Cuadro',
30
+ shuffleLabel: 'Aleatorizar emparejamientos',
31
+ scoreLabel: 'Activar Marcadores (Opcional)',
32
+ dragHint: 'Arrastra para mover',
33
+ roundFinal: 'Final',
34
+ roundSemifinal: 'Semifinales',
35
+ roundQuarter: 'Cuartos',
36
+ roundPrefix: 'Ronda',
37
+ byeLabel: 'Pase Directo',
38
+ waiting: 'Esperando...',
39
+ emptyRound: 'Ronda vacía',
40
+ confirmClearPlayers: '¿Borrar toda la lista de jugadores?',
41
+ alertMinPlayers: 'Necesitas al menos 2 jugadores.',
42
+ alertLoadFailed: 'No se pudo cargar el torneo.',
43
+ confirmDeleteTournament: '¿Borrar este torneo del historial permanentemente?',
44
+ toastShareLimit: 'Solo se pueden compartir torneos de hasta 32 jugadores',
45
+ toastShareError: 'Error al generar el enlace',
46
+ toastShareCopied: 'Enlace copiado al portapapeles',
47
+ toastShareFailed: 'No se pudo copiar. URL:',
48
+ toastFinished: '¡Torneo Finalizado!',
49
+ defaultName: 'Torneo',
50
+ };
51
+
52
+ const faqData = [
53
+ { question: '¿Cómo funciona la eliminación directa?', answer: 'Es un sistema de competición donde el participante que pierde un encuentro queda automáticamente eliminado del torneo. Los ganadores avanzan a la siguiente ronda (octavos, cuartos, semifinal) hasta que solo quedan dos para la gran final.' },
54
+ { question: '¿Qué pasa si tengo un número impar de jugadores?', answer: 'Nuestra herramienta gestiona automáticamente los "BYEs". Algunos jugadores pasarán directamente a la segunda ronda sin jugar en la primera para que el cuadro siempre acabe en potencias de dos (2, 4, 8, 16...).' },
55
+ { question: '¿Puedo guardar y compartir el cuadro del torneo?', answer: 'Sí, puedes compartir el bracket mediante un enlace único generado al instante. Al ser una herramienta sin registro, los datos se mantienen en tu navegador mientras la pestaña esté abierta.' },
56
+ { question: '¿Sirve para torneos de eSports como FIFA o LoL?', answer: 'Totalmente. Está diseñado para ser rápido y visual, ideal para gestionar torneos rápidos de consola, juegos de PC o incluso competiciones de juegos de mesa y cartas.' },
57
+ { question: '¿Es gratis crear torneos?', answer: 'Sí, completamente gratuito y sin restricciones. No hay planes premium, límites de participantes, marcas de agua, ni anuncios intrusivos. Todo funciona offline en tu navegador.' },
58
+ { question: '¿Se borran mis datos si cierro el navegador?', answer: 'No. Utilizamos LocalStorage para guardar todos tus torneos automáticamente en tu dispositivo. Puedes cerrar la pestaña, apagar el ordenador y volver días después: tu torneo seguirá ahí. El historial completo también se mantiene persistente.' },
59
+ { question: '¿Cómo funciona el botón "Siguiente Partido"?', answer: 'El sistema detecta automáticamente el próximo enfrentamiento que está listo para jugarse (ambos participantes confirmados) pero aún sin resultado. Al pulsar "Siguiente Partido", la vista salta directamente a ese match.' },
60
+ ];
61
+
62
+ const howTo = [
63
+ { name: 'Introducir participantes', text: 'Escribe los nombres de los jugadores o equipos que van a participar en la competición.' },
64
+ { name: 'Generar el cuadro', text: 'Pulsa el botón de generar. El sistema creará automáticamente los enfrentamientos y las rondas necesarias.' },
65
+ { name: 'Actualizar resultados', text: 'Haz clic en los participantes ganadores de cada partido para que avancen automáticamente a la siguiente fase del bracket.' },
66
+ { name: 'Descargar y finalizar', text: 'Una vez completado el torneo, descarga el resultado final para guardar el recuerdo de la victoria.' },
67
+ ];
68
+
69
+ const seo = [
70
+ {
71
+ type: 'title' as const,
72
+ text: 'Generador de Brackets y Organizador de Torneos Online',
73
+ level: 2 as const,
74
+ },
75
+ {
76
+ type: 'paragraph' as const,
77
+ html: 'Gestiona tus competiciones deportivas, de videojuegos o de mesa con el organizador de torneos más completo, gratuito y sin registro. Crea cuadros de eliminatoria directa (brackets) visuales e interactivos en segundos, con <strong>sistema de puntuación integrado</strong>, historial automático y navegación inteligente entre partidos. Todo funciona offline, directamente en tu navegador.',
78
+ },
79
+ {
80
+ type: 'title' as const,
81
+ text: '¿Cómo crear un Torneo de Eliminación Directa?',
82
+ level: 2 as const,
83
+ },
84
+ {
85
+ type: 'paragraph' as const,
86
+ html: 'Organizar una competición profesional nunca ha sido tan sencillo. <strong>Nombra tu torneo</strong>, añade participantes (uno a uno o pegando una lista separada por comas), aleatoriza los cruces si quieres, genera el bracket, gestiona los resultados tocando al ganador de cada encuentro y usa el botón "Siguiente Partido" para navegar entre partidos sin resolver.',
87
+ },
88
+ {
89
+ type: 'title' as const,
90
+ text: 'Funcionalidades Avanzadas',
91
+ level: 2 as const,
92
+ },
93
+ {
94
+ type: 'list' as const,
95
+ items: [
96
+ '<strong>Entrada masiva:</strong> Añade varios participantes a la vez separados por comas.',
97
+ '<strong>Marcadores exactos:</strong> Sistema de puntuación con scores tipo 3-1 ó 21-19.',
98
+ '<strong>Navegación inteligente:</strong> Botón "Siguiente Partido" que salta al próximo encuentro pendiente.',
99
+ '<strong>Bracket arrastrable:</strong> Vista de escritorio con scroll libre para torneos grandes.',
100
+ '<strong>Historial persistente:</strong> Todos los torneos se guardan en tu navegador automáticamente.',
101
+ '<strong>Walkovers automáticos:</strong> Resolución de pases directos y Byes sin intervención manual.',
102
+ '<strong>Compartir por URL:</strong> Genera un enlace comprimido para enviar el bracket a cualquiera.',
103
+ ],
104
+ },
105
+ {
106
+ type: 'title' as const,
107
+ text: 'Ideal para cualquier tipo de competición',
108
+ level: 2 as const,
109
+ },
110
+ {
111
+ type: 'comparative' as const,
112
+ columns: 3 as const,
113
+ items: [
114
+ {
115
+ title: 'Videojuegos y eSports',
116
+ description: 'Perfecto para FIFA, FC25, Valorant, League of Legends, Street Fighter, Tekken, Super Smash Bros o Rocket League.',
117
+ icon: 'mdi:controller-classic',
118
+ points: ['Partidas rápidas', 'Sin límite de equipos', 'Compartible al instante'],
119
+ },
120
+ {
121
+ title: 'Deportes y Pistas',
122
+ description: 'Gestiona cuadros de Pádel, Tenis, Ping Pong, Bádminton, Fútbol Sala o Baloncesto 3x3.',
123
+ icon: 'mdi:trophy-outline',
124
+ points: ['Marcadores integrados', 'Vista móvil optimizada', 'Sin papel ni pizarra'],
125
+ },
126
+ {
127
+ title: 'Juegos de Mesa y Cartas',
128
+ description: 'Organiza partidas de Magic: The Gathering, Pokémon TCG, Yu-Gi-Oh!, Catan, Ajedrez o Dominó.',
129
+ icon: 'mdi:cards-playing-outline',
130
+ points: ['Hasta 64 jugadores', 'Historial de rondas', 'Gestión de Byes'],
131
+ },
132
+ ],
133
+ },
134
+ {
135
+ type: 'title' as const,
136
+ text: '¿Qué son los "Byes" o Pases Directos?',
137
+ level: 2 as const,
138
+ },
139
+ {
140
+ type: 'paragraph' as const,
141
+ html: 'En un torneo de eliminación directa ideal, el número de participantes debe ser una potencia de 2 (4, 8, 16, 32...). Cuando tienes un número impar o no potencia de 2 —por ejemplo 7, 10 o 13 jugadores—, el sistema asigna automáticamente <strong>"Byes"</strong> en la primera ronda. Un "Bye" significa que un participante pasa directamente a la siguiente fase sin jugar. Nuestro algoritmo calcula y asigna estos pases de forma justa y automática.',
142
+ },
143
+ {
144
+ type: 'title' as const,
145
+ text: 'Instantáneo, Gratuito y Sin Registro',
146
+ level: 2 as const,
147
+ },
148
+ {
149
+ type: 'paragraph' as const,
150
+ html: 'Cero fricción. Sin cuentas, sin instalaciones, sin esperas. Añade participantes y genera tu torneo al instante. Todo se guarda automáticamente en tu navegador con <strong>LocalStorage</strong>: puedes cerrar la pestaña, apagar el ordenador y volver días después. Tu torneo y el historial completo seguirán ahí.',
151
+ },
152
+ ];
153
+
154
+ const schemas: [WithContext<FAQPage>, WithContext<HowTo>, WithContext<SoftwareApplication>] = [
155
+ {
156
+ '@context': 'https://schema.org',
157
+ '@type': 'FAQPage',
158
+ mainEntity: faqData.map((f) => ({ '@type': 'Question', name: f.question, acceptedAnswer: { '@type': 'Answer', text: f.answer } })),
159
+ },
160
+ {
161
+ '@context': 'https://schema.org',
162
+ '@type': 'HowTo',
163
+ name: title,
164
+ description,
165
+ step: howTo.map((s) => ({ '@type': 'HowToStep', name: s.name, text: s.text })),
166
+ },
167
+ {
168
+ '@context': 'https://schema.org',
169
+ '@type': 'SoftwareApplication',
170
+ name: title,
171
+ description,
172
+ applicationCategory: 'SportsApplication',
173
+ operatingSystem: 'Web',
174
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
175
+ },
176
+ ];
177
+
178
+ export const content: ToolLocaleContent<TournamentBracketUI> = { slug, title, description, ui, seo, faqTitle: 'Preguntas frecuentes', faq: faqData, bibliography: [], howTo, schemas };
@@ -0,0 +1,160 @@
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { TournamentBracketUI } from '../ui';
4
+
5
+ const slug = 'tournoi';
6
+ const title = 'Générateur de Brackets et Organisateur de Tournois Gratuit';
7
+ const description = 'Organisez des tournois et créez des brackets à élimination directe gratuitement et sans inscription. Idéal pour FIFA, Padel, eSports et jeux de société. 100% Mobile Friendly.';
8
+
9
+ const ui: TournamentBracketUI = {
10
+ tournamentInProgress: 'Tournoi en cours',
11
+ nextMatch: 'Match suivant',
12
+ share: 'Partager',
13
+ backNew: 'Retour / Nouveau',
14
+ back: 'Retour',
15
+ newTournament: 'Nouveau tournoi',
16
+ setupSubtitle: 'Configurez et générez votre tableau de compétition.',
17
+ tournamentNameLabel: 'Nom du tournoi',
18
+ tournamentNamePlaceholder: 'Ex. Tournoi d\'été',
19
+ addPlayersLabel: 'Ajouter des participants',
20
+ addPlayerPlaceholder: 'Nom... ou plusieurs séparés par des virgules',
21
+ playersLabel: 'Joueurs',
22
+ clearAll: 'Tout effacer',
23
+ emptyList: 'La liste est vide',
24
+ howItWorks: 'Comment ça marche ?',
25
+ howItWorksText: 'Ajoutez des participants, donnez un nom et générez. Le système créera automatiquement les confrontations et gérera les "Byes" (passages directs) si le nombre est impair.',
26
+ historyLabel: 'Historique',
27
+ noHistory: 'Aucun tournoi sauvegardé',
28
+ noOldTournaments: 'Aucun tournoi précédent',
29
+ generateBtn: 'Générer le tableau',
30
+ shuffleLabel: 'Mélanger les confrontations',
31
+ scoreLabel: 'Activer les scores (Optionnel)',
32
+ dragHint: 'Glisser pour déplacer',
33
+ roundFinal: 'Finale',
34
+ roundSemifinal: 'Demi-finales',
35
+ roundQuarter: 'Quarts de finale',
36
+ roundPrefix: 'Tour',
37
+ byeLabel: 'Passage direct',
38
+ waiting: 'En attente...',
39
+ emptyRound: 'Tour vide',
40
+ confirmClearPlayers: 'Effacer toute la liste des joueurs ?',
41
+ alertMinPlayers: 'Vous avez besoin d\'au moins 2 joueurs.',
42
+ alertLoadFailed: 'Impossible de charger le tournoi.',
43
+ confirmDeleteTournament: 'Supprimer définitivement ce tournoi de l\'historique ?',
44
+ toastShareLimit: 'Seuls les tournois de 32 joueurs maximum peuvent être partagés',
45
+ toastShareError: 'Erreur lors de la génération du lien',
46
+ toastShareCopied: 'Lien copié dans le presse-papiers',
47
+ toastShareFailed: 'Impossible de copier. URL :',
48
+ toastFinished: 'Tournoi terminé !',
49
+ defaultName: 'Tournoi',
50
+ };
51
+
52
+ const faqData = [
53
+ { question: 'Comment fonctionne l\'élimination directe ?', answer: 'C\'est un système de compétition où le participant qui perd un match est automatiquement éliminé du tournoi. Les gagnants avancent au tour suivant (huitièmes, quarts, demi-finales) jusqu\'à ce qu\'il n\'en reste que deux pour la grande finale.' },
54
+ { question: 'Que se passe-t-il si j\'ai un nombre impair de joueurs ?', answer: 'Notre outil gère automatiquement les "BYEs". Certains joueurs passeront directement au deuxième tour sans jouer au premier pour que le tableau se termine toujours en puissances de deux (2, 4, 8, 16...).' },
55
+ { question: 'Puis-je sauvegarder et partager le tableau du tournoi ?', answer: 'Oui, vous pouvez partager le bracket via un lien unique généré instantanément. En tant qu\'outil sans inscription, les données sont conservées dans votre navigateur tant que l\'onglet est ouvert.' },
56
+ { question: 'Ça marche pour des tournois eSports comme FIFA ou LoL ?', answer: 'Absolument. Il est conçu pour être rapide et visuel, idéal pour gérer des tournois rapides de console, jeux PC ou même des compétitions de jeux de société et de cartes.' },
57
+ { question: 'La création de tournois est-elle gratuite ?', answer: 'Oui, complètement gratuit et sans restrictions. Pas de plans premium, de limites de participants, de filigranes, ni de publicités intrusives. Tout fonctionne hors ligne dans votre navigateur.' },
58
+ { question: 'Mes données sont-elles supprimées si je ferme le navigateur ?', answer: 'Non. Nous utilisons le LocalStorage pour sauvegarder automatiquement tous vos tournois sur votre appareil. Vous pouvez fermer l\'onglet, éteindre l\'ordinateur et revenir des jours plus tard : votre tournoi sera toujours là.' },
59
+ { question: 'Comment fonctionne le bouton "Match suivant" ?', answer: 'Le système détecte automatiquement la prochaine confrontation prête à être jouée (les deux participants confirmés) mais sans résultat. En appuyant sur "Match suivant", la vue saute directement à ce match.' },
60
+ ];
61
+
62
+ const howTo = [
63
+ { name: 'Saisir les participants', text: 'Écrivez les noms des joueurs ou équipes qui vont participer à la compétition.' },
64
+ { name: 'Générer le tableau', text: 'Cliquez sur le bouton générer. Le système créera automatiquement les confrontations et les tours nécessaires.' },
65
+ { name: 'Mettre à jour les résultats', text: 'Cliquez sur le participant gagnant de chaque match pour qu\'il avance automatiquement à la phase suivante du bracket.' },
66
+ { name: 'Terminer', text: 'Une fois le tournoi terminé, le champion final est affiché.' },
67
+ ];
68
+
69
+ const seo = [
70
+ {
71
+ type: 'title' as const,
72
+ text: 'Générateur de Brackets et Organisateur de Tournois en Ligne',
73
+ level: 2 as const,
74
+ },
75
+ {
76
+ type: 'paragraph' as const,
77
+ html: 'Gérez vos compétitions sportives, de jeux vidéo ou de société avec l\'organisateur de tournois le plus complet, gratuit et sans inscription. Créez des tableaux d\'élimination directe visuels et interactifs en quelques secondes, avec un <strong>système de score intégré</strong>, un historique automatique et une navigation intelligente entre les matchs. Tout fonctionne hors ligne, directement dans votre navigateur.',
78
+ },
79
+ {
80
+ type: 'title' as const,
81
+ text: 'Comment créer un tournoi à élimination directe ?',
82
+ level: 2 as const,
83
+ },
84
+ {
85
+ type: 'paragraph' as const,
86
+ html: '<strong>Nommez votre tournoi</strong>, ajoutez des participants (un par un ou en collant une liste séparée par des virgules), mélangez si vous voulez, générez le tableau, gérez les résultats en appuyant sur le gagnant de chaque match et utilisez le bouton "Match suivant" pour naviguer entre les matchs non résolus.',
87
+ },
88
+ {
89
+ type: 'title' as const,
90
+ text: 'Fonctionnalités Avancées',
91
+ level: 2 as const,
92
+ },
93
+ {
94
+ type: 'list' as const,
95
+ items: [
96
+ '<strong>Saisie massive :</strong> Ajoutez plusieurs participants à la fois séparés par des virgules.',
97
+ '<strong>Scores exacts :</strong> Système de score avec résultats type 3-1 ou 21-19.',
98
+ '<strong>Navigation intelligente :</strong> Le bouton "Match suivant" saute au prochain match en attente.',
99
+ '<strong>Bracket déplaçable :</strong> Vue bureau avec défilement libre pour les grands tournois.',
100
+ '<strong>Historique persistant :</strong> Tous les tournois sauvegardés automatiquement dans votre navigateur.',
101
+ '<strong>Walkovers automatiques :</strong> Byes et passages directs résolus sans intervention manuelle.',
102
+ '<strong>Partage par URL :</strong> Générez un lien compressé pour envoyer le bracket à tout le monde.',
103
+ ],
104
+ },
105
+ {
106
+ type: 'title' as const,
107
+ text: 'Idéal pour tout type de compétition',
108
+ level: 2 as const,
109
+ },
110
+ {
111
+ type: 'comparative' as const,
112
+ columns: 3 as const,
113
+ items: [
114
+ {
115
+ title: 'Jeux vidéo & eSports',
116
+ description: 'Parfait pour FIFA, FC25, Valorant, League of Legends, Street Fighter, Tekken ou Rocket League.',
117
+ icon: 'mdi:controller-classic',
118
+ points: ['Matchs rapides', 'Sans limite d\'équipes', 'Partageable instantanément'],
119
+ },
120
+ {
121
+ title: 'Sports & Raquettes',
122
+ description: 'Gérez des tableaux de Padel, Tennis, Ping Pong, Badminton, Futsal ou Basket 3x3.',
123
+ icon: 'mdi:trophy-outline',
124
+ points: ['Scores intégrés', 'Vue mobile optimisée', 'Sans papier ni tableau'],
125
+ },
126
+ {
127
+ title: 'Jeux de société & Cartes',
128
+ description: 'Organisez des parties de Magic, Pokémon TCG, Yu-Gi-Oh!, Catan, Échecs ou Dominos.',
129
+ icon: 'mdi:cards-playing-outline',
130
+ points: ['Jusqu\'à 64 joueurs', 'Historique des tours', 'Gestion des Byes'],
131
+ },
132
+ ],
133
+ },
134
+ {
135
+ type: 'title' as const,
136
+ text: 'Que sont les "Byes" ou passages directs ?',
137
+ level: 2 as const,
138
+ },
139
+ {
140
+ type: 'paragraph' as const,
141
+ html: 'Dans un tournoi à élimination directe idéal, le nombre de participants doit être une puissance de 2 (4, 8, 16, 32...). Quand vous avez un nombre impair ou non puissance de 2 —par exemple 7, 10 ou 13 joueurs—, le système assigne automatiquement des <strong>"Byes"</strong> au premier tour. Un "Bye" signifie qu\'un participant passe directement à la phase suivante sans jouer. Notre algorithme calcule et assigne ces passages de façon juste et automatique.',
142
+ },
143
+ {
144
+ type: 'title' as const,
145
+ text: 'Instantané, Gratuit et Sans Inscription',
146
+ level: 2 as const,
147
+ },
148
+ {
149
+ type: 'paragraph' as const,
150
+ html: 'Zéro friction. Sans comptes, sans installations, sans attente. Ajoutez des participants et générez votre tournoi instantanément. Tout est sauvegardé automatiquement dans votre navigateur via <strong>LocalStorage</strong> : fermez l\'onglet, éteignez l\'ordinateur et revenez des jours plus tard. Votre tournoi et l\'historique complet seront toujours là.',
151
+ },
152
+ ];
153
+
154
+ const schemas: [WithContext<FAQPage>, WithContext<HowTo>, WithContext<SoftwareApplication>] = [
155
+ { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faqData.map((f) => ({ '@type': 'Question', name: f.question, acceptedAnswer: { '@type': 'Answer', text: f.answer } })) },
156
+ { '@context': 'https://schema.org', '@type': 'HowTo', name: title, description, step: howTo.map((s) => ({ '@type': 'HowToStep', name: s.name, text: s.text })) },
157
+ { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'SportsApplication', operatingSystem: 'Web', offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' } },
158
+ ];
159
+
160
+ export const content: ToolLocaleContent<TournamentBracketUI> = { slug, title, description, ui, seo, faqTitle: 'Questions fréquentes', faq: faqData, bibliography: [], howTo, schemas };
@@ -0,0 +1,34 @@
1
+ import type { SportsToolEntry, ToolLocaleContent, ToolDefinition } from '../../types';
2
+ import TournamentBracketComponent from './component.astro';
3
+ import TournamentBracketSEO from './seo.astro';
4
+ import TournamentBracketBibliography from './bibliography.astro';
5
+ import type { TournamentBracketUI } from './ui';
6
+
7
+ export type { TournamentBracketUI };
8
+ export type TournamentBracketLocaleContent = ToolLocaleContent<TournamentBracketUI>;
9
+
10
+ import { content as es } from './i18n/es';
11
+ import { content as en } from './i18n/en';
12
+ import { content as fr } from './i18n/fr';
13
+
14
+ export const tournamentBracket: SportsToolEntry<TournamentBracketUI> = {
15
+ id: 'tournament-bracket',
16
+ icons: {
17
+ bg: 'mdi:sitemap',
18
+ fg: 'mdi:trophy',
19
+ },
20
+ i18n: {
21
+ es: async () => es,
22
+ en: async () => en,
23
+ fr: async () => fr,
24
+ },
25
+ };
26
+
27
+ export { TournamentBracketComponent, TournamentBracketSEO, TournamentBracketBibliography };
28
+
29
+ export const TOURNAMENT_BRACKET_TOOL: ToolDefinition = {
30
+ entry: tournamentBracket,
31
+ Component: TournamentBracketComponent,
32
+ SEOComponent: TournamentBracketSEO,
33
+ BibliographyComponent: TournamentBracketBibliography,
34
+ };
@@ -0,0 +1,106 @@
1
+ import type { TournamentUIMediator } from '../ui/mediator';
2
+ import type { TournamentRenderer } from '../tournament.renderer';
3
+ import type { TournamentBracketUI } from '../ui';
4
+ import type { TournamentManager } from './manager';
5
+ import { TournamentStorage } from './storage';
6
+ import { TournamentNavigator } from '../ui/navigator';
7
+ import { TournamentSharing } from './sharing';
8
+
9
+ export class ActiveController {
10
+ private activeRoundMobile = 0;
11
+ private manager: TournamentManager | null = null;
12
+
13
+ constructor(
14
+ private mediator: TournamentUIMediator,
15
+ private renderer: TournamentRenderer,
16
+ private ui: TournamentBracketUI
17
+ ) {
18
+ this.bindEvents();
19
+ }
20
+
21
+ public setManager(manager: TournamentManager) { this.manager = manager; this.activeRoundMobile = 0; }
22
+
23
+ private bindEvents() {
24
+ this.mediator.btnNextMatch?.addEventListener('click', () => this.scrollToNextMatch());
25
+ this.mediator.enableTitleEditing((name) => {
26
+ if (this.manager && name !== this.manager.name) { this.manager.name = name; this.saveCurrentState(); }
27
+ this.render();
28
+ });
29
+ document.getElementById('share-tournament-btn')?.addEventListener('click', () => { void this.shareTournament(); });
30
+ }
31
+
32
+ public render() {
33
+ if (!this.manager) return;
34
+ this.mediator.updateHeader(this.manager.name, new Date(this.manager.createdAt).toLocaleDateString());
35
+ this.renderer.renderMobileView(this.manager, this.activeRoundMobile, (i) => { this.activeRoundMobile = i; this.render(); });
36
+ this.renderer.renderDesktopView(this.manager);
37
+ this.attachMatchListeners();
38
+ }
39
+
40
+ private attachMatchListeners() {
41
+ document.querySelectorAll('.tb-match-btn').forEach((btn) => {
42
+ btn.addEventListener('click', (e) => {
43
+ const t = e.currentTarget as HTMLElement;
44
+ if (!t.hasAttribute('disabled')) this.selectWinner(t.dataset.matchId!, t.dataset.winnerId!);
45
+ });
46
+ });
47
+ document.querySelectorAll('.tb-score-input').forEach((input) => {
48
+ input.addEventListener('change', (e) => {
49
+ const t = e.target as HTMLInputElement;
50
+ if (t.dataset.matchId && t.dataset.player) this.updateScore(t.dataset.matchId, t.dataset.player, t.value);
51
+ });
52
+ input.addEventListener('click', (e) => e.stopPropagation());
53
+ });
54
+ }
55
+
56
+ private parseScore(value: string): number | null {
57
+ if (value.trim() === '') return null;
58
+ const n = parseInt(value);
59
+ return isNaN(n) ? null : n;
60
+ }
61
+
62
+ private updateScore(matchId: string, playerNum: string, value: string) {
63
+ if (!this.manager) return;
64
+ const match = this.manager.findMatch(matchId);
65
+ if (!match) return;
66
+ const val = this.parseScore(value);
67
+ const s1 = playerNum === '1' ? val : (match.score1 ?? null);
68
+ const s2 = playerNum === '2' ? val : (match.score2 ?? null);
69
+ this.manager.setScore(matchId, s1, s2);
70
+ this.saveCurrentState();
71
+ this.render();
72
+ if (this.manager.status === 'FINISHED') this.mediator.showVictoryToast();
73
+ }
74
+
75
+ private selectWinner(matchId: string, winnerId: string) {
76
+ if (!this.manager) return;
77
+ this.manager.setWinner(matchId, winnerId);
78
+ this.saveCurrentState();
79
+ this.render();
80
+ if (this.manager.status === 'FINISHED') this.mediator.showVictoryToast();
81
+ }
82
+
83
+ private saveCurrentState() {
84
+ if (!this.manager) return;
85
+ TournamentStorage.saveTournament(this.manager, TournamentStorage.loadHistory());
86
+ }
87
+
88
+ private scrollToNextMatch() {
89
+ if (!this.manager) return;
90
+ const next = TournamentNavigator.findNextPlayableMatch(this.manager);
91
+ if (!next) { if (!TournamentNavigator.isTournamentUnfinished(this.manager)) this.mediator.showVictoryToast(); return; }
92
+ TournamentNavigator.scrollToMatch(next.id, this.manager, this.activeRoundMobile, {
93
+ onMobileRoundChange: (i) => { this.activeRoundMobile = i; this.render(); },
94
+ onShowToast: () => this.mediator.showVictoryToast(),
95
+ });
96
+ }
97
+
98
+ private async shareTournament() {
99
+ if (!this.manager) return;
100
+ if (!TournamentSharing.canShare(this.manager)) { this.mediator.showToast(this.ui.toastShareLimit, 'error'); return; }
101
+ const url = TournamentSharing.generateShareUrl(this.manager);
102
+ if (!url) { this.mediator.showToast(this.ui.toastShareError, 'error'); return; }
103
+ const ok = await TournamentSharing.copyToClipboard(url);
104
+ this.mediator.showToast(ok ? this.ui.toastShareCopied : `${this.ui.toastShareFailed} ${url}`, ok ? 'success' : 'error');
105
+ }
106
+ }