${escapeHtml(issue.title)}
${issue.body ? escapeHtml(issue.body.replace(/\s+/g, " ").slice(0, 140) + "...") : ""}
${milestone ? `` : ""} ${tags ? `` : ""} ${ACTIVITY_CTA[lang]}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 `
${issue.body ? escapeHtml(issue.body.replace(/\s+/g, " ").slice(0, 140) + "...") : ""}${escapeHtml(issue.title)}