Vergangene Mitgliedschaften sichtbar, Antworten nicht mehr aus dem Cache
Wer sich aus einem Gruppen-CC herausredesignt hat, war trotzdem darin - der CC verschwand aber aus seiner Liste. Jetzt steht er am Fuss der Kette, in die der Spieler von dort aus gewechselt ist, gekennzeichnet als "war dabei" und mit dem Hinweis, wem er gehoert und wohin es weiterging. Verschieben oder entscheiden laesst sich daran nichts: es ist der Platz eines anderen. Frueheres, das zu keinem heutigen Platz fuehrt, steht unter "Frueher genutzt". Dasselbe gilt fuer einen gewoehnlichen Austritt. Neu eingetragene CCs tauchten manchmal erst nach einem Neuladen auf. Die Antworten der Schnittstelle trugen keinen Cache-Header, und ohne Angabe darf ein Browser selbst entscheiden, wie lange er eine GET-Antwort aufhebt - dann steht nach dem Anlegen die alte Liste auf dem Schirm. Sie tragen jetzt no-store, was nebenbei verhindert, dass Spielerdaten im Cache liegen bleiben. Ausserdem: die Pfeile zwischen den Kettengliedern sind groesser und tragen die Akzentfarbe - sie sind das Einzige, was die Leserichtung verraet. Und "Zurueknehmen" heisst jetzt "Loeschen", weil genau das passiert.
This commit is contained in:
@@ -32,20 +32,47 @@ export type ChainAction =
|
||||
interface Kette {
|
||||
lineageId: string;
|
||||
glieder: Character[];
|
||||
/**
|
||||
* CCs, in denen der Spieler einmal steckte und die zu dieser Kette führten
|
||||
* - ein Gruppen-CC, aus dem er sich herausredesignt hat. Sie gehören nicht
|
||||
* ihm, stehen aber am Anfang seiner Geschichte und deshalb unten an der
|
||||
* Kette. Verschieben lässt sich daran nichts: es ist der Platz eines
|
||||
* anderen.
|
||||
*/
|
||||
herkunft: Character[];
|
||||
}
|
||||
|
||||
function baueKetten(characters: Character[]): Kette[] {
|
||||
/** Die Mitgliedschaft dieses Spielers an einem CC. */
|
||||
function eigeneMitgliedschaft(character: Character, playerId: number) {
|
||||
return (character.members ?? []).find((m) => m.playerId === playerId);
|
||||
}
|
||||
|
||||
/// Ein Platz, den der Spieler gerade belegt oder auf den er zurückkehren kann.
|
||||
function istSeinPlatz(character: Character, playerId: number): boolean {
|
||||
const eigene = eigeneMitgliedschaft(character, playerId);
|
||||
if (!eigene) return false;
|
||||
return eigene.leftAt === null || eigene.inactiveReason !== null;
|
||||
}
|
||||
|
||||
function baueKetten(characters: Character[], playerId: number): {
|
||||
ketten: Kette[];
|
||||
vergangen: Character[];
|
||||
} {
|
||||
const eigene = characters.filter((c) => istSeinPlatz(c, playerId));
|
||||
const verlassen = characters.filter((c) => !istSeinPlatz(c, playerId));
|
||||
|
||||
const nach = new Map<string, Character[]>();
|
||||
for (const c of characters) {
|
||||
for (const c of eigene) {
|
||||
const liste = nach.get(c.lineageId) ?? [];
|
||||
liste.push(c);
|
||||
nach.set(c.lineageId, liste);
|
||||
}
|
||||
|
||||
return [...nach.entries()]
|
||||
const ketten: Kette[] = [...nach.entries()]
|
||||
.map(([lineageId, glieder]) => ({
|
||||
lineageId,
|
||||
glieder: [...glieder].sort((a, b) => a.chainPosition - b.chainPosition),
|
||||
herkunft: [] as Character[],
|
||||
}))
|
||||
// Ketten mit einem aktiven CC zuerst - daran arbeitet man.
|
||||
.sort((a, b) => {
|
||||
@@ -54,6 +81,22 @@ function baueKetten(characters: Character[]): Kette[] {
|
||||
if (aktivA !== aktivB) return aktivA - aktivB;
|
||||
return (a.glieder[0]?.createdAt ?? '').localeCompare(b.glieder[0]?.createdAt ?? '');
|
||||
});
|
||||
|
||||
// Verlassene CCs an die Kette hängen, in die der Spieler von dort aus
|
||||
// gewechselt ist. Wo sich das nicht sagen lässt, stehen sie für sich.
|
||||
const vergangen: Character[] = [];
|
||||
for (const c of verlassen) {
|
||||
const nachfolgerId = eigeneMitgliedschaft(c, playerId)?.successorId ?? null;
|
||||
const ziel =
|
||||
nachfolgerId === null
|
||||
? undefined
|
||||
: ketten.find((k) => k.glieder.some((g) => g.id === nachfolgerId));
|
||||
|
||||
if (ziel) ziel.herkunft.push(c);
|
||||
else vergangen.push(c);
|
||||
}
|
||||
|
||||
return { ketten, vergangen };
|
||||
}
|
||||
|
||||
export default function ChainView({
|
||||
@@ -76,9 +119,9 @@ export default function ChainView({
|
||||
const [ziehe, setZiehe] = useState<Character | null>(null);
|
||||
const [ueber, setUeber] = useState<string | null>(null);
|
||||
|
||||
const ketten = baueKetten(characters);
|
||||
const { ketten, vergangen } = baueKetten(characters, playerId);
|
||||
|
||||
if (ketten.length === 0) {
|
||||
if (ketten.length === 0 && vergangen.length === 0) {
|
||||
return (
|
||||
<section className="block">
|
||||
<h2>CCs</h2>
|
||||
@@ -196,7 +239,7 @@ export default function ChainView({
|
||||
{i < vonObenNachUnten.length - 1 && (
|
||||
<>
|
||||
<div className="chain-arrow" aria-hidden="true">
|
||||
↑
|
||||
⬆
|
||||
</div>
|
||||
{zone(
|
||||
kette.lineageId,
|
||||
@@ -209,14 +252,91 @@ export default function ChainView({
|
||||
))}
|
||||
|
||||
{zone(kette.lineageId, null, `${kette.lineageId}-unten`)}
|
||||
|
||||
{kette.herkunft.map((frueher) => (
|
||||
<div key={frueher.id}>
|
||||
<div className="chain-arrow" aria-hidden="true">
|
||||
⬆
|
||||
</div>
|
||||
<FruehererEintrag character={frueher} playerId={playerId} onAction={onAction} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{vergangen.length > 0 && (
|
||||
<>
|
||||
<h3 className="chain-past-head">Früher genutzt</h3>
|
||||
<p className="muted small">
|
||||
CCs, in denen dieser Spieler einmal steckte, die aber keinem seiner Plätze mehr
|
||||
vorausgehen. Sie gehören jemand anderem.
|
||||
</p>
|
||||
<div className="chains">
|
||||
{vergangen.map((character) => (
|
||||
<FruehererEintrag
|
||||
key={character.id}
|
||||
character={character}
|
||||
playerId={playerId}
|
||||
onAction={onAction}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein CC, in dem der Spieler einmal steckte. Er gehört ihm nicht mehr,
|
||||
* deshalb gibt es hier nichts zu entscheiden und nichts zu verschieben -
|
||||
* nur nachzulesen, wann er dabei war und wohin es danach ging.
|
||||
*/
|
||||
function FruehererEintrag({
|
||||
character,
|
||||
playerId,
|
||||
onAction,
|
||||
}: {
|
||||
character: Character;
|
||||
playerId: number;
|
||||
onAction: (action: ChainAction, character: Character) => void;
|
||||
}) {
|
||||
const eigene = eigeneMitgliedschaft(character, playerId);
|
||||
const thread = character.forumThreads?.[0];
|
||||
const besitzer = (character.members ?? []).find((m) => m.playerId === character.playerId);
|
||||
|
||||
return (
|
||||
<article className="chain-entry is-past">
|
||||
<div className="chain-entry-head">
|
||||
{thread ? (
|
||||
<a href={thread.url} target="_blank" rel="noreferrer" className="link-strong">
|
||||
{character.name}
|
||||
</a>
|
||||
) : (
|
||||
<strong>{character.name}</strong>
|
||||
)}
|
||||
<span className="badge">war dabei</span>
|
||||
</div>
|
||||
|
||||
<p className="muted small">
|
||||
{eigene?.joinedAt && `Dabei ab ${formatDate(eigene.joinedAt)}`}
|
||||
{eigene?.leftAt && ` bis ${formatDate(eigene.leftAt)}`}
|
||||
{eigene?.successor && ` · weiter als „${eigene.successor.name}"`}
|
||||
{besitzer && besitzer.playerId !== playerId && ` · gehört ${besitzer.player.displayName}`}
|
||||
</p>
|
||||
{eigene?.note && <p className="muted small">{eigene.note}</p>}
|
||||
|
||||
<div className="chain-actions">
|
||||
<button type="button" className="btn btn-tiny btn-ghost" onClick={() => onAction('links', character)}>
|
||||
Verweise
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function ChainEntry({
|
||||
character,
|
||||
playerId,
|
||||
@@ -377,7 +497,7 @@ function ChainEntry({
|
||||
)}
|
||||
{aktiv && (
|
||||
<button type="button" className="btn btn-tiny btn-danger" onClick={() => onAction('revert', character)}>
|
||||
Zurücknehmen
|
||||
Löschen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -550,9 +550,9 @@ export default function PlayerDetail() {
|
||||
|
||||
{dialog.type === 'revert' && (
|
||||
<ReasonModal
|
||||
title={`"${dialog.character.name}" zurücknehmen`}
|
||||
title={`"${dialog.character.name}" löschen`}
|
||||
hint="Der CC wird gelöscht, verbrauchte Guthaben werden zurückgebucht. Für Fehleinträge gedacht."
|
||||
confirmLabel="Zurücknehmen"
|
||||
confirmLabel="Löschen"
|
||||
danger
|
||||
busy={busy}
|
||||
onClose={() => setDialog({ type: 'none' })}
|
||||
@@ -563,7 +563,7 @@ export default function PlayerDetail() {
|
||||
method: 'POST',
|
||||
body: { reason },
|
||||
}),
|
||||
`"${dialog.character.name}" wurde zurückgenommen.`,
|
||||
`"${dialog.character.name}" wurde gelöscht.`,
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
+17
-3
@@ -1168,12 +1168,26 @@ textarea {
|
||||
letter-spacing: -0.1em;
|
||||
}
|
||||
|
||||
/* Der Pfeil zeigt zum jüngeren Eintrag, also nach oben. */
|
||||
/* Der Pfeil zeigt zum jüngeren Eintrag, also nach oben. Er ist bewusst
|
||||
deutlich: er ist das Einzige, was die Leserichtung der Kette verrät. */
|
||||
.chain-arrow {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
color: var(--accent);
|
||||
font-size: 1.4rem;
|
||||
line-height: 1;
|
||||
padding: 0.2rem 0;
|
||||
padding: 0.35rem 0;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
/* Ein CC, der jemand anderem gehört und nur zur Geschichte beiträgt. */
|
||||
.chain-entry.is-past {
|
||||
opacity: 0.7;
|
||||
border-style: dotted;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.chain-past-head {
|
||||
margin: 1.5rem 0 0.25rem;
|
||||
}
|
||||
|
||||
/* Erscheint nur, solange etwas gezogen wird. */
|
||||
|
||||
Reference in New Issue
Block a user