From bb023465bd4b8e8fefa721262b37b5b4b1e7a20f Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Thu, 2 Apr 2026 18:13:06 +0200 Subject: [PATCH 01/23] Add typed suggestion selector for audio lessons (Words only / Topic / Situation) Replace the free-text topic input with a pill-based type selector that distinguishes between plain word lessons, topic-themed lessons, and situational roleplay lessons. Font size bumped to 16px to prevent mobile zoom, maxLength increased to 48 chars. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/api/dailyAudio.js | 5 +- src/dailyAudio/TodayAudio.js | 89 +++++++++++++++++++--------- src/dailyAudio/TopicSuggestion.sc.js | 53 +++++++++++++++++ 3 files changed, 119 insertions(+), 28 deletions(-) create mode 100644 src/dailyAudio/TopicSuggestion.sc.js diff --git a/src/api/dailyAudio.js b/src/api/dailyAudio.js index 6688ed7be..05bd28dc7 100644 --- a/src/api/dailyAudio.js +++ b/src/api/dailyAudio.js @@ -43,7 +43,7 @@ Zeeguu_API.prototype.getTodaysLesson = function (callback, onError) { }); }; -Zeeguu_API.prototype.generateDailyLesson = function (callback, onError, topicSuggestion) { +Zeeguu_API.prototype.generateDailyLesson = function (callback, onError, topicSuggestion, suggestionType) { this.apiLog("POST generate_daily_lesson"); const formData = new FormData(); @@ -51,6 +51,9 @@ Zeeguu_API.prototype.generateDailyLesson = function (callback, onError, topicSug formData.append("timezone_offset", timezoneOffset); if (topicSuggestion) { formData.append("topic_suggestion", topicSuggestion); + if (suggestionType) { + formData.append("suggestion_type", suggestionType); + } } fetch(this._appendSessionToUrl("generate_daily_lesson"), { diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index 99c073170..cefded67d 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -11,7 +11,19 @@ import { FEEDBACK_OPTIONS, FEEDBACK_CODES_NAME } from "../components/FeedbackCon import Word from "../words/Word"; import useListeningSession from "../hooks/useListeningSession"; import { AUDIO_STATUS, GENERATION_PROGRESS } from "./AudioLessonConstants"; - +import { + SuggestionWrapper, + PillRow, + TypePill, + SuggestionInput, + HintText, +} from "./TopicSuggestion.sc"; + +const SUGGESTION_TYPES = { + words: { label: "Words only", placeholder: null, hint: null }, + topic: { label: "Topic", placeholder: "e.g. cooking, sports", hint: "The lesson will revolve around this theme" }, + situation: { label: "Situation", placeholder: "e.g. at a restaurant, job interview", hint: "The lesson will be a roleplay in this scenario" }, +}; export function wordsAsTile(words) { if (!words || !words.length) return ""; @@ -23,6 +35,7 @@ export function wordsAsTile(words) { } const TOPIC_STORAGE_KEY_PREFIX = "zeeguu_lesson_topic_"; +const TOPIC_TYPE_STORAGE_KEY_PREFIX = "zeeguu_lesson_topic_type_"; export default function TodayAudio({ setShowTabs }) { const api = useContext(APIContext); @@ -34,6 +47,9 @@ export default function TodayAudio({ setShowTabs }) { const [topicSuggestion, setTopicSuggestion] = useState( () => localStorage.getItem(TOPIC_STORAGE_KEY_PREFIX + lang) || "", ); + const [suggestionType, setSuggestionType] = useState( + () => localStorage.getItem(TOPIC_TYPE_STORAGE_KEY_PREFIX + lang) || "words", + ); // Poll for progress when generating useEffect(() => { @@ -281,6 +297,7 @@ export default function TodayAudio({ setShowTabs }) { localStorage.setItem(generatingKey, "true"); const trimmedTopic = topicSuggestion.trim() || null; + const typeToSend = trimmedTopic && suggestionType !== "words" ? suggestionType : null; api.generateDailyLesson( (data) => { if (data.status === AUDIO_STATUS.GENERATING) { @@ -324,6 +341,7 @@ export default function TodayAudio({ setShowTabs }) { setError(errorMsg); }, trimmedTopic, + typeToSend, ); }; @@ -473,32 +491,49 @@ export default function TodayAudio({ setShowTabs }) {

Generate a personalized audio lesson based on the words you're learning.

- { - const val = e.target.value; - setTopicSuggestion(val); - if (val.trim()) { - localStorage.setItem(TOPIC_STORAGE_KEY_PREFIX + lang, val); - } else { - localStorage.removeItem(TOPIC_STORAGE_KEY_PREFIX + lang); - } - }} - style={{ - width: "100%", - maxWidth: "300px", - padding: "8px 12px", - border: "1px solid var(--border-light)", - borderRadius: "4px", - fontSize: "14px", - color: "var(--text-primary)", - backgroundColor: "var(--bg-secondary)", - textAlign: "center", - }} - /> + + + {Object.entries(SUGGESTION_TYPES).map(([key, { label }]) => ( + { + if (suggestionType === key) return; + setSuggestionType(key); + localStorage.setItem(TOPIC_TYPE_STORAGE_KEY_PREFIX + lang, key); + if (key === "words") { + setTopicSuggestion(""); + localStorage.removeItem(TOPIC_STORAGE_KEY_PREFIX + lang); + } + }} + > + {label} + + ))} + +
+ { + const val = e.target.value; + setTopicSuggestion(val); + if (val.trim()) { + localStorage.setItem(TOPIC_STORAGE_KEY_PREFIX + lang, val); + } else { + localStorage.removeItem(TOPIC_STORAGE_KEY_PREFIX + lang); + } + }} + /> + {SUGGESTION_TYPES[suggestionType]?.hint || "\u00A0"} +
+
); } diff --git a/src/dailyAudio/TopicSuggestion.sc.js b/src/dailyAudio/TopicSuggestion.sc.js new file mode 100644 index 000000000..1ffed1f57 --- /dev/null +++ b/src/dailyAudio/TopicSuggestion.sc.js @@ -0,0 +1,53 @@ +import styled from "styled-components"; +import { blue100, blue700, blue900, lightGrey } from "../components/colors"; + +export const SuggestionWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + width: 100%; + max-width: 300px; +`; + +export const PillRow = styled.div` + display: flex; + gap: 0.4rem; +`; + +export const TypePill = styled.button` + padding: 0.4rem 1rem; + border-radius: 2rem; + border: 1.5px solid ${({ $selected }) => ($selected ? blue700 : lightGrey)}; + background: ${({ $selected }) => ($selected ? blue100 : "var(--bg-secondary)")}; + color: ${({ $selected }) => ($selected ? blue900 : "var(--text-primary)")}; + font-family: inherit; + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + transition: border-color 150ms, background-color 150ms; + + &:active { + transform: scale(0.96); + transition: transform 80ms; + } +`; + +export const SuggestionInput = styled.input` + width: 100%; + padding: 8px 12px; + border: 1px solid var(--border-light); + border-radius: 4px; + font-size: 16px; + font-family: inherit; + color: var(--text-primary); + background-color: var(--bg-secondary); + text-align: center; +`; + +export const HintText = styled.p` + font-size: 0.75rem; + color: var(--text-secondary); + margin: 0; + text-align: center; +`; From a0213ba049ccbcdc53eefb129c02a7d3b854e079 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 13:28:33 +0200 Subject: [PATCH 02/23] Show character count near input limit Display N/48 counter when suggestion text reaches 40+ chars. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/TodayAudio.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index cefded67d..b538082d5 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -531,7 +531,11 @@ export default function TodayAudio({ setShowTabs }) { } }} /> - {SUGGESTION_TYPES[suggestionType]?.hint || "\u00A0"} + + {topicSuggestion.length >= 40 + ? `${topicSuggestion.length}/48` + : SUGGESTION_TYPES[suggestionType]?.hint || "\u00A0"} + From 9f127fb2dc828e9cd6e20357eca4fea37016291a Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 13:35:19 +0200 Subject: [PATCH 03/23] Extract MAX_SUGGESTION_LENGTH constant to avoid magic numbers Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/TodayAudio.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index b538082d5..7c639cd9d 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -19,6 +19,8 @@ import { HintText, } from "./TopicSuggestion.sc"; +const MAX_SUGGESTION_LENGTH = 48; + const SUGGESTION_TYPES = { words: { label: "Words only", placeholder: null, hint: null }, topic: { label: "Topic", placeholder: "e.g. cooking, sports", hint: "The lesson will revolve around this theme" }, @@ -518,7 +520,7 @@ export default function TodayAudio({ setShowTabs }) { { @@ -532,8 +534,8 @@ export default function TodayAudio({ setShowTabs }) { }} /> - {topicSuggestion.length >= 40 - ? `${topicSuggestion.length}/48` + {topicSuggestion.length >= MAX_SUGGESTION_LENGTH - 8 + ? `${topicSuggestion.length}/${MAX_SUGGESTION_LENGTH}` : SUGGESTION_TYPES[suggestionType]?.hint || "\u00A0"} From 0d5ed0c0bffef012c001cd0e1b91c377bc311c59 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 13:51:43 +0200 Subject: [PATCH 04/23] UI polish: textarea input, inline Context label, bump limit to 80 - Switch input to textarea so text wraps on mobile - Move Context: label inline with pills - Drop separate "Context:" heading - Rename "Words only" to "Automatic" - Update description to mention dialogues - Self-contained hint texts per type - Increase max suggestion length from 48 to 80 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/TodayAudio.js | 25 +++++++++++++------------ src/dailyAudio/TopicSuggestion.sc.js | 6 +++++- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index 7c639cd9d..6ff9e81aa 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -19,12 +19,12 @@ import { HintText, } from "./TopicSuggestion.sc"; -const MAX_SUGGESTION_LENGTH = 48; +const MAX_SUGGESTION_LENGTH = 80; const SUGGESTION_TYPES = { - words: { label: "Words only", placeholder: null, hint: null }, - topic: { label: "Topic", placeholder: "e.g. cooking, sports", hint: "The lesson will revolve around this theme" }, - situation: { label: "Situation", placeholder: "e.g. at a restaurant, job interview", hint: "The lesson will be a roleplay in this scenario" }, + auto: { label: "Automatic", placeholder: null, hint: null }, + topic: { label: "Topic", placeholder: "e.g. cooking, sports", hint: "Dialogues will be themed around this topic" }, + situation: { label: "Situation", placeholder: "e.g. at a restaurant, job interview", hint: "Dialogues will simulate this real-life scenario" }, }; export function wordsAsTile(words) { @@ -50,7 +50,7 @@ export default function TodayAudio({ setShowTabs }) { () => localStorage.getItem(TOPIC_STORAGE_KEY_PREFIX + lang) || "", ); const [suggestionType, setSuggestionType] = useState( - () => localStorage.getItem(TOPIC_TYPE_STORAGE_KEY_PREFIX + lang) || "words", + () => localStorage.getItem(TOPIC_TYPE_STORAGE_KEY_PREFIX + lang) || "auto", ); // Poll for progress when generating @@ -299,7 +299,7 @@ export default function TodayAudio({ setShowTabs }) { localStorage.setItem(generatingKey, "true"); const trimmedTopic = topicSuggestion.trim() || null; - const typeToSend = trimmedTopic && suggestionType !== "words" ? suggestionType : null; + const typeToSend = trimmedTopic && suggestionType !== "auto" ? suggestionType : null; api.generateDailyLesson( (data) => { if (data.status === AUDIO_STATUS.GENERATING) { @@ -491,10 +491,11 @@ export default function TodayAudio({ setShowTabs }) { Daily Lesson

- Generate a personalized audio lesson based on the words you're learning. + A listening lesson with three of your words, each practiced in a short dialogue.

- + + Context: {Object.entries(SUGGESTION_TYPES).map(([key, { label }]) => ( ))} -
+
{ const val = e.target.value; setTopicSuggestion(val); diff --git a/src/dailyAudio/TopicSuggestion.sc.js b/src/dailyAudio/TopicSuggestion.sc.js index 1ffed1f57..81dac0ff9 100644 --- a/src/dailyAudio/TopicSuggestion.sc.js +++ b/src/dailyAudio/TopicSuggestion.sc.js @@ -33,7 +33,7 @@ export const TypePill = styled.button` } `; -export const SuggestionInput = styled.input` +export const SuggestionInput = styled.textarea` width: 100%; padding: 8px 12px; border: 1px solid var(--border-light); @@ -43,6 +43,10 @@ export const SuggestionInput = styled.input` color: var(--text-primary); background-color: var(--bg-secondary); text-align: center; + resize: none; + overflow: hidden; + min-height: 2.4rem; + field-sizing: content; `; export const HintText = styled.p` From cf216cbc9b2e2a21449cd377219b3cb73a0126a4 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 14:22:28 +0200 Subject: [PATCH 05/23] UI polish: extract styled-components, fix layout centering - Extract GenerateButton, DescriptionText, InputArea to styled-components - Replace buggy e.target.style mouse handlers with CSS :hover/:active - Dynamic description text based on selected pill type - Vertically center generate view with calc(100vh - 260px) - Textarea input with newline stripping, flex-wrap for narrow screens - Clean up unused color imports Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/TodayAudio.js | 93 +++++++++++----------------- src/dailyAudio/TopicSuggestion.sc.js | 59 ++++++++++++++++-- 2 files changed, 91 insertions(+), 61 deletions(-) diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index 6ff9e81aa..f31095411 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useState } from "react"; -import { orange500, orange600, orange800, zeeguuOrange } from "../components/colors"; +import { orange500, zeeguuOrange } from "../components/colors"; import { APIContext } from "../contexts/APIContext"; import { UserContext } from "../contexts/UserContext"; import LoadingAnimation from "../components/LoadingAnimation"; @@ -17,14 +17,29 @@ import { TypePill, SuggestionInput, HintText, + GenerateButton, + DescriptionText, + InputArea, } from "./TopicSuggestion.sc"; const MAX_SUGGESTION_LENGTH = 80; const SUGGESTION_TYPES = { - auto: { label: "Automatic", placeholder: null, hint: null }, - topic: { label: "Topic", placeholder: "e.g. cooking, sports", hint: "Dialogues will be themed around this topic" }, - situation: { label: "Situation", placeholder: "e.g. at a restaurant, job interview", hint: "Dialogues will simulate this real-life scenario" }, + auto: { + label: "Automatic", + description: "A listening lesson with three of your words, each in a short dialogue.", + placeholder: null, + }, + topic: { + label: "Topic", + description: "A listening lesson with three of your words, each practiced in a short dialogue on a given topic.", + placeholder: "e.g. cooking, sports", + }, + situation: { + label: "Situation", + description: "A listening lesson with three of your words, each practiced in a dialogue simulating a real-world scenario.", + placeholder: "e.g. at a restaurant, job interview", + }, }; export function wordsAsTile(words) { @@ -439,7 +454,7 @@ export default function TodayAudio({ setShowTabs }) { flexDirection: "column", alignItems: "center", justifyContent: "center", - minHeight: "400px", + minHeight: "calc(100vh - 260px)", }} > {error && ( @@ -447,55 +462,13 @@ export default function TodayAudio({ setShowTabs }) { {error} )} - -

- A listening lesson with three of your words, each practiced in a short dialogue. -

+ - Context: {Object.entries(SUGGESTION_TYPES).map(([key, { label }]) => ( ))} -
+ + {SUGGESTION_TYPES[suggestionType].description} + + { - const val = e.target.value; + const val = e.target.value.replace(/\n/g, " "); setTopicSuggestion(val); if (val.trim()) { localStorage.setItem(TOPIC_STORAGE_KEY_PREFIX + lang, val); @@ -537,9 +513,9 @@ export default function TodayAudio({ setShowTabs }) { {topicSuggestion.length >= MAX_SUGGESTION_LENGTH - 8 ? `${topicSuggestion.length}/${MAX_SUGGESTION_LENGTH}` - : SUGGESTION_TYPES[suggestionType]?.hint || "\u00A0"} + : "\u00A0"} -
+
); @@ -558,12 +534,15 @@ export default function TodayAudio({ setShowTabs }) { return (
-

+

{lessonData.is_completed && } - {lessonData.topic_suggestion - ? `${lessonData.topic_suggestion}: ${wordsAsTile(words)}` - : wordsAsTile(words)} + {wordsAsTile(words)}

+ {lessonData.topic_suggestion && ( +

+ {lessonData.suggestion_type === "situation" ? "Situation" : "Topic"}: {lessonData.topic_suggestion} +

+ )} {error &&
{error}
} diff --git a/src/dailyAudio/TopicSuggestion.sc.js b/src/dailyAudio/TopicSuggestion.sc.js index 81dac0ff9..704dfbf90 100644 --- a/src/dailyAudio/TopicSuggestion.sc.js +++ b/src/dailyAudio/TopicSuggestion.sc.js @@ -1,13 +1,13 @@ import styled from "styled-components"; -import { blue100, blue700, blue900, lightGrey } from "../components/colors"; +import { blue100, blue700, blue900, lightGrey, orange500, orange600, orange800 } from "../components/colors"; export const SuggestionWrapper = styled.div` display: flex; flex-direction: column; align-items: center; - gap: 0.5rem; + gap: 0.8rem; width: 100%; - max-width: 300px; + max-width: 360px; `; export const PillRow = styled.div` @@ -44,11 +44,62 @@ export const SuggestionInput = styled.textarea` background-color: var(--bg-secondary); text-align: center; resize: none; - overflow: hidden; min-height: 2.4rem; field-sizing: content; `; +export const GenerateButton = styled.button` + width: 150px; + height: 150px; + border-radius: 50%; + background-color: ${orange500}; + color: white; + border: none; + font-size: 16px; + font-weight: 600; + cursor: pointer; + box-shadow: 0px 0.3rem ${orange800}; + transition: all 0.3s ease-in-out; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + line-height: 1.2; + margin-bottom: 30px; + + &:hover { + background-color: ${orange600}; + } + + &:active { + box-shadow: none; + transform: translateY(0.2em); + transition: all 0.08s ease-in; + } +`; + +export const DescriptionText = styled.p` + font-size: 0.9rem; + color: var(--text-secondary); + margin: 0; + margin-top: 0.3em; + text-align: center; + max-width: 500px; + height: 4.5em; + display: flex; + align-items: center; + justify-content: center; +`; + +export const InputArea = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + width: 100%; + visibility: ${({ $hidden }) => ($hidden ? "hidden" : "visible")}; +`; + export const HintText = styled.p` font-size: 0.75rem; color: var(--text-secondary); From 017162148d1b8a731178c34dd23fe1c1609081d0 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 14:37:18 +0200 Subject: [PATCH 06/23] Move GenerateButton to its own styled-component file Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/GenerateButton.sc.js | 32 ++++++++++++++++++++++++++++ src/dailyAudio/TodayAudio.js | 2 +- src/dailyAudio/TopicSuggestion.sc.js | 32 +--------------------------- 3 files changed, 34 insertions(+), 32 deletions(-) create mode 100644 src/dailyAudio/GenerateButton.sc.js diff --git a/src/dailyAudio/GenerateButton.sc.js b/src/dailyAudio/GenerateButton.sc.js new file mode 100644 index 000000000..be0d882e1 --- /dev/null +++ b/src/dailyAudio/GenerateButton.sc.js @@ -0,0 +1,32 @@ +import styled from "styled-components"; +import { orange500, orange600, orange800 } from "../components/colors"; + +export const GenerateButton = styled.button` + width: 150px; + height: 150px; + border-radius: 50%; + background-color: ${orange500}; + color: white; + border: none; + font-size: 16px; + font-weight: 600; + cursor: pointer; + box-shadow: 0px 0.3rem ${orange800}; + transition: all 0.3s ease-in-out; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + line-height: 1.2; + margin-bottom: 30px; + + &:hover { + background-color: ${orange600}; + } + + &:active { + box-shadow: none; + transform: translateY(0.2em); + transition: all 0.08s ease-in; + } +`; diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index f31095411..b34598787 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -17,10 +17,10 @@ import { TypePill, SuggestionInput, HintText, - GenerateButton, DescriptionText, InputArea, } from "./TopicSuggestion.sc"; +import { GenerateButton } from "./GenerateButton.sc"; const MAX_SUGGESTION_LENGTH = 80; diff --git a/src/dailyAudio/TopicSuggestion.sc.js b/src/dailyAudio/TopicSuggestion.sc.js index 704dfbf90..f5b27af67 100644 --- a/src/dailyAudio/TopicSuggestion.sc.js +++ b/src/dailyAudio/TopicSuggestion.sc.js @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { blue100, blue700, blue900, lightGrey, orange500, orange600, orange800 } from "../components/colors"; +import { blue100, blue700, blue900, lightGrey } from "../components/colors"; export const SuggestionWrapper = styled.div` display: flex; @@ -48,36 +48,6 @@ export const SuggestionInput = styled.textarea` field-sizing: content; `; -export const GenerateButton = styled.button` - width: 150px; - height: 150px; - border-radius: 50%; - background-color: ${orange500}; - color: white; - border: none; - font-size: 16px; - font-weight: 600; - cursor: pointer; - box-shadow: 0px 0.3rem ${orange800}; - transition: all 0.3s ease-in-out; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - line-height: 1.2; - margin-bottom: 30px; - - &:hover { - background-color: ${orange600}; - } - - &:active { - box-shadow: none; - transform: translateY(0.2em); - transition: all 0.08s ease-in; - } -`; - export const DescriptionText = styled.p` font-size: 0.9rem; color: var(--text-secondary); From 96ea298687f88c7f34cf1fcc4e534916b5487a8d Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 14:47:28 +0200 Subject: [PATCH 07/23] Rename topic_suggestion to suggestion across frontend Co-Authored-By: Claude Opus 4.6 (1M context) --- src/api/dailyAudio.js | 6 +++--- src/dailyAudio/TodayAudio.js | 38 ++++++++++++++++++------------------ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/api/dailyAudio.js b/src/api/dailyAudio.js index 05bd28dc7..2f4498b2a 100644 --- a/src/api/dailyAudio.js +++ b/src/api/dailyAudio.js @@ -43,14 +43,14 @@ Zeeguu_API.prototype.getTodaysLesson = function (callback, onError) { }); }; -Zeeguu_API.prototype.generateDailyLesson = function (callback, onError, topicSuggestion, suggestionType) { +Zeeguu_API.prototype.generateDailyLesson = function (callback, onError, suggestion, suggestionType) { this.apiLog("POST generate_daily_lesson"); const formData = new FormData(); const timezoneOffset = getTimezoneOffsetMinutes(); formData.append("timezone_offset", timezoneOffset); - if (topicSuggestion) { - formData.append("topic_suggestion", topicSuggestion); + if (suggestion) { + formData.append("suggestion", suggestion); if (suggestionType) { formData.append("suggestion_type", suggestionType); } diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index b34598787..d1380f741 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -51,8 +51,8 @@ export function wordsAsTile(words) { return capitalized_comma_separated_words; } -const TOPIC_STORAGE_KEY_PREFIX = "zeeguu_lesson_topic_"; -const TOPIC_TYPE_STORAGE_KEY_PREFIX = "zeeguu_lesson_topic_type_"; +const SUGGESTION_STORAGE_KEY_PREFIX = "zeeguu_lesson_topic_"; +const SUGGESTION_TYPE_STORAGE_KEY_PREFIX = "zeeguu_lesson_topic_type_"; export default function TodayAudio({ setShowTabs }) { const api = useContext(APIContext); @@ -61,11 +61,11 @@ export default function TodayAudio({ setShowTabs }) { const [isLoading, setIsLoading] = useState(false); const [isGenerating, setIsGenerating] = useState(false); const [generationProgress, setGenerationProgress] = useState(null); - const [topicSuggestion, setTopicSuggestion] = useState( - () => localStorage.getItem(TOPIC_STORAGE_KEY_PREFIX + lang) || "", + const [suggestion, setSuggestion] = useState( + () => localStorage.getItem(SUGGESTION_STORAGE_KEY_PREFIX + lang) || "", ); const [suggestionType, setSuggestionType] = useState( - () => localStorage.getItem(TOPIC_TYPE_STORAGE_KEY_PREFIX + lang) || "auto", + () => localStorage.getItem(SUGGESTION_TYPE_STORAGE_KEY_PREFIX + lang) || "auto", ); // Poll for progress when generating @@ -196,7 +196,7 @@ export default function TodayAudio({ setShowTabs }) { // Update page title and playback time when lessonData changes useEffect(() => { if (lessonData && lessonData.words) { - const topicPrefix = lessonData.topic_suggestion ? `${lessonData.topic_suggestion}: ` : ""; + const topicPrefix = lessonData.suggestion ? `${lessonData.suggestion}: ` : ""; document.title = `[${new Date().toLocaleDateString("en-US", { month: "short", day: "numeric", @@ -313,7 +313,7 @@ export default function TodayAudio({ setShowTabs }) { // Set localStorage flag to track generation across page reloads localStorage.setItem(generatingKey, "true"); - const trimmedTopic = topicSuggestion.trim() || null; + const trimmedTopic = suggestion.trim() || null; const typeToSend = trimmedTopic && suggestionType !== "auto" ? suggestionType : null; api.generateDailyLesson( (data) => { @@ -479,10 +479,10 @@ export default function TodayAudio({ setShowTabs }) { onClick={() => { if (suggestionType === key) return; setSuggestionType(key); - localStorage.setItem(TOPIC_TYPE_STORAGE_KEY_PREFIX + lang, key); + localStorage.setItem(SUGGESTION_TYPE_STORAGE_KEY_PREFIX + lang, key); if (key === "auto") { - setTopicSuggestion(""); - localStorage.removeItem(TOPIC_STORAGE_KEY_PREFIX + lang); + setSuggestion(""); + localStorage.removeItem(SUGGESTION_STORAGE_KEY_PREFIX + lang); } }} > @@ -498,21 +498,21 @@ export default function TodayAudio({ setShowTabs }) { rows={1} placeholder={SUGGESTION_TYPES[suggestionType]?.placeholder || ""} maxLength={MAX_SUGGESTION_LENGTH} - value={topicSuggestion} + value={suggestion} tabIndex={suggestionType === "auto" ? -1 : 0} onChange={(e) => { const val = e.target.value.replace(/\n/g, " "); - setTopicSuggestion(val); + setSuggestion(val); if (val.trim()) { - localStorage.setItem(TOPIC_STORAGE_KEY_PREFIX + lang, val); + localStorage.setItem(SUGGESTION_STORAGE_KEY_PREFIX + lang, val); } else { - localStorage.removeItem(TOPIC_STORAGE_KEY_PREFIX + lang); + localStorage.removeItem(SUGGESTION_STORAGE_KEY_PREFIX + lang); } }} /> - {topicSuggestion.length >= MAX_SUGGESTION_LENGTH - 8 - ? `${topicSuggestion.length}/${MAX_SUGGESTION_LENGTH}` + {suggestion.length >= MAX_SUGGESTION_LENGTH - 8 + ? `${suggestion.length}/${MAX_SUGGESTION_LENGTH}` : "\u00A0"} @@ -534,13 +534,13 @@ export default function TodayAudio({ setShowTabs }) { return (
-

+

{lessonData.is_completed && } {wordsAsTile(words)}

- {lessonData.topic_suggestion && ( + {lessonData.suggestion && (

- {lessonData.suggestion_type === "situation" ? "Situation" : "Topic"}: {lessonData.topic_suggestion} + {lessonData.suggestion_type === "situation" ? "Situation" : "Topic"}: {lessonData.suggestion}

)} From f4b88fb21f166c07914991dd27aa9e00721ca76e Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:00:01 +0200 Subject: [PATCH 08/23] Persist suggestion text per type, rename storage keys Each pill type (topic/situation) keeps its own saved text in localStorage. Switching pills restores previously typed text. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/TodayAudio.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index d1380f741..4db0f2b2e 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -51,8 +51,8 @@ export function wordsAsTile(words) { return capitalized_comma_separated_words; } -const SUGGESTION_STORAGE_KEY_PREFIX = "zeeguu_lesson_topic_"; -const SUGGESTION_TYPE_STORAGE_KEY_PREFIX = "zeeguu_lesson_topic_type_"; +const SELECTED_SUGGESTION_TYPE = "zeeguu_lesson_topic_type_"; +const suggestionKey = (type, lang) => `zeeguu_lesson_suggestion_${type}_${lang}`; export default function TodayAudio({ setShowTabs }) { const api = useContext(APIContext); @@ -61,12 +61,13 @@ export default function TodayAudio({ setShowTabs }) { const [isLoading, setIsLoading] = useState(false); const [isGenerating, setIsGenerating] = useState(false); const [generationProgress, setGenerationProgress] = useState(null); - const [suggestion, setSuggestion] = useState( - () => localStorage.getItem(SUGGESTION_STORAGE_KEY_PREFIX + lang) || "", - ); const [suggestionType, setSuggestionType] = useState( - () => localStorage.getItem(SUGGESTION_TYPE_STORAGE_KEY_PREFIX + lang) || "auto", + () => localStorage.getItem(SELECTED_SUGGESTION_TYPE + lang) || "auto", ); + const [suggestion, setSuggestion] = useState(() => { + const savedType = localStorage.getItem(SELECTED_SUGGESTION_TYPE + lang) || "auto"; + return localStorage.getItem(suggestionKey(savedType, lang)) || ""; + }); // Poll for progress when generating useEffect(() => { @@ -479,11 +480,8 @@ export default function TodayAudio({ setShowTabs }) { onClick={() => { if (suggestionType === key) return; setSuggestionType(key); - localStorage.setItem(SUGGESTION_TYPE_STORAGE_KEY_PREFIX + lang, key); - if (key === "auto") { - setSuggestion(""); - localStorage.removeItem(SUGGESTION_STORAGE_KEY_PREFIX + lang); - } + localStorage.setItem(SELECTED_SUGGESTION_TYPE + lang, key); + setSuggestion(key === "auto" ? "" : localStorage.getItem(suggestionKey(key, lang)) || ""); }} > {label} @@ -503,10 +501,11 @@ export default function TodayAudio({ setShowTabs }) { onChange={(e) => { const val = e.target.value.replace(/\n/g, " "); setSuggestion(val); + const key = suggestionKey(suggestionType, lang); if (val.trim()) { - localStorage.setItem(SUGGESTION_STORAGE_KEY_PREFIX + lang, val); + localStorage.setItem(key, val); } else { - localStorage.removeItem(SUGGESTION_STORAGE_KEY_PREFIX + lang); + localStorage.removeItem(key); } }} /> From 4608140f221a060731d2788d671cc1c9d91e2dd9 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:01:47 +0200 Subject: [PATCH 09/23] Fix localStorage key string to match new naming Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/TodayAudio.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index 4db0f2b2e..7f6dbfd67 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -51,7 +51,7 @@ export function wordsAsTile(words) { return capitalized_comma_separated_words; } -const SELECTED_SUGGESTION_TYPE = "zeeguu_lesson_topic_type_"; +const SELECTED_SUGGESTION_TYPE = "zeeguu_lesson_suggestion_type_"; const suggestionKey = (type, lang) => `zeeguu_lesson_suggestion_${type}_${lang}`; export default function TodayAudio({ setShowTabs }) { From 21f0a36617cf0b473a826284c800ea06ee8f2e4c Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:02:18 +0200 Subject: [PATCH 10/23] Fix localStorage key prefix: zeeguu_ -> audio_lesson_ Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/TodayAudio.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index 7f6dbfd67..abe8287ce 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -51,8 +51,8 @@ export function wordsAsTile(words) { return capitalized_comma_separated_words; } -const SELECTED_SUGGESTION_TYPE = "zeeguu_lesson_suggestion_type_"; -const suggestionKey = (type, lang) => `zeeguu_lesson_suggestion_${type}_${lang}`; +const SELECTED_SUGGESTION_TYPE = "audio_lesson_suggestion_type_"; +const suggestionKey = (type, lang) => `audio_lesson_suggestion_${type}_${lang}`; export default function TodayAudio({ setShowTabs }) { const api = useContext(APIContext); From 143637554fa2e8797fd7738ab49223a9086661ba Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:06:52 +0200 Subject: [PATCH 11/23] Clean up leftover topic naming: drop suggestion from tab title, rename trimmedTopic Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/TodayAudio.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index abe8287ce..d4019e57a 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -197,11 +197,10 @@ export default function TodayAudio({ setShowTabs }) { // Update page title and playback time when lessonData changes useEffect(() => { if (lessonData && lessonData.words) { - const topicPrefix = lessonData.suggestion ? `${lessonData.suggestion}: ` : ""; document.title = `[${new Date().toLocaleDateString("en-US", { month: "short", day: "numeric", - })}] Daily Audio: ${topicPrefix}${wordsAsTile(words)}`; + })}] Daily Audio: ${wordsAsTile(words)}`; // Initialize playback time from lesson data const initialTime = lessonData.pause_position_seconds || lessonData.position_seconds || lessonData.progress_seconds || 0; @@ -314,8 +313,8 @@ export default function TodayAudio({ setShowTabs }) { // Set localStorage flag to track generation across page reloads localStorage.setItem(generatingKey, "true"); - const trimmedTopic = suggestion.trim() || null; - const typeToSend = trimmedTopic && suggestionType !== "auto" ? suggestionType : null; + const trimmedSuggestion = suggestion.trim() || null; + const typeToSend = trimmedSuggestion && suggestionType !== "auto" ? suggestionType : null; api.generateDailyLesson( (data) => { if (data.status === AUDIO_STATUS.GENERATING) { @@ -358,7 +357,7 @@ export default function TodayAudio({ setShowTabs }) { } setError(errorMsg); }, - trimmedTopic, + trimmedSuggestion, typeToSend, ); }; From 8658e60801d79be45b699efccd2bb7a7e94a1639 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:11:38 +0200 Subject: [PATCH 12/23] Extract CenteringContainer and lesson view styled-components Move inline styles to CenteringContainer, LessonTitle, SuggestionSubtitle, CompletionCheck, and LessonWrapper. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/GenerateButton.sc.js | 9 +++++++++ src/dailyAudio/LessonView.sc.js | 25 ++++++++++++++++++++++++ src/dailyAudio/TodayAudio.js | 30 +++++++++++------------------ 3 files changed, 45 insertions(+), 19 deletions(-) create mode 100644 src/dailyAudio/LessonView.sc.js diff --git a/src/dailyAudio/GenerateButton.sc.js b/src/dailyAudio/GenerateButton.sc.js index be0d882e1..b2a5dc8da 100644 --- a/src/dailyAudio/GenerateButton.sc.js +++ b/src/dailyAudio/GenerateButton.sc.js @@ -1,6 +1,15 @@ import styled from "styled-components"; import { orange500, orange600, orange800 } from "../components/colors"; +export const CenteringContainer = styled.div` + padding: 20px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: calc(100vh - 260px); +`; + export const GenerateButton = styled.button` width: 150px; height: 150px; diff --git a/src/dailyAudio/LessonView.sc.js b/src/dailyAudio/LessonView.sc.js new file mode 100644 index 000000000..4207e75f8 --- /dev/null +++ b/src/dailyAudio/LessonView.sc.js @@ -0,0 +1,25 @@ +import styled from "styled-components"; +import { zeeguuOrange } from "../components/colors"; + +export const LessonWrapper = styled.div` + padding: 20px; +`; + +export const LessonTitle = styled.h2` + color: ${zeeguuOrange}; + margin-bottom: ${({ $compact }) => ($compact ? "4px" : "10px")}; + display: flex; + align-items: center; + gap: 8px; +`; + +export const SuggestionSubtitle = styled.p` + font-size: 0.85rem; + color: var(--text-secondary); + margin-bottom: 10px; +`; + +export const CompletionCheck = styled.span` + color: #28a745; + font-size: 20px; +`; diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index d4019e57a..31660cb5d 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -20,7 +20,8 @@ import { DescriptionText, InputArea, } from "./TopicSuggestion.sc"; -import { GenerateButton } from "./GenerateButton.sc"; +import { CenteringContainer, GenerateButton } from "./GenerateButton.sc"; +import { LessonWrapper, LessonTitle, SuggestionSubtitle, CompletionCheck } from "./LessonView.sc"; const MAX_SUGGESTION_LENGTH = 80; @@ -447,16 +448,7 @@ export default function TodayAudio({ setShowTabs }) { // Can generate lesson - show the generate button if (canGenerateLesson === true) { return ( -
+ {error && ( {error} @@ -515,7 +507,7 @@ export default function TodayAudio({ setShowTabs }) { -
+ ); } @@ -531,15 +523,15 @@ export default function TodayAudio({ setShowTabs }) { return ( -
-

- {lessonData.is_completed && } + + + {lessonData.is_completed && } {wordsAsTile(words)} -

+ {lessonData.suggestion && ( -

+ {lessonData.suggestion_type === "situation" ? "Situation" : "Topic"}: {lessonData.suggestion} -

+ )} {error &&
{error}
} @@ -666,6 +658,6 @@ export default function TodayAudio({ setShowTabs }) { preselectedCategory={FEEDBACK_CODES_NAME.DAILY_AUDIO} />
-
+ ); } From cc7346761c9772f7fbc8e185079584b9d72d08b4 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:13:35 +0200 Subject: [PATCH 13/23] Rename CenteringContainer to VerticalCentering Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/GenerateButton.sc.js | 2 +- src/dailyAudio/TodayAudio.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dailyAudio/GenerateButton.sc.js b/src/dailyAudio/GenerateButton.sc.js index b2a5dc8da..71ad10ff7 100644 --- a/src/dailyAudio/GenerateButton.sc.js +++ b/src/dailyAudio/GenerateButton.sc.js @@ -1,7 +1,7 @@ import styled from "styled-components"; import { orange500, orange600, orange800 } from "../components/colors"; -export const CenteringContainer = styled.div` +export const VerticalCentering = styled.div` padding: 20px; display: flex; flex-direction: column; diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index 31660cb5d..70b724717 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -20,7 +20,7 @@ import { DescriptionText, InputArea, } from "./TopicSuggestion.sc"; -import { CenteringContainer, GenerateButton } from "./GenerateButton.sc"; +import { VerticalCentering, GenerateButton } from "./GenerateButton.sc"; import { LessonWrapper, LessonTitle, SuggestionSubtitle, CompletionCheck } from "./LessonView.sc"; const MAX_SUGGESTION_LENGTH = 80; @@ -448,7 +448,7 @@ export default function TodayAudio({ setShowTabs }) { // Can generate lesson - show the generate button if (canGenerateLesson === true) { return ( - + {error && ( {error} @@ -507,7 +507,7 @@ export default function TodayAudio({ setShowTabs }) { - + ); } From 5aff1df883085d23445dda43efddf079842f0654 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:15:57 +0200 Subject: [PATCH 14/23] Extract shortDate() helper for page title Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/TodayAudio.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index 70b724717..c511c4cd6 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -23,6 +23,10 @@ import { import { VerticalCentering, GenerateButton } from "./GenerateButton.sc"; import { LessonWrapper, LessonTitle, SuggestionSubtitle, CompletionCheck } from "./LessonView.sc"; +function shortDate() { + return `[${new Date().toLocaleDateString("en-US", { month: "short", day: "numeric" })}]`; +} + const MAX_SUGGESTION_LENGTH = 80; const SUGGESTION_TYPES = { @@ -198,10 +202,7 @@ export default function TodayAudio({ setShowTabs }) { // Update page title and playback time when lessonData changes useEffect(() => { if (lessonData && lessonData.words) { - document.title = `[${new Date().toLocaleDateString("en-US", { - month: "short", - day: "numeric", - })}] Daily Audio: ${wordsAsTile(words)}`; + document.title = shortDate() + " Daily Audio: " + wordsAsTile(words); // Initialize playback time from lesson data const initialTime = lessonData.pause_position_seconds || lessonData.position_seconds || lessonData.progress_seconds || 0; From 0560768bda51fb30d5412cbe44e47245d60c1bd8 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:17:35 +0200 Subject: [PATCH 15/23] Rename TypePill to SelectablePill for reusability Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/TodayAudio.js | 6 +++--- src/dailyAudio/TopicSuggestion.sc.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index c511c4cd6..84d316665 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -14,7 +14,7 @@ import { AUDIO_STATUS, GENERATION_PROGRESS } from "./AudioLessonConstants"; import { SuggestionWrapper, PillRow, - TypePill, + SelectablePill, SuggestionInput, HintText, DescriptionText, @@ -463,7 +463,7 @@ export default function TodayAudio({ setShowTabs }) { {Object.entries(SUGGESTION_TYPES).map(([key, { label }]) => ( - {label} - + ))} diff --git a/src/dailyAudio/TopicSuggestion.sc.js b/src/dailyAudio/TopicSuggestion.sc.js index f5b27af67..23ccfe79d 100644 --- a/src/dailyAudio/TopicSuggestion.sc.js +++ b/src/dailyAudio/TopicSuggestion.sc.js @@ -15,7 +15,7 @@ export const PillRow = styled.div` gap: 0.4rem; `; -export const TypePill = styled.button` +export const SelectablePill = styled.button` padding: 0.4rem 1rem; border-radius: 2rem; border: 1.5px solid ${({ $selected }) => ($selected ? blue700 : lightGrey)}; From 716ee6e8b50a7e4a3f80c1963180c132aa744e81 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:27:55 +0200 Subject: [PATCH 16/23] Extract SuggestionSelector component with clear button - Move pill selector, description, and input into SuggestionSelector - Add MUI CancelIcon clear button inside the textarea - Extract InputWrapper and ClearButton styled-components - Refocus textarea after clearing Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/SuggestionSelector.js | 101 +++++++++++++++++++++++++++ src/dailyAudio/TodayAudio.js | 88 +++-------------------- src/dailyAudio/TopicSuggestion.sc.js | 21 +++++- 3 files changed, 128 insertions(+), 82 deletions(-) create mode 100644 src/dailyAudio/SuggestionSelector.js diff --git a/src/dailyAudio/SuggestionSelector.js b/src/dailyAudio/SuggestionSelector.js new file mode 100644 index 000000000..b79b271ab --- /dev/null +++ b/src/dailyAudio/SuggestionSelector.js @@ -0,0 +1,101 @@ +import React, { useRef } from "react"; +import CancelIcon from "@mui/icons-material/Cancel"; +import { + SuggestionWrapper, + PillRow, + SelectablePill, + SuggestionInput, + DescriptionText, + InputArea, + InputWrapper, + ClearButton, +} from "./TopicSuggestion.sc"; + +const MAX_SUGGESTION_LENGTH = 80; + +const SUGGESTION_TYPES = { + auto: { + label: "Automatic", + description: "A listening lesson with three of your words, each in a short dialogue.", + placeholder: null, + }, + topic: { + label: "Topic", + description: "A listening lesson with three of your words, each practiced in a short dialogue on a given topic.", + placeholder: "e.g. cooking, sports", + }, + situation: { + label: "Situation", + description: "A listening lesson with three of your words, each practiced in a dialogue simulating a real-world scenario.", + placeholder: "e.g. at a restaurant, job interview", + }, +}; + +const SELECTED_SUGGESTION_TYPE = "audio_lesson_suggestion_type_"; +const suggestionKey = (type, lang) => `audio_lesson_suggestion_${type}_${lang}`; + +export { SUGGESTION_TYPES, suggestionKey, SELECTED_SUGGESTION_TYPE, MAX_SUGGESTION_LENGTH }; + +export default function SuggestionSelector({ suggestionType, setSuggestionType, suggestion, setSuggestion, lang }) { + const inputRef = useRef(null); + + return ( + + + {Object.entries(SUGGESTION_TYPES).map(([key, { label }]) => ( + { + if (suggestionType === key) return; + setSuggestionType(key); + localStorage.setItem(SELECTED_SUGGESTION_TYPE + lang, key); + setSuggestion(key === "auto" ? "" : localStorage.getItem(suggestionKey(key, lang)) || ""); + }} + > + {label} + + ))} + + + {SUGGESTION_TYPES[suggestionType].description} + + + + { + const val = e.target.value.replace(/\n/g, " "); + setSuggestion(val); + const key = suggestionKey(suggestionType, lang); + if (val.trim()) { + localStorage.setItem(key, val); + } else { + localStorage.removeItem(key); + } + }} + /> + {suggestion && ( + { + setSuggestion(""); + localStorage.removeItem(suggestionKey(suggestionType, lang)); + inputRef.current?.focus(); + }} + > + + + )} + + + + ); +} diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index 84d316665..7d5bba4e6 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -11,42 +11,14 @@ import { FEEDBACK_OPTIONS, FEEDBACK_CODES_NAME } from "../components/FeedbackCon import Word from "../words/Word"; import useListeningSession from "../hooks/useListeningSession"; import { AUDIO_STATUS, GENERATION_PROGRESS } from "./AudioLessonConstants"; -import { - SuggestionWrapper, - PillRow, - SelectablePill, - SuggestionInput, - HintText, - DescriptionText, - InputArea, -} from "./TopicSuggestion.sc"; import { VerticalCentering, GenerateButton } from "./GenerateButton.sc"; +import SuggestionSelector, { SELECTED_SUGGESTION_TYPE, suggestionKey } from "./SuggestionSelector"; import { LessonWrapper, LessonTitle, SuggestionSubtitle, CompletionCheck } from "./LessonView.sc"; function shortDate() { return `[${new Date().toLocaleDateString("en-US", { month: "short", day: "numeric" })}]`; } -const MAX_SUGGESTION_LENGTH = 80; - -const SUGGESTION_TYPES = { - auto: { - label: "Automatic", - description: "A listening lesson with three of your words, each in a short dialogue.", - placeholder: null, - }, - topic: { - label: "Topic", - description: "A listening lesson with three of your words, each practiced in a short dialogue on a given topic.", - placeholder: "e.g. cooking, sports", - }, - situation: { - label: "Situation", - description: "A listening lesson with three of your words, each practiced in a dialogue simulating a real-world scenario.", - placeholder: "e.g. at a restaurant, job interview", - }, -}; - export function wordsAsTile(words) { if (!words || !words.length) return ""; @@ -56,9 +28,6 @@ export function wordsAsTile(words) { return capitalized_comma_separated_words; } -const SELECTED_SUGGESTION_TYPE = "audio_lesson_suggestion_type_"; -const suggestionKey = (type, lang) => `audio_lesson_suggestion_${type}_${lang}`; - export default function TodayAudio({ setShowTabs }) { const api = useContext(APIContext); const { userDetails, setUserDetails } = useContext(UserContext); @@ -460,54 +429,13 @@ export default function TodayAudio({ setShowTabs }) {
Daily Lesson - - - {Object.entries(SUGGESTION_TYPES).map(([key, { label }]) => ( - { - if (suggestionType === key) return; - setSuggestionType(key); - localStorage.setItem(SELECTED_SUGGESTION_TYPE + lang, key); - setSuggestion(key === "auto" ? "" : localStorage.getItem(suggestionKey(key, lang)) || ""); - }} - > - {label} - - ))} - - - {SUGGESTION_TYPES[suggestionType].description} - - - { - const val = e.target.value.replace(/\n/g, " "); - setSuggestion(val); - const key = suggestionKey(suggestionType, lang); - if (val.trim()) { - localStorage.setItem(key, val); - } else { - localStorage.removeItem(key); - } - }} - /> - - {suggestion.length >= MAX_SUGGESTION_LENGTH - 8 - ? `${suggestion.length}/${MAX_SUGGESTION_LENGTH}` - : "\u00A0"} - - - + ); } diff --git a/src/dailyAudio/TopicSuggestion.sc.js b/src/dailyAudio/TopicSuggestion.sc.js index 23ccfe79d..723d3f1ae 100644 --- a/src/dailyAudio/TopicSuggestion.sc.js +++ b/src/dailyAudio/TopicSuggestion.sc.js @@ -35,7 +35,7 @@ export const SelectablePill = styled.button` export const SuggestionInput = styled.textarea` width: 100%; - padding: 8px 12px; + padding: 12px 32px 12px 12px; border: 1px solid var(--border-light); border-radius: 4px; font-size: 16px; @@ -44,7 +44,6 @@ export const SuggestionInput = styled.textarea` background-color: var(--bg-secondary); text-align: center; resize: none; - min-height: 2.4rem; field-sizing: content; `; @@ -70,6 +69,24 @@ export const InputArea = styled.div` visibility: ${({ $hidden }) => ($hidden ? "hidden" : "visible")}; `; +export const InputWrapper = styled.div` + position: relative; + width: 100%; + display: flex; + align-items: center; +`; + +export const ClearButton = styled.span` + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + cursor: pointer; + color: var(--text-secondary); + font-size: 18px; + display: flex; +`; + export const HintText = styled.p` font-size: 0.75rem; color: var(--text-secondary); From ac69af75aad1badb1216c5a1a65455d89514f7d5 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:30:21 +0200 Subject: [PATCH 17/23] Extract getSavedSuggestion/getSavedSuggestionType helpers, rename typeToSend Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/SuggestionSelector.js | 9 ++++++++- src/dailyAudio/TodayAudio.js | 11 +++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/dailyAudio/SuggestionSelector.js b/src/dailyAudio/SuggestionSelector.js index b79b271ab..1df478fa8 100644 --- a/src/dailyAudio/SuggestionSelector.js +++ b/src/dailyAudio/SuggestionSelector.js @@ -34,7 +34,14 @@ const SUGGESTION_TYPES = { const SELECTED_SUGGESTION_TYPE = "audio_lesson_suggestion_type_"; const suggestionKey = (type, lang) => `audio_lesson_suggestion_${type}_${lang}`; -export { SUGGESTION_TYPES, suggestionKey, SELECTED_SUGGESTION_TYPE, MAX_SUGGESTION_LENGTH }; +export function getSavedSuggestion(lang) { + const savedType = localStorage.getItem(SELECTED_SUGGESTION_TYPE + lang) || "auto"; + return localStorage.getItem(suggestionKey(savedType, lang)) || ""; +} + +export function getSavedSuggestionType(lang) { + return localStorage.getItem(SELECTED_SUGGESTION_TYPE + lang) || "auto"; +} export default function SuggestionSelector({ suggestionType, setSuggestionType, suggestion, setSuggestion, lang }) { const inputRef = useRef(null); diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index 7d5bba4e6..66db3c261 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -12,7 +12,7 @@ import Word from "../words/Word"; import useListeningSession from "../hooks/useListeningSession"; import { AUDIO_STATUS, GENERATION_PROGRESS } from "./AudioLessonConstants"; import { VerticalCentering, GenerateButton } from "./GenerateButton.sc"; -import SuggestionSelector, { SELECTED_SUGGESTION_TYPE, suggestionKey } from "./SuggestionSelector"; +import SuggestionSelector, { getSavedSuggestion, getSavedSuggestionType } from "./SuggestionSelector"; import { LessonWrapper, LessonTitle, SuggestionSubtitle, CompletionCheck } from "./LessonView.sc"; function shortDate() { @@ -36,11 +36,10 @@ export default function TodayAudio({ setShowTabs }) { const [isGenerating, setIsGenerating] = useState(false); const [generationProgress, setGenerationProgress] = useState(null); const [suggestionType, setSuggestionType] = useState( - () => localStorage.getItem(SELECTED_SUGGESTION_TYPE + lang) || "auto", + () => getSavedSuggestionType(lang), ); const [suggestion, setSuggestion] = useState(() => { - const savedType = localStorage.getItem(SELECTED_SUGGESTION_TYPE + lang) || "auto"; - return localStorage.getItem(suggestionKey(savedType, lang)) || ""; + return getSavedSuggestion(lang); }); // Poll for progress when generating @@ -285,7 +284,7 @@ export default function TodayAudio({ setShowTabs }) { localStorage.setItem(generatingKey, "true"); const trimmedSuggestion = suggestion.trim() || null; - const typeToSend = trimmedSuggestion && suggestionType !== "auto" ? suggestionType : null; + const suggestionTypeToSend = trimmedSuggestion && suggestionType !== "auto" ? suggestionType : null; api.generateDailyLesson( (data) => { if (data.status === AUDIO_STATUS.GENERATING) { @@ -329,7 +328,7 @@ export default function TodayAudio({ setShowTabs }) { setError(errorMsg); }, trimmedSuggestion, - typeToSend, + suggestionTypeToSend, ); }; From f45198f4ba5994030a0be868d410dad810afc84e Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:33:57 +0200 Subject: [PATCH 18/23] Extract LessonPlaybackView component Move the entire lesson playback UI (audio player, completion banner, word list, feedback) into its own component. TodayAudio now just orchestrates state and delegates to sub-views. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/LessonPlaybackView.js | 154 +++++++++++++++++++++++++++ src/dailyAudio/TodayAudio.js | 154 +++------------------------ 2 files changed, 167 insertions(+), 141 deletions(-) create mode 100644 src/dailyAudio/LessonPlaybackView.js diff --git a/src/dailyAudio/LessonPlaybackView.js b/src/dailyAudio/LessonPlaybackView.js new file mode 100644 index 000000000..28b4ec790 --- /dev/null +++ b/src/dailyAudio/LessonPlaybackView.js @@ -0,0 +1,154 @@ +import React, { useState } from "react"; +import CustomAudioPlayer from "../components/CustomAudioPlayer"; +import FeedbackModal from "../components/FeedbackModal"; +import { FEEDBACK_OPTIONS, FEEDBACK_CODES_NAME } from "../components/FeedbackConstants"; +import Word from "../words/Word"; +import { AUDIO_STATUS } from "./AudioLessonConstants"; +import { LessonWrapper, LessonTitle, SuggestionSubtitle, CompletionCheck } from "./LessonView.sc"; +import { wordsAsTile } from "./TodayAudio"; + +export default function LessonPlaybackView({ + lessonData, + setLessonData, + words, + error, + api, + userDetails, + setUserDetails, + listeningSession, + currentPlaybackTime, + setCurrentPlaybackTime, +}) { + const [openFeedback, setOpenFeedback] = useState(false); + + return ( + + + {lessonData.is_completed && } + {wordsAsTile(words)} + + {lessonData.suggestion && ( + + {lessonData.suggestion_type === "situation" ? "Situation" : "Topic"}: {lessonData.suggestion} + + )} + + {error &&
{error}
} + +
+ {!lessonData.is_completed && ( +

Here's your daily lesson! Listen to improve your comprehension skills.

+ )} + + { + if (lessonData.lesson_id) { + api.updateLessonState(lessonData.lesson_id, "resume"); + listeningSession.start(); + setUserDetails((prev) => ({ ...prev, daily_audio_status: AUDIO_STATUS.IN_PROGRESS })); + } + }} + onPause={() => { + listeningSession.pause(); + }} + onProgressUpdate={(progressSeconds) => { + setCurrentPlaybackTime(progressSeconds); + if (lessonData.lesson_id) { + api.updateLessonState(lessonData.lesson_id, "pause", progressSeconds); + } + }} + onEnded={() => { + listeningSession.end(); + if (lessonData.lesson_id) { + api.updateLessonState(lessonData.lesson_id, "complete", null, () => { + setLessonData((prev) => ({ + ...prev, + is_completed: true, + completed_at: new Date().toISOString(), + })); + setUserDetails((prev) => ({ ...prev, daily_audio_status: AUDIO_STATUS.COMPLETED })); + }); + } + }} + onError={() => {}} + style={{ + width: "100%", + marginBottom: "20px", + maxWidth: "600px", + margin: "0 auto 20px auto", + }} + /> + + {lessonData.is_completed && ( +
+ + ✓ Lesson completed! Great job on finishing today's lesson. + +
+ )} + + {words && words.length > 0 && ( +
+

+ Words in this lesson +

+ {words.map((word, index) => ( + + ))} +
+ )} + +
+ +
+ + +
+
+ ); +} diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index 66db3c261..84ba7b7d1 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -5,15 +5,11 @@ import { UserContext } from "../contexts/UserContext"; import LoadingAnimation from "../components/LoadingAnimation"; import EmptyState from "../components/EmptyState"; import FullWidthErrorMsg from "../components/FullWidthErrorMsg.sc"; -import CustomAudioPlayer from "../components/CustomAudioPlayer"; -import FeedbackModal from "../components/FeedbackModal"; -import { FEEDBACK_OPTIONS, FEEDBACK_CODES_NAME } from "../components/FeedbackConstants"; -import Word from "../words/Word"; import useListeningSession from "../hooks/useListeningSession"; import { AUDIO_STATUS, GENERATION_PROGRESS } from "./AudioLessonConstants"; import { VerticalCentering, GenerateButton } from "./GenerateButton.sc"; import SuggestionSelector, { getSavedSuggestion, getSavedSuggestionType } from "./SuggestionSelector"; -import { LessonWrapper, LessonTitle, SuggestionSubtitle, CompletionCheck } from "./LessonView.sc"; +import LessonPlaybackView from "./LessonPlaybackView"; function shortDate() { return `[${new Date().toLocaleDateString("en-US", { month: "short", day: "numeric" })}]`; @@ -451,141 +447,17 @@ export default function TodayAudio({ setShowTabs }) { return ( - - - {lessonData.is_completed && } - {wordsAsTile(words)} - - {lessonData.suggestion && ( - - {lessonData.suggestion_type === "situation" ? "Situation" : "Topic"}: {lessonData.suggestion} - - )} - - {error &&
{error}
} - -
- {!lessonData.is_completed && ( -

Here's your daily lesson! Listen to improve your comprehension skills.

- )} - - { - if (lessonData.lesson_id) { - api.updateLessonState(lessonData.lesson_id, "resume"); - // Start or resume listening session - listeningSession.start(); - // Update context so navigation dot disappears (in_progress) - setUserDetails((prev) => ({ ...prev, daily_audio_status: AUDIO_STATUS.IN_PROGRESS })); - } - }} - onPause={() => { - // Pause listening session (accumulates time, doesn't end) - listeningSession.pause(); - }} - onProgressUpdate={(progressSeconds) => { - setCurrentPlaybackTime(progressSeconds); - if (lessonData.lesson_id) { - // Use pause action to save progress position - api.updateLessonState(lessonData.lesson_id, "pause", progressSeconds); - } - }} - onEnded={() => { - // End listening session when audio ends - listeningSession.end(); - if (lessonData.lesson_id) { - api.updateLessonState(lessonData.lesson_id, "complete", null, () => { - // Update local state to show completion immediately - setLessonData((prev) => ({ - ...prev, - is_completed: true, - completed_at: new Date().toISOString(), - })); - // Update context so navigation dot disappears - setUserDetails((prev) => ({ ...prev, daily_audio_status: AUDIO_STATUS.COMPLETED })); - }); - } - }} - onError={() => {}} - style={{ - width: "100%", - marginBottom: "20px", - maxWidth: "600px", - margin: "0 auto 20px auto", - }} - /> - - {lessonData.is_completed && ( -
- - ✓ Lesson completed! Great job on finishing today's lesson. - -
- )} - - {/* Display word details with type badges */} - {words && words.length > 0 && ( -
-

- Words in this lesson -

- {words.map((word, index) => ( - - ))} -
- )} - -
- -
- - -
-
+ ); } From adc8b1f7062088ff468d9d1e41f0ccfd0d060bcf Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:34:42 +0200 Subject: [PATCH 19/23] Reuse getSavedSuggestionType inside getSavedSuggestion Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/SuggestionSelector.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/dailyAudio/SuggestionSelector.js b/src/dailyAudio/SuggestionSelector.js index 1df478fa8..a5a8001e6 100644 --- a/src/dailyAudio/SuggestionSelector.js +++ b/src/dailyAudio/SuggestionSelector.js @@ -34,15 +34,14 @@ const SUGGESTION_TYPES = { const SELECTED_SUGGESTION_TYPE = "audio_lesson_suggestion_type_"; const suggestionKey = (type, lang) => `audio_lesson_suggestion_${type}_${lang}`; -export function getSavedSuggestion(lang) { - const savedType = localStorage.getItem(SELECTED_SUGGESTION_TYPE + lang) || "auto"; - return localStorage.getItem(suggestionKey(savedType, lang)) || ""; -} - export function getSavedSuggestionType(lang) { return localStorage.getItem(SELECTED_SUGGESTION_TYPE + lang) || "auto"; } +export function getSavedSuggestion(lang) { + return localStorage.getItem(suggestionKey(getSavedSuggestionType(lang), lang)) || ""; +} + export default function SuggestionSelector({ suggestionType, setSuggestionType, suggestion, setSuggestion, lang }) { const inputRef = useRef(null); From c6b915fcd45a3f3f0fd34e97a1570773fa98f45e Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:37:20 +0200 Subject: [PATCH 20/23] Extract reusable ClearableInput component Move input wrapper, styled textarea, and clear button into a generic ClearableInput in components/. SuggestionSelector now uses it. Remove redundant styled-components from TopicSuggestion.sc.js. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/ClearableInput.js | 64 ++++++++++++++++++++++++++++ src/dailyAudio/SuggestionSelector.js | 58 ++++++++++--------------- src/dailyAudio/TopicSuggestion.sc.js | 37 ---------------- 3 files changed, 86 insertions(+), 73 deletions(-) create mode 100644 src/components/ClearableInput.js diff --git a/src/components/ClearableInput.js b/src/components/ClearableInput.js new file mode 100644 index 000000000..93495ff8f --- /dev/null +++ b/src/components/ClearableInput.js @@ -0,0 +1,64 @@ +import React, { useRef } from "react"; +import styled from "styled-components"; +import CancelIcon from "@mui/icons-material/Cancel"; + +const Wrapper = styled.div` + position: relative; + width: 100%; + display: flex; + align-items: center; +`; + +const StyledInput = styled.textarea` + width: 100%; + padding: 12px 32px 12px 12px; + border: 1px solid var(--border-light); + border-radius: 4px; + font-size: 16px; + font-family: inherit; + color: var(--text-primary); + background-color: var(--bg-secondary); + text-align: center; + resize: none; + field-sizing: content; +`; + +const ClearBtn = styled.span` + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + cursor: pointer; + color: var(--text-secondary); + font-size: 18px; + display: flex; +`; + +export default function ClearableInput({ value, onChange, onClear, placeholder, maxLength, tabIndex, rows = 1, ...props }) { + const inputRef = useRef(null); + + return ( + + + {value && ( + { + onClear?.(); + inputRef.current?.focus(); + }} + > + + + )} + + ); +} diff --git a/src/dailyAudio/SuggestionSelector.js b/src/dailyAudio/SuggestionSelector.js index a5a8001e6..5e25ea295 100644 --- a/src/dailyAudio/SuggestionSelector.js +++ b/src/dailyAudio/SuggestionSelector.js @@ -1,14 +1,11 @@ -import React, { useRef } from "react"; -import CancelIcon from "@mui/icons-material/Cancel"; +import React from "react"; +import ClearableInput from "../components/ClearableInput"; import { SuggestionWrapper, PillRow, SelectablePill, - SuggestionInput, DescriptionText, InputArea, - InputWrapper, - ClearButton, } from "./TopicSuggestion.sc"; const MAX_SUGGESTION_LENGTH = 80; @@ -70,37 +67,26 @@ export default function SuggestionSelector({ suggestionType, setSuggestionType, {SUGGESTION_TYPES[suggestionType].description}
- - { - const val = e.target.value.replace(/\n/g, " "); - setSuggestion(val); - const key = suggestionKey(suggestionType, lang); - if (val.trim()) { - localStorage.setItem(key, val); - } else { - localStorage.removeItem(key); - } - }} - /> - {suggestion && ( - { - setSuggestion(""); - localStorage.removeItem(suggestionKey(suggestionType, lang)); - inputRef.current?.focus(); - }} - > - - - )} - + { + const val = e.target.value.replace(/\n/g, " "); + setSuggestion(val); + const key = suggestionKey(suggestionType, lang); + if (val.trim()) { + localStorage.setItem(key, val); + } else { + localStorage.removeItem(key); + } + }} + onClear={() => { + setSuggestion(""); + localStorage.removeItem(suggestionKey(suggestionType, lang)); + }} + />
); diff --git a/src/dailyAudio/TopicSuggestion.sc.js b/src/dailyAudio/TopicSuggestion.sc.js index 723d3f1ae..740432565 100644 --- a/src/dailyAudio/TopicSuggestion.sc.js +++ b/src/dailyAudio/TopicSuggestion.sc.js @@ -33,20 +33,6 @@ export const SelectablePill = styled.button` } `; -export const SuggestionInput = styled.textarea` - width: 100%; - padding: 12px 32px 12px 12px; - border: 1px solid var(--border-light); - border-radius: 4px; - font-size: 16px; - font-family: inherit; - color: var(--text-primary); - background-color: var(--bg-secondary); - text-align: center; - resize: none; - field-sizing: content; -`; - export const DescriptionText = styled.p` font-size: 0.9rem; color: var(--text-secondary); @@ -69,27 +55,4 @@ export const InputArea = styled.div` visibility: ${({ $hidden }) => ($hidden ? "hidden" : "visible")}; `; -export const InputWrapper = styled.div` - position: relative; - width: 100%; - display: flex; - align-items: center; -`; - -export const ClearButton = styled.span` - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - cursor: pointer; - color: var(--text-secondary); - font-size: 18px; - display: flex; -`; -export const HintText = styled.p` - font-size: 0.75rem; - color: var(--text-secondary); - margin: 0; - text-align: center; -`; From e512562f710d4cfbd1339efac01d5cf91e3d7c6d Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:40:08 +0200 Subject: [PATCH 21/23] Remove leftover useRef from SuggestionSelector Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/SuggestionSelector.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/dailyAudio/SuggestionSelector.js b/src/dailyAudio/SuggestionSelector.js index 5e25ea295..f48d9692e 100644 --- a/src/dailyAudio/SuggestionSelector.js +++ b/src/dailyAudio/SuggestionSelector.js @@ -40,8 +40,6 @@ export function getSavedSuggestion(lang) { } export default function SuggestionSelector({ suggestionType, setSuggestionType, suggestion, setSuggestion, lang }) { - const inputRef = useRef(null); - return ( From 8612af3b4f22f0d59405aede1ee540c39d92809a Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:50:36 +0200 Subject: [PATCH 22/23] Simplify: extract successGreen color, move wordsAsTile to audioUtils, rename TopicSuggestion.sc.js - Add successGreen to colors.js, replace hardcoded #28a745 - Move wordsAsTile and shortDate to audioUtils.js (breaks circular dep) - Rename TopicSuggestion.sc.js to SuggestionSelector.sc.js Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 12 +++++++++++- .npmrc | 1 + src/components/colors.js | 2 ++ src/dailyAudio/LessonPlaybackView.js | 5 +++-- src/dailyAudio/LessonView.sc.js | 4 ++-- src/dailyAudio/PastLessons.js | 2 +- src/dailyAudio/SuggestionSelector.js | 6 +++++- ...icSuggestion.sc.js => SuggestionSelector.sc.js} | 0 src/dailyAudio/TodayAudio.js | 14 +------------- src/dailyAudio/audioUtils.js | 12 ++++++++++++ 10 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 .npmrc rename src/dailyAudio/{TopicSuggestion.sc.js => SuggestionSelector.sc.js} (100%) create mode 100644 src/dailyAudio/audioUtils.js diff --git a/.gitignore b/.gitignore index ca2c8d13d..b63b5b822 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,14 @@ android/*.json android/fastlane/play-store-credentials.json android/fastlane/report.xml -*.md \ No newline at end of file +*.md + +# Claude Code +.claude/ + +# Eclipse/Buildship IDE files (Android Studio) +android/.project +android/.settings/ +android/app/.classpath +android/app/.project +android/app/.settings/ \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..7253a5cee --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +min-release-age=7 diff --git a/src/components/colors.js b/src/components/colors.js index 2f669e1ea..1de72e9da 100644 --- a/src/components/colors.js +++ b/src/components/colors.js @@ -64,6 +64,7 @@ let zeeguuViolet = "#4a0d67"; let darkGreen = "#006400"; let alertGreen = "#4caf50"; //careful when changing this color. It is defined to match the color in the success-alert to undo feedback submits. let matchGreen = "#B3F78F"; +let successGreen = "#28a745"; let translationHover = "#2f76ac"; let lightOrange = "#ffe5b9"; let brown = "#A46A00"; @@ -156,6 +157,7 @@ export { darkGreen, alertGreen, matchGreen, + successGreen, lightOrange, translationHover, brown, diff --git a/src/dailyAudio/LessonPlaybackView.js b/src/dailyAudio/LessonPlaybackView.js index 28b4ec790..377d5719d 100644 --- a/src/dailyAudio/LessonPlaybackView.js +++ b/src/dailyAudio/LessonPlaybackView.js @@ -3,9 +3,10 @@ import CustomAudioPlayer from "../components/CustomAudioPlayer"; import FeedbackModal from "../components/FeedbackModal"; import { FEEDBACK_OPTIONS, FEEDBACK_CODES_NAME } from "../components/FeedbackConstants"; import Word from "../words/Word"; +import { successGreen } from "../components/colors"; import { AUDIO_STATUS } from "./AudioLessonConstants"; import { LessonWrapper, LessonTitle, SuggestionSubtitle, CompletionCheck } from "./LessonView.sc"; -import { wordsAsTile } from "./TodayAudio"; +import { wordsAsTile } from "./audioUtils"; export default function LessonPlaybackView({ lessonData, @@ -97,7 +98,7 @@ export default function LessonPlaybackView({ borderRadius: "4px", }} > - + ✓ Lesson completed! Great job on finishing today's lesson.
diff --git a/src/dailyAudio/LessonView.sc.js b/src/dailyAudio/LessonView.sc.js index 4207e75f8..cc290d237 100644 --- a/src/dailyAudio/LessonView.sc.js +++ b/src/dailyAudio/LessonView.sc.js @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { zeeguuOrange } from "../components/colors"; +import { zeeguuOrange, successGreen } from "../components/colors"; export const LessonWrapper = styled.div` padding: 20px; @@ -20,6 +20,6 @@ export const SuggestionSubtitle = styled.p` `; export const CompletionCheck = styled.span` - color: #28a745; + color: ${successGreen}; font-size: 20px; `; diff --git a/src/dailyAudio/PastLessons.js b/src/dailyAudio/PastLessons.js index c7c48ec36..698637f73 100644 --- a/src/dailyAudio/PastLessons.js +++ b/src/dailyAudio/PastLessons.js @@ -4,7 +4,7 @@ import { APIContext } from "../contexts/APIContext"; import { UserContext } from "../contexts/UserContext"; import LoadingAnimation from "../components/LoadingAnimation"; import CustomAudioPlayer from "../components/CustomAudioPlayer"; -import { wordsAsTile } from "./TodayAudio"; +import { wordsAsTile } from "./audioUtils"; export default function PastLessons() { const api = useContext(APIContext); diff --git a/src/dailyAudio/SuggestionSelector.js b/src/dailyAudio/SuggestionSelector.js index f48d9692e..841f351f3 100644 --- a/src/dailyAudio/SuggestionSelector.js +++ b/src/dailyAudio/SuggestionSelector.js @@ -6,7 +6,7 @@ import { SelectablePill, DescriptionText, InputArea, -} from "./TopicSuggestion.sc"; +} from "./SuggestionSelector.sc"; const MAX_SUGGESTION_LENGTH = 80; @@ -42,6 +42,7 @@ export function getSavedSuggestion(lang) { export default function SuggestionSelector({ suggestionType, setSuggestionType, suggestion, setSuggestion, lang }) { return ( + {Object.entries(SUGGESTION_TYPES).map(([key, { label }]) => ( ))} + {SUGGESTION_TYPES[suggestionType].description} + + ); } diff --git a/src/dailyAudio/TopicSuggestion.sc.js b/src/dailyAudio/SuggestionSelector.sc.js similarity index 100% rename from src/dailyAudio/TopicSuggestion.sc.js rename to src/dailyAudio/SuggestionSelector.sc.js diff --git a/src/dailyAudio/TodayAudio.js b/src/dailyAudio/TodayAudio.js index 84ba7b7d1..2872a4d4b 100644 --- a/src/dailyAudio/TodayAudio.js +++ b/src/dailyAudio/TodayAudio.js @@ -10,19 +10,7 @@ import { AUDIO_STATUS, GENERATION_PROGRESS } from "./AudioLessonConstants"; import { VerticalCentering, GenerateButton } from "./GenerateButton.sc"; import SuggestionSelector, { getSavedSuggestion, getSavedSuggestionType } from "./SuggestionSelector"; import LessonPlaybackView from "./LessonPlaybackView"; - -function shortDate() { - return `[${new Date().toLocaleDateString("en-US", { month: "short", day: "numeric" })}]`; -} - -export function wordsAsTile(words) { - if (!words || !words.length) return ""; - - const comma_separated_words = words.map((word) => word.origin || word).join(", "); - const capitalized_comma_separated_words = - comma_separated_words.charAt(0).toUpperCase() + comma_separated_words.slice(1); - return capitalized_comma_separated_words; -} +import { wordsAsTile, shortDate } from "./audioUtils"; export default function TodayAudio({ setShowTabs }) { const api = useContext(APIContext); diff --git a/src/dailyAudio/audioUtils.js b/src/dailyAudio/audioUtils.js new file mode 100644 index 000000000..b5aaccbf8 --- /dev/null +++ b/src/dailyAudio/audioUtils.js @@ -0,0 +1,12 @@ +export function wordsAsTile(words) { + if (!words || !words.length) return ""; + + const comma_separated_words = words.map((word) => word.origin || word).join(", "); + const capitalized_comma_separated_words = + comma_separated_words.charAt(0).toUpperCase() + comma_separated_words.slice(1); + return capitalized_comma_separated_words; +} + +export function shortDate() { + return `[${new Date().toLocaleDateString("en-US", { month: "short", day: "numeric" })}]`; +} From 8b889901a75af974096ed6b081e047bfb7dd7702 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 3 Apr 2026 15:51:32 +0200 Subject: [PATCH 23/23] Remove redundant lesson description text Co-Authored-By: Claude Opus 4.6 (1M context) --- src/dailyAudio/LessonPlaybackView.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/dailyAudio/LessonPlaybackView.js b/src/dailyAudio/LessonPlaybackView.js index 377d5719d..a55067256 100644 --- a/src/dailyAudio/LessonPlaybackView.js +++ b/src/dailyAudio/LessonPlaybackView.js @@ -37,9 +37,6 @@ export default function LessonPlaybackView({ {error &&
{error}
}
- {!lessonData.is_completed && ( -

Here's your daily lesson! Listen to improve your comprehension skills.

- )}