diff --git a/src/api/translations.js b/src/api/translations.js index 45ca1c5b5..efbee921e 100644 --- a/src/api/translations.js +++ b/src/api/translations.js @@ -244,3 +244,11 @@ 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}`).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 f6a2f242e..e9c28bd2c 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; @@ -87,6 +89,18 @@ export default function Translate() { setTitle("Translate"); }, []); + // Handle searchWord passed from history navigation + useEffect(() => { + if (location.state?.searchWord) { + const word = location.state.searchWord; + setSearchWord(word); + performSearch(word, true); // Skip logging - already in history + // 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) { return translation.toLowerCase(); } @@ -156,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, skipHistory = false) { + if (!word || !isValidWord(word)) { setError("Please enter a valid word or phrase"); return; } @@ -230,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) { @@ -258,6 +271,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 diff --git a/src/translate/TranslateHistory.js b/src/translate/TranslateHistory.js index 016b772b8..ce7f1c369 100644 --- a/src/translate/TranslateHistory.js +++ b/src/translate/TranslateHistory.js @@ -1,17 +1,83 @@ -import React from "react"; -import styled from "styled-components"; - -const ComingSoon = styled.div` - text-align: center; - padding: 3rem 1rem; - color: #666; - font-style: italic; -`; +import React, { useContext, useState, useEffect } from "react"; +import { useHistory } from "react-router-dom"; +import { formatDistanceToNow } from "date-fns"; +import { APIContext } from "../contexts/APIContext"; +import LoadingAnimation from "../components/LoadingAnimation"; +import { setTitle } from "../assorted/setTitle"; +import * as s from "./Translate.sc"; 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(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + 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 }); + } + + function formatTime(isoString) { + return formatDistanceToNow(new Date(isoString), { addSuffix: true }) + .replace("about ", ""); + } + + 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)} + style={{ cursor: "pointer" }} + > + + + {item.search_word} + + {formatTime(item.search_time)} + + + ))} + ); }