Add unlink flow for linked games

This commit is contained in:
2026-02-05 11:23:41 +01:00
parent 70a4f7ed61
commit fcd23edc6b
5 changed files with 97 additions and 1 deletions

View File

@@ -58,4 +58,6 @@ export const adminApi = {
factoryReset: () => request("/api/admin/factory-reset", { method: "POST" }),
linkSuggestions: (sourceSuggestionId, targetSuggestionId) =>
request("/api/admin/link-suggestions", { method: "POST", body: { sourceSuggestionId, targetSuggestionId } }),
unlinkSuggestions: (suggestionId) =>
request("/api/admin/unlink-suggestions", { method: "POST", body: { suggestionId } }),
};

View File

@@ -118,6 +118,11 @@ const translations = {
"admin.linkTargetPlaceholder": "Select game B (parent)",
"admin.linkValidation": "Choose two different games to link.",
"admin.linkDone": "Games linked. Votes cleared.",
"admin.unlinkTitle": "Remove links?",
"admin.unlinkBody": "Remove all links involving \"{name}\"? This clears votes and unfinalizes voters in this group: {peers}.",
"admin.unlinkConfirm": "Remove links",
"admin.unlinkDone": "Links removed. Votes cleared.",
"admin.unlinkUnknownPeers": "linked games",
"toast.unexpected": "Unexpected error",
"toast.registered": "Registered",
@@ -258,6 +263,11 @@ const translations = {
"admin.linkTargetPlaceholder": "Spiel B (Eltern) wählen",
"admin.linkValidation": "Wähle zwei verschiedene Spiele aus.",
"admin.linkDone": "Spiele verknüpft. Stimmen gelöscht.",
"admin.unlinkTitle": "Links entfernen?",
"admin.unlinkBody": "Alle Links zu \"{name}\" entfernen? Dadurch werden Stimmen gelöscht und Finalisierungen aufgehoben für: {peers}.",
"admin.unlinkConfirm": "Links entfernen",
"admin.unlinkDone": "Links entfernt. Stimmen gelöscht.",
"admin.unlinkUnknownPeers": "verknüpfte Spiele",
"toast.unexpected": "Unerwarteter Fehler",
"toast.registered": "Registriert",

View File

@@ -3,6 +3,7 @@ import { t } from "./i18n.js";
import { state, getSavedUsername, setSavedUsername } from "./state.js";
import { $, toast } from "./dom.js";
import { setupCardVisualHover, triggerCelebration } from "./effects.js";
import { adminApi } from "./api.js";
export function setAuthUI(isAuthed) {
const main = document.querySelector("main");
@@ -307,7 +308,9 @@ export function buildCard(
? t("card.linkedWith", { names: linkedTitles.join(", ") })
: t("card.linked")
: "";
const linkChip = linked ? `<span class="chip icon link-chip" title="${linkTooltip}">🔗</span>` : "";
const linkChip = linked
? `<button class="chip icon link-chip${state.me?.isAdmin ? " link-chip-action" : ""}" data-unlink="${s.id}" type="button" title="${linkTooltip}">🔗</button>`
: "";
const visual = hasImage
? `<button class="card-visual" data-img="${s.screenshotUrl}" aria-label="${t("card.openScreenshot")}" style="background-image:url('${s.screenshotUrl}')"></button>`
: `<div class="card-visual"></div>`;
@@ -351,6 +354,10 @@ export function buildCard(
setupCardVisualHover(btn, s.screenshotUrl);
btn.addEventListener("click", () => openLightbox(s.screenshotUrl, s.name));
}
if (linked && state.me?.isAdmin) {
const unlinkBtn = card.querySelector("[data-unlink]");
unlinkBtn?.addEventListener("click", () => openUnlinkConfirm(s));
}
if (allowEdit) {
const editBtn = card.querySelector("[data-edit]");
editBtn?.addEventListener("click", () =>
@@ -890,6 +897,27 @@ function syncLinkedSliders(sourceEl, value) {
});
}
function openUnlinkConfirm(s) {
const peers = linkedPeerTitles(s);
const names = peers.length ? peers.join(", ") : t("admin.unlinkUnknownPeers");
openConfirmModal({
title: t("admin.unlinkTitle"),
body: t("admin.unlinkBody", { name: s.name, peers: names }),
confirmLabel: t("admin.unlinkConfirm"),
cancelLabel: t("modal.cancel"),
onConfirm: async (close) => {
try {
await adminApi.unlinkSuggestions(s.id);
toast(t("admin.unlinkDone"));
close();
await window.refreshPhaseData();
} catch (err) {
toast(err.message, true);
}
}
});
}
export function updatePhaseNav() {
const isAdmin = !!state.me?.isAdmin;
const phase = state.phase;