From fd86dc15734f3b7126d88f0130897c597100e30a Mon Sep 17 00:00:00 2001
From: polwex <polwex@sortug.com>
Date: Thu, 15 May 2025 20:32:25 +0700
Subject: m

---
 src/zoom/Word.tsx | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 194 insertions(+)
 create mode 100644 src/zoom/Word.tsx

(limited to 'src/zoom/Word.tsx')

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);
-- 
cgit v1.2.3

