Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Sorteo Simple es una aplicación web estática para elegir ganadores de forma al
- Permite volver a sortear manteniendo la misma base de participantes.
- Puede excluir ganadores previos para rondas sucesivas.
- Guarda el estado en `localStorage`.
- Guarda el idioma seleccionado en `localStorage`.
- Permite alternar la interfaz entre español, inglés y portugués.
- Copia el resultado al portapapeles.
- Exporta el resultado a un archivo `.txt`.
- Muestra una revelación animada de ganadores.
Expand All @@ -23,7 +25,7 @@ Sorteo Simple es una aplicación web estática para elegir ganadores de forma al
- `index.html`: interfaz principal.
- `styles.css`: sistema visual responsive.
- `raffle-core.js`: lógica pura del sorteo y validaciones reutilizables.
- `scripts.js`: integración con DOM, persistencia y acciones de la UI.
- `scripts.js`: integración con DOM, persistencia, traducciones y acciones de la UI.
- `favicon.svg`: marca visual y favicon del sitio.
- `site.webmanifest`: metadatos básicos de identidad del sitio.
- `tests/raffle-core.test.js`: pruebas básicas del módulo puro.
Expand All @@ -32,9 +34,10 @@ Sorteo Simple es una aplicación web estática para elegir ganadores de forma al
## Cómo usarlo

1. Abre `index.html` en tu navegador.
2. Escribe participantes, uno por línea.
3. Ajusta la cantidad de premios y, si quieres, las opciones avanzadas.
4. Ejecuta el sorteo, vuelve a sortear, copia o exporta el resultado.
2. Elige el idioma desde el selector `ES / EN / PT`.
3. Escribe participantes, uno por línea.
4. Ajusta la cantidad de premios y, si quieres, las opciones avanzadas.
5. Ejecuta el sorteo, vuelve a sortear, copia o exporta el resultado.

## Verificación rápida

Expand All @@ -50,3 +53,4 @@ Sorteo Simple es una aplicación web estática para elegir ganadores de forma al
- Historial acumulado de ganadores excluidos.
- Separación entre lógica pura y código de interfaz.
- Favicon y branding propio.
- Soporte de interfaz multilenguaje.
45 changes: 26 additions & 19 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,34 @@
<div class="page-shell">
<main class="app">
<section class="hero">
<p class="eyebrow">Sorteos sin vueltas</p>
<div class="hero-topbar">
<p id="heroEyebrow" class="eyebrow">Sorteos sin vueltas</p>
<div class="language-switcher" aria-label="Language selector">
<button type="button" class="lang-button is-active" data-lang="es">ES</button>
<button type="button" class="lang-button" data-lang="en">EN</button>
<button type="button" class="lang-button" data-lang="pt">PT</button>
</div>
</div>
<div class="hero-title">
<img src="favicon.svg" alt="" class="hero-mark">
<h1>Sorteo Simple</h1>
</div>
<p class="hero-copy">
<p id="heroCopy" class="hero-copy">
Cargá participantes, definí premios y obtené ganadores al instante con una herramienta simple y clara.
</p>
</section>

<section class="workspace" aria-label="Herramienta de sorteo">
<article class="panel panel-form">
<div class="panel-heading">
<h2>Configurar sorteo</h2>
<p>Prepará la lista, ajustá opciones y dejá todo listo para sortear.</p>
<h2 id="formHeading">Configurar sorteo</h2>
<p id="formSubheading">Prepará la lista, ajustá opciones y dejá todo listo para sortear.</p>
</div>

<form id="raffleForm" novalidate>
<div class="panel-top">
<div class="field-group compact">
<label for="cantPremios">Cantidad de premios</label>
<label id="prizeLabel" for="cantPremios">Cantidad de premios</label>
<input type="number" id="cantPremios" name="cantPremios" value="1" min="1" max="100"
inputmode="numeric" autocomplete="off" aria-describedby="prizeHelp prizeError"
required>
Expand All @@ -51,7 +58,7 @@ <h2>Configurar sorteo</h2>

<div class="field-group field-group-fill">
<div class="field-header">
<label for="formControlTextArea1">Participantes</label>
<label id="participantsLabel" for="formControlTextArea1">Participantes</label>
<span id="participantsCounter" class="counter">0 participantes</span>
</div>
<textarea class="form-textarea" id="formControlTextArea1" name="participantes" rows="12"
Expand All @@ -68,15 +75,15 @@ <h2>Configurar sorteo</h2>
</div>

<details class="options-card">
<summary>Opciones avanzadas</summary>
<summary id="advancedOptionsSummary">Opciones avanzadas</summary>
<div class="options-content">
<label class="toggle">
<input type="checkbox" id="excludePreviousWinners">
<span>Excluir ganadores previos al volver a sortear</span>
<span id="excludePreviousLabel">Excluir ganadores previos al volver a sortear</span>
</label>
<label class="toggle">
<input type="checkbox" id="autoRemoveDuplicates">
<span>Quitar repetidos automáticamente al sortear</span>
<span id="autoRemoveDuplicatesLabel">Quitar repetidos automáticamente al sortear</span>
</label>
</div>
</details>
Expand All @@ -91,39 +98,39 @@ <h2>Configurar sorteo</h2>

<article class="panel panel-result">
<div class="panel-heading">
<h2>Resultado</h2>
<p>Visualizá los ganadores, copiá el texto o exportalo para compartirlo.</p>
<h2 id="resultHeading">Resultado</h2>
<p id="resultSubheading">Visualizá los ganadores, copiá el texto o exportalo para compartirlo.</p>
</div>

<div class="panel-top">
<div class="result-top">
<div class="result-summary" aria-live="polite">
<div>
<span class="summary-label">Premios</span>
<span id="summaryPrizesLabel" class="summary-label">Premios</span>
<strong id="summaryPrizes">1</strong>
</div>
<div>
<span class="summary-label">Participantes válidos</span>
<span id="summaryParticipantsLabel" class="summary-label">Participantes válidos</span>
<strong id="summaryParticipants">0</strong>
</div>
</div>

<p class="result-meta" aria-live="polite">
<span><strong id="summaryDuplicateParticipants">0</strong> repetidos</span>
<span><strong id="summaryDuplicateParticipants">0</strong> <span id="summaryDuplicatesLabel">repetidos</span></span>
</p>
</div>
</div>

<div class="field-group field-group-fill">
<div class="field-header">
<label for="formControlTextArea2">Ganadores</label>
<label id="winnersLabel" for="formControlTextArea2">Ganadores</label>
<span id="resultBadge" class="counter counter-neutral">Sin sorteo</span>
</div>
<div id="winnerReveal" class="winner-reveal" aria-live="polite"></div>
<div id="resultStage" class="result-stage">
<div id="emptyState" class="empty-state">
<strong>Todo listo para empezar</strong>
<p>Cuando ejecutes el sorteo, vas a ver aquí los ganadores numerados y vas a poder copiarlos o exportarlos.</p>
<strong id="emptyStateTitle">Todo listo para empezar</strong>
<p id="emptyStateText">Cuando ejecutes el sorteo, vas a ver aquí los ganadores numerados y vas a poder copiarlos o exportarlos.</p>
</div>
<textarea class="form-textarea result-area" id="formControlTextArea2" name="resultado"
rows="12" placeholder="Todavía no hay un sorteo realizado" readonly
Expand All @@ -133,7 +140,7 @@ <h2>Resultado</h2>
</div>

<div class="history-card" aria-live="polite">
<span class="summary-label">Ganadores acumulados excluidos</span>
<span id="historyLabel" class="summary-label">Ganadores acumulados excluidos</span>
<p id="historyText">Todavía no se excluyeron ganadores previos.</p>
</div>

Expand All @@ -147,7 +154,7 @@ <h2>Resultado</h2>
</main>

<footer class="site-footer">
<p>Creado por E.G.</p>
<p id="footerText">Creado por E.G.</p>
<div class="footer-links">
<a href="https://www.linkedin.com/in/emanuel-guzzetti/" target="_blank" rel="noopener noreferrer"
aria-label="Perfil de LinkedIn">
Expand Down
9 changes: 5 additions & 4 deletions raffle-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@
const uniqueParticipants = dedupeParticipants(participants);

if (participants.length === 0) {
return { valid: false, message: "Ingresa al menos un participante para realizar el sorteo." };
return { valid: false, code: "no_participants", message: "Ingresa al menos un participante para realizar el sorteo." };
}

if (prizeCount < MIN_PRIZES || prizeCount > MAX_PRIZES) {
return { valid: false, message: "La cantidad de premios debe estar entre 1 y 100." };
return { valid: false, code: "invalid_prize_count", message: "La cantidad de premios debe estar entre 1 y 100." };
}

const eligibleParticipants = buildEligibleParticipants(
Expand All @@ -70,17 +70,18 @@
);

if (eligibleParticipants.length === 0) {
return { valid: false, message: "No quedan participantes disponibles con las opciones actuales." };
return { valid: false, code: "no_eligible_participants", message: "No quedan participantes disponibles con las opciones actuales." };
}

if (prizeCount > eligibleParticipants.length) {
return {
valid: false,
code: "too_many_prizes",
message: "La cantidad de premios no puede superar a los participantes disponibles.",
};
}

return { valid: true, message: "" };
return { valid: true, code: "", message: "" };
}

function pickWinners(participants, prizeCount, excludedParticipants) {
Expand Down
Loading
Loading