Files
readest/apps/readest-app/src/app/reader/components/annotator/WiktionaryPopup.tsx
T
2024-12-10 14:14:34 +01:00

181 lines
5.3 KiB
TypeScript

import React, { useEffect, useRef, useState } from 'react';
import { Position } from '@/utils/sel';
import Popup from '@/components/Popup';
type Definition = {
definition: string;
examples?: string[];
};
type Result = {
partOfSpeech: string;
definitions: Definition[];
language: string;
};
interface WiktionaryPopupProps {
word: string;
lang?: string;
position: Position;
trianglePosition: Position;
popupWidth: number;
popupHeight: number;
}
const WiktionaryPopup: React.FC<WiktionaryPopupProps> = ({
word,
lang,
position,
trianglePosition,
popupWidth,
popupHeight,
}) => {
const [lookupWord, setLookupWord] = useState(word);
const isLookingUp = useRef(false);
const interceptDictLinks = (definition: string): HTMLElement[] => {
const container = document.createElement('div');
container.innerHTML = definition;
const links = container.querySelectorAll<HTMLAnchorElement>('a[rel="mw:WikiLink"]');
links.forEach((link) => {
const title = link.getAttribute('title');
if (title) {
link.addEventListener('click', (event) => {
event.preventDefault();
setLookupWord(title);
isLookingUp.current = false;
});
link.className = 'text-primary underline cursor-pointer';
}
});
return Array.from(container.childNodes) as HTMLElement[];
};
useEffect(() => {
if (isLookingUp.current) {
return;
}
isLookingUp.current = true;
const main = document.querySelector('main') as HTMLElement;
const footer = document.querySelector('footer') as HTMLElement;
const fetchDefinitions = async (word: string, language?: string) => {
main.innerHTML = '';
footer.dataset['state'] = 'loading';
try {
const response = await fetch(
`https://en.wiktionary.org/api/rest_v1/page/definition/${word}`,
);
if (!response.ok) {
throw new Error('Failed to fetch definitions');
}
const json = await response.json();
const results: Result[] | undefined = language
? json[language] || json['en']
: json[Object.keys(json)[0]!];
if (!results || results.length === 0) {
throw new Error('No results found');
}
const hgroup = document.createElement('hgroup');
const h1 = document.createElement('h1');
h1.innerText = word;
h1.className = 'text-lg font-bold';
const p = document.createElement('p');
p.innerText = results[0]!.language;
p.className = 'text-sm italic opacity-75';
hgroup.append(h1, p);
main.append(hgroup);
results.forEach(({ partOfSpeech, definitions }: Result) => {
const h2 = document.createElement('h2');
h2.innerText = partOfSpeech;
h2.className = 'text-base font-semibold mt-4';
const ol = document.createElement('ol');
ol.className = 'pl-8 list-decimal';
definitions.forEach(({ definition, examples }: Definition) => {
if (!definition) return;
const li = document.createElement('li');
const processedContent = interceptDictLinks(definition);
li.append(...processedContent);
if (examples) {
const ul = document.createElement('ul');
ul.className = 'pl-8 list-disc text-sm italic opacity-75';
examples.forEach((example) => {
const exampleLi = document.createElement('li');
exampleLi.innerHTML = example;
ul.appendChild(exampleLi);
});
li.appendChild(ul);
}
ol.appendChild(li);
});
main.appendChild(h2);
main.appendChild(ol);
});
footer.dataset['state'] = 'loaded';
} catch (error) {
console.error(error);
footer.dataset['state'] = 'error';
const div = document.createElement('div');
div.className =
'flex flex-col items-center justify-center w-full h-full text-center absolute inset-0';
const h1 = document.createElement('h1');
h1.innerText = 'Error';
h1.className = 'text-lg font-bold';
const p = document.createElement('p');
p.innerHTML = `Unable to load the word. Try searching directly on <a href="https://en.wiktionary.org/w/index.php?search=${encodeURIComponent(
word,
)}" target="_blank" rel="noopener noreferrer" class="text-primary underline">Wiktionary</a>.`;
div.append(h1, p);
main.append(div);
}
};
const langCode = typeof lang === 'string' ? lang : lang?.[0];
fetchDefinitions(lookupWord, langCode);
}, [lookupWord, lang]);
return (
<div>
<Popup
trianglePosition={trianglePosition}
width={popupWidth}
height={popupHeight}
position={position}
className='select-text overflow-y-auto'
>
<main className='p-4 font-sans' />
<footer className='hidden data-[state=loaded]:block data-[state=error]:hidden data-[state=loading]:hidden'>
<div className='p-4 text-sm opacity-60'>
From <a id='link'>Wiktionary</a>, released under the{' '}
<a href='https://creativecommons.org/licenses/by-sa/4.0/'>CC BY-SA License</a>.
</div>
</footer>
</Popup>
</div>
);
};
export default WiktionaryPopup;