const ACTIVITIES_CONFIG = { apiBase: "https://openbokeron.uma.es/gitea/api/v1", repo: "OpenBokeron/Actividades", state: "open", limit: 9, }; const ACTIVITY_LABELS = { cooking: "cocinando", }; const ACTIVITY_BADGES = { es: { cooking: "Cocinando", }, en: { cooking: "Cooking", }, }; const ACTIVITY_BADGE_STYLE = { cooking: "status-cooking", }; const ACTIVITY_CTA = { es: "Ver en Gitea", en: "View on Gitea", }; const ACTIVITY_EMPTY = { es: "Ahora mismo no hay actividades abiertas. Vuelve pronto o mira el tablero completo.", en: "There are no open activities right now. Come back soon or check the full board.", }; const FALLBACK_TEXT = { es: "Oops, la hemos liado al querer cargar el tablero. Puedes verlo directamente en Gitea:", en: "Oops, we messed up while trying to load the board. You can still open it in Gitea:", }; const FALLBACK_LINK = { es: "Abrir tablero", en: "Open board", }; const LANGUAGE_LABELS = { es: { milestone: "Hito", comments: "comentarios", updated: "Actualizado", noCooking: "No hay actividades en desarrollo. Mira el tablero completo.", noIdeas: "No hay ideas nuevas ahora mismo.", }, en: { milestone: "Milestone", comments: "comments", updated: "Updated", noCooking: "No activities in development. Check the full board.", noIdeas: "There are no new ideas right now.", }, }; const escapeHtml = (value) => { if (value === null || value === undefined) { return ""; } return String(value).replace(/[&<>"']/g, (char) => { const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; return map[char] || char; }); }; const container = document.querySelector("[data-activities-root]"); if (container) { const grid = container.querySelector("[data-activities-grid]"); const lang = document.documentElement.lang.startsWith("es") ? "es" : "en"; const labels = LANGUAGE_LABELS[lang]; const fetchActivities = async () => { const url = `${ACTIVITIES_CONFIG.apiBase}/repos/${ACTIVITIES_CONFIG.repo}/issues?state=${ACTIVITIES_CONFIG.state}&limit=${ACTIVITIES_CONFIG.limit}`; const response = await fetch(url, { headers: { Accept: "application/json", }, }); if (!response.ok) { throw new Error(`Request failed: ${response.status}`); } return response.json(); }; const formatDate = (value) => { const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } return new Intl.DateTimeFormat(lang, { year: "numeric", month: "short", day: "numeric", }).format(date); }; const getLabelNames = (issue) => (issue.labels || []).map((label) => label.name.toLowerCase()); const hasLabel = (issue, label) => getLabelNames(issue).includes(label); const renderTags = (tags) => { if (!tags || tags.length === 0) { return ""; } return tags .filter((label) => label.name.toLowerCase() !== ACTIVITY_LABELS.cooking) .slice(0, 3) .map((label) => `${label.name}`) .join(""); }; const getBadge = (issue) => { if (hasLabel(issue, ACTIVITY_LABELS.cooking)) { return { text: ACTIVITY_BADGES[lang].cooking, className: ACTIVITY_BADGE_STYLE.cooking, }; } return null; }; const renderActivity = (issue) => { const tags = renderTags(issue.labels); const milestone = issue.milestone?.title; const updated = formatDate(issue.updated_at); const badge = getBadge(issue); const comments = issue.comments || 0; return `
${badge ? `${badge.text}` : ""}

${escapeHtml(issue.title)}

${issue.body ? escapeHtml(issue.body.replace(/\s+/g, " ").slice(0, 140) + "...") : ""}

${labels.updated} ${updated} ${comments} ${labels.comments} #${issue.number}
${milestone ? `
${labels.milestone}: ${milestone}
` : ""} ${tags ? `
${tags}
` : ""} ${ACTIVITY_CTA[lang]}
`; }; const renderIdeas = (ideas) => { const ideasList = container.querySelector("[data-activities-ideas-list]"); if (!ideasList) { return; } if (!ideas || ideas.length === 0) { ideasList.innerHTML = `
  • ${labels.noIdeas}
  • `; return; } ideasList.innerHTML = ideas .map( (issue) => `
  • ${escapeHtml(issue.title)} #${issue.number}
    ${ACTIVITY_CTA[lang]}
  • ` ) .join(""); }; const renderActivities = (issues) => { if (!issues || issues.length === 0) { grid.innerHTML = `
    ${ACTIVITY_EMPTY[lang]}
    `; renderIdeas([]); return; } const cooking = issues.filter((issue) => hasLabel(issue, ACTIVITY_LABELS.cooking)); const ideas = issues.filter((issue) => !hasLabel(issue, ACTIVITY_LABELS.cooking)); const cookingTitles = new Set(cooking.map((issue) => issue.title)); const filteredIdeas = ideas.filter((issue) => !cookingTitles.has(issue.title)); if (cooking.length === 0) { grid.innerHTML = `
    ${labels.noCooking}
    `; } else { grid.innerHTML = cooking.map(renderActivity).join(""); } renderIdeas(filteredIdeas); }; const renderFallback = () => { grid.innerHTML = `
    ${FALLBACK_TEXT[lang]} ${FALLBACK_LINK[lang]}
    `; const ideasList = container.querySelector("[data-activities-ideas-list]"); if (ideasList) { ideasList.innerHTML = `
  • ${labels.noIdeas}
  • `; } }; fetchActivities() .then((issues) => { const sorted = [...issues].sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); renderActivities(sorted); }) .catch(() => { renderFallback(); }); }