Add locations to todo items and automate Gitea deploy
Some checks failed
Redeploy Docker Compose / redeploy (push) Failing after 2s

This commit is contained in:
2026-03-26 18:44:33 +01:00
parent 0ec3222873
commit 124de9f5b8
8 changed files with 220 additions and 14 deletions

View File

@@ -25,6 +25,18 @@ function isUpgradeResult(result: SearchResult): result is UpgradeSearchResult {
return 'isUpgrade' in result;
}
function normalizeKnownLocations(value: unknown): string[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const normalized = value
.map((location) => (typeof location === 'string' ? location.trim() : ''))
.filter((location) => Boolean(location));
return normalized.length > 0 ? normalized : undefined;
}
function ItemIcon({
src,
alt,
@@ -85,6 +97,9 @@ function loadTodoItems(): TodoItem[] {
typeof row.name === 'string' &&
typeof row.iconUrl === 'string' &&
(row.rarity === undefined || typeof row.rarity === 'string') &&
(row.knownLocations === undefined ||
(Array.isArray(row.knownLocations) &&
row.knownLocations.every((location: unknown) => typeof location === 'string'))) &&
typeof row.quantity === 'number' &&
typeof row.completed === 'boolean'
);
@@ -92,6 +107,7 @@ function loadTodoItems(): TodoItem[] {
.map((item) => ({
...item,
rarity: typeof item.rarity === 'string' ? item.rarity : undefined,
knownLocations: normalizeKnownLocations(item.knownLocations),
quantity: Math.max(1, Math.floor(item.quantity)),
}));
} catch {
@@ -177,6 +193,7 @@ function decodeListFromUrl(encoded: string, allItems: SearchItem[]): TodoItem[]
name: match.name,
iconUrl: match.iconUrl,
rarity: match.rarity,
knownLocations: match.knownLocations,
quantity,
completed,
};
@@ -212,11 +229,14 @@ export default function App() {
return;
}
const rarityBySlug = new Map(allItems.map((item) => [item.slug, item.rarity]));
const itemMetadataBySlug = new Map(
allItems.map((item) => [item.slug, { rarity: item.rarity, knownLocations: item.knownLocations }]),
);
setTodoItems((prevItems) =>
prevItems.map((item) => ({
...item,
rarity: item.rarity ?? rarityBySlug.get(item.slug),
rarity: item.rarity ?? itemMetadataBySlug.get(item.slug)?.rarity,
knownLocations: item.knownLocations ?? itemMetadataBySlug.get(item.slug)?.knownLocations,
})),
);
@@ -359,7 +379,14 @@ export default function App() {
const existing = prevItems.find((entry) => entry.slug === item.slug);
if (existing) {
return prevItems.map((entry) =>
entry.slug === item.slug ? { ...entry, quantity: entry.quantity + 1 } : entry,
entry.slug === item.slug
? {
...entry,
quantity: entry.quantity + 1,
rarity: entry.rarity ?? item.rarity,
knownLocations: entry.knownLocations ?? item.knownLocations,
}
: entry,
);
}
@@ -370,6 +397,7 @@ export default function App() {
name: item.name,
iconUrl: item.iconUrl,
rarity: item.rarity,
knownLocations: item.knownLocations,
quantity: 1,
completed: false,
},
@@ -398,6 +426,8 @@ export default function App() {
nextItems[existingIndex] = {
...nextItems[existingIndex],
quantity: nextItems[existingIndex].quantity + amount,
rarity: nextItems[existingIndex].rarity ?? salvageEntry.rarity,
knownLocations: nextItems[existingIndex].knownLocations ?? salvageEntry.knownLocations,
};
continue;
}
@@ -408,6 +438,7 @@ export default function App() {
name: salvageEntry.name,
iconUrl: salvageEntry.iconUrl,
rarity: salvageEntry.rarity,
knownLocations: salvageEntry.knownLocations,
quantity: amount,
completed: false,
});
@@ -501,7 +532,9 @@ export default function App() {
<header>
<div className="header-top">
<div>
<h1>marathon.todo</h1>
<h1>
marathon.todo <span className="app-version">v0.0.1</span>
</h1>
{!minimalMode ? <p>Plan what to loot (or do) in your next Marathon raid.</p> : null}
</div>
{!minimalMode ? (
@@ -738,12 +771,17 @@ export default function App() {
<ItemIcon src={item.iconUrl} alt="" size={37} />
</span>
<span
className="item-name"
style={getRarityGlowColor(item.rarity) ? { color: getRarityGlowColor(item.rarity)! } : undefined}
>
{item.name}
</span>
<div className="item-main">
<span
className="item-name"
style={getRarityGlowColor(item.rarity) ? { color: getRarityGlowColor(item.rarity)! } : undefined}
>
{item.name}
</span>
{item.knownLocations && item.knownLocations.length > 0 ? (
<span className="item-locations">{item.knownLocations.join(', ')}</span>
) : null}
</div>
{minimalMode ? (
<span className="minimal-qty">x{item.quantity}</span>

View File

@@ -15,10 +15,38 @@ interface PopularResponse {
let catalogPromise: Promise<CatalogResponse> | null = null;
function normalizeKnownLocations(value: unknown): string[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const normalized = value
.map((location) => (typeof location === 'string' ? location.trim() : ''))
.filter((location) => Boolean(location));
return normalized.length > 0 ? normalized : undefined;
}
function normalizeCatalog(payload: CatalogResponse): CatalogResponse {
const normalizedItems = payload.items.map((item) => ({
...item,
knownLocations: normalizeKnownLocations(item.knownLocations),
}));
const normalizedUpgrades = payload.upgrades.map((upgrade) => {
if (Array.isArray(upgrade.levels)) {
return upgrade;
return {
...upgrade,
levels: upgrade.levels.map((level) => ({
...level,
salvage: Array.isArray(level.salvage)
? level.salvage.map((salvageItem) => ({
...salvageItem,
knownLocations: normalizeKnownLocations(salvageItem.knownLocations),
}))
: [],
})),
};
}
const legacyUpgrade = upgrade as UpgradeSearchResult & { salvage?: UpgradeSearchResult['levels'][number]['salvage'] };
@@ -26,12 +54,21 @@ function normalizeCatalog(payload: CatalogResponse): CatalogResponse {
return {
...upgrade,
levels: [{ level: 1, salvage }],
levels: [
{
level: 1,
salvage: salvage.map((salvageItem) => ({
...salvageItem,
knownLocations: normalizeKnownLocations(salvageItem.knownLocations),
})),
},
],
};
});
return {
...payload,
items: normalizedItems,
upgrades: normalizedUpgrades,
};
}

View File

@@ -20,7 +20,7 @@
--ui-accent: #caf61d;
--color-text-main: #e8edf7;
--color-text-input: #f2f6ff;
--color-text-muted: #d6dbe8;
--color-text-muted: #7f828a;
--color-text-done: #8d9bbb;
--color-border: #444444;
--color-hover: #31373d;
@@ -74,6 +74,11 @@ h2 {
color: var(--ui-accent);
}
.app-version {
font-size: 0.35em;
color: var(--color-text-muted);
}
label {
color: var(--ui-accent);
}
@@ -257,6 +262,7 @@ input[type='number'] {
justify-content: center;
flex: 0 0 auto;
overflow: visible;
align-self: flex-start;
}
.result-name {
@@ -322,7 +328,7 @@ input[type='number'] {
padding: 0.65rem;
display: grid;
grid-template-columns: auto 1fr auto auto;
align-items: center;
align-items: flex-start;
gap: 0.5rem;
background: var(--ui-dark-elevated);
cursor: pointer;
@@ -351,6 +357,7 @@ input[type='number'] {
font-weight: 700;
min-width: 52px;
text-align: right;
align-self: center;
}
.todo-item.highlighted {
@@ -359,12 +366,28 @@ input[type='number'] {
.item-name {
overflow-wrap: anywhere;
line-height: 1.2;
}
.item-main {
min-width: 0;
display: grid;
gap: 0.2rem;
align-self: flex-start;
}
.item-locations {
overflow-wrap: anywhere;
font-size: 0.72rem;
color: var(--color-text-muted);
line-height: 1.2;
}
.quantity-field {
display: flex;
align-items: center;
gap: 0.35rem;
align-self: center;
}
.quantity-field input {
@@ -400,6 +423,10 @@ input[type='number'] {
color: var(--color-text-done);
}
.todo-item.done .item-locations {
color: var(--color-text-done);
}
.todo-item.done .minimal-qty,
.todo-item.done .quantity-field input {
text-decoration: line-through;
@@ -434,6 +461,7 @@ input[type='number'] {
align-items: center;
justify-content: center;
cursor: pointer;
align-self: center;
}
.delete:hover {

View File

@@ -4,6 +4,7 @@ export interface SearchItem {
name: string;
iconUrl: string;
rarity?: string;
knownLocations?: string[];
}
export interface UpgradeSalvageItem {
@@ -12,6 +13,7 @@ export interface UpgradeSalvageItem {
name: string;
iconUrl: string;
rarity?: string;
knownLocations?: string[];
}
export interface UpgradeLevel {
@@ -38,6 +40,7 @@ export interface TodoItem {
name: string;
iconUrl: string;
rarity?: string;
knownLocations?: string[];
quantity: number;
completed: boolean;
}