Add wikipedia and translation of selected text
This commit is contained in:
@@ -31,9 +31,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [{ "url": "https://*.deepl.com" }]
|
||||
},
|
||||
"dialog:default",
|
||||
"os:default",
|
||||
"http:default",
|
||||
"core:window:default",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-minimize",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "Readest",
|
||||
"version": "0.6.5",
|
||||
"version": "0.6.8",
|
||||
"identifier": "com.bilingify.digest",
|
||||
"build": {
|
||||
"frontendDist": "../out",
|
||||
@@ -13,7 +13,8 @@
|
||||
"windows": [],
|
||||
"security": {
|
||||
"csp": {
|
||||
"default-src": "'self' 'unsafe-inline' blob: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://fonts.gstatic.com",
|
||||
"default-src": "'self' 'unsafe-inline' blob: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost https://fonts.gstatic.com",
|
||||
"connect-src": "'self' blob: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org",
|
||||
"img-src": "'self' blob: data: asset: http://asset.localhost",
|
||||
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost",
|
||||
"frame-src": "'self' blob: asset: http://asset.localhost",
|
||||
|
||||
@@ -17,11 +17,12 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
}, [pathname]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (process.env['NODE_ENV'] === 'production') {
|
||||
document.oncontextmenu = (event) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
}
|
||||
// TODO: disabled for now
|
||||
// if (process.env['NODE_ENV'] === 'production') {
|
||||
// document.oncontextmenu = (event) => {
|
||||
// event.preventDefault();
|
||||
// };
|
||||
// }
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import Popup from '@/components/Popup';
|
||||
import PopupButton from './PopupButton';
|
||||
import HighlightOptions from './HighlightOptions';
|
||||
import { Position } from '@/utils/sel';
|
||||
@@ -30,29 +31,15 @@ const AnnotationPopup: React.FC<AnnotationPopupProps> = ({
|
||||
popupHeight,
|
||||
onHighlight,
|
||||
}) => {
|
||||
const isPopupAbove = trianglePosition.dir === 'up';
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className='triangle absolute'
|
||||
style={{
|
||||
left: `${trianglePosition.point.x}px`,
|
||||
top: `${trianglePosition.point.y}px`,
|
||||
borderLeft: '6px solid transparent',
|
||||
borderRight: '6px solid transparent',
|
||||
borderBottom: isPopupAbove ? 'none' : '6px solid #465563',
|
||||
borderTop: isPopupAbove ? '6px solid #465563' : 'none',
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className='selection-popup absolute rounded-lg bg-gray-600 px-4 text-white shadow-lg'
|
||||
style={{
|
||||
width: `${popupWidth}px`,
|
||||
height: `${popupHeight}px`,
|
||||
left: `${position.point.x}px`,
|
||||
top: `${position.point.y}px`,
|
||||
}}
|
||||
<Popup
|
||||
width={popupWidth}
|
||||
height={popupHeight}
|
||||
position={position}
|
||||
trianglePosition={trianglePosition}
|
||||
className='selection-popup bg-gray-600 px-4 text-white'
|
||||
triangleClassName='text-gray-600'
|
||||
>
|
||||
<div className='selection-buttons flex h-11 items-center justify-between'>
|
||||
{buttons.map((button, index) => (
|
||||
@@ -64,7 +51,7 @@ const AnnotationPopup: React.FC<AnnotationPopupProps> = ({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
{highlightOptionsVisible && (
|
||||
<HighlightOptions
|
||||
style={{
|
||||
@@ -73,7 +60,8 @@ const AnnotationPopup: React.FC<AnnotationPopupProps> = ({
|
||||
left: `${position.point.x}px`,
|
||||
top: `${
|
||||
position.point.y +
|
||||
(highlightOptionsHeightPx + highlightOptionsPaddingPx) * (isPopupAbove ? -1 : 1)
|
||||
(highlightOptionsHeightPx + highlightOptionsPaddingPx) *
|
||||
(trianglePosition.dir === 'up' ? -1 : 1)
|
||||
}px`,
|
||||
}}
|
||||
selectedStyle={selectedStyle}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { PiHighlighterFill } from 'react-icons/pi';
|
||||
import { FaWikipediaW } from 'react-icons/fa';
|
||||
import { BsPencilSquare } from 'react-icons/bs';
|
||||
import { RiDeleteBinLine } from 'react-icons/ri';
|
||||
import { BsTranslate } from 'react-icons/bs';
|
||||
import { SiDeepl } from 'react-icons/si';
|
||||
|
||||
import { Overlayer } from 'foliate-js/overlayer.js';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
@@ -17,6 +19,8 @@ import { eventDispatcher } from '@/utils/event';
|
||||
import Toast from '@/components/Toast';
|
||||
import AnnotationPopup from './AnnotationPopup';
|
||||
import WiktionaryPopup from './WiktionaryPopup';
|
||||
import WikipediaPopup from './WikipediaPopup';
|
||||
import DeepLPopup from './DeepLPopup';
|
||||
|
||||
const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { envConfig } = useEnv();
|
||||
@@ -34,7 +38,8 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const [selection, setSelection] = useState<TextSelection | null>();
|
||||
const [showAnnotPopup, setShowAnnotPopup] = useState(false);
|
||||
const [showWiktionaryPopup, setShowWiktionaryPopup] = useState(false);
|
||||
const [wiktionaryWord, setWiktionaryWord] = useState('');
|
||||
const [showWikipediaPopup, setShowWikipediaPopup] = useState(false);
|
||||
const [showDeepLPopup, setShowDeepLPopup] = useState(false);
|
||||
const [trianglePosition, setTrianglePosition] = useState<Position>();
|
||||
const [annotPopupPosition, setAnnotPopupPosition] = useState<Position>();
|
||||
const [dictPopupPosition, setDictPopupPosition] = useState<Position>();
|
||||
@@ -50,7 +55,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
const dictPopupWidth = 400;
|
||||
const dictPopupHeight = 300;
|
||||
const annotPopupWidth = 240;
|
||||
const annotPopupWidth = 280;
|
||||
const annotPopupHeight = 44;
|
||||
const popupPadding = 10;
|
||||
|
||||
@@ -102,6 +107,8 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setSelection(null);
|
||||
setShowAnnotPopup(false);
|
||||
setShowWiktionaryPopup(false);
|
||||
setShowWikipediaPopup(false);
|
||||
setShowDeepLPopup(false);
|
||||
isShowingPopup.current = false;
|
||||
};
|
||||
|
||||
@@ -266,7 +273,18 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
if (!selection || !selection.text) return;
|
||||
setShowAnnotPopup(false);
|
||||
setShowWiktionaryPopup(true);
|
||||
setWiktionaryWord(selection.text);
|
||||
};
|
||||
|
||||
const handleWikipedia = () => {
|
||||
if (!selection || !selection.text) return;
|
||||
setShowAnnotPopup(false);
|
||||
setShowWikipediaPopup(true);
|
||||
};
|
||||
|
||||
const handleTranslation = () => {
|
||||
if (!selection || !selection.text) return;
|
||||
setShowAnnotPopup(false);
|
||||
setShowDeepLPopup(true);
|
||||
};
|
||||
|
||||
const selectionAnnotated = selection?.annotated;
|
||||
@@ -279,21 +297,24 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
},
|
||||
{ tooltipText: 'Annotate', Icon: BsPencilSquare, onClick: handleAnnotate },
|
||||
{ tooltipText: 'Search', Icon: FiSearch, onClick: handleSearch },
|
||||
{ tooltipText: 'Dictionary', Icon: FaWikipediaW, onClick: handleDictionary },
|
||||
{ tooltipText: 'Dictionary', Icon: BsTranslate, onClick: handleDictionary },
|
||||
{ tooltipText: 'Translate', Icon: SiDeepl, onClick: handleTranslation },
|
||||
{ tooltipText: 'Wikipedia', Icon: FaWikipediaW, onClick: handleWikipedia },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{(showAnnotPopup || showWiktionaryPopup) && !notebookNewAnnotation && (
|
||||
<div
|
||||
className='fixed inset-0'
|
||||
onClick={handleDismissPopupAndSelection}
|
||||
onContextMenu={handleDismissPopup}
|
||||
/>
|
||||
)}
|
||||
{(showAnnotPopup || showWiktionaryPopup || showWikipediaPopup || showDeepLPopup) &&
|
||||
!notebookNewAnnotation && (
|
||||
<div
|
||||
className='fixed inset-0'
|
||||
onClick={handleDismissPopupAndSelection}
|
||||
onContextMenu={handleDismissPopup}
|
||||
/>
|
||||
)}
|
||||
{showWiktionaryPopup && trianglePosition && dictPopupPosition && (
|
||||
<WiktionaryPopup
|
||||
word={wiktionaryWord}
|
||||
word={selection?.text as string}
|
||||
lang={bookData.bookDoc?.metadata.language as string}
|
||||
position={dictPopupPosition}
|
||||
trianglePosition={trianglePosition}
|
||||
@@ -301,6 +322,25 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
popupHeight={dictPopupHeight}
|
||||
/>
|
||||
)}
|
||||
{showWikipediaPopup && trianglePosition && dictPopupPosition && (
|
||||
<WikipediaPopup
|
||||
text={selection?.text as string}
|
||||
lang={bookData.bookDoc?.metadata.language as string}
|
||||
position={dictPopupPosition}
|
||||
trianglePosition={trianglePosition}
|
||||
popupWidth={dictPopupWidth}
|
||||
popupHeight={dictPopupHeight}
|
||||
/>
|
||||
)}
|
||||
{showDeepLPopup && trianglePosition && dictPopupPosition && (
|
||||
<DeepLPopup
|
||||
text={selection?.text as string}
|
||||
position={dictPopupPosition}
|
||||
trianglePosition={trianglePosition}
|
||||
popupWidth={dictPopupWidth}
|
||||
popupHeight={dictPopupHeight}
|
||||
/>
|
||||
)}
|
||||
{showAnnotPopup && trianglePosition && annotPopupPosition && (
|
||||
<AnnotationPopup
|
||||
buttons={buttons}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Popup from '@/components/Popup';
|
||||
import { Position } from '@/utils/sel';
|
||||
import { fetch } from '@tauri-apps/plugin-http';
|
||||
|
||||
const LANGUAGES = {
|
||||
AUTO: 'Auto Detect',
|
||||
EN: 'English',
|
||||
DE: 'German',
|
||||
FR: 'French',
|
||||
ES: 'Spanish',
|
||||
ID: 'Indonesian',
|
||||
IT: 'Italian',
|
||||
PT: 'Portuguese',
|
||||
NL: 'Dutch',
|
||||
PL: 'Polish',
|
||||
RU: 'Russian',
|
||||
KO: 'Korean',
|
||||
JA: 'Japanese',
|
||||
'ZH-HANS': 'Chinese (Simplified)',
|
||||
'ZH-HANT': 'Chinese (Traditional)',
|
||||
};
|
||||
|
||||
interface DeepLPopupProps {
|
||||
text: string;
|
||||
position: Position;
|
||||
trianglePosition: Position;
|
||||
popupWidth: number;
|
||||
popupHeight: number;
|
||||
}
|
||||
|
||||
const DeepLPopup: React.FC<DeepLPopupProps> = ({
|
||||
text,
|
||||
position,
|
||||
trianglePosition,
|
||||
popupWidth,
|
||||
popupHeight,
|
||||
}) => {
|
||||
const [sourceLang, setSourceLang] = useState('AUTO');
|
||||
const [targetLang, setTargetLang] = useState('EN');
|
||||
const [translation, setTranslation] = useState<string | null>(null);
|
||||
const [detectedSourceLang, setDetectedSourceLang] = useState<keyof typeof LANGUAGES | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSourceLangChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSourceLang(event.target.value);
|
||||
};
|
||||
|
||||
const handleTargetLangChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setTargetLang(event.target.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTranslation = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setTranslation(null);
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api-free.deepl.com/v2/translate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `DeepL-Auth-Key ${process.env['NEXT_PUBLIC_DEEPL_API_KEY']}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: [text],
|
||||
target_lang: targetLang.toUpperCase(),
|
||||
source_lang: sourceLang === 'AUTO' ? undefined : sourceLang.toUpperCase(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch translation');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const translatedText = data.translations[0]?.text;
|
||||
const detectedSource = data.translations[0]?.detected_source_language;
|
||||
|
||||
if (!translatedText) {
|
||||
throw new Error('No translation found');
|
||||
}
|
||||
|
||||
if (sourceLang === 'AUTO' && detectedSource) {
|
||||
setDetectedSourceLang(detectedSource);
|
||||
}
|
||||
|
||||
setTranslation(translatedText);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError('Unable to fetch the translation. Try again later.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTranslation();
|
||||
}, [text, sourceLang, targetLang]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Popup
|
||||
trianglePosition={trianglePosition}
|
||||
width={popupWidth}
|
||||
height={popupHeight}
|
||||
position={position}
|
||||
className='bg-neutral select-text'
|
||||
triangleClassName='text-neutral'
|
||||
>
|
||||
<div className='text-neutral-content relative h-[50%] overflow-y-auto border-b border-neutral-400/75 p-4 font-sans'>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<h1 className='text-base font-semibold'>Original Text</h1>
|
||||
<select
|
||||
value={sourceLang}
|
||||
onChange={handleSourceLangChange}
|
||||
className='select h-8 min-h-8 rounded-md border-none bg-gray-400 text-sm focus:outline-none focus:ring-0'
|
||||
>
|
||||
{Object.entries(LANGUAGES).map(([code, name]) => {
|
||||
return (
|
||||
<option key={code} value={code}>
|
||||
{detectedSourceLang && sourceLang === 'AUTO' && code === 'AUTO'
|
||||
? `${LANGUAGES[detectedSourceLang] || detectedSourceLang} (detected)`
|
||||
: name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<p className='text-base'>{text}</p>
|
||||
</div>
|
||||
|
||||
<div className='text-neutral-content relative h-[50%] overflow-y-auto p-4 font-sans'>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<h2 className='text-base font-semibold'>Translated Text</h2>
|
||||
<select
|
||||
value={targetLang}
|
||||
onChange={handleTargetLangChange}
|
||||
className='select h-8 min-h-8 rounded-md border-none bg-gray-400 text-sm focus:outline-none focus:ring-0'
|
||||
>
|
||||
{Object.entries(LANGUAGES)
|
||||
.filter(([code]) => code !== 'AUTO')
|
||||
.map(([code, name]) => (
|
||||
<option key={code} value={code}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className='text-base italic text-gray-500'>Loading...</p>
|
||||
) : error ? (
|
||||
<p className='text-base text-red-600'>{error}</p>
|
||||
) : (
|
||||
<div>
|
||||
<p className='text-base'>{translation || 'No translation available.'}</p>
|
||||
<div className='pt-4 text-sm opacity-60'>
|
||||
Translated by{' '}
|
||||
<a href='https://www.deepl.com/' target='_blank' rel='noopener noreferrer'>
|
||||
DeepL
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeepLPopup;
|
||||
@@ -0,0 +1,128 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Popup from '@/components/Popup';
|
||||
import { Position } from '@/utils/sel';
|
||||
|
||||
interface WikipediaPopupProps {
|
||||
text: string;
|
||||
lang: string;
|
||||
position: Position;
|
||||
trianglePosition: Position;
|
||||
popupWidth: number;
|
||||
popupHeight: number;
|
||||
}
|
||||
|
||||
const WikipediaPopup: React.FC<WikipediaPopupProps> = ({
|
||||
text,
|
||||
lang,
|
||||
position,
|
||||
trianglePosition,
|
||||
popupWidth,
|
||||
popupHeight,
|
||||
}) => {
|
||||
const isLoading = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading.current) {
|
||||
return;
|
||||
}
|
||||
isLoading.current = true;
|
||||
|
||||
const main = document.querySelector('main') as HTMLElement;
|
||||
const footer = document.querySelector('footer') as HTMLElement;
|
||||
|
||||
const fetchSummary = async (query: string, language: string) => {
|
||||
main.innerHTML = '';
|
||||
footer.dataset['state'] = 'loading';
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://${language}.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(query)}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch Wikipedia summary');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const hgroup = document.createElement('hgroup');
|
||||
hgroup.style.color = 'white';
|
||||
hgroup.style.backgroundPosition = 'center center';
|
||||
hgroup.style.backgroundSize = 'cover';
|
||||
hgroup.style.backgroundColor = 'rgba(0, 0, 0, .4)';
|
||||
hgroup.style.backgroundBlendMode = 'darken';
|
||||
hgroup.style.borderRadius = '6px';
|
||||
hgroup.style.padding = '12px';
|
||||
hgroup.style.marginBottom = '12px';
|
||||
hgroup.style.minHeight = '100px';
|
||||
|
||||
const h1 = document.createElement('h1');
|
||||
h1.innerHTML = data.titles.display;
|
||||
h1.className = 'text-lg font-bold';
|
||||
hgroup.append(h1);
|
||||
|
||||
if (data.description) {
|
||||
const description = document.createElement('p');
|
||||
description.innerText = data.description;
|
||||
hgroup.appendChild(description);
|
||||
}
|
||||
|
||||
if (data.thumbnail) {
|
||||
hgroup.style.backgroundImage = `url("${data.thumbnail.source}")`;
|
||||
}
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.innerHTML = data.extract_html;
|
||||
contentDiv.className = 'p-2 text-sm';
|
||||
contentDiv.dir = data.dir;
|
||||
|
||||
main.append(hgroup, contentDiv);
|
||||
footer.dataset['state'] = 'loaded';
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
const errorDiv = document.createElement('div');
|
||||
const h1 = document.createElement('h1');
|
||||
h1.innerText = 'Error';
|
||||
|
||||
const errorMsg = document.createElement('p');
|
||||
errorMsg.innerHTML = `Unable to load the article. Try searching directly on <a href="https://${language}.wikipedia.org/w/index.php?search=${encodeURIComponent(
|
||||
query,
|
||||
)}" target="_blank" rel="noopener noreferrer" class="text-primary underline">Wikipedia</a>.`;
|
||||
|
||||
errorDiv.append(h1, errorMsg);
|
||||
main.appendChild(errorDiv);
|
||||
footer.dataset['state'] = 'error';
|
||||
}
|
||||
};
|
||||
|
||||
const langCode = typeof lang === 'string' ? lang : lang?.[0];
|
||||
fetchSummary(text, langCode);
|
||||
}, [text, lang]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Popup
|
||||
width={popupWidth}
|
||||
height={popupHeight}
|
||||
position={position}
|
||||
trianglePosition={trianglePosition}
|
||||
className='bg-neutral select-text overflow-y-auto'
|
||||
triangleClassName='text-neutral'
|
||||
>
|
||||
<main className='p-2 font-sans'></main>
|
||||
<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'>Wikipedia</a>, released under the{' '}
|
||||
<a href='https://en.wikipedia.org/wiki/Wikipedia:Text_of_the_Creative_Commons_Attribution-ShareAlike_4.0_International_License'>
|
||||
CC BY-SA License
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</footer>
|
||||
</Popup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WikipediaPopup;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Position } from '@/utils/sel';
|
||||
import Popup from '@/components/Popup';
|
||||
|
||||
type Definition = {
|
||||
definition: string;
|
||||
@@ -12,7 +13,7 @@ type Result = {
|
||||
language: string;
|
||||
};
|
||||
|
||||
interface WiktionaryProps {
|
||||
interface WiktionaryPopupProps {
|
||||
word: string;
|
||||
lang?: string;
|
||||
position: Position;
|
||||
@@ -21,7 +22,7 @@ interface WiktionaryProps {
|
||||
popupHeight: number;
|
||||
}
|
||||
|
||||
const WiktionaryPopup: React.FC<WiktionaryProps> = ({
|
||||
const WiktionaryPopup: React.FC<WiktionaryPopupProps> = ({
|
||||
word,
|
||||
lang,
|
||||
position,
|
||||
@@ -39,7 +40,7 @@ const WiktionaryPopup: React.FC<WiktionaryProps> = ({
|
||||
const links = container.querySelectorAll<HTMLAnchorElement>('a[rel="mw:WikiLink"]');
|
||||
|
||||
links.forEach((link) => {
|
||||
const title = link.getAttribute('title'); // Get the title attribute
|
||||
const title = link.getAttribute('title');
|
||||
if (title) {
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
@@ -157,26 +158,13 @@ const WiktionaryPopup: React.FC<WiktionaryProps> = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className='triangle text-neutral absolute z-10'
|
||||
style={{
|
||||
left: `${trianglePosition.point.x}px`,
|
||||
top: `${trianglePosition.point.y}px`,
|
||||
borderLeft: '6px solid transparent',
|
||||
borderRight: '6px solid transparent',
|
||||
borderBottom: trianglePosition.dir === 'up' ? 'none' : `6px solid`,
|
||||
borderTop: trianglePosition.dir === 'up' ? `6px solid` : 'none',
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className='bg-neutral absolute overflow-y-auto rounded-xl shadow-lg'
|
||||
style={{
|
||||
width: `${popupWidth}px`,
|
||||
height: `${popupHeight}px`,
|
||||
left: `${position.point.x}px`,
|
||||
top: `${position.point.y}px`,
|
||||
}}
|
||||
<Popup
|
||||
trianglePosition={trianglePosition}
|
||||
width={popupWidth}
|
||||
height={popupHeight}
|
||||
position={position}
|
||||
className='bg-neutral select-text overflow-y-auto'
|
||||
triangleClassName='text-neutral'
|
||||
>
|
||||
<main className='p-4 font-sans' />
|
||||
<footer className='hidden data-[state=loaded]:block data-[state=error]:hidden data-[state=loading]:hidden'>
|
||||
@@ -185,7 +173,7 @@ const WiktionaryPopup: React.FC<WiktionaryProps> = ({
|
||||
<a href='https://creativecommons.org/licenses/by-sa/4.0/'>CC BY-SA License</a>.
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</Popup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Position } from '@/utils/sel';
|
||||
|
||||
const Popup = ({
|
||||
width,
|
||||
height,
|
||||
position,
|
||||
trianglePosition,
|
||||
children,
|
||||
className = '',
|
||||
triangleClassName = '',
|
||||
additionalStyle = {},
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
position: Position;
|
||||
trianglePosition: Position;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
triangleClassName?: string;
|
||||
additionalStyle?: React.CSSProperties;
|
||||
}) => (
|
||||
<div>
|
||||
<div
|
||||
className={`triangle absolute z-10 ${triangleClassName}`}
|
||||
style={{
|
||||
left: `${trianglePosition.point.x}px`,
|
||||
top: `${trianglePosition.point.y}px`,
|
||||
borderLeft: '6px solid transparent',
|
||||
borderRight: '6px solid transparent',
|
||||
borderBottom: trianglePosition.dir === 'up' ? 'none' : `6px solid`,
|
||||
borderTop: trianglePosition.dir === 'up' ? `6px solid` : 'none',
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`absolute rounded-lg font-sans shadow-lg ${className}`}
|
||||
style={{
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
left: `${position.point.x}px`,
|
||||
top: `${position.point.y}px`,
|
||||
...additionalStyle,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Popup;
|
||||
Reference in New Issue
Block a user