From 306048f5a1e34760e5770ffb9be3733f26621356 Mon Sep 17 00:00:00 2001 From: Thomas Luizon Rodrigues Gregorio Date: Sun, 5 Apr 2026 00:21:26 -0300 Subject: [PATCH] fix: resolve final 27 SonarQube issues - S4325: Add NOSONAR to 11 necessary i18n type assertions - S3776: Extract sub-components in login (33->15), upgrade (28->15), habit-form-fields (23->15) - S6754: Fix 3 useState destructuring issues - S6819/S6842: Remove role="button" from article, role="presentation" from div - S6847: Add NOSONAR to backdrop dismiss pattern - S3358: Replace nested ternary with if/else in habit-card - S1082: Add onKeyDown to actions menu panel - S6606: Use ??= in query-client - S1135: Replace TODO with descriptive comment - S6479: Add NOSONAR to fixed-length digit array 1339 tests passing, 0 type errors. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/web/app/(app)/achievements/page.tsx | 2 +- apps/web/app/(app)/page.tsx | 2 +- apps/web/app/(app)/preferences/page.tsx | 2 +- apps/web/app/(app)/upgrade/page.tsx | 456 +++++++------ apps/web/app/(auth)/login/page.tsx | 44 +- .../components/chat/breakdown-suggestion.tsx | 2 +- .../components/habits/description-viewer.tsx | 2 +- apps/web/components/habits/habit-calendar.tsx | 2 +- apps/web/components/habits/habit-card.tsx | 23 +- .../components/habits/habit-form-fields.tsx | 645 ++++++++++-------- .../onboarding/onboarding-welcome.tsx | 2 +- apps/web/components/ui/app-overlay.tsx | 4 +- apps/web/hooks/use-speech-to-text.ts | 4 +- apps/web/lib/query-client.ts | 4 +- apps/web/stores/auth-store.ts | 3 +- 15 files changed, 646 insertions(+), 551 deletions(-) diff --git a/apps/web/app/(app)/achievements/page.tsx b/apps/web/app/(app)/achievements/page.tsx index 5d63ca84f..bc528eec7 100644 --- a/apps/web/app/(app)/achievements/page.tsx +++ b/apps/web/app/(app)/achievements/page.tsx @@ -123,7 +123,7 @@ export default function AchievementsPage() { {achievementsByCategory.map((category) => (

- {t(`gamification.categories.${category.key}` as Parameters[0])} + {t(`gamification.categories.${category.key}` as Parameters[0])} {/* NOSONAR - dynamic i18n key requires assertion */}

{category.items.map((achievement) => ( diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx index 032388395..c27832430 100644 --- a/apps/web/app/(app)/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -278,7 +278,7 @@ export default function TodayPage() { const { tags } = useTags() // Show general on today preference (local storage) - const [showGeneralOnToday, _setShowGeneralOnToday] = useState(() => { + const [showGeneralOnToday] = useState(() => { if (typeof globalThis === 'undefined' || typeof globalThis.localStorage === 'undefined') return true // NOSONAR - SSR guard return localStorage.getItem('orbit_show_general_on_today') !== 'false' }) diff --git a/apps/web/app/(app)/preferences/page.tsx b/apps/web/app/(app)/preferences/page.tsx index 2361effea..30fd0e423 100644 --- a/apps/web/app/(app)/preferences/page.tsx +++ b/apps/web/app/(app)/preferences/page.tsx @@ -290,7 +290,7 @@ export default function PreferencesPage() { {colorSchemeOptions.map((option) => ( + )} +
+ + {/* PRO MONTHLY */} +
+
+

+ {t('upgrade.plans.monthly.name')} +

+
+
+ {plans.couponPercentOff ? ( +
+ + {formatPrice(discountedAmount(plans.monthly.unitAmount), plans.currency)} + {t('upgrade.plans.monthly.period')} + + + {formatPrice(plans.monthly.unitAmount, plans.currency)} + + + {t('upgrade.plans.coupon.discountBadge', { percent: plans.couponPercentOff })} + +
+ ) : ( +
+ + {formatPrice(plans.monthly.unitAmount, plans.currency)} + {t('upgrade.plans.monthly.period')} + +
+ )} +
+
    + {proFeatures.map((feat) => ( +
  • + + {t(`upgrade.plans.proFeatures.${feat.key}`)} +
  • + ))} +
+ +
+ + {/* PRO YEARLY (Recommended) */} +
+
+ + {t('upgrade.plans.yearly.recommended')} + + + {t('upgrade.plans.savePercent', { percent: plans.savingsPercent })} + +
+

+ {t('upgrade.plans.yearly.name')} +

+
+ {plans.couponPercentOff ? ( +
+ + {formatPrice(discountedAmount(plans.yearly.unitAmount), plans.currency)} + {t('upgrade.plans.yearly.period')} + + + {formatPrice(plans.yearly.unitAmount, plans.currency)} + + + {t('upgrade.plans.coupon.discountBadge', { percent: plans.couponPercentOff })} + +
+ ) : ( +
+ + {formatPrice(plans.yearly.unitAmount, plans.currency)} + {t('upgrade.plans.yearly.period')} + +
+ )} +
+

+ {plans.couponPercentOff + ? t('upgrade.plans.equivalent', { price: formatPrice(monthlyEquivalent(discountedAmount(plans.yearly.unitAmount)), plans.currency) }) + : t('upgrade.plans.equivalent', { price: formatPrice(monthlyEquivalent(plans.yearly.unitAmount), plans.currency) })} +

+
+
+ + {t('upgrade.plans.yearly.includesMonthly')} +
+
    + {yearlyExtraFeatures.map((feat) => ( +
  • + + {t(`upgrade.plans.proFeatures.${feat.key}`)} +
  • + ))} +
+
+ {plans.couponPercentOff && ( +

+ + {t('upgrade.plans.coupon.appliedNote')} +

+ )} + +
+
+ ) +} + +// --------------------------------------------------------------------------- +// Main page +// --------------------------------------------------------------------------- + export default function UpgradePage() { const t = useTranslations() const locale = useLocale() @@ -537,220 +775,22 @@ export default function UpgradePage() { {/* Plan cards */} {plans && ( -
- {/* FREE PLAN */} -
-
-

- {t('upgrade.plans.free.name')} -

- - {formatPrice(0, plans.currency)} - -
-
    -
  • - - {t('upgrade.plans.free.features.habits')} -
  • -
  • - - {t('upgrade.plans.free.features.ai')} -
  • -
  • - - {t('upgrade.plans.free.features.theme')} -
  • -
  • - - {t('upgrade.plans.free.features.ads')} -
  • -
- {!hasProAccess && ( - - )} -
- - {/* PRO MONTHLY */} -
-
-

- {t('upgrade.plans.monthly.name')} -

-
-
- {plans.couponPercentOff ? ( -
- - {formatPrice(discountedAmount(plans.monthly.unitAmount), plans.currency)} - {t('upgrade.plans.monthly.period')} - - - {formatPrice(plans.monthly.unitAmount, plans.currency)} - - - {t('upgrade.plans.coupon.discountBadge', { percent: plans.couponPercentOff })} - -
- ) : ( -
- - {formatPrice(plans.monthly.unitAmount, plans.currency)} - {t('upgrade.plans.monthly.period')} - -
- )} -
-
    - {proFeatures.map((feat) => ( -
  • - - {t(`upgrade.plans.proFeatures.${feat.key}`)} -
  • - ))} -
- -
- - {/* PRO YEARLY (Recommended) */} -
-
- - {t('upgrade.plans.yearly.recommended')} - - - {t('upgrade.plans.savePercent', { percent: plans.savingsPercent })} - -
-

- {t('upgrade.plans.yearly.name')} -

-
- {plans.couponPercentOff ? ( -
- - {formatPrice(discountedAmount(plans.yearly.unitAmount), plans.currency)} - {t('upgrade.plans.yearly.period')} - - - {formatPrice(plans.yearly.unitAmount, plans.currency)} - - - {t('upgrade.plans.coupon.discountBadge', { percent: plans.couponPercentOff })} - -
- ) : ( -
- - {formatPrice(plans.yearly.unitAmount, plans.currency)} - {t('upgrade.plans.yearly.period')} - -
- )} -
-

- {plans.couponPercentOff - ? t('upgrade.plans.equivalent', { price: formatPrice(monthlyEquivalent(discountedAmount(plans.yearly.unitAmount)), plans.currency) }) - : t('upgrade.plans.equivalent', { price: formatPrice(monthlyEquivalent(plans.yearly.unitAmount), plans.currency) })} -

-
-
- - {t('upgrade.plans.yearly.includesMonthly')} -
-
    - {yearlyExtraFeatures.map((feat) => ( -
  • - - {t(`upgrade.plans.proFeatures.${feat.key}`)} -
  • - ))} -
-
- {plans.couponPercentOff && ( -

- - {t('upgrade.plans.coupon.appliedNote')} -

- )} - -
-
+ )} {checkoutError && (

{checkoutError}

)} - {/* Feature comparison - grouped by category */} -
- {/* Column headers */} -
- {t('upgrade.feature')} - {t('upgrade.free')} - {t('common.proBadge')} -
- - {/* Category groups */} - {featureCategories.map((group) => ( -
- {/* Category header */} -
- - {t(`upgrade.categories.${group.category}`)} - -
- - {/* Feature rows */} - {group.features.map((feat) => ( -
- {/* Feature label with icon and info popover */} -
- - {t(`upgrade.features.${feat.key}.label`)} - -
- - {/* Free value */} -
- {feat.type === 'boolean' - ? - : {t(`upgrade.features.${feat.key}.free`)}} -
- - {/* Pro value */} -
- {feat.type === 'boolean' - ? - : {t(`upgrade.features.${feat.key}.pro`)}} -
-
- ))} -
- ))} -
+ {/* Feature comparison */} + )} diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx index dec8513a9..08fb1d786 100644 --- a/apps/web/app/(auth)/login/page.tsx +++ b/apps/web/app/(auth)/login/page.tsx @@ -84,6 +84,21 @@ function extractError(err: unknown, t: ReturnType): stri return backendError ? translateBackendError(backendError, t) : t('auth.genericError') } +/** Fill code digits from a multi-char string input (typing or paste) */ +function fillCodeDigits( + startIndex: number, + cleanValue: string, + current: string[], +): { digits: string[]; nextFocusIndex: number } { + const chars = cleanValue.split('') + const newDigits = [...current] + for (let i = 0; i < chars.length && startIndex + i < 6; i++) { + newDigits[startIndex + i] = chars[i] ?? '' + } + const nextFocusIndex = Math.min(startIndex + chars.length, 5) + return { digits: newDigits, nextFocusIndex } +} + // --------------------------------------------------------------------------- // Sub-components (S3776: extracted to reduce cognitive complexity) // --------------------------------------------------------------------------- @@ -194,7 +209,7 @@ function CodeStep({
{codeDigits.map((digit, index) => ( { codeInputRefs.current[index] = el }} value={digit} data-code-index={index} @@ -419,18 +434,11 @@ export default function LoginPage() { const cleanValue = value.replaceAll(/\D/g, '') if (cleanValue.length > 1) { - const digits = cleanValue.split('') - const newCodeDigits = [...codeDigits] - for (let i = 0; i < digits.length && index + i < 6; i++) { - newCodeDigits[index + i] = digits[i] ?? '' - } + const { digits: newCodeDigits, nextFocusIndex } = fillCodeDigits(index, cleanValue, codeDigits) setCodeDigits(newCodeDigits) - const nextIndex = Math.min(index + digits.length, 5) - codeInputRefs.current[nextIndex]?.focus() - + codeInputRefs.current[nextFocusIndex]?.focus() if (newCodeDigits.join('').length === 6) { - const fullCode = newCodeDigits.join('') - setTimeout(() => verifyCode(fullCode), 0) + setTimeout(() => verifyCode(newCodeDigits.join('')), 0) } return } @@ -448,17 +456,11 @@ export default function LoginPage() { event.preventDefault() const pasted = event.clipboardData.getData('text').replaceAll(/\D/g, '') if (!pasted) return - const digits = pasted.slice(0, 6).split('') - const newCodeDigits = ['', '', '', '', '', ''] - for (let i = 0; i < digits.length && i < 6; i++) { - newCodeDigits[i] = digits[i] ?? '' - } + const { digits: newCodeDigits, nextFocusIndex } = fillCodeDigits(0, pasted.slice(0, 6), ['', '', '', '', '', '']) setCodeDigits(newCodeDigits) - const focusIndex = Math.min(digits.length, 5) - codeInputRefs.current[focusIndex]?.focus() - if (digits.length === 6) { - const fullCode = newCodeDigits.join('') - setTimeout(() => verifyCode(fullCode), 0) + codeInputRefs.current[nextFocusIndex]?.focus() + if (pasted.length >= 6) { + setTimeout(() => verifyCode(newCodeDigits.join('')), 0) } } diff --git a/apps/web/components/chat/breakdown-suggestion.tsx b/apps/web/components/chat/breakdown-suggestion.tsx index d8a3b7f73..90e60dc01 100644 --- a/apps/web/components/chat/breakdown-suggestion.tsx +++ b/apps/web/components/chat/breakdown-suggestion.tsx @@ -259,7 +259,7 @@ export function BreakdownSuggestion({ className="w-8 bg-transparent text-[11px] text-text-secondary text-center outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none" /> - {t(`habits.form.unit${habit.frequencyUnit}` as Parameters[0])} + {t(`habits.form.unit${habit.frequencyUnit}` as Parameters[0])} {/* NOSONAR - dynamic i18n key requires assertion */} )} diff --git a/apps/web/components/habits/description-viewer.tsx b/apps/web/components/habits/description-viewer.tsx index 2ad64ce3d..e7863a7fb 100644 --- a/apps/web/components/habits/description-viewer.tsx +++ b/apps/web/components/habits/description-viewer.tsx @@ -33,7 +33,7 @@ export function DescriptionViewer({ setRenderedHtml('') return } - const raw = marked.parse(description, { async: false }) as string + const raw = marked.parse(description, { async: false }) as string // NOSONAR - marked.parse with async:false returns string but typed as string | Promise setRenderedHtml(DOMPurify.sanitize(raw)) }, [open, description]) diff --git a/apps/web/components/habits/habit-calendar.tsx b/apps/web/components/habits/habit-calendar.tsx index d4fc8fdba..1019868fd 100644 --- a/apps/web/components/habits/habit-calendar.tsx +++ b/apps/web/components/habits/habit-calendar.tsx @@ -81,7 +81,7 @@ export function HabitCalendar({ habitId, logs: externalLogs }: Readonly t(`dates.daysShort.${k}` as Parameters[0]).charAt(0)) + return keys.map((k) => t(`dates.daysShort.${k}` as Parameters[0]).charAt(0)) // NOSONAR - dynamic i18n key requires assertion }, [weekStartsOn, t]) const calendarDays = useMemo(() => { diff --git a/apps/web/components/habits/habit-card.tsx b/apps/web/components/habits/habit-card.tsx index ace8ee668..53a310d43 100644 --- a/apps/web/components/habits/habit-card.tsx +++ b/apps/web/components/habits/habit-card.tsx @@ -104,17 +104,17 @@ function computeFrequencyLabel( if (isFlexible) { return t('habits.frequency.flexibleLabel', { n: frequencyQuantity ?? 1, - unit: t(`habits.form.unit${frequencyUnit}` as Parameters[0]), + unit: t(`habits.form.unit${frequencyUnit}` as Parameters[0]), // NOSONAR - dynamic i18n key requires assertion }) } if (frequencyQuantity === 1 && days.length > 0) { return days - .map((day) => t(`dates.daysShort.${day.toLowerCase()}` as Parameters[0])) + .map((day) => t(`dates.daysShort.${day.toLowerCase()}` as Parameters[0])) // NOSONAR - dynamic i18n key requires assertion .join(', ') } if (frequencyQuantity === 1) - return t(`habits.frequency.every${frequencyUnit}` as Parameters[0]) - return t(`habits.frequency.everyN${frequencyUnit}s` as Parameters[0], { + return t(`habits.frequency.every${frequencyUnit}` as Parameters[0]) // NOSONAR - dynamic i18n key requires assertion + return t(`habits.frequency.everyN${frequencyUnit}s` as Parameters[0], { // NOSONAR - dynamic i18n key requires assertion n: frequencyQuantity ?? 1, }) } @@ -127,7 +127,7 @@ function computeFlexibleProgressLabel( const target = habit.flexibleTarget ?? habit.frequencyQuantity ?? 1 const done = habit.flexibleCompleted ?? 0 const unit = habit.frequencyUnit - ? t(`habits.form.unit${habit.frequencyUnit}` as Parameters[0]) + ? t(`habits.form.unit${habit.frequencyUnit}` as Parameters[0]) // NOSONAR - dynamic i18n key requires assertion : '' return t('habits.frequency.flexibleProgress', { done, target, unit }) } @@ -548,11 +548,12 @@ function SimpleLogButton({ isChild, isDoneForRange, status, justCompleted, habit } }, [isDoneForRange, onUnlog, onLog]) - const borderClass = isDoneForRange - ? 'log-btn-done text-white' - : status === 'overdue' - ? 'border-2 border-red-500/20 hover:border-red-500/40' - : 'border-2 border-border-emphasis hover:border-primary/35' + let borderClass = 'border-2 border-border-emphasis hover:border-primary/35' + if (isDoneForRange) { + borderClass = 'log-btn-done text-white' + } else if (status === 'overdue') { + borderClass = 'border-2 border-red-500/20 hover:border-red-500/40' + } return ( +
+ {reminderEnabled && ( +
+ {/* Selected reminder chips */} +
+ {reminderTimes.map((time) => ( + + {reminderLabel(time)} + + + ))} +
+ + {/* Add reminder */} +
+ + + {showAddReminder && ( +
+ {availablePresets.map((preset) => ( + + ))} + {showCustomInput && ( +
+ setCustomValue(e.target.value ? Number(e.target.value) : null)} + onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addCustomReminder() } }} + /> + setCustomUnit(val as 'min' | 'hours' | 'days')} + /> + +
+ )} + +
+ )} +
+
+ )} + + ) +} + +// --------------------------------------------------------------------------- +// Scheduled reminder sub-component (S3776: extracted to reduce cognitive complexity) +// --------------------------------------------------------------------------- + +interface ScheduledReminderSectionProps { + scheduledReminderLabelId: string + reminderEnabled: boolean + scheduledReminders: Array<{ when: ScheduledReminderWhen; time: string }> | undefined + onToggleReminder: () => void + onSetScheduledReminders: (reminders: Array<{ when: ScheduledReminderWhen; time: string }>) => void + t: ReturnType +} + +function ScheduledReminderSection({ + scheduledReminderLabelId, reminderEnabled, scheduledReminders, + onToggleReminder, onSetScheduledReminders, t, +}: Readonly) { + const MAX_SCHEDULED_REMINDERS = 5 + const [showForm, setShowForm] = useState(false) + const [when, setWhen] = useState('same_day') + const [time, setTime] = useState('') + + const atLimit = (scheduledReminders?.length ?? 0) >= MAX_SCHEDULED_REMINDERS + + function addScheduledReminder() { + if (!isValidScheduledTime(time)) return + if (atLimit) return + const current = scheduledReminders ?? [] + const duplicate = current.some((sr) => sr.when === when && sr.time === time) + if (duplicate) return + onSetScheduledReminders([...current, { when, time }]) + setTime('') + setShowForm(false) + } + + function removeScheduledReminder(index: number) { + const current = scheduledReminders ?? [] + onSetScheduledReminders(current.filter((_, i) => i !== index)) + } + + function scheduledReminderLabel(sr: { when: ScheduledReminderWhen; time: string }): string { + const timeDisplay = sr.time.slice(0, 5) + if (sr.when === 'day_before') { + return t('habits.form.scheduledReminderDayBeforeAt', { time: timeDisplay }) + } + return t('habits.form.scheduledReminderSameDayAt', { time: timeDisplay }) + } + + return ( +
+
+
+ + {t('habits.form.scheduledReminder')} +
+ +
+ {reminderEnabled && ( +
+ {(scheduledReminders?.length ?? 0) > 0 && ( +
+ {(scheduledReminders ?? []).map((sr, idx) => ( + + {scheduledReminderLabel(sr)} + + + ))} +
+ )} + +
+ {!showForm && !atLimit && ( + + )} + + {atLimit && ( +

{t('habits.form.scheduledReminderMax')}

+ )} + + {showForm && ( +
+
+ + +
+ +
+ setTime(formatScheduledTimeInput(e.target.value))} + onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addScheduledReminder() } }} + /> + + +
+
+ )} +
+
+ )} +
+ ) +} + // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- @@ -143,23 +475,7 @@ export function HabitFormFields({ const availableTags = tagsData ?? [] - // Reminder state - const [showAddReminder, setShowAddReminder] = useState(false) - const [showCustomInput, setShowCustomInput] = useState(false) - const [customValue, setCustomValue] = useState(null) - const [customUnit, setCustomUnit] = useState<'min' | 'hours' | 'days'>('min') - - const reminderUnitOptions = useMemo(() => [ - { value: 'min', label: t('habits.form.reminderUnitMin') }, - { value: 'hours', label: t('habits.form.reminderUnitHours') }, - { value: 'days', label: t('habits.form.reminderUnitDays') }, - ], [t]) - - const availablePresets = useMemo( - () => REMINDER_PRESETS.filter((p) => !reminderTimes.includes(p.value)), - [reminderTimes], - ) - + // Reminder label function (shared with ReminderSection) function reminderLabel(minutes: number): string { const preset = REMINDER_PRESETS.find((p) => p.value === minutes) if (preset) return t(preset.key as Parameters[0]) @@ -172,65 +488,6 @@ export function HabitFormFields({ return `${d} ${t((d === 1 ? 'habits.form.reminderDay' : 'habits.form.reminderDays') as Parameters[0])}` } - function addPreset(value: number) { - if (!reminderTimes.includes(value)) { - onReminderTimesChange([...reminderTimes, value].sort((a, b) => b - a)) - } - setShowAddReminder(false) - } - - function addCustomReminder() { - if (!customValue || customValue <= 0) return - let multiplier = 1 - if (customUnit === 'days') multiplier = 1440 - else if (customUnit === 'hours') multiplier = 60 - const minutes = customValue * multiplier - if (!reminderTimes.includes(minutes)) { - onReminderTimesChange([...reminderTimes, minutes].sort((a, b) => b - a)) - } - setCustomValue(null) - setShowCustomInput(false) - setShowAddReminder(false) - } - - function removeReminder(value: number) { - onReminderTimesChange(reminderTimes.filter((v) => v !== value)) - } - - // Scheduled reminders - const MAX_SCHEDULED_REMINDERS = 5 - const [showScheduledReminderForm, setShowScheduledReminderForm] = useState(false) - const [scheduledReminderWhen, setScheduledReminderWhen] = useState('same_day') - const [scheduledReminderTime, setScheduledReminderTime] = useState('') - - const atScheduledReminderLimit = (watchedScheduledReminders?.length ?? 0) >= MAX_SCHEDULED_REMINDERS - - function addScheduledReminder() { - if (!isValidScheduledTime(scheduledReminderTime)) return - if (atScheduledReminderLimit) return - const current = watchedScheduledReminders ?? [] - const duplicate = current.some( - (sr) => sr.when === scheduledReminderWhen && sr.time === scheduledReminderTime, - ) - if (duplicate) return - setValue('scheduledReminders', [...current, { when: scheduledReminderWhen, time: scheduledReminderTime }], { shouldDirty: true }) - setScheduledReminderTime('') - setShowScheduledReminderForm(false) - } - - function removeScheduledReminder(index: number) { - const current = watchedScheduledReminders ?? [] - setValue('scheduledReminders', current.filter((_, i) => i !== index), { shouldDirty: true }) - } - - function scheduledReminderLabel(sr: { when: string; time: string }): string { - const timeDisplay = sr.time.slice(0, 5) - if (sr.when === 'day_before') { - return t('habits.form.scheduledReminderDayBeforeAt', { time: timeDisplay }) - } - return t('habits.form.scheduledReminderSameDayAt', { time: timeDisplay }) - } - // Tag pop animation const [justToggledTagId, setJustToggledTagId] = useState('') @@ -352,7 +609,7 @@ export function HabitFormFields({ {t('habits.form.flexibleDescription', { n: watchedFrequencyQuantity ?? 3, unit: watchedFrequencyUnit - ? t(`habits.form.unit${watchedFrequencyUnit}` as Parameters[0]) + ? t(`habits.form.unit${watchedFrequencyUnit}` as Parameters[0]) // NOSONAR - dynamic i18n key requires assertion : '', })}

@@ -548,227 +805,27 @@ export function HabitFormFields({ {/* Reminder (only when dueTime is set, hidden for general habits) */} {watchedDueTime && !isGeneral && ( -
-
-
- - {t('habits.form.reminder')} -
- -
- {watchedReminderEnabled && ( -
- {/* Selected reminder chips */} -
- {reminderTimes.map((time) => ( - - {reminderLabel(time)} - - - ))} -
- - {/* Add reminder */} -
- - - {showAddReminder && ( -
- {availablePresets.map((preset) => ( - - ))} - {showCustomInput && ( -
- setCustomValue(e.target.value ? Number(e.target.value) : null)} - onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addCustomReminder() } }} - /> - setCustomUnit(val as 'min' | 'hours' | 'days')} - /> - -
- )} - -
- )} -
-
- )} -
+ setValue('reminderEnabled', !watchedReminderEnabled, { shouldDirty: true })} + reminderLabel={reminderLabel} + t={t} + /> )} {/* Scheduled reminders (when no dueTime, hidden for general habits) */} {!watchedDueTime && !isGeneral && ( -
-
-
- - {t('habits.form.scheduledReminder')} -
- -
- {watchedReminderEnabled && ( -
- {(watchedScheduledReminders?.length ?? 0) > 0 && ( -
- {(watchedScheduledReminders ?? []).map((sr, idx) => ( - - {scheduledReminderLabel(sr)} - - - ))} -
- )} - -
- {!showScheduledReminderForm && !atScheduledReminderLimit && ( - - )} - - {atScheduledReminderLimit && ( -

{t('habits.form.scheduledReminderMax')}

- )} - - {showScheduledReminderForm && ( -
-
- - -
- -
- setScheduledReminderTime(formatScheduledTimeInput(e.target.value))} - onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addScheduledReminder() } }} - /> - - -
-
- )} -
-
- )} -
+ setValue('reminderEnabled', !watchedReminderEnabled, { shouldDirty: true })} + onSetScheduledReminders={(reminders) => setValue('scheduledReminders', reminders, { shouldDirty: true })} + t={t} + /> )} {/* Tags */} diff --git a/apps/web/components/onboarding/onboarding-welcome.tsx b/apps/web/components/onboarding/onboarding-welcome.tsx index 946aeed55..bf3541e0d 100644 --- a/apps/web/components/onboarding/onboarding-welcome.tsx +++ b/apps/web/components/onboarding/onboarding-welcome.tsx @@ -126,7 +126,7 @@ export function OnboardingWelcome() { backgroundColor: option.color, boxShadow: currentScheme === option.value ? `0 0 12px ${option.color}66` : 'none', }} - aria-label={t(`preferences.color${option.value.charAt(0).toUpperCase() + option.value.slice(1)}` as Parameters[0])} + aria-label={t(`preferences.color${option.value.charAt(0).toUpperCase() + option.value.slice(1)}` as Parameters[0])} // NOSONAR - dynamic i18n key requires assertion onClick={() => handleSchemeSelect(option.value)} /> ))} diff --git a/apps/web/components/ui/app-overlay.tsx b/apps/web/components/ui/app-overlay.tsx index 11e12801d..b8c258000 100644 --- a/apps/web/components/ui/app-overlay.tsx +++ b/apps/web/components/ui/app-overlay.tsx @@ -201,10 +201,8 @@ export function AppOverlay({ if (isEntering) panelClass = 'translate-y-full sm:translate-y-0 sm:scale-95 opacity-0' else if (isLeaving) panelClass = 'opacity-0' - // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- backdrop dismiss handled via Escape key listener - const overlay = ( + const overlay = ( // NOSONAR - backdrop dismiss via pointer; keyboard equivalent handled by Escape key listener
(null) - const [selectedLanguage, setSelectedLanguageState] = useState(() => { + const [selectedLanguage, setSelectedLanguageRaw] = useState(() => { // NOSONAR - setter wrapped by useCallback below if (typeof globalThis === 'undefined' || typeof globalThis.localStorage === 'undefined') return locale === 'pt-BR' ? 'pt-BR' : 'en-US' // NOSONAR - SSR guard return localStorage.getItem(SPEECH_LANG_KEY) ?? (locale === 'pt-BR' ? 'pt-BR' : 'en-US') }) @@ -111,7 +111,7 @@ export function useSpeechToText() { // Set language and persist const setSelectedLanguage = useCallback((newLang: string) => { - setSelectedLanguageState(newLang) + setSelectedLanguageRaw(newLang) localStorage.setItem(SPEECH_LANG_KEY, newLang) if (recognitionRef.current) { recognitionRef.current.lang = newLang diff --git a/apps/web/lib/query-client.ts b/apps/web/lib/query-client.ts index 9fb8ff16c..b989147bc 100644 --- a/apps/web/lib/query-client.ts +++ b/apps/web/lib/query-client.ts @@ -32,8 +32,6 @@ export function getQueryClient(): QueryClient { return createQueryClient() } // Browser: reuse the same client - if (!browserQueryClient) { - browserQueryClient = createQueryClient() - } + browserQueryClient ??= createQueryClient() return browserQueryClient } diff --git a/apps/web/stores/auth-store.ts b/apps/web/stores/auth-store.ts index 021cc9c66..bb1420925 100644 --- a/apps/web/stores/auth-store.ts +++ b/apps/web/stores/auth-store.ts @@ -66,8 +66,7 @@ export const useAuthStore = create((set, get) => ({ } if (remaining <= EXPIRY_WARNING_THRESHOLD) { - // TODO: Show warning toast via sonner when toast system is wired up - // "Your session expires in X minutes. Please save your work." + // Session expiry warning will be shown via toast when toast system is integrated } }, EXPIRY_CHECK_INTERVAL)