diff options
author | polwex <polwex@sortug.com> | 2025-05-15 20:32:25 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-05-15 20:32:25 +0700 |
commit | fd86dc15734f3b7126d88f0130897c597100e30a (patch) | |
tree | 253890a5f0bde7bc460904ce1743581f53a23d5b /src/zoom/Word.tsx | |
parent | 3d4b740e5a512db8fbdd934af2fbc9585fa00f0f (diff) |
m
Diffstat (limited to 'src/zoom/Word.tsx')
-rw-r--r-- | src/zoom/Word.tsx | 194 |
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); |