Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 42 additions & 3 deletions src/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const FlowSessionRepo = { createFlowSession, /* other methods */ }
- Components → React Query Hooks → API Services → Repository Layer
- Never skip layers or access repositories directly from hooks
- Never call Tauri commands directly from hooks
- **CRITICAL: Never call API services directly from components** - always use React Query hooks

### State Management Priority

Expand All @@ -86,8 +87,16 @@ const { data: flowSessions } = useGetFlowSessions()
// ✅ Good: UI state with Zustand
const { isMenuOpen, toggleMenu } = useUIStore()

// ✅ Good: Mutations with React Query
const updateMutation = useUpdatePreferences()
updateMutation.mutate(data)

// ❌ Bad: Database state with Zustand
const { flowSessions, fetchFlowSessions } = useFlowSessionStore() // Don't do this

// ❌ Bad: Direct API calls from components
import { slackApi } from '@/api/ebbApi/slackApi'
await slackApi.updatePreferences(data) // Don't do this - use React Query hook instead
```

## Tauri Command Patterns
Expand Down Expand Up @@ -238,27 +247,57 @@ export default function MyPage() {

### Data Fetching in Components

Always use React Query hooks:
Always use React Query hooks - never call API services directly:

```typescript
import { useGetFlowSessions } from '../api/hooks/useFlowSession'
import { useGetFlowSessions, useUpdateFlowSession } from '../api/hooks/useFlowSession'

export default function FlowSessionList() {
const { data: sessions, isLoading, error } = useGetFlowSessions()
const updateMutation = useUpdateFlowSession()

const handleUpdate = (id: string, data: UpdateData) => {
updateMutation.mutate({ id, data }, {
onSuccess: () => {
toast.success('Updated successfully')
},
onError: (error) => {
logAndToastError('Update failed', error)
}
})
}

if (isLoading) return <div>Loading...</div>
if (error) return <div>Error loading sessions</div>

return (
<div>
{sessions?.map(session => (
<div key={session.id}>{session.objective}</div>
<div key={session.id}>
{session.objective}
<button
onClick={() => handleUpdate(session.id, newData)}
disabled={updateMutation.isPending}
>
{updateMutation.isPending ? 'Updating...' : 'Update'}
</button>
</div>
))}
</div>
)
}
```

**❌ Never do this in components:**
```typescript
// DON'T import and call API services directly
import { FlowSessionApi } from '../api/ebbApi/flowSessionApi'

const handleUpdate = async () => {
await FlowSessionApi.updateFlowSession(id, data) // Wrong - violates layer pattern
}
```

## File Organization

- `src/api/hooks/`: React Query hooks (Layer 1)
Expand Down
16 changes: 14 additions & 2 deletions src/api/hooks/useSlack.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'
import { slackApi } from '../ebbApi/slackApi'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { slackApi, SlackPreferences } from '../ebbApi/slackApi'

const slackKeys = {
all: ['slack'] as const,
Expand All @@ -16,3 +16,15 @@ export const useSlackStatus = () => {
retry: false,
})
}

export const useUpdateSlackPreferences = () => {
const queryClient = useQueryClient()

return useMutation({
mutationFn: (preferences: SlackPreferences) => slackApi.updatePreferences(preferences),
onSuccess: () => {
// Invalidate and refetch slack status to get updated preferences
queryClient.invalidateQueries({ queryKey: slackKeys.status() })
},
})
}
80 changes: 78 additions & 2 deletions src/components/SlackFocusToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { SlackIcon } from '@/components/icons/SlackIcon'
import { Switch } from '@/components/ui/switch'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useSlackStatus } from '@/api/hooks/useSlack'
import { useSlackStatus, useUpdateSlackPreferences } from '@/api/hooks/useSlack'
import { logAndToastError } from '@/lib/utils/ebbError.util'
import { initiateSlackOAuth } from '@/lib/utils/slackAuth.util'
import { Skeleton } from '@/components/ui/skeleton'
Expand All @@ -29,7 +30,18 @@ export function SlackFocusToggle({ slackSettings, onSlackSettingsChange }: Slack
const navigate = useNavigate()

const [showDialog, setShowDialog] = useState(false)
const [customStatusText, setCustomStatusText] = useState('')
const [customStatusEmoji, setCustomStatusEmoji] = useState('')
const { data: slackStatus, isLoading: slackStatusLoading } = useSlackStatus()
const updatePreferencesMutation = useUpdateSlackPreferences()

// Load current preferences when dialog opens
useEffect(() => {
if (showDialog && slackStatus?.preferences) {
setCustomStatusText(slackStatus.preferences.custom_status_text || '')
setCustomStatusEmoji(slackStatus.preferences.custom_status_emoji || '')
}
}, [showDialog, slackStatus?.preferences])

const handleSlackToggle = async () => {
if (!user) {
Expand Down Expand Up @@ -60,6 +72,31 @@ export function SlackFocusToggle({ slackSettings, onSlackSettingsChange }: Slack
}
}

const validateEmoji = (emoji: string): boolean => {
if (!emoji) return true // Empty is valid
return emoji.startsWith(':') && emoji.endsWith(':') && emoji.length > 2
}

const handleSavePreferences = async () => {
if (!validateEmoji(customStatusEmoji)) {
toast.error('Emoji must be in format :emoji_name: (e.g., :brain:)')
return
}

updatePreferencesMutation.mutate({
custom_status_text: customStatusText,
custom_status_emoji: customStatusEmoji
}, {
onSuccess: () => {
toast.success('Slack preferences updated')
setShowDialog(false)
},
onError: (error) => {
logAndToastError('Failed to update Slack preferences', error)
}
})
}

const navigateToSettings = () => {
setShowDialog(false)
// Navigate to settings page - you may need to adjust this based on your routing
Expand Down Expand Up @@ -128,6 +165,45 @@ export function SlackFocusToggle({ slackSettings, onSlackSettingsChange }: Slack
/>
</div>

<div className="space-y-4">
<div className="space-y-2">
<label htmlFor="custom-status-text" className="text-sm font-medium">Custom Status Text</label>
<Input
id="custom-status-text"
value={customStatusText}
onChange={(e) => setCustomStatusText(e.target.value)}
placeholder="e.g., In a focus session"
maxLength={100}
/>
<div className="text-xs text-muted-foreground">
Status message shown during focus sessions
</div>
</div>

<div className="space-y-2">
<label htmlFor="custom-status-emoji" className="text-sm font-medium">Custom Status Emoji</label>
<Input
id="custom-status-emoji"
value={customStatusEmoji}
onChange={(e) => setCustomStatusEmoji(e.target.value)}
placeholder="e.g., :brain:"
maxLength={50}
className={!validateEmoji(customStatusEmoji) ? 'border-red-500' : ''}
/>
<div className="text-xs text-muted-foreground">
Emoji in format :emoji_name: (e.g., :brain:, :rocket:)
</div>
</div>

<Button
onClick={handleSavePreferences}
className="w-full"
disabled={updatePreferencesMutation.isPending}
>
{updatePreferencesMutation.isPending ? 'Saving...' : 'Save Preferences'}
</Button>
</div>

<div className="pt-4 border-t">
<Button
variant="outline"
Expand Down