initial commit

This commit is contained in:
gulimabr
2026-01-28 15:34:58 -03:00
commit 08825bf817
23 changed files with 5251 additions and 0 deletions

204
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,204 @@
import { useMemo, useState } from "react";
type Definition = {
source: string;
title: string;
url: string;
definition: string;
};
type DefinitionResponse = {
term: string;
results: Definition[];
taxonomy?: TaxonomyMatch[];
};
type TaxonomyMatch = {
category: string;
class_name: string;
class_code: string;
type_description?: string | null;
type_code?: string | null;
annex?: string | null;
full_name: string;
};
const API_BASE_URL =
import.meta.env.VITE_API_BASE_URL?.toString() || "http://localhost:8000";
export default function App() {
const [term, setTerm] = useState("");
const [results, setResults] = useState<Definition[]>([]);
const [taxonomy, setTaxonomy] = useState<TaxonomyMatch[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const canSearch = term.trim().length > 0 && !loading;
const apiUrl = useMemo(() => {
const url = new URL("/api/definitions", API_BASE_URL);
url.searchParams.set("term", term.trim());
return url.toString();
}, [term]);
const handleSearch = async (event: React.FormEvent) => {
event.preventDefault();
if (!canSearch) return;
setLoading(true);
setError(null);
try {
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error("Failed to fetch definitions.");
}
const data = (await response.json()) as DefinitionResponse;
setResults(data.results ?? []);
setTaxonomy(data.taxonomy ?? []);
} catch (err) {
setError(err instanceof Error ? err.message : "Something went wrong.");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen px-6 py-12">
<div className="mx-auto flex w-full max-w-3xl flex-col gap-8">
<header className="space-y-3">
<p className="text-sm font-semibold uppercase tracking-wide text-sky-600">
TermSearch
</p>
<h1 className="text-4xl font-semibold text-slate-900">
Oil &amp; Gas term definitions
</h1>
<p className="text-base text-slate-600">
Search multiple glossary sources from a single interface.
</p>
</header>
<form
onSubmit={handleSearch}
className="flex flex-col gap-4 rounded-2xl bg-white p-6 shadow-sm"
>
<label className="text-sm font-medium text-slate-700" htmlFor="term">
Search term
</label>
<div className="flex flex-col gap-3 sm:flex-row">
<input
id="term"
name="term"
type="text"
value={term}
onChange={(event) => setTerm(event.target.value)}
placeholder="Ex: gas lift"
className="flex-1 rounded-xl border border-slate-200 px-4 py-3 text-base focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
/>
<button
type="submit"
disabled={!canSearch}
className="rounded-xl bg-sky-600 px-6 py-3 text-base font-semibold text-white transition hover:bg-sky-700 disabled:cursor-not-allowed disabled:bg-slate-300"
>
{loading ? "Searching..." : "Search"}
</button>
</div>
<p className="text-xs text-slate-500">
API base: <span className="font-medium">{API_BASE_URL}</span>
</p>
</form>
<section className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-slate-800">Results</h2>
<span className="text-sm text-slate-500">
{results.length} {results.length === 1 ? "source" : "sources"}
</span>
</div>
{error ? (
<div className="rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-600">
{error}
</div>
) : null}
{results.length === 0 && !loading ? (
<div className="rounded-xl border border-dashed border-slate-200 bg-white p-6 text-sm text-slate-500">
No definitions yet. Try searching for a term.
</div>
) : null}
<div className="space-y-3">
{results.map((result) => (
<article
key={`${result.source}-${result.title}`}
className="rounded-xl border border-slate-100 bg-white p-5 shadow-sm"
>
<h3 className="text-sm font-semibold uppercase tracking-wide text-sky-600">
{result.source}
</h3>
<p className="mt-2 text-lg font-semibold text-slate-900">
{result.title}
</p>
<a
href={result.url}
target="_blank"
rel="noreferrer"
className="mt-2 inline-flex text-sm font-medium text-sky-600 hover:text-sky-700"
>
View source
</a>
<p className="mt-2 text-base text-slate-700">
{result.definition}
</p>
</article>
))}
</div>
</section>
<section className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-slate-800">
ISO 14224 Taxonomy
</h2>
<span className="text-sm text-slate-500">
{taxonomy.length} {taxonomy.length === 1 ? "match" : "matches"}
</span>
</div>
{taxonomy.length === 0 && !loading ? (
<div className="rounded-xl border border-dashed border-slate-200 bg-white p-6 text-sm text-slate-500">
No taxonomy matches found.
</div>
) : null}
<div className="space-y-3">
{taxonomy.map((item) => (
<article
key={`${item.class_code}-${item.type_code ?? "class"}`}
className="rounded-xl border border-slate-100 bg-white p-5 shadow-sm"
>
<p className="text-xs font-semibold uppercase tracking-wide text-emerald-600">
{item.category}
</p>
<h3 className="mt-2 text-lg font-semibold text-slate-900">
{item.full_name}
</h3>
<div className="mt-2 flex flex-wrap gap-3 text-sm text-slate-600">
<span>Class: {item.class_name} ({item.class_code})</span>
{item.type_description ? (
<span>
Type: {item.type_description} ({item.type_code})
</span>
) : null}
{item.annex ? <span>Annex: {item.annex}</span> : null}
</div>
</article>
))}
</div>
</section>
</div>
</div>
);
}