From 40d436ad9cc585c2b848498ab875df52d8db33fd Mon Sep 17 00:00:00 2001 From: Trey t Date: Mon, 13 Apr 2026 10:07:46 -0500 Subject: [PATCH] Highlight main tenses with Essential badges and focus mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mark 6 core tenses (presente, pretérito, imperfecto, futuro, subjuntivo presente, imperativo) as essential. Add "Common Tenses" quick action in Practice to drill only these. Show "Essential" badge on core tenses in Guide > Tenses list. Closes #3 Co-Authored-By: Claude Opus 4.6 (1M context) --- Conjuga/Conjuga/Models/TenseInfo.swift | 16 ++++++++ .../Services/PracticeSessionService.swift | 18 +++++++++ .../ViewModels/PracticeViewModel.swift | 1 + Conjuga/Conjuga/Views/Guide/GuideView.swift | 25 +++++++++--- .../Conjuga/Views/Practice/PracticeView.swift | 38 +++++++++++++++++++ 5 files changed, 92 insertions(+), 6 deletions(-) diff --git a/Conjuga/Conjuga/Models/TenseInfo.swift b/Conjuga/Conjuga/Models/TenseInfo.swift index 445bc27..19d1f9a 100644 --- a/Conjuga/Conjuga/Models/TenseInfo.swift +++ b/Conjuga/Conjuga/Models/TenseInfo.swift @@ -29,6 +29,18 @@ enum TenseID: String, CaseIterable, Codable, Sendable, Hashable { .ind_futuro, ] + /// The 6 most essential tenses every learner should master. + static let coreTenses: Set = [ + .ind_presente, + .ind_preterito, + .ind_imperfecto, + .ind_futuro, + .subj_presente, + .imp_afirmativo, + ] + + static let coreTenseIDs = coreTenses.map(\.rawValue) + static let defaultPracticeIDs = defaultPractice.map(\.rawValue) } @@ -39,6 +51,10 @@ struct TenseInfo: Identifiable, Hashable, Sendable { let mood: String let order: Int + var isCore: Bool { + TenseID(rawValue: id).map { TenseID.coreTenses.contains($0) } ?? false + } + static let all: [TenseInfo] = [ TenseInfo(id: TenseID.ind_presente.rawValue, spanish: "Indicativo Presente", english: "Present", mood: "Indicative", order: 0), TenseInfo(id: TenseID.ind_preterito.rawValue, spanish: "Indicativo Pretérito", english: "Preterite", mood: "Indicative", order: 1), diff --git a/Conjuga/Conjuga/Services/PracticeSessionService.swift b/Conjuga/Conjuga/Services/PracticeSessionService.swift index a3c6bd2..2082730 100644 --- a/Conjuga/Conjuga/Services/PracticeSessionService.swift +++ b/Conjuga/Conjuga/Services/PracticeSessionService.swift @@ -58,6 +58,10 @@ struct PracticeSessionService { if let form = pickIrregularForm(filter: filter) { return loadCard(from: form) } + case .commonTenses: + if let form = pickCommonTenseForm() { + return loadCard(from: form) + } case .none: break } @@ -231,6 +235,20 @@ struct PracticeSessionService { ) } + private func pickCommonTenseForm() -> VerbForm? { + let settings = settings() + let coreTenseIDs = TenseID.coreTenseIDs + let verbs = referenceStore.fetchVerbs(selectedLevel: settings.selectedLevel) + guard let verb = verbs.randomElement() else { return nil } + + let forms = referenceStore.fetchVerbForms(verbId: verb.id).filter { form in + coreTenseIDs.contains(form.tenseId) && + (settings.showVosotros || form.personIndex != 4) + } + + return forms.randomElement() + } + private func pickRandomForm() -> VerbForm? { let settings = settings() let verbs = referenceStore.fetchVerbs(selectedLevel: settings.selectedLevel) diff --git a/Conjuga/Conjuga/ViewModels/PracticeViewModel.swift b/Conjuga/Conjuga/ViewModels/PracticeViewModel.swift index a928cbb..b5943a0 100644 --- a/Conjuga/Conjuga/ViewModels/PracticeViewModel.swift +++ b/Conjuga/Conjuga/ViewModels/PracticeViewModel.swift @@ -45,6 +45,7 @@ enum FocusMode: Sendable { case none case weakVerbs case irregularity(IrregularityFilter) + case commonTenses } @MainActor diff --git a/Conjuga/Conjuga/Views/Guide/GuideView.swift b/Conjuga/Conjuga/Views/Guide/GuideView.swift index 6330fb6..12e3a88 100644 --- a/Conjuga/Conjuga/Views/Guide/GuideView.swift +++ b/Conjuga/Conjuga/Views/Guide/GuideView.swift @@ -100,12 +100,25 @@ private struct TenseRowView: View { let tense: TenseInfo var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(tense.english) - .font(.headline) - Text(tense.spanish) - .font(.subheadline) - .foregroundStyle(.secondary) + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(tense.english) + .font(.headline) + Text(tense.spanish) + .font(.subheadline) + .foregroundStyle(.secondary) + } + + Spacer() + + if tense.isCore { + Text("Essential") + .font(.caption2.weight(.semibold)) + .foregroundStyle(.orange) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background(.orange.opacity(0.12), in: Capsule()) + } } } } diff --git a/Conjuga/Conjuga/Views/Practice/PracticeView.swift b/Conjuga/Conjuga/Views/Practice/PracticeView.swift index 9e583b6..0193a54 100644 --- a/Conjuga/Conjuga/Views/Practice/PracticeView.swift +++ b/Conjuga/Conjuga/Views/Practice/PracticeView.swift @@ -135,6 +135,44 @@ struct PracticeView: View { .font(.headline) .frame(maxWidth: .infinity, alignment: .leading) + // Common tenses focus + Button { + viewModel.practiceMode = .flashcard + viewModel.focusMode = .commonTenses + viewModel.sessionCorrect = 0 + viewModel.sessionTotal = 0 + viewModel.loadNextCard( + localContext: modelContext, + cloudContext: cloudModelContext + ) + withAnimation { isPracticing = true } + } label: { + HStack(spacing: 14) { + Image(systemName: "star.fill") + .font(.title3) + .foregroundStyle(.orange) + .frame(width: 32) + + VStack(alignment: .leading, spacing: 2) { + Text("Common Tenses") + .font(.subheadline.weight(.semibold)) + Text("Practice the 6 most essential tenses") + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.caption) + .foregroundStyle(.tertiary) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + .tint(.primary) + .glassEffect(in: RoundedRectangle(cornerRadius: 14)) + // Weak verbs focus Button { viewModel.practiceMode = .flashcard