---
phase: 32-state-update-optimization
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
  - jam-ui/src/components/client/JKSessionScreen.js
  - jam-ui/src/components/client/JKResyncButton.js
  - jam-ui/src/components/client/JKVideoButton.js
autonomous: true

must_haves:
  truths:
    - "resyncLoading state lives in JKResyncButton (not JKSessionScreen)"
    - "videoLoading state lives in JKVideoButton (not JKSessionScreen)"
    - "Loading state changes don't re-render JKSessionScreen"
  artifacts:
    - path: "jam-ui/src/components/client/JKResyncButton.js"
      provides: "Self-contained resync button with loading state"
      exports: ["JKResyncButton"]
    - path: "jam-ui/src/components/client/JKVideoButton.js"
      provides: "Self-contained video button with loading state"
      exports: ["JKVideoButton"]
    - path: "jam-ui/src/components/client/JKSessionScreen.js"
      provides: "Uses extracted button components"
      contains: "JKResyncButton"
  key_links:
    - from: "jam-ui/src/components/client/JKSessionScreen.js"
      to: "JKResyncButton"
      via: "import and render"
      pattern: "<JKResyncButton"
    - from: "jam-ui/src/components/client/JKSessionScreen.js"
      to: "JKVideoButton"
      via: "import and render"
      pattern: "<JKVideoButton"
---

<objective>
Colocate loading states to button components

Purpose: Move resyncLoading (COLOC-01) and videoLoading (COLOC-02) from JKSessionScreen to their respective button components. This follows state colocation principles - state should live in the component that uses it, preventing parent re-renders.

Output: JKResyncButton and JKVideoButton components with colocated loading state, JKSessionScreen simplified
</objective>

<execution_context>
@/Users/nuwan/.claude/get-shit-done/workflows/execute-plan.md
@/Users/nuwan/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/32-state-update-optimization/32-RESEARCH.md

# Source files
@jam-ui/src/components/client/JKSessionScreen.js
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create JKResyncButton component</name>
  <files>jam-ui/src/components/client/JKResyncButton.js</files>
  <action>
Create a new self-contained button component with colocated loading state.

```javascript
import React, { useState, useCallback, memo } from 'react';
import { Button, Spinner } from 'reactstrap';
import { toast } from 'react-toastify';
import PropTypes from 'prop-types';

/**
 * Self-contained resync button with colocated loading state.
 * Loading state changes only re-render this component, not the parent.
 *
 * State colocation: https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster
 */
const JKResyncButton = memo(({ resyncAudio, className }) => {
  const [loading, setLoading] = useState(false);

  const handleClick = useCallback(async (e) => {
    e.preventDefault();
    if (loading) return;

    setLoading(true);
    try {
      await resyncAudio();
      // Silent success (matches legacy behavior)
    } catch (error) {
      if (error.message === 'timeout') {
        toast.error('Audio resync timed out. Please try again.');
      } else {
        toast.error('Audio resync failed: ' + (error.message || 'Unknown error'));
      }
    } finally {
      setLoading(false);
    }
  }, [resyncAudio, loading]);

  return (
    <Button
      className={className || 'btn-custom-outline'}
      outline
      size="md"
      onClick={handleClick}
      disabled={loading}
    >
      {loading ? (
        <>
          <Spinner size="sm" /> Resyncing...
        </>
      ) : (
        'Resync'
      )}
    </Button>
  );
});

JKResyncButton.displayName = 'JKResyncButton';

JKResyncButton.propTypes = {
  resyncAudio: PropTypes.func.isRequired,
  className: PropTypes.string
};

export default JKResyncButton;
```

Key design decisions:
- memo() wrapper prevents re-renders from parent prop stability
- Loading state is local - changes don't propagate up
- Same error handling as original handleResync
- displayName for React DevTools debugging
- PropTypes for documentation
  </action>
  <verify>
File exists and exports component:
`ls -la jam-ui/src/components/client/JKResyncButton.js`

Has useState for loading:
`grep "useState(false)" jam-ui/src/components/client/JKResyncButton.js`

Has memo wrapper:
`grep "memo(" jam-ui/src/components/client/JKResyncButton.js`
  </verify>
  <done>
JKResyncButton component created with colocated loading state
  </done>
</task>

<task type="auto">
  <name>Task 2: Create JKVideoButton component</name>
  <files>jam-ui/src/components/client/JKVideoButton.js</files>
  <action>
Create a new self-contained video button component with colocated loading state.

```javascript
import React, { useState, useCallback, memo } from 'react';
import { Button, Spinner } from 'reactstrap';
import { toast } from 'react-toastify';
import PropTypes from 'prop-types';
import videoIcon from '../../assets/images/icons8-video-call-50.png';

/**
 * Self-contained video button with colocated loading state.
 * Loading state changes only re-render this component, not the parent.
 *
 * State colocation: https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster
 */
const JKVideoButton = memo(({
  canVideo,
  getVideoUrl,
  onUpgradePrompt,
  className
}) => {
  const [loading, setLoading] = useState(false);

  // Open external link in new window/tab
  const openExternalLink = useCallback((url) => {
    window.open(url, '_blank', 'noopener,noreferrer');
  }, []);

  const handleClick = useCallback(async () => {
    if (!canVideo()) {
      onUpgradePrompt();
      return;
    }

    try {
      setLoading(true);

      // Get video conferencing room URL from server
      const response = await getVideoUrl();
      const videoUrl = `${response.url}&audiooff=true`;

      // Open video URL in new browser window/tab
      openExternalLink(videoUrl);

    } catch (error) {
      toast.error('Failed to start video session');
    } finally {
      // Keep loading state for 10 seconds to prevent multiple clicks
      setTimeout(() => setLoading(false), 10000);
    }
  }, [canVideo, getVideoUrl, onUpgradePrompt, openExternalLink]);

  return (
    <Button
      className={className || 'btn-custom-outline'}
      outline
      size="md"
      onClick={handleClick}
      disabled={loading}
    >
      <img
        src={videoIcon}
        alt="Video"
        style={{ width: '20px', height: '20px', marginRight: '0.3rem' }}
      />
      {loading && <Spinner size="sm" />}
      &nbsp;Video
    </Button>
  );
});

JKVideoButton.displayName = 'JKVideoButton';

JKVideoButton.propTypes = {
  canVideo: PropTypes.func.isRequired,
  getVideoUrl: PropTypes.func.isRequired,
  onUpgradePrompt: PropTypes.func.isRequired,
  className: PropTypes.string
};

export default JKVideoButton;
```

Key design decisions:
- 10-second loading timeout preserved from original
- canVideo and onUpgradePrompt as props for flexibility
- Video icon imported directly (same path as original)
- memo() for render optimization
  </action>
  <verify>
File exists and exports component:
`ls -la jam-ui/src/components/client/JKVideoButton.js`

Has useState for loading:
`grep "useState(false)" jam-ui/src/components/client/JKVideoButton.js`

Has 10-second timeout:
`grep "10000" jam-ui/src/components/client/JKVideoButton.js`
  </verify>
  <done>
JKVideoButton component created with colocated loading state
  </done>
</task>

<task type="auto">
  <name>Task 3: Refactor JKSessionScreen to use extracted components</name>
  <files>jam-ui/src/components/client/JKSessionScreen.js</files>
  <action>
Replace inline button implementations with the new components.

1. Add imports at top:
```javascript
import JKResyncButton from './JKResyncButton';
import JKVideoButton from './JKVideoButton';
```

2. Remove useState declarations (around lines 202-205):
```javascript
// DELETE these lines:
const [videoLoading, setVideoLoading] = useState(false);
const [resyncLoading, setResyncLoading] = useState(false);
```

3. Remove handleResync function (around lines 1056-1075):
```javascript
// DELETE the entire handleResync function
// const handleResync = useCallback(async (e) => { ... });
```

4. Remove handleVideoClick function (around lines 977-1002):
```javascript
// DELETE the entire handleVideoClick function
// const handleVideoClick = async () => { ... };
```

5. Update the button JSX in the toolbar section (around lines 1341-1388).

Find the Video button:
```jsx
<Button className='btn-custom-outline' outline size="md" onClick={handleVideoClick} disabled={videoLoading}>
  <img src={videoIcon} alt="Video" style={{ width: '20px', height: '20px', marginRight: '0.3rem' }} />
  {videoLoading && (<Spinner size="sm" />)}
  &nbsp;Video
</Button>
```

Replace with:
```jsx
<JKVideoButton
  canVideo={canVideo}
  getVideoUrl={() => getVideoConferencingRoomUrl(currentSession.id)}
  onUpgradePrompt={showVideoUpgradePrompt}
/>
```

Find the Resync button:
```jsx
<Button className='btn-custom-outline' outline size="md" onClick={handleResync} disabled={resyncLoading}>
  <img src={resyncIcon} alt="Resync" style={{ width: '20px', height: '20px', marginRight: '0.3rem' }} />
  {resyncLoading ? <><Spinner size="sm" /> Resyncing...</> : 'Resync'}
</Button>
```

Replace with:
```jsx
<JKResyncButton resyncAudio={resyncAudio} />
```

6. Keep the following functions in JKSessionScreen (they're still needed):
- `canVideo` (permission check)
- `showVideoUpgradePrompt` (toast display)
- `resyncAudio` (from useMediaActions)

7. Remove videoIcon import if no longer used elsewhere:
```javascript
// Check if videoIcon is used elsewhere, if not remove:
// import videoIcon from '../../assets/images/icons8-video-call-50.png';
```
  </action>
  <verify>
New components imported:
`grep "JKResyncButton\|JKVideoButton" jam-ui/src/components/client/JKSessionScreen.js | head -5`

Old state removed:
`grep "videoLoading\|resyncLoading" jam-ui/src/components/client/JKSessionScreen.js | wc -l` should be 0

Components used in JSX:
`grep "<JKResyncButton\|<JKVideoButton" jam-ui/src/components/client/JKSessionScreen.js`

ESLint passes:
`cd jam-ui && npx eslint src/components/client/JKSessionScreen.js src/components/client/JKResyncButton.js src/components/client/JKVideoButton.js --max-warnings=0`
  </verify>
  <done>
JKSessionScreen refactored to use JKResyncButton and JKVideoButton, loading state no longer in parent
  </done>
</task>

</tasks>

<verification>
1. New components exist:
   ```bash
   ls -la jam-ui/src/components/client/JKResyncButton.js jam-ui/src/components/client/JKVideoButton.js
   ```

2. Loading state removed from JKSessionScreen:
   ```bash
   grep -c "videoLoading\|resyncLoading" jam-ui/src/components/client/JKSessionScreen.js
   ```
   Should return 0

3. Components used:
   ```bash
   grep "<JKResyncButton\|<JKVideoButton" jam-ui/src/components/client/JKSessionScreen.js
   ```
   Should show both component usages

4. ESLint passes:
   ```bash
   cd jam-ui && npx eslint src/components/client/JKSessionScreen.js src/components/client/JKResyncButton.js src/components/client/JKVideoButton.js --max-warnings=0
   ```
</verification>

<success_criteria>
- [ ] JKResyncButton.js created with local loading state
- [ ] JKVideoButton.js created with local loading state
- [ ] JKSessionScreen imports and uses both new components
- [ ] videoLoading and resyncLoading useState removed from JKSessionScreen
- [ ] handleResync and handleVideoClick removed from JKSessionScreen
- [ ] All files pass ESLint
</success_criteria>

<output>
After completion, create `.planning/phases/32-state-update-optimization/32-03-SUMMARY.md`
</output>
