Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bb02346
Add typed suggestion selector for audio lessons (Words only / Topic /…
mircealungu Apr 2, 2026
a0213ba
Show character count near input limit
mircealungu Apr 3, 2026
9f127fb
Extract MAX_SUGGESTION_LENGTH constant to avoid magic numbers
mircealungu Apr 3, 2026
0d5ed0c
UI polish: textarea input, inline Context label, bump limit to 80
mircealungu Apr 3, 2026
cf216cb
UI polish: extract styled-components, fix layout centering
mircealungu Apr 3, 2026
0171621
Move GenerateButton to its own styled-component file
mircealungu Apr 3, 2026
96ea298
Rename topic_suggestion to suggestion across frontend
mircealungu Apr 3, 2026
f4b88fb
Persist suggestion text per type, rename storage keys
mircealungu Apr 3, 2026
4608140
Fix localStorage key string to match new naming
mircealungu Apr 3, 2026
21f0a36
Fix localStorage key prefix: zeeguu_ -> audio_lesson_
mircealungu Apr 3, 2026
1436375
Clean up leftover topic naming: drop suggestion from tab title, renam…
mircealungu Apr 3, 2026
8658e60
Extract CenteringContainer and lesson view styled-components
mircealungu Apr 3, 2026
cc73467
Rename CenteringContainer to VerticalCentering
mircealungu Apr 3, 2026
5aff1df
Extract shortDate() helper for page title
mircealungu Apr 3, 2026
0560768
Rename TypePill to SelectablePill for reusability
mircealungu Apr 3, 2026
716ee6e
Extract SuggestionSelector component with clear button
mircealungu Apr 3, 2026
ac69af7
Extract getSavedSuggestion/getSavedSuggestionType helpers, rename typ…
mircealungu Apr 3, 2026
f45198f
Extract LessonPlaybackView component
mircealungu Apr 3, 2026
adc8b1f
Reuse getSavedSuggestionType inside getSavedSuggestion
mircealungu Apr 3, 2026
c6b915f
Extract reusable ClearableInput component
mircealungu Apr 3, 2026
e512562
Remove leftover useRef from SuggestionSelector
mircealungu Apr 3, 2026
8612af3
Simplify: extract successGreen color, move wordsAsTile to audioUtils,…
mircealungu Apr 3, 2026
8b88990
Remove redundant lesson description text
mircealungu Apr 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,14 @@ android/*.json
android/fastlane/play-store-credentials.json
android/fastlane/report.xml

*.md
*.md

# Claude Code
.claude/

# Eclipse/Buildship IDE files (Android Studio)
android/.project
android/.settings/
android/app/.classpath
android/app/.project
android/app/.settings/
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
min-release-age=7
9 changes: 6 additions & 3 deletions src/api/dailyAudio.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,17 @@ Zeeguu_API.prototype.getTodaysLesson = function (callback, onError) {
});
};

Zeeguu_API.prototype.generateDailyLesson = function (callback, onError, topicSuggestion) {
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);
}
}

fetch(this._appendSessionToUrl("generate_daily_lesson"), {
Expand Down
64 changes: 64 additions & 0 deletions src/components/ClearableInput.js
Original file line number Diff line number Diff line change
@@ -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 (
<Wrapper>
<StyledInput
ref={inputRef}
rows={rows}
placeholder={placeholder}
maxLength={maxLength}
value={value}
tabIndex={tabIndex}
onChange={onChange}
{...props}
/>
{value && (
<ClearBtn
onClick={() => {
onClear?.();
inputRef.current?.focus();
}}
>
<CancelIcon fontSize="inherit" />
</ClearBtn>
)}
</Wrapper>
);
}
2 changes: 2 additions & 0 deletions src/components/colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -156,6 +157,7 @@ export {
darkGreen,
alertGreen,
matchGreen,
successGreen,
lightOrange,
translationHover,
brown,
Expand Down
41 changes: 41 additions & 0 deletions src/dailyAudio/GenerateButton.sc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import styled from "styled-components";
import { orange500, orange600, orange800 } from "../components/colors";

export const VerticalCentering = 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;
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;
}
`;
152 changes: 152 additions & 0 deletions src/dailyAudio/LessonPlaybackView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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 { successGreen } from "../components/colors";
import { AUDIO_STATUS } from "./AudioLessonConstants";
import { LessonWrapper, LessonTitle, SuggestionSubtitle, CompletionCheck } from "./LessonView.sc";
import { wordsAsTile } from "./audioUtils";

export default function LessonPlaybackView({
lessonData,
setLessonData,
words,
error,
api,
userDetails,
setUserDetails,
listeningSession,
currentPlaybackTime,
setCurrentPlaybackTime,
}) {
const [openFeedback, setOpenFeedback] = useState(false);

return (
<LessonWrapper>
<LessonTitle $compact={!!lessonData.suggestion}>
{lessonData.is_completed && <CompletionCheck>✓</CompletionCheck>}
{wordsAsTile(words)}
</LessonTitle>
{lessonData.suggestion && (
<SuggestionSubtitle>
{lessonData.suggestion_type === "situation" ? "Situation" : "Topic"}: {lessonData.suggestion}
</SuggestionSubtitle>
)}

{error && <div style={{ color: "red", marginBottom: "20px" }}>{error}</div>}

<div>

<CustomAudioPlayer
src={lessonData.audio_url}
initialProgress={
lessonData.pause_position_seconds || lessonData.position_seconds || lessonData.progress_seconds || 0
}
language={userDetails?.learned_language}
title={lessonData.words ? wordsAsTile(lessonData.words) : "Daily Audio Lesson"}
artist="Zeeguu Daily Lesson"
onPlay={() => {
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 && (
<div
style={{
marginBottom: "20px",
marginTop: "20px",
padding: "12px",
backgroundColor: "var(--bg-secondary)",
border: "1px solid #28a745",
borderRadius: "4px",
}}
>
<span style={{ color: successGreen, fontWeight: "500", fontSize: "14px" }}>
✓ Lesson completed! Great job on finishing today's lesson.
</span>
</div>
)}

{words && words.length > 0 && (
<div style={{ marginTop: "30px", marginBottom: "20px" }}>
<h3 style={{ fontSize: "16px", fontWeight: "600", marginBottom: "12px", color: "var(--text-primary)" }}>
Words in this lesson
</h3>
{words.map((word, index) => (
<Word
key={index}
bookmark={word}
disableEdit={true}
compact={true}
showRanking={false}
/>
))}
</div>
)}

<div style={{ marginTop: "40px", textAlign: "center" }}>
<button
onClick={() => setOpenFeedback(true)}
style={{
backgroundColor: "transparent",
color: "var(--text-faint)",
border: "none",
borderRadius: "0",
padding: "4px 8px",
fontSize: "12px",
cursor: "pointer",
textDecoration: "underline",
}}
>
Feedback
</button>
</div>

<FeedbackModal
prefixMsg={lessonData
? `Daily Audio Lesson - Playback time: ${Math.floor(currentPlaybackTime / 60)}:${(currentPlaybackTime % 60).toFixed(0).padStart(2, '0')} | Lesson ID: ${lessonData.lesson_id} | Words: ${wordsAsTile(lessonData.words)} | Date: ${new Date(lessonData.created_at || Date.now()).toLocaleDateString()}`
: "Daily Audio Lesson Feedback"
}
open={openFeedback}
setOpen={setOpenFeedback}
componentCategories={FEEDBACK_OPTIONS.ALL}
preselectedCategory={FEEDBACK_CODES_NAME.DAILY_AUDIO}
/>
</div>
</LessonWrapper>
);
}
25 changes: 25 additions & 0 deletions src/dailyAudio/LessonView.sc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import styled from "styled-components";
import { zeeguuOrange, successGreen } 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: ${successGreen};
font-size: 20px;
`;
2 changes: 1 addition & 1 deletion src/dailyAudio/PastLessons.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading