albion/web/src/pages/ItemsPage.tsx
Заид Омар Медхат 7ba275dd77 Phase 1: scaffold + game data ingestion
npm workspaces (Fastify+TS server, Vite+React web), SQLite schema,
ao-bin-dumps downloader and parser (items, recipes, enchant upgrades,
EN localization), item browser UI and debug API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 02:53:19 +05:00

133 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from 'react';
interface SearchResult {
unique_name: string;
name_en: string | null;
tier: number | null;
enchantment: number;
shop_category: string | null;
shop_subcategory: string | null;
}
interface RecipeInput {
input_item: string;
count: number;
max_return_amount: number | null;
name_en: string | null;
}
interface Recipe {
id: number;
source: string;
variant: number;
amount_crafted: number;
silver_cost: number;
focus_cost: number;
inputs: RecipeInput[];
}
interface ItemDetail {
item: SearchResult & { weight: number | null; item_value: number | null; crafting_category: string | null };
recipes: Recipe[];
usedIn: { output_item: string; name_en: string | null }[];
}
export default function ItemsPage() {
const [q, setQ] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
const [detail, setDetail] = useState<ItemDetail | null>(null);
useEffect(() => {
if (!q.trim()) {
setResults([]);
return;
}
const t = setTimeout(() => {
fetch(`/api/items/search?q=${encodeURIComponent(q)}`)
.then((r) => r.json())
.then((d) => setResults(d.results ?? []));
}, 250);
return () => clearTimeout(t);
}, [q]);
const open = (uniqueName: string) => {
fetch(`/api/items/${encodeURIComponent(uniqueName)}`)
.then((r) => r.json())
.then(setDetail);
};
return (
<div>
<h2>Item Browser</h2>
<div className="card">
<input
type="text"
placeholder="Search items (e.g. bag, T4_BAG, claymore)..."
value={q}
onChange={(e) => setQ(e.target.value)}
/>
{results.length > 0 && (
<table style={{ marginTop: 12 }}>
<thead>
<tr><th>Name</th><th>ID</th><th>Tier</th><th>Category</th></tr>
</thead>
<tbody>
{results.map((r) => (
<tr key={r.unique_name} className="clickable" onClick={() => open(r.unique_name)}>
<td>
{r.name_en ?? <span className="muted">(unnamed)</span>}
{r.enchantment > 0 && <span className="tag">.{r.enchantment}</span>}
</td>
<td className="muted">{r.unique_name}</td>
<td>{r.tier ?? '—'}</td>
<td className="muted">{r.shop_subcategory ?? r.shop_category ?? '—'}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{detail && (
<div className="card">
<h3>
{detail.item.name_en ?? detail.item.unique_name}
<span className="tag">{detail.item.unique_name}</span>
</h3>
<p className="muted">
Tier {detail.item.tier ?? '—'} · weight {detail.item.weight ?? '—'} · item value{' '}
{detail.item.item_value ?? '—'} · {detail.item.crafting_category ?? 'no crafting category'}
</p>
{detail.recipes.length === 0 && <p className="muted">No crafting recipe (gathered, dropped, or vendor item).</p>}
{detail.recipes.map((r) => (
<div key={r.id} style={{ marginBottom: 12 }}>
<strong>
{r.source === 'upgrade' ? 'Enchanting (upgrade)' : 'Crafting'} recipe
{r.variant > 0 ? ` (alt ${r.variant})` : ''}
</strong>
<span className="tag">focus {r.focus_cost}</span>
{r.amount_crafted !== 1 && <span className="tag">crafts {r.amount_crafted}</span>}
<ul>
{r.inputs.map((inp) => (
<li key={inp.input_item}>
{inp.count}× {inp.name_en ?? inp.input_item}{' '}
{inp.max_return_amount === 0 && <span className="tag">no return</span>}
</li>
))}
</ul>
</div>
))}
{detail.usedIn.length > 0 && (
<>
<strong>Used in</strong>
<p className="muted" style={{ marginTop: 4 }}>
{detail.usedIn.slice(0, 20).map((u) => u.name_en ?? u.output_item).join(', ')}
{detail.usedIn.length > 20 ? ` … +${detail.usedIn.length - 20} more` : ''}
</p>
</>
)}
</div>
)}
</div>
);
}