Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
65 changes: 64 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **Enhanced "I'm Stuck" Button** (#234)
- Now extends timer by 5 minutes when clicked
- Automatically opens hints panel to help user get unstuck
- Records user intent for session analytics

### Fixed
- **Timer Settings Not Applying** (#234)
- Fixed timer limits (Auto/Off/Fixed) not updating after changing settings
- Added cache clearing for adaptiveLimitsService when settings are saved
- Timer now listens for Chrome storage changes to refresh limits in real-time

- **Sidebar Form State Lost on Close** (#234)
- Fixed form data being reset when closing and reopening sidebar
- Changed ProblemTime, ProblemStats, Settings, and ProblemGenerator to use CSS display instead of unmounting
- State now persists across sidebar open/close cycles

- **useStrategy Infinite Loop** (#234)
- Fixed hook causing constant re-renders and console spam
- Stabilized loadStrategyData callback with proper dependency management

- **Timer "Still Working" Prompt UI** (#234)
- Redesigned from full-width banner to centered modal popup
- Reduced countdown overlay text size from 8rem to 1.5rem
- Fixed button click handlers not responding

### Changed
- **Codebase Cleanup**
- Deleted unused popup files (popup.html, popup.js, popup.jsx) - extension uses dashboard directly
Expand All @@ -16,6 +42,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updated components to use CSS variables for dark mode support
- Updated CLAUDE.md with theming guidelines

### Tests
- Added regression test for timer settings cache clearing
- Added regression tests for cross-context theme sync

---

## [1.1.0] - Post Chrome Web Store Release
Expand Down Expand Up @@ -72,6 +102,9 @@ All changes after the Chrome Web Store release on November 19th, 2025.

### Fixed
- **Reduced ESLint Warnings from 22 to 0** (#211)
- **Fixed CSS Scoping for Content Scripts** (#153) - Prevented extension styles from bleeding into host pages
- **Fixed Dashboard Card Styling and [object Object] Bug** (#234) - Added dashboard.css, fixed card rendering
- **Fixed Missing getAllProblems Method** (#216) - Added method to ProblemService
- **Fixed Goals page calculations** (#201) - Weekly accuracy, problems per week targets
- **Fixed dashboard text visibility in light/dark modes** (#194)
- **Fixed onboarding modal text visibility in dark mode** (#194)
Expand Down Expand Up @@ -114,10 +147,40 @@ All changes after the Chrome Web Store release on November 19th, 2025.
- **Removed Test Code from Production Build** (#205) - 56% bundle size reduction

### Refactored
- **Comment Cleanup per Clean Code Chapter 4** (#213)
- **Applied Newspaper Rule and Fixed max-lines ESLint Warnings** (#214)
- Extracted helper modules following Clean Code Chapter 5 principles
- All files now comply with max-lines limits

- **Complete Folder Reorganization and Dead Code Cleanup** (#222)
- Restructured project folders for better organization
- Removed unused code and files

- **Service Files Renamed to camelCase Convention** (#220)
- Standardized service file naming across codebase

- **Renamed computeTimePerformanceScore to calculateTimePerformanceScore** (#215)
- Consistent function naming convention

- **Shifted Service Tests to Contract-Testing Pattern** (#238)
- Improved test architecture for better maintainability

- **Added JSDoc Data Contracts for Key Service Functions** (#239)
- Better documentation and type hints

- **Comment Cleanup per Clean Code Chapter 4** (#218)
- Removed 230+ lines of commented-out dead code
- Applied "Don't comment bad code—rewrite it" principle

- **Cleaned Up Unnecessary Files in chrome-extension-app Root** (#219)
- Removed leftover and redundant files

- **Removed Redundant Tests and Dead Code** (#237)
- Cleaned up test files and unused code

### Tests Added
- **Unit Tests for Extracted Helper Modules** (#226)
- Comprehensive test coverage for new helper files

---

## [1.0.0] - 2025-11-19
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ jest.mock('../../app/services/dashboard/dashboardService.js');
jest.mock('../../shared/services/problem/problemService.js');
jest.mock('../../shared/services/session/sessionService.js');
jest.mock('../../shared/services/attempts/tagServices.js');
jest.mock('../../shared/services/attempts/adaptiveLimitsService.js');

// Import the adaptiveLimitsService mock for cache clearing tests
import { adaptiveLimitsService } from '../../shared/services/attempts/adaptiveLimitsService.js';

// Import the actual handler to test its behavior
import { storageHandlers } from '../handlers/storageHandlers.js';

// Mock Chrome APIs
global.chrome = {
Expand Down Expand Up @@ -129,6 +136,40 @@ describe('Message Handlers - Storage Operations', () => {
expect(StorageService.setSettings).toHaveBeenCalledWith(settings);
expect(result).toHaveProperty('success');
});

/**
* REGRESSION TEST: Timer settings cache clearing
*
* This test ensures that when settings are saved, the adaptiveLimitsService
* cache is cleared so timer limit changes (Auto/Off/Fixed) take effect immediately.
*
* Without this, users had to restart the browser for timer limit changes to apply.
* See fix: fix/timer-settings-not-responding branch
*/
it('should clear adaptiveLimitsService cache when settings are saved', async () => {
// ARRANGE: Set up the mocks
// - StorageService.setSettings will resolve successfully
// - adaptiveLimitsService.clearCache is a mock function we can spy on
StorageService.setSettings.mockResolvedValue({ status: 'success' });
adaptiveLimitsService.clearCache = jest.fn();

// Create mock functions for the handler callback pattern
const sendResponse = jest.fn();
const finishRequest = jest.fn();
const request = { message: { limit: 'Fixed', sessionLength: 5 } };

// ACT: Call the actual handler
// The handler uses a callback pattern, so we need to wait for it
storageHandlers.setSettings(request, {}, sendResponse, finishRequest);

// Wait for the async operations to complete
// (StorageService.setSettings returns a Promise)
await new Promise(resolve => setTimeout(resolve, 10));

// ASSERT: Verify the cache was cleared
expect(adaptiveLimitsService.clearCache).toHaveBeenCalled();
expect(sendResponse).toHaveBeenCalledWith({ status: 'success' });
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { StorageService } from "../../shared/services/storage/storageService.js";
import { backupIndexedDB, getBackupFile } from "../../shared/db/migrations/backupDB.js";
import { getWelcomeBackStrategy, createDiagnosticSession, processDiagnosticResults, createAdaptiveRecalibrationSession, processAdaptiveSessionCompletion } from "../../shared/services/schedule/recalibrationService.js";
import { adaptiveLimitsService } from "../../shared/services/attempts/adaptiveLimitsService.js";

export const storageHandlers = {
backupIndexedDB: (_request, _dependencies, sendResponse, _finishRequest) => {
Expand Down Expand Up @@ -60,6 +61,11 @@ export const storageHandlers = {
setSettings: (request, _dependencies, sendResponse, finishRequest) => {
StorageService.setSettings(request.message)
.then((result) => {
// Clear adaptiveLimitsService cache so new settings take effect immediately
// This fixes the bug where timer limit changes weren't being applied
adaptiveLimitsService.clearCache();
console.log("Settings saved - cleared adaptiveLimitsService cache for immediate effect");

if (chrome.storage && chrome.storage.local) {
chrome.storage.local.set({
settings: request.message
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, {
useMemo,
useEffect,
} from "react";
import SmartPopover from './SmartPopover.jsx';
import { useFloatingHintState } from '../../hooks/useFloatingHintState.js';
Expand All @@ -24,17 +25,26 @@ function FloatingHintButton({
interviewConfig = null,
sessionType = null,
uiMode = 'full-support',
forceOpen = false,
}) {
// Use the original hooks
const {
opened,
setOpened,
expandedHints,
setExpandedHints,
const {
opened,
setOpened,
expandedHints,
setExpandedHints,
buttonRef,
hintsUsed,
setHintsUsed
} = useFloatingHintState();

// Handle external force open trigger
useEffect(() => {
if (forceOpen && !opened) {
setOpened(true);
if (onOpen) onOpen();
}
}, [forceOpen, opened, setOpened, onOpen]);

const colors = useHintThemeColors();

Expand Down
145 changes: 89 additions & 56 deletions chrome-extension-app/src/content/components/timer/TimerComponents.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Timer UI Components
*/

import React from "react";
import React, { useRef, useEffect } from "react";
import {
HiPlay,
HiPause,
Expand All @@ -20,76 +20,108 @@ export const CountdownOverlay = ({ countdownValue }) => (
</div>
);

// Still working prompt component
export const StillWorkingPrompt = ({ getTimerClass, handleClose, handleStillWorking, handleStuck, handleMoveOn }) => (
<div className="timer-banner still-working-prompt">
<div className="timer-banner-header">
<h1 className={getTimerClass()}>Time Check</h1>
<HiXMark
onClick={handleClose}
className="close-icon"
title="Close timer and return to menu"
aria-label="Close timer and return to menu"
/>
</div>
// Still working prompt component - Modal style popup with focus trap
export const StillWorkingPrompt = ({ getTimerClass, handleClose, handleStillWorking, handleStuck, handleMoveOn }) => {
const modalRef = useRef(null);
const firstButtonRef = useRef(null);

// Focus first button on mount and trap focus within modal
useEffect(() => {
firstButtonRef.current?.focus();

// Store previously focused element to restore on close
const previouslyFocused = document.activeElement;
return () => {
previouslyFocused?.focus?.();
};
}, []);

const onButtonClick = (handler) => (e) => {
e.preventDefault();
e.stopPropagation();
handler();
};

const handleKeyDown = (e) => {
if (e.key === 'Escape') {
handleClose();
return;
}

// Focus trap: cycle through focusable elements
if (e.key === 'Tab' && modalRef.current) {
const focusable = modalRef.current.querySelectorAll('button');
const first = focusable[0];
const last = focusable[focusable.length - 1];

if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
};

return (
<div
className="still-working-overlay"
onClick={onButtonClick(handleClose)}
onKeyDown={handleKeyDown}
role="presentation"
>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
<div
ref={modalRef}
className="still-working-modal"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-labelledby="still-working-title"
aria-describedby="still-working-description"
>
<div className="still-working-header">
<h2 id="still-working-title" className={getTimerClass()}>Time Check</h2>
<HiXMark
onClick={onButtonClick(handleClose)}
className="close-icon"
title="Close"
aria-label="Close"
/>
</div>

<div className="timer-banner-content">
<div style={{ textAlign: "center", padding: "10px" }}>
<p>You&apos;ve exceeded the recommended interview time.</p>
<p>How are you feeling about this problem?</p>

<div
style={{
display: "flex",
gap: "10px",
justifyContent: "center",
marginTop: "15px",
}}
>
<div id="still-working-description" className="still-working-content">
<p>You&apos;ve exceeded the recommended time.</p>
<p>How are you feeling about this problem?</p>
</div>

<div className="still-working-buttons">
<button
onClick={handleStillWorking}
style={{
padding: "8px 12px",
backgroundColor: "var(--cm-success, #4CAF50)",
color: "var(--cm-btn-text, white)",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
ref={firstButtonRef}
onClick={onButtonClick(handleStillWorking)}
className="still-working-btn btn-progress"
>
Still Making Progress
</button>
<button
onClick={handleStuck}
style={{
padding: "8px 12px",
backgroundColor: "var(--cm-warning, #FF9800)",
color: "var(--cm-btn-text, white)",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
onClick={onButtonClick(handleStuck)}
className="still-working-btn btn-stuck"
>
I&apos;m Stuck
</button>
<button
onClick={handleMoveOn}
style={{
padding: "8px 12px",
backgroundColor: "var(--cm-error, #f44336)",
color: "var(--cm-btn-text, white)",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
onClick={onButtonClick(handleMoveOn)}
className="still-working-btn btn-move-on"
>
Move On
</button>
</div>
</div>
</div>
</div>
);
);
};

// Timer header component
export const TimerHeader = ({ sessionType, isUnlimitedMode, getTimerClass, handleClose }) => (
Expand Down Expand Up @@ -152,7 +184,7 @@ export const TimerContent = ({ displayTime, toggleTimer, sessionType, interviewC
export const TimerControls = ({
handleReset, sessionType, hasFirstPlan, isTimerRunning, recordFirstPlan,
processedTags, state, handleHintOpen, handleHintClose, handleHintClick,
interviewConfig, uiMode, handleStop, handleStart, handleComplete
interviewConfig, uiMode, handleStop, handleStart, handleComplete, forceHintsOpen
}) => (
<div className="timer-banner-controls">
<HiArrowPath
Expand Down Expand Up @@ -194,6 +226,7 @@ export const TimerControls = ({
interviewConfig={interviewConfig}
sessionType={sessionType}
uiMode={uiMode}
forceOpen={forceHintsOpen}
/>
</div>
)}
Expand Down
Loading