This commit is contained in:
jose-rZM
2025-12-15 11:42:17 +01:00
commit c3f7af7379
33 changed files with 5004 additions and 0 deletions

276
frontend/src/App.svelte Normal file
View File

@@ -0,0 +1,276 @@
<script>
import { onMount } from 'svelte';
import { API_BASE, ENABLE_POLLING } from './config';
import { getCiStatus, getMenu, getPrices } from './services/api';
import { prettify } from './utils/text';
let menu = null;
let prices = [];
let loadingMenu = true;
let loadingPrices = true;
let errorMessage = '';
let ciStatus = null;
let loadingCiStatus = true;
async function fetchMenu() {
loadingMenu = true;
try {
menu = await getMenu();
} catch (error) {
errorMessage = `Hoy no cafete (good ending): ${error.message}`;
} finally {
loadingMenu = false;
}
}
async function fetchPrices() {
loadingPrices = true;
try {
prices = await getPrices();
} catch (error) {
errorMessage = `No pudimos cargar los precios: ${error.message}`;
} finally {
loadingPrices = false;
}
}
async function fetchCiStatus() {
loadingCiStatus = true;
try {
ciStatus = await getCiStatus();
} catch (error) {
errorMessage = `No se pudo obtener el estado del sistema: ${error.message}`;
} finally {
loadingCiStatus = false;
}
}
async function loadInitialData() {
await Promise.all([fetchMenu(), fetchPrices(), fetchCiStatus()]);
}
onMount(() => {
loadInitialData();
if (!ENABLE_POLLING) return;
const pricesInterval = setInterval(fetchPrices, 8000);
const ciInterval = setInterval(fetchCiStatus, 10000);
return () => {
clearInterval(pricesInterval);
clearInterval(ciInterval);
};
});
</script>
<main>
<section class="hero">
<div class="hero-copy">
<p class="eyebrow">Taller CI/CD con Jenkins</p>
<h1>UwU</h1>
<p class="lede">
FastAPI + Svelte para pipelines CI/CD. No se nos ha ocurrido nada mejor para el
taller así que hemos hecho un proyectito basado en una cafetería que para nada nada
está inspirada en la de nuestra querida escuela.
</p>
<div class="actions">
<button on:click={() => fetchMenu()} class="ghost">Refrescar menú</button>
<button on:click={() => fetchPrices()}>Recalcular desayunos</button>
</div>
<p class="meta">
Backend: {API_BASE} · Endpoints: /menu · /prices · /prices/:item · /health
</p>
</div>
<div class="price-hero">
<p class="pill">Convenio universitario</p>
{#if menu}
<div class="price-stack">
<span class="old">{menu.university_deal.old_price.toFixed(2)}</span>
<span class="new">{menu.university_deal.current_price.toFixed(2)}</span>
</div>
<p class="tiny">{menu.university_deal.note}</p>
{:else}
<p class="tiny">Esperando el menú...</p>
{/if}
</div>
</section>
{#if errorMessage}
<div class="banner error">
<p>{errorMessage}</p>
</div>
{/if}
<section class="cards">
<article class="card menu-card">
<div class="card-head">
<div>
<p class="label">Menú del día</p>
</div>
{#if loadingMenu}
<span class="tag">cargando...</span>
{/if}
</div>
{#if menu}
<div class={`menu-layout ${menu?.alternative?.items?.length ? 'with-alternative' : ''}`}>
<div class="menu-primary">
<div class="menu-grid">
<div>
<p class="section-title">Primeros</p>
<ul>
{#each menu.starters as starter}
<li>{starter}</li>
{/each}
</ul>
</div>
<div>
<p class="section-title">Segundos</p>
<ul>
{#each menu.mains as main}
<li class:fish={main.toLowerCase().includes('pescado') || main.toLowerCase().includes('pedro')}>{main}</li>
{/each}
</ul>
</div>
<div>
<p class="section-title">Guarnición</p>
<ul>
{#each menu.garnish as garnish}
<li>{garnish}</li>
{/each}
</ul>
</div>
<div>
<p class="section-title">Postres</p>
<ul>
{#each menu.desserts as dessert}
<li>{dessert}</li>
{/each}
</ul>
</div>
</div>
<div class="notes">
{#each menu.notes as note}
<span>{note}</span>
{/each}
</div>
</div>
{#if menu.alternative?.items?.length}
<aside class="menu-alternative">
<p class="label alt-label">{menu.alternative.title}</p>
<p class="alt-price">
<span class="alt-price-value"
>{menu.alternative.price?.toFixed(2) ?? '5.00'}</span
>
<span class="alt-price-meta">Pues por si no quieres el menú</span>
</p>
<ul class="alt-items">
{#each menu.alternative.items as alternativeItem}
<li>{alternativeItem}</li>
{/each}
</ul>
<span class="chip subtle">Mira si te estás planteando esto, mejor quédate sin comer</span>
</aside>
{/if}
</div>
<div class="deal">
<div>
<p class="caption">Menú convenio</p>
<div class="price-line">
<span class="old">{menu.university_deal.old_price.toFixed(2)}</span>
<span class="new"
>{menu.university_deal.current_price.toFixed(2)}</span
>
</div>
<small class="tiny">{menu.university_deal.note}</small>
</div>
<div class="cta">
<span class="chip">El espeto consejo del día: {menu.espetos_tip}</span>
<span class="chip subtle">
Última actualización {menu.availability.last_updated}
</span>
</div>
</div>
{:else if !loadingMenu}
<p>No pudimos leer el menú.</p>
{/if}
</article>
<article class="card ci-card">
<div class="card-head">
<div>
<p class="label">Estado del sistema</p>
<p class="sub">Información de build y backend</p>
</div>
{#if loadingCiStatus}
<span class="tag">comprobando...</span>
{/if}
</div>
{#if ciStatus}
<div class="ci-grid">
<div class="ci-item">
<p class="caption">API</p>
<p class="highlight">
{ciStatus.status === 'ok' ? '🟢 Operativa' : '🔴 Caída'}
</p>
</div>
<div class="ci-item">
<p class="caption">Build</p>
<p class="highlight">
#{ciStatus.build}
</p>
</div>
<div class="ci-item">
<p class="caption">Commit</p>
<p class="highlight mono">
{ciStatus.commit?.slice(0, 7)}
</p>
</div>
<div class="ci-item">
<p class="caption">Uptime</p>
<p class="highlight">
{ciStatus.uptime_seconds}
</p>
</div>
</div>
<p class="meta">
Autor: {ciStatus.author}
</p>
{:else if !loadingCiStatus}
<p>No se pudo obtener el estado del sistema.</p>
{/if}
</article>
<article class="card">
<div class="card-head">
<div class="label">Desayunos</div>
{#if loadingPrices}
<span class="tag">cargando...</span>
{/if}
</div>
{#if prices.length}
<div class="price-grid">
{#each prices as price}
<div class="price-card">
<p class="item">{prettify(price.item)}</p>
<p class="value">{price.price}</p>
<p class="timestamp">{price.generated_at}</p>
</div>
{/each}
</div>
<p class="meta">
Dependiendo de si vas por la mañana o por la tarde los precios cambian. No sé,
como no ponen los precios al público... :p
</p>
{:else if !loadingPrices}
<p>No hay precios que mostrar.</p>
{/if}
</article>
</section>
</main>