From bcda58db55c1e1b73ff571de2fcea1aa8c275e3a Mon Sep 17 00:00:00 2001 From: Mircea Filip Lungu Date: Tue, 17 Feb 2026 14:59:24 +0100 Subject: [PATCH 1/5] Add translation history UI for Translation Tab - Add getTranslationHistory() API method - Implement TranslateHistory component showing recent searches - Update Translate component to handle navigation from history - Clicking history items triggers search for that word Co-Authored-By: Claude Opus 4.5 --- src/api/translations.js | 4 + src/translate/Translate.js | 18 +++- src/translate/TranslateHistory.js | 165 +++++++++++++++++++++++++++++- 3 files changed, 181 insertions(+), 6 deletions(-) diff --git a/src/api/translations.js b/src/api/translations.js index 45ca1c5b5..dd52bc347 100644 --- a/src/api/translations.js +++ b/src/api/translations.js @@ -244,3 +244,7 @@ Zeeguu_API.prototype.basicTranlsate = function (from_lang, to_lang, phrase) { body: `phrase=${phrase}`, }); }; + +Zeeguu_API.prototype.getTranslationHistory = function (limit = 50) { + return this.apiGet(`/translation_history?limit=${limit}`); +}; diff --git a/src/translate/Translate.js b/src/translate/Translate.js index f6a2f242e..72c8a1871 100644 --- a/src/translate/Translate.js +++ b/src/translate/Translate.js @@ -1,4 +1,5 @@ import React, { useContext, useState, useEffect, useRef } from "react"; +import { useLocation } from "react-router-dom"; import { toast } from "react-toastify"; import { APIContext } from "../contexts/APIContext"; import { UserContext } from "../contexts/UserContext"; @@ -51,6 +52,7 @@ export default function Translate() { const api = useContext(APIContext); const { userDetails } = useContext(UserContext); const { updateExercisesCounter } = useContext(ExercisesCounterContext); + const location = useLocation(); const learnedLang = userDetails?.learned_language; const nativeLang = userDetails?.native_language; @@ -82,11 +84,25 @@ export default function Translate() { const { speak, isSpeaking } = useSpeech(); const searchWordRef = useRef(""); + const formRef = useRef(null); useEffect(() => { setTitle("Translate"); }, []); + // Handle searchWord passed from history navigation + useEffect(() => { + if (location.state?.searchWord) { + setSearchWord(location.state.searchWord); + // Trigger search after state is set + setTimeout(() => { + formRef.current?.requestSubmit(); + }, 0); + // Clear the state so refreshing doesn't re-trigger + window.history.replaceState({}, document.title); + } + }, [location.state]); + function getTranslationKey(translation) { return translation.toLowerCase(); } @@ -412,7 +428,7 @@ export default function Translate() { return ( <> -
+ Enter word or phrase {activeDirection && ( diff --git a/src/translate/TranslateHistory.js b/src/translate/TranslateHistory.js index 016b772b8..d385cb5f6 100644 --- a/src/translate/TranslateHistory.js +++ b/src/translate/TranslateHistory.js @@ -1,17 +1,172 @@ -import React from "react"; +import React, { useContext, useState, useEffect } from "react"; +import { useHistory } from "react-router-dom"; import styled from "styled-components"; +import { APIContext } from "../contexts/APIContext"; +import LoadingAnimation from "../components/LoadingAnimation"; +import { zeeguuOrange } from "../components/colors"; +import { setTitle } from "../assorted/setTitle"; -const ComingSoon = styled.div` +const Container = styled.div` + margin-top: 1rem; +`; + +const Header = styled.h3` + margin-bottom: 1rem; + font-weight: 500; + color: #333; +`; + +const HistoryList = styled.div` + display: flex; + flex-direction: column; + gap: 0.5rem; +`; + +const HistoryItem = styled.div` + background: white; + border: 1px solid #e0e0e0; + border-radius: 0.5rem; + padding: 0.75rem 1rem; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + transition: border-color 0.2s; + + &:hover { + border-color: ${zeeguuOrange}; + } +`; + +const WordInfo = styled.div` + flex: 1; +`; + +const SearchWord = styled.span` + font-weight: 600; + color: #333; + font-size: 1.1rem; +`; + +const Translation = styled.span` + color: #666; + margin-left: 0.5rem; +`; + +const Arrow = styled.span` + color: #999; + margin: 0 0.5rem; +`; + +const LanguageInfo = styled.div` + font-size: 0.8rem; + color: #888; + margin-top: 0.25rem; +`; + +const TimeStamp = styled.div` + font-size: 0.8rem; + color: #999; + white-space: nowrap; + margin-left: 1rem; +`; + +const EmptyState = styled.div` text-align: center; padding: 3rem 1rem; color: #666; font-style: italic; `; +function formatTimeAgo(isoString) { + const date = new Date(isoString); + const now = new Date(); + const diffMs = now - date; + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffMins < 1) return "just now"; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + + return date.toLocaleDateString(); +} + export default function TranslateHistory() { + const api = useContext(APIContext); + const history = useHistory(); + + const [historyItems, setHistoryItems] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + setTitle("Translation History"); + loadHistory(); + }, []); + + function loadHistory() { + setIsLoading(true); + setError(""); + + api.getTranslationHistory(50) + .then((data) => { + setHistoryItems(data); + setIsLoading(false); + }) + .catch((err) => { + console.error("Failed to load translation history:", err); + setError("Failed to load history"); + setIsLoading(false); + }); + } + + function handleItemClick(item) { + history.push("/translate", { searchWord: item.search_word }); + } + + if (isLoading) { + return ; + } + + if (error) { + return {error}; + } + + if (historyItems.length === 0) { + return ( + + No translation history yet. Search for words in the Translate tab to see them here. + + ); + } + return ( - - Translation history coming soon... - + +
Recent Searches
+ + {historyItems.map((item) => ( + handleItemClick(item)}> + +
+ {item.search_word} + {item.translation && ( + <> + + {item.translation} + + )} +
+ + {item.from_language} → {item.to_language} + +
+ {formatTimeAgo(item.search_time)} +
+ ))} +
+
); } From 054914acae642b8043855cd90b96da1b8d4380ea Mon Sep 17 00:00:00 2001 From: Mircea Filip Lungu Date: Tue, 17 Feb 2026 19:21:33 +0100 Subject: [PATCH 2/5] Fix translation history API return type and eslint warning - Extract res.data from apiGet response in getTranslationHistory - Add eslint-disable for intentional empty deps array in useEffect Co-Authored-By: Claude Opus 4.5 --- src/api/translations.js | 2 +- src/translate/TranslateHistory.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/translations.js b/src/api/translations.js index dd52bc347..98535dc0d 100644 --- a/src/api/translations.js +++ b/src/api/translations.js @@ -246,5 +246,5 @@ Zeeguu_API.prototype.basicTranlsate = function (from_lang, to_lang, phrase) { }; Zeeguu_API.prototype.getTranslationHistory = function (limit = 50) { - return this.apiGet(`/translation_history?limit=${limit}`); + return this.apiGet(`/translation_history?limit=${limit}`).then((res) => res.data); }; diff --git a/src/translate/TranslateHistory.js b/src/translate/TranslateHistory.js index d385cb5f6..6cce82d1c 100644 --- a/src/translate/TranslateHistory.js +++ b/src/translate/TranslateHistory.js @@ -105,6 +105,7 @@ export default function TranslateHistory() { useEffect(() => { setTitle("Translation History"); loadHistory(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); function loadHistory() { From a2c0a2614c84a32db090fcce7b7040001cbb519d Mon Sep 17 00:00:00 2001 From: Mircea Filip Lungu Date: Tue, 17 Feb 2026 19:31:13 +0100 Subject: [PATCH 3/5] Refactor: call performSearch directly instead of setTimeout hack - Extract search logic into performSearch(word) function - Call performSearch directly from history navigation useEffect - Remove formRef since form submission is no longer needed for auto-search Co-Authored-By: Claude Opus 4.5 --- src/translate/Translate.js | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/translate/Translate.js b/src/translate/Translate.js index 72c8a1871..f849bd847 100644 --- a/src/translate/Translate.js +++ b/src/translate/Translate.js @@ -84,7 +84,6 @@ export default function Translate() { const { speak, isSpeaking } = useSpeech(); const searchWordRef = useRef(""); - const formRef = useRef(null); useEffect(() => { setTitle("Translate"); @@ -93,14 +92,13 @@ export default function Translate() { // Handle searchWord passed from history navigation useEffect(() => { if (location.state?.searchWord) { - setSearchWord(location.state.searchWord); - // Trigger search after state is set - setTimeout(() => { - formRef.current?.requestSubmit(); - }, 0); + const word = location.state.searchWord; + setSearchWord(word); + performSearch(word); // Clear the state so refreshing doesn't re-trigger window.history.replaceState({}, document.title); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.state]); function getTranslationKey(translation) { @@ -172,14 +170,8 @@ export default function Translate() { return true; } - function handleSearch(e) { - e.preventDefault(); - if (!searchWord.trim()) return; - - const word = searchWord.trim(); - - // Validate input - if (!isValidWord(word)) { + function performSearch(word) { + if (!word || !isValidWord(word)) { setError("Please enter a valid word or phrase"); return; } @@ -274,6 +266,13 @@ export default function Translate() { }); } + function handleSearch(e) { + e.preventDefault(); + const word = searchWord.trim(); + if (!word) return; + performSearch(word); + } + // displayKey: the key used for caching (based on what's displayed in UI) // word: the word in learned language (for generating examples) // translation: the translation in native language @@ -428,7 +427,7 @@ export default function Translate() { return ( <> - + Enter word or phrase {activeDirection && ( From 112dc34cbd9226290b46f2c82d9efebdae3b9e5f Mon Sep 17 00:00:00 2001 From: Mircea Filip Lungu Date: Tue, 17 Feb 2026 20:07:41 +0100 Subject: [PATCH 4/5] Refactor TranslateHistory: use date-fns and existing styles - Use formatDistanceToNow from date-fns instead of custom formatTimeAgo - Reuse styles from Translate.sc.js (TranslationCard, NoResults, etc.) - Reduced from 173 to 93 lines Co-Authored-By: Claude Opus 4.5 --- src/translate/TranslateHistory.js | 148 +++++++----------------------- 1 file changed, 34 insertions(+), 114 deletions(-) diff --git a/src/translate/TranslateHistory.js b/src/translate/TranslateHistory.js index 6cce82d1c..f127feada 100644 --- a/src/translate/TranslateHistory.js +++ b/src/translate/TranslateHistory.js @@ -1,98 +1,10 @@ import React, { useContext, useState, useEffect } from "react"; import { useHistory } from "react-router-dom"; -import styled from "styled-components"; +import { formatDistanceToNow } from "date-fns"; import { APIContext } from "../contexts/APIContext"; import LoadingAnimation from "../components/LoadingAnimation"; -import { zeeguuOrange } from "../components/colors"; import { setTitle } from "../assorted/setTitle"; - -const Container = styled.div` - margin-top: 1rem; -`; - -const Header = styled.h3` - margin-bottom: 1rem; - font-weight: 500; - color: #333; -`; - -const HistoryList = styled.div` - display: flex; - flex-direction: column; - gap: 0.5rem; -`; - -const HistoryItem = styled.div` - background: white; - border: 1px solid #e0e0e0; - border-radius: 0.5rem; - padding: 0.75rem 1rem; - display: flex; - justify-content: space-between; - align-items: center; - cursor: pointer; - transition: border-color 0.2s; - - &:hover { - border-color: ${zeeguuOrange}; - } -`; - -const WordInfo = styled.div` - flex: 1; -`; - -const SearchWord = styled.span` - font-weight: 600; - color: #333; - font-size: 1.1rem; -`; - -const Translation = styled.span` - color: #666; - margin-left: 0.5rem; -`; - -const Arrow = styled.span` - color: #999; - margin: 0 0.5rem; -`; - -const LanguageInfo = styled.div` - font-size: 0.8rem; - color: #888; - margin-top: 0.25rem; -`; - -const TimeStamp = styled.div` - font-size: 0.8rem; - color: #999; - white-space: nowrap; - margin-left: 1rem; -`; - -const EmptyState = styled.div` - text-align: center; - padding: 3rem 1rem; - color: #666; - font-style: italic; -`; - -function formatTimeAgo(isoString) { - const date = new Date(isoString); - const now = new Date(); - const diffMs = now - date; - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffMins < 1) return "just now"; - if (diffMins < 60) return `${diffMins}m ago`; - if (diffHours < 24) return `${diffHours}h ago`; - if (diffDays < 7) return `${diffDays}d ago`; - - return date.toLocaleDateString(); -} +import * as s from "./Translate.sc"; export default function TranslateHistory() { const api = useContext(APIContext); @@ -128,46 +40,54 @@ export default function TranslateHistory() { history.push("/translate", { searchWord: item.search_word }); } + function formatTime(isoString) { + return formatDistanceToNow(new Date(isoString), { addSuffix: true }) + .replace("about ", ""); + } + if (isLoading) { return ; } if (error) { - return {error}; + return {error}; } if (historyItems.length === 0) { return ( - + No translation history yet. Search for words in the Translate tab to see them here. - + ); } return ( - -
Recent Searches
- - {historyItems.map((item) => ( - handleItemClick(item)}> - -
- {item.search_word} + + Recent Searches + {historyItems.map((item) => ( + handleItemClick(item)} + style={{ cursor: "pointer" }} + > + + + + {item.search_word} {item.translation && ( - <> - - {item.translation} - + + → {item.translation} + )} -
- + + {item.from_language} → {item.to_language} - -
- {formatTimeAgo(item.search_time)} -
- ))} -
-
+ + + {formatTime(item.search_time)} + + + ))} + ); } From e9720b62ca9facc14cd46c622533c2f846427755 Mon Sep 17 00:00:00 2001 From: Mircea Filip Lungu Date: Tue, 17 Feb 2026 20:24:24 +0100 Subject: [PATCH 5/5] Simplify translation history: log search_word, skip re-searches - logTranslationSearch now sends search_word (not meaning_id) - performSearch takes skipHistory param - true when from history - Simplified history display (just search_word + time) - Uses date-fns and existing styles from Translate.sc.js Co-Authored-By: Claude Opus 4.5 --- src/api/translations.js | 4 ++++ src/translate/Translate.js | 9 +++++++-- src/translate/TranslateHistory.js | 12 +----------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/api/translations.js b/src/api/translations.js index 98535dc0d..efbee921e 100644 --- a/src/api/translations.js +++ b/src/api/translations.js @@ -248,3 +248,7 @@ Zeeguu_API.prototype.basicTranlsate = function (from_lang, to_lang, phrase) { Zeeguu_API.prototype.getTranslationHistory = function (limit = 50) { return this.apiGet(`/translation_history?limit=${limit}`).then((res) => res.data); }; + +Zeeguu_API.prototype.logTranslationSearch = function (searchWord) { + return this._post("log_translation_search", `search_word=${encodeURIComponent(searchWord)}`); +}; diff --git a/src/translate/Translate.js b/src/translate/Translate.js index f849bd847..e9c28bd2c 100644 --- a/src/translate/Translate.js +++ b/src/translate/Translate.js @@ -94,7 +94,7 @@ export default function Translate() { if (location.state?.searchWord) { const word = location.state.searchWord; setSearchWord(word); - performSearch(word); + performSearch(word, true); // Skip logging - already in history // Clear the state so refreshing doesn't re-trigger window.history.replaceState({}, document.title); } @@ -170,7 +170,7 @@ export default function Translate() { return true; } - function performSearch(word) { + function performSearch(word, skipHistory = false) { if (!word || !isValidWord(word)) { setError("Please enter a valid word or phrase"); return; @@ -238,6 +238,11 @@ export default function Translate() { setTranslations(finalTranslations); setActiveDirection(direction); + // Log to history (only for new searches, not from history navigation) + if (!skipHistory && finalTranslations.length > 0) { + api.logTranslationSearch(word); + } + // Auto-fetch examples for each translation (skip for long phrases) const wordCount = word.split(/\s+/).length; if (wordCount <= 3 && finalTranslations.length > 0) { diff --git a/src/translate/TranslateHistory.js b/src/translate/TranslateHistory.js index f127feada..ce7f1c369 100644 --- a/src/translate/TranslateHistory.js +++ b/src/translate/TranslateHistory.js @@ -72,17 +72,7 @@ export default function TranslateHistory() { > - - {item.search_word} - {item.translation && ( - - → {item.translation} - - )} - - - {item.from_language} → {item.to_language} - + {item.search_word} {formatTime(item.search_time)}