112 lines
4.9 KiB
TypeScript
112 lines
4.9 KiB
TypeScript
import { useState } from "react";
|
|
import { Button, Spinner } from "react-bootstrap";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
import { faMagnifyingGlass, faArrowUpRightFromSquare, faBookmark } from "@fortawesome/free-solid-svg-icons";
|
|
import { CalorieSearchResult, searchCalories } from "../../../types";
|
|
|
|
type Props = {
|
|
/** Název jídla, ke kterému se energie hledá. */
|
|
name: string,
|
|
/** Zavolá se po výběru návrhu s energií na 100 g v kcal. */
|
|
onPick: (caloriesPer100g: number) => void,
|
|
};
|
|
|
|
/** Návrhy pocházející z vlastní knihovny se odlišují ikonou a popiskem. */
|
|
const LIBRARY_ORIGIN = 'library';
|
|
|
|
/**
|
|
* Vyhledání energetické hodnoty k názvu jídla.
|
|
*
|
|
* Návrhy chodí z vlastní knihovny dřívějších zadání a od externího poskytovatele.
|
|
* Když je poskytovatel nedostupný (což se stává), zůstane aspoň knihovna a odkaz
|
|
* na KalorickéTabulky.cz, odkud si hodnotu uživatel opíše ručně — automaticky se
|
|
* odtud nic nestahuje, veřejné API nemají.
|
|
*/
|
|
export default function CalorieLookup({ name, onPick }: Readonly<Props>) {
|
|
const [result, setResult] = useState<CalorieSearchResult | undefined>();
|
|
const [searching, setSearching] = useState(false);
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const canSearch = name.trim().length > 0 && !searching;
|
|
|
|
/**
|
|
* Zkopíruje název jídla do schránky při otevření Kalorických tabulek.
|
|
* Jejich vyhledávání nejde předvyplnit z URL, takže aspoň stačí vložit.
|
|
*/
|
|
const copyName = () => {
|
|
navigator.clipboard?.writeText(name.trim())
|
|
.then(() => setCopied(true))
|
|
.catch(() => { /* schránka není dostupná, odkaz se stejně otevře */ });
|
|
};
|
|
|
|
const search = async () => {
|
|
if (!canSearch) return;
|
|
setSearching(true);
|
|
const response = await searchCalories({ query: { q: name.trim() } });
|
|
setResult(response.data);
|
|
setCopied(false);
|
|
setSearching(false);
|
|
};
|
|
|
|
return (
|
|
<div className="calorie-lookup">
|
|
<Button variant="outline-secondary" size="sm" onClick={search} disabled={!canSearch}>
|
|
{searching
|
|
? <Spinner animation="border" size="sm" />
|
|
: <FontAwesomeIcon icon={faMagnifyingGlass} />}
|
|
{' '}Najít kalorie
|
|
</Button>
|
|
|
|
{!canSearch && !name.trim().length && (
|
|
<span className="calorie-lookup-hint">nejdřív vyplňte název jídla</span>
|
|
)}
|
|
|
|
{result && !searching && (
|
|
<div className="calorie-lookup-results">
|
|
{result.suggestions.map(suggestion => (
|
|
<button
|
|
type="button"
|
|
className="calorie-suggestion"
|
|
key={`${suggestion.origin}-${suggestion.name}-${suggestion.caloriesPer100g}`}
|
|
onClick={() => onPick(suggestion.caloriesPer100g)}
|
|
title="Použít tuto hodnotu"
|
|
>
|
|
{suggestion.origin === LIBRARY_ORIGIN && (
|
|
<FontAwesomeIcon icon={faBookmark} className="calorie-suggestion-icon" />
|
|
)}
|
|
<span className="calorie-suggestion-name">{suggestion.name}</span>
|
|
{suggestion.brand && (
|
|
<span className="calorie-suggestion-brand">{suggestion.brand}</span>
|
|
)}
|
|
<span className="calorie-suggestion-value">{suggestion.caloriesPer100g} kcal/100 g</span>
|
|
</button>
|
|
))}
|
|
|
|
{!result.suggestions.length && (
|
|
<div className="calorie-lookup-hint">
|
|
Nic se nenašlo{!result.providerAvailable && ' a externí databáze je nedostupná'}.
|
|
</div>
|
|
)}
|
|
|
|
{result.providerAvailable === false && !!result.suggestions.length && (
|
|
<div className="calorie-lookup-hint">
|
|
Externí databáze je nedostupná, návrhy jsou jen z vašich dřívějších zadání.
|
|
</div>
|
|
)}
|
|
|
|
<a
|
|
className="calorie-lookup-external"
|
|
href={result.externalSearchUrl}
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
onClick={copyName}
|
|
>
|
|
{copied ? 'Název zkopírován, vložte ho do jejich hledání' : 'Otevřít KalorickéTabulky.cz'}
|
|
{' '}<FontAwesomeIcon icon={faArrowUpRightFromSquare} />
|
|
</a>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|