Documentation Index
Fetch the complete documentation index at: https://mintlify.com/ashcroft08/provesa-web/llms.txt
Use this file to discover all available pages before exploring further.
The Concursos Service manages promotional contests and sweepstakes, including contest details, winner records, and image uploads to Cloudinary.
Overview
This service provides comprehensive contest management with two main entities: Concursos (contests) and Ganadores (winners). It handles public-facing data, admin operations, and automatic image uploads to Cloudinary.
Dependencies
concursosRepository - Database operations for contests and winners
uploadRepository - Cloudinary image upload operations
Public Methods
getPublicData()
Retrieves the active contest with its winners for public display.
import { concursosService } from '$lib/server/services/concursos.service.js';
const { concurso, ganadores } = await concursosService.getPublicData();
Active contest object or null if none active:
id: Contest identifier
title: Main title text
titleHighlight: Highlighted portion of title
description: Contest description
imageUrl: Cloudinary URL to contest image
badgeText: Badge label (e.g., “Sorteo Activo”)
closeDate: Contest close date string
prizeName: Prize description
ctaText: Call-to-action text
disclaimer: Legal disclaimer text
isActive: Boolean indicating active status
sortOrder: Display order number
Array of winner objects for the active contest:
id: Winner identifier
concursoId: Associated contest ID
winnerName: Winner’s name
prize: Prize won
testimonial: Winner testimonial
imageUrl: Cloudinary URL to winner photo
dateLabel: Date label (e.g., “Marzo 2024”)
sortOrder: Display order
Example Response
{
concurso: {
id: 1,
title: 'Gran Sorteo',
titleHighlight: 'PROVESA 2024',
description: 'Participa y gana increíbles premios',
imageUrl: 'https://res.cloudinary.com/provesa/image/upload/v123/provesa/concursos/sorteo.jpg',
badgeText: 'Sorteo Activo',
closeDate: '31 de Diciembre 2024',
prizeName: 'Camioneta 0KM',
ctaText: 'Ver Marcas Auspiciantes',
disclaimer: 'Válido hasta agotar stock. Aplican términos y condiciones.',
isActive: true,
sortOrder: 0
},
ganadores: [
{
id: 1,
concursoId: 1,
winnerName: 'Juan Pérez',
prize: 'Camioneta 0KM',
testimonial: '¡No puedo creer que gané! Gracias PROVESA',
imageUrl: 'https://res.cloudinary.com/provesa/image/upload/v123/provesa/concursos/ganadores/juan.jpg',
dateLabel: 'Diciembre 2023',
sortOrder: 0
}
]
}
Implementation Details
Source: src/lib/server/services/concursos.service.js:8-16
Returns empty array for ganadores if no active contest exists.
Contest Management (Admin)
getAllConcursos()
Retrieves all contests ordered by sortOrder.
import { concursosService } from '$lib/server/services/concursos.service.js';
const concursos = await concursosService.getAllConcursos();
Array of all contest objects, ordered by sortOrder ascending
Implementation Details
Source: src/lib/server/services/concursos.service.js:20-22
addConcurso()
Creates a new contest with optional image upload.
import { concursosService } from '$lib/server/services/concursos.service.js';
// From SvelteKit form action
const formData = await request.formData();
const concurso = await concursosService.addConcurso(formData);
FormData object containing contest fields
Highlighted portion of title (optional)
Contest image file (uploaded to Cloudinary folder: provesa/concursos)
Badge text (default: “Sorteo Activo”)
Call-to-action text (default: “Ver Marcas Auspiciantes”)
“true” or “false” - active status
Newly created contest object with auto-generated sortOrder
Sort Order Calculation
The service automatically calculates sortOrder as the maximum existing sortOrder + 1:
const existing = await concursosRepository.getAll();
const sortOrder = existing.length > 0
? Math.max(...existing.map(c => c.sortOrder)) + 1
: 0;
Implementation Details
Source: src/lib/server/services/concursos.service.js:29-59
updateConcurso()
Updates an existing contest with optional new image.
import { concursosService } from '$lib/server/services/concursos.service.js';
const formData = await request.formData();
await concursosService.updateConcurso(1, formData);
FormData with fields to update (same as addConcurso)
If a new image file is provided, it uploads to Cloudinary and updates imageUrl. Otherwise, existing image URL is preserved.
Implementation Details
Source: src/lib/server/services/concursos.service.js:65-88
deleteConcurso()
Deletes a contest by ID.
import { concursosService } from '$lib/server/services/concursos.service.js';
await concursosService.deleteConcurso(1);
This does NOT delete associated winners or images from Cloudinary.
Implementation Details
Source: src/lib/server/services/concursos.service.js:91-93
Winner Management (Admin)
getAllGanadores()
Retrieves all winners across all contests.
import { concursosService } from '$lib/server/services/concursos.service.js';
const ganadores = await concursosService.getAllGanadores();
Array of all winner objects, ordered by sortOrder ascending
Implementation Details
Source: src/lib/server/services/concursos.service.js:24-26
addGanador()
Creates a new winner record with optional photo upload.
import { concursosService } from '$lib/server/services/concursos.service.js';
const formData = await request.formData();
const ganador = await concursosService.addGanador(formData);
FormData object containing winner fields
Contest ID (parsed to integer, nullable)
Winner testimonial or quote
Winner photo (uploaded to Cloudinary folder: provesa/concursos/ganadores)
Date label (e.g., “Marzo 2024”)
Newly created winner object with auto-generated sortOrder
Implementation Details
Source: src/lib/server/services/concursos.service.js:98-124
updateGanador()
Updates an existing winner record.
import { concursosService } from '$lib/server/services/concursos.service.js';
const formData = await request.formData();
await concursosService.updateGanador(1, formData);
FormData with fields to update (same as addGanador)
Implementation Details
Source: src/lib/server/services/concursos.service.js:130-149
deleteGanador()
Deletes a winner record by ID.
import { concursosService } from '$lib/server/services/concursos.service.js';
await concursosService.deleteGanador(1);
Implementation Details
Source: src/lib/server/services/concursos.service.js:152-154
Usage Examples
Public Contest Page
// src/routes/concursos/+page.server.js
import { concursosService } from '$lib/server/services/concursos.service.js';
export async function load() {
const { concurso, ganadores } = await concursosService.getPublicData();
return { concurso, ganadores };
}
<!-- src/routes/concursos/+page.svelte -->
<script>
export let data;
</script>
{#if data.concurso}
<section class="concurso-hero">
<div class="badge">{data.concurso.badgeText}</div>
<h1>
{data.concurso.title}
<span class="highlight">{data.concurso.titleHighlight}</span>
</h1>
<p>{data.concurso.description}</p>
{#if data.concurso.imageUrl}
<img src="{data.concurso.imageUrl}" alt="{data.concurso.title}" />
{/if}
<div class="prize">
<h2>Premio: {data.concurso.prizeName}</h2>
<p>Cierre: {data.concurso.closeDate}</p>
</div>
<button>{data.concurso.ctaText}</button>
<p class="disclaimer">{data.concurso.disclaimer}</p>
</section>
{#if data.ganadores.length > 0}
<section class="ganadores">
<h2>Ganadores Anteriores</h2>
<div class="ganadores-grid">
{#each data.ganadores as ganador}
<div class="ganador-card">
{#if ganador.imageUrl}
<img src="{ganador.imageUrl}" alt="{ganador.winnerName}" />
{/if}
<h3>{ganador.winnerName}</h3>
<p class="prize">{ganador.prize}</p>
<p class="date">{ganador.dateLabel}</p>
{#if ganador.testimonial}
<blockquote>{ganador.testimonial}</blockquote>
{/if}
</div>
{/each}
</div>
</section>
{/if}
{:else}
<p>No hay concursos activos en este momento.</p>
{/if}
Admin Contest Management
// src/routes/admin/concursos/+page.server.js
import { concursosService } from '$lib/server/services/concursos.service.js';
import { fail } from '@sveltejs/kit';
export async function load() {
const [concursos, ganadores] = await Promise.all([
concursosService.getAllConcursos(),
concursosService.getAllGanadores()
]);
return { concursos, ganadores };
}
export const actions = {
addConcurso: async ({ request }) => {
const formData = await request.formData();
try {
await concursosService.addConcurso(formData);
return { success: true };
} catch (error) {
console.error('Error creating contest:', error);
return fail(500, { error: 'Error al crear concurso' });
}
},
updateConcurso: async ({ request }) => {
const formData = await request.formData();
const id = parseInt(formData.get('id'));
try {
await concursosService.updateConcurso(id, formData);
return { success: true };
} catch (error) {
console.error('Error updating contest:', error);
return fail(500, { error: 'Error al actualizar concurso' });
}
},
deleteConcurso: async ({ request }) => {
const formData = await request.formData();
const id = parseInt(formData.get('id'));
try {
await concursosService.deleteConcurso(id);
return { success: true };
} catch (error) {
console.error('Error deleting contest:', error);
return fail(500, { error: 'Error al eliminar concurso' });
}
},
addGanador: async ({ request }) => {
const formData = await request.formData();
try {
await concursosService.addGanador(formData);
return { success: true };
} catch (error) {
console.error('Error creating winner:', error);
return fail(500, { error: 'Error al crear ganador' });
}
},
deleteGanador: async ({ request }) => {
const formData = await request.formData();
const id = parseInt(formData.get('id'));
try {
await concursosService.deleteGanador(id);
return { success: true };
} catch (error) {
console.error('Error deleting winner:', error);
return fail(500, { error: 'Error al eliminar ganador' });
}
}
};
<!-- AdminConcursoForm.svelte -->
<script>
import { enhance } from '$app/forms';
export let concurso = null; // null for new, object for edit
let uploading = false;
</script>
<form
method="POST"
action="?/{concurso ? 'updateConcurso' : 'addConcurso'}"
enctype="multipart/form-data"
use:enhance={() => {
uploading = true;
return async ({ update }) => {
await update();
uploading = false;
};
}}
>
{#if concurso}
<input type="hidden" name="id" value="{concurso.id}" />
{/if}
<div>
<label for="title">Título Principal *</label>
<input
type="text"
id="title"
name="title"
value="{concurso?.title || ''}"
required
/>
</div>
<div>
<label for="titleHighlight">Título Destacado</label>
<input
type="text"
id="titleHighlight"
name="titleHighlight"
value="{concurso?.titleHighlight || ''}"
/>
</div>
<div>
<label for="description">Descripción *</label>
<textarea
id="description"
name="description"
rows="4"
required
>{concurso?.description || ''}</textarea>
</div>
<div>
<label for="image">Imagen del Concurso</label>
<input
type="file"
id="image"
name="image"
accept="image/*"
/>
{#if concurso?.imageUrl}
<img src="{concurso.imageUrl}" alt="Preview" class="preview" />
{/if}
</div>
<div>
<label for="badgeText">Texto del Badge</label>
<input
type="text"
id="badgeText"
name="badgeText"
value="{concurso?.badgeText || 'Sorteo Activo'}"
/>
</div>
<div>
<label for="prizeName">Premio *</label>
<input
type="text"
id="prizeName"
name="prizeName"
value="{concurso?.prizeName || ''}"
required
/>
</div>
<div>
<label for="closeDate">Fecha de Cierre</label>
<input
type="text"
id="closeDate"
name="closeDate"
value="{concurso?.closeDate || ''}"
placeholder="31 de Diciembre 2024"
/>
</div>
<div>
<label for="ctaText">Texto del Botón</label>
<input
type="text"
id="ctaText"
name="ctaText"
value="{concurso?.ctaText || 'Ver Marcas Auspiciantes'}"
/>
</div>
<div>
<label for="disclaimer">Disclaimer Legal</label>
<textarea
id="disclaimer"
name="disclaimer"
rows="2"
>{concurso?.disclaimer || ''}</textarea>
</div>
<div class="checkbox">
<input
type="checkbox"
id="isActive"
name="isActive"
value="true"
checked="{concurso?.isActive || false}"
/>
<label for="isActive">Concurso Activo</label>
</div>
<button type="submit" disabled={uploading}>
{uploading ? 'Guardando...' : concurso ? 'Actualizar' : 'Crear'}
</button>
</form>
Data Structures
Concurso Object
interface Concurso {
id: number;
title: string;
titleHighlight: string;
description: string;
imageUrl: string;
badgeText: string;
closeDate: string;
prizeName: string;
ctaText: string;
disclaimer: string;
isActive: boolean;
sortOrder: number;
}
Ganador Object
interface Ganador {
id: number;
concursoId: number | null;
winnerName: string;
prize: string;
testimonial: string;
imageUrl: string;
dateLabel: string;
sortOrder: number;
}
Best Practices
Only one contest should be active (isActive: true) at a time for clear user experience.
Validate image files before upload. Recommended max size: 2MB, formats: JPG, PNG, WebP.
Use descriptive sortOrder values to control display order in admin interfaces.
The getPublicData() method returns the first active contest. Ensure only one contest is marked active.
Store Cloudinary public_id with records to enable easier image deletion and management.
Contest Activation Helper
import { concursosService } from '$lib/server/services/concursos.service.js';
/**
* Activates a contest and deactivates all others
*/
async function activateConcurso(id) {
const concursos = await concursosService.getAllConcursos();
// Deactivate all
for (const concurso of concursos) {
if (concurso.isActive) {
const formData = new FormData();
formData.set('isActive', 'false');
// Copy other fields
Object.keys(concurso).forEach(key => {
if (key !== 'isActive' && key !== 'id') {
formData.set(key, concurso[key]);
}
});
await concursosService.updateConcurso(concurso.id, formData);
}
}
// Activate target
const target = concursos.find(c => c.id === id);
if (target) {
const formData = new FormData();
formData.set('isActive', 'true');
Object.keys(target).forEach(key => {
if (key !== 'isActive' && key !== 'id') {
formData.set(key, target[key]);
}
});
await concursosService.updateConcurso(id, formData);
}
}
Database Schema
Concursos Table
CREATE TABLE concursos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
title_highlight TEXT,
description TEXT NOT NULL,
image_url TEXT,
badge_text TEXT DEFAULT 'Sorteo Activo',
close_date TEXT,
prize_name TEXT,
cta_text TEXT DEFAULT 'Ver Marcas Auspiciantes',
disclaimer TEXT,
is_active BOOLEAN DEFAULT FALSE,
sort_order INTEGER DEFAULT 0
);
CREATE INDEX idx_concursos_active ON concursos(is_active);
CREATE INDEX idx_concursos_sort ON concursos(sort_order);
Concursos Ganadores Table
CREATE TABLE concursos_ganadores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
concurso_id INTEGER,
winner_name TEXT NOT NULL,
prize TEXT NOT NULL,
testimonial TEXT,
image_url TEXT,
date_label TEXT,
sort_order INTEGER DEFAULT 0,
FOREIGN KEY (concurso_id) REFERENCES concursos(id) ON DELETE SET NULL
);
CREATE INDEX idx_ganadores_concurso ON concursos_ganadores(concurso_id);
CREATE INDEX idx_ganadores_sort ON concursos_ganadores(sort_order);