summaryrefslogtreecommitdiff
path: root/src/zoom/Word.tsx
diff options
context:
space:
mode:
authorpolwex <polwex@sortug.com>2025-05-15 20:32:25 +0700
committerpolwex <polwex@sortug.com>2025-05-15 20:32:25 +0700
commitfd86dc15734f3b7126d88f0130897c597100e30a (patch)
tree253890a5f0bde7bc460904ce1743581f53a23d5b /src/zoom/Word.tsx
parent3d4b740e5a512db8fbdd934af2fbc9585fa00f0f (diff)
m
Diffstat (limited to 'src/zoom/Word.tsx')
-rw-r--r--src/zoom/Word.tsx194
1 files changed, 194 insertions, 0 deletions
diff --git a/src/zoom/Word.tsx b/src/zoom/Word.tsx
new file mode 100644
index 0000000..c665004
--- /dev/null
+++ b/src/zoom/Word.tsx
@@ -0,0 +1,194 @@
+import React, { memo, useCallback, useEffect, useState } from "react";
+import { motion } from "motion/react";
+import { ViewProps, LoadingStatus, WordData } from "./logic/types";
+// import { fetchWord } from "../logic/calls";
+import { wordVariants, createHoverEffect } from "./animations";
+import { useZoom } from "./hooks/useZoom";
+import { NLP } from "sortug-ai";
+
+interface Props extends ViewProps {
+ word: NLP.Stanza.Word;
+}
+
+function Word({ rawText, context, idx, word }: Props) {
+ const { viewState, handleElementClick } = useZoom();
+ const { level, wIndex } = viewState;
+ const selected = wIndex === idx;
+ const isFocused = level === "word" && selected;
+
+ // State for word data
+ const [loading, setLoading] = useState<LoadingStatus>("pending");
+ const [wordData, setData] = useState<WordData | null>(null);
+ const [error, setError] = useState<string | null>(null);
+
+ // Fetch word details when selected
+ const getMeaning = useCallback(() => {
+ setLoading("loading");
+
+ // Try to fetch the word data
+ // fetchWord(rawText, "en")
+ // .then((res) => {
+ // if ("error" in res) {
+ // setError(`Error loading word data: ${res.error}`);
+ // setLoading("error");
+ // } else {
+ // setData(res.ok);
+ // setLoading("success");
+ // }
+ // })
+ // .catch((err) => {
+ // setError(`Failed to fetch word data: ${err.message}`);
+ // setLoading("error");
+ // });
+ }, [rawText]);
+
+ // Load word data when the word is selected
+ useEffect(() => {
+ if (isFocused && !wordData && loading === "pending") {
+ getMeaning();
+ }
+ }, [isFocused, getMeaning, wordData, loading]);
+
+ return (
+ <>
+ {/* Overlay backdrop when word is selected */}
+ {isFocused && <div className="word-backdrop" aria-hidden="true"></div>}
+
+ <motion.div
+ key={idx + rawText}
+ className={`word-container ${selected ? "selected" : ""}`}
+ custom={selected}
+ variants={wordVariants}
+ initial="clause"
+ animate={level}
+ onClick={(e) => handleElementClick(e, idx)}
+ whileHover={
+ level === "clause"
+ ? createHoverEffect(level, "clause", "255, 200, 200")
+ : {}
+ }
+ style={{
+ backgroundColor: isFocused ? "white" : undefined,
+ boxShadow: isFocused ? "0 8px 32px rgba(0, 0, 0, 0.15)" : undefined,
+ }}
+ >
+ {level === "clause" || !selected ? (
+ <span className="word">{rawText}</span>
+ ) : (
+ <div className="word-details-wrapper">
+ {loading === "loading" && (
+ <div className="word-loading">
+ <div className="spinner" />
+ <p>Loading word information...</p>
+ </div>
+ )}
+
+ {loading === "error" && (
+ <div className="word-error">
+ <p>{error || "Failed to load word information"}</p>
+ </div>
+ )}
+
+ {loading === "success" && wordData && (
+ <div className="word-content">
+ <div className="word-header">
+ <h2 className="word-title">{wordData.spelling}</h2>
+
+ {/* Syllables section moved to header - for next level of zoom */}
+ <div className="syllables-compact">
+ {Array.from({ length: wordData.syllables || 1 }).map(
+ (_, i) => {
+ // Create a simple syllable division (not linguistically accurate)
+ const syllableLength = Math.ceil(
+ rawText.length / (wordData.syllables || 1),
+ );
+ const start = i * syllableLength;
+ const end = Math.min(
+ start + syllableLength,
+ rawText.length,
+ );
+ const syllable = rawText.substring(start, end);
+
+ return (
+ <motion.div
+ key={i}
+ className="syllable"
+ whileHover={{
+ scale: 1.1,
+ backgroundColor: "rgba(200, 255, 200, 0.4)",
+ }}
+ onClick={(e) => {
+ e.stopPropagation();
+ handleElementClick(e, i);
+ }}
+ >
+ {syllable}
+ </motion.div>
+ );
+ },
+ )}
+ </div>
+ </div>
+
+ {/* Pronunciation */}
+ {wordData.ipa && wordData.ipa.length > 0 && (
+ <div className="word-phonetics">
+ <h3>Pronunciation</h3>
+ {wordData.ipa.map((pronunciation, i) => (
+ <div key={i} className="pronunciation-item">
+ <span className="ipa">{pronunciation.ipa}</span>
+ {pronunciation.tags &&
+ pronunciation.tags.length > 0 && (
+ <span className="pronunciation-tags">
+ {pronunciation.tags.join(", ")}
+ </span>
+ )}
+ </div>
+ ))}
+ </div>
+ )}
+
+ {/* Word Senses/Meanings */}
+ {wordData.senses && wordData.senses.length > 0 && (
+ <div className="word-meanings">
+ <h3>Meanings</h3>
+
+ {wordData.senses.map((sense, i) => (
+ <div key={i} className="sense-container">
+ <div className="sense-header">
+ {sense.pos && (
+ <span className="pos-tag">{sense.pos}</span>
+ )}
+ {sense.etymology && (
+ <span className="etymology">{sense.etymology}</span>
+ )}
+ </div>
+
+ {sense.senses && sense.senses.length > 0 && (
+ <ul className="sense-list">
+ {sense.senses.map((subSense, j) => (
+ <li key={j} className="sense-item">
+ {subSense.glosses &&
+ subSense.glosses.length > 0 && (
+ <div className="glosses">
+ {subSense.glosses.join("; ")}
+ </div>
+ )}
+ </li>
+ ))}
+ </ul>
+ )}
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+ )}
+ </motion.div>
+ </>
+ );
+}
+
+export default memo(Word);