236 lines
7.1 KiB
JavaScript
236 lines
7.1 KiB
JavaScript
const ACTIVITIES_CONFIG = {
|
|
apiBase: "https://openbokeron.uma.es/gitea/api/v1",
|
|
repo: "OpenBokeron/Actividades",
|
|
state: "open",
|
|
};
|
|
|
|
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}`;
|
|
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) => `<span class="activity-tag">${label.name}</span>`)
|
|
.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 `
|
|
<article class="activity-card">
|
|
<div class="activity-head">
|
|
${badge ? `<span class="activity-status ${badge.className}">${badge.text}</span>` : ""}
|
|
</div>
|
|
<h3>${escapeHtml(issue.title)}</h3>
|
|
<p>${issue.body ? escapeHtml(issue.body.replace(/\s+/g, " ").slice(0, 140) + "...") : ""}</p>
|
|
<div class="activity-meta">
|
|
<span>${labels.updated} ${updated}</span>
|
|
<span>${comments} ${labels.comments}</span>
|
|
<span>#${issue.number}</span>
|
|
</div>
|
|
${milestone ? `<div class="activity-meta">${labels.milestone}: ${milestone}</div>` : ""}
|
|
${tags ? `<div class="activity-tags">${tags}</div>` : ""}
|
|
<a class="activity-link" href="${issue.html_url}" target="_blank" rel="noopener">${ACTIVITY_CTA[lang]}</a>
|
|
</article>
|
|
`;
|
|
};
|
|
|
|
const renderIdeas = (ideas) => {
|
|
const ideasList = container.querySelector("[data-activities-ideas-list]");
|
|
|
|
if (!ideasList) {
|
|
return;
|
|
}
|
|
|
|
if (!ideas || ideas.length === 0) {
|
|
ideasList.innerHTML = `<li class="activities-idea">${labels.noIdeas}</li>`;
|
|
return;
|
|
}
|
|
|
|
ideasList.innerHTML = ideas
|
|
.map(
|
|
(issue) => `
|
|
<li class="activities-idea">
|
|
<div>
|
|
<strong>${escapeHtml(issue.title)}</strong>
|
|
<span class="activity-meta">#${issue.number}</span>
|
|
</div>
|
|
<a href="${issue.html_url}" target="_blank" rel="noopener">${ACTIVITY_CTA[lang]}</a>
|
|
</li>
|
|
`
|
|
)
|
|
.join("");
|
|
};
|
|
|
|
const renderActivities = (issues) => {
|
|
if (!issues || issues.length === 0) {
|
|
grid.innerHTML = `<div class="activities-empty">${ACTIVITY_EMPTY[lang]}</div>`;
|
|
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 = `<div class="activities-empty">${labels.noCooking}</div>`;
|
|
} else {
|
|
grid.innerHTML = cooking.map(renderActivity).join("");
|
|
}
|
|
|
|
renderIdeas(filteredIdeas);
|
|
};
|
|
|
|
const renderFallback = () => {
|
|
grid.innerHTML = `
|
|
<div class="activities-empty">
|
|
${FALLBACK_TEXT[lang]}
|
|
<a href="https://openbokeron.uma.es/gitea/OpenBokeron/-/projects/5" target="_blank" rel="noopener">${FALLBACK_LINK[lang]}</a>
|
|
</div>
|
|
`;
|
|
|
|
const ideasList = container.querySelector("[data-activities-ideas-list]");
|
|
if (ideasList) {
|
|
ideasList.innerHTML = `<li class="activities-idea">${labels.noIdeas}</li>`;
|
|
}
|
|
};
|
|
|
|
fetchActivities()
|
|
.then((issues) => {
|
|
const sorted = [...issues].sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
|
|
renderActivities(sorted);
|
|
})
|
|
.catch(() => {
|
|
renderFallback();
|
|
});
|
|
}
|