Skip to main content

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();
concurso
object | null
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
ganadores
array
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();
concursos
array
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
FormData
required
FormData object containing contest fields

FormData Fields

title
string
required
Main contest title
titleHighlight
string
Highlighted portion of title (optional)
description
string
required
Contest description
image
File
Contest image file (uploaded to Cloudinary folder: provesa/concursos)
badgeText
string
Badge text (default: “Sorteo Activo”)
closeDate
string
Contest close date
prizeName
string
Prize description
ctaText
string
Call-to-action text (default: “Ver Marcas Auspiciantes”)
disclaimer
string
Legal disclaimer text
isActive
string
“true” or “false” - active status
concurso
object
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);
id
number
required
Contest ID to update
formData
FormData
required
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);
id
number
required
Contest ID to delete
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();
ganadores
array
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
FormData
required
FormData object containing winner fields

FormData Fields

concursoId
string
required
Contest ID (parsed to integer, nullable)
winnerName
string
required
Winner’s name
prize
string
required
Prize won
testimonial
string
Winner testimonial or quote
image
File
Winner photo (uploaded to Cloudinary folder: provesa/concursos/ganadores)
dateLabel
string
Date label (e.g., “Marzo 2024”)
ganador
object
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);
id
number
required
Winner ID to update
formData
FormData
required
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);
id
number
required
Winner ID to delete

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' });
    }
  }
};

Admin Form Component

<!-- 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);

Build docs developers (and LLMs) love