Skip to content

Commit 29cc364

Browse files
authored
Enable updating slack status/emoji (#133)
1 parent 989a977 commit 29cc364

3 files changed

Lines changed: 134 additions & 7 deletions

File tree

src/CLAUDE.md

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const FlowSessionRepo = { createFlowSession, /* other methods */ }
6969
- Components → React Query Hooks → API Services → Repository Layer
7070
- Never skip layers or access repositories directly from hooks
7171
- Never call Tauri commands directly from hooks
72+
- **CRITICAL: Never call API services directly from components** - always use React Query hooks
7273

7374
### State Management Priority
7475

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

90+
// ✅ Good: Mutations with React Query
91+
const updateMutation = useUpdatePreferences()
92+
updateMutation.mutate(data)
93+
8994
// ❌ Bad: Database state with Zustand
9095
const { flowSessions, fetchFlowSessions } = useFlowSessionStore() // Don't do this
96+
97+
// ❌ Bad: Direct API calls from components
98+
import { slackApi } from '@/api/ebbApi/slackApi'
99+
await slackApi.updatePreferences(data) // Don't do this - use React Query hook instead
91100
```
92101

93102
## Tauri Command Patterns
@@ -238,27 +247,57 @@ export default function MyPage() {
238247

239248
### Data Fetching in Components
240249

241-
Always use React Query hooks:
250+
Always use React Query hooks - never call API services directly:
242251

243252
```typescript
244-
import { useGetFlowSessions } from '../api/hooks/useFlowSession'
253+
import { useGetFlowSessions, useUpdateFlowSession } from '../api/hooks/useFlowSession'
245254

246255
export default function FlowSessionList() {
247256
const { data: sessions, isLoading, error } = useGetFlowSessions()
257+
const updateMutation = useUpdateFlowSession()
258+
259+
const handleUpdate = (id: string, data: UpdateData) => {
260+
updateMutation.mutate({ id, data }, {
261+
onSuccess: () => {
262+
toast.success('Updated successfully')
263+
},
264+
onError: (error) => {
265+
logAndToastError('Update failed', error)
266+
}
267+
})
268+
}
248269

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

252273
return (
253274
<div>
254275
{sessions?.map(session => (
255-
<div key={session.id}>{session.objective}</div>
276+
<div key={session.id}>
277+
{session.objective}
278+
<button
279+
onClick={() => handleUpdate(session.id, newData)}
280+
disabled={updateMutation.isPending}
281+
>
282+
{updateMutation.isPending ? 'Updating...' : 'Update'}
283+
</button>
284+
</div>
256285
))}
257286
</div>
258287
)
259288
}
260289
```
261290

291+
**❌ Never do this in components:**
292+
```typescript
293+
// DON'T import and call API services directly
294+
import { FlowSessionApi } from '../api/ebbApi/flowSessionApi'
295+
296+
const handleUpdate = async () => {
297+
await FlowSessionApi.updateFlowSession(id, data) // Wrong - violates layer pattern
298+
}
299+
```
300+
262301
## File Organization
263302

264303
- `src/api/hooks/`: React Query hooks (Layer 1)

src/api/hooks/useSlack.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useQuery } from '@tanstack/react-query'
2-
import { slackApi } from '../ebbApi/slackApi'
1+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
2+
import { slackApi, SlackPreferences } from '../ebbApi/slackApi'
33

44
const slackKeys = {
55
all: ['slack'] as const,
@@ -16,3 +16,15 @@ export const useSlackStatus = () => {
1616
retry: false,
1717
})
1818
}
19+
20+
export const useUpdateSlackPreferences = () => {
21+
const queryClient = useQueryClient()
22+
23+
return useMutation({
24+
mutationFn: (preferences: SlackPreferences) => slackApi.updatePreferences(preferences),
25+
onSuccess: () => {
26+
// Invalidate and refetch slack status to get updated preferences
27+
queryClient.invalidateQueries({ queryKey: slackKeys.status() })
28+
},
29+
})
30+
}

src/components/SlackFocusToggle.tsx

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import { useState } from 'react'
1+
import { useState, useEffect } from 'react'
22
import { SlackIcon } from '@/components/icons/SlackIcon'
33
import { Switch } from '@/components/ui/switch'
44
import { Button } from '@/components/ui/button'
5+
import { Input } from '@/components/ui/input'
56
import {
67
Dialog,
78
DialogContent,
89
DialogHeader,
910
DialogTitle,
1011
} from '@/components/ui/dialog'
1112
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
12-
import { useSlackStatus } from '@/api/hooks/useSlack'
13+
import { useSlackStatus, useUpdateSlackPreferences } from '@/api/hooks/useSlack'
1314
import { logAndToastError } from '@/lib/utils/ebbError.util'
1415
import { initiateSlackOAuth } from '@/lib/utils/slackAuth.util'
1516
import { Skeleton } from '@/components/ui/skeleton'
@@ -29,7 +30,18 @@ export function SlackFocusToggle({ slackSettings, onSlackSettingsChange }: Slack
2930
const navigate = useNavigate()
3031

3132
const [showDialog, setShowDialog] = useState(false)
33+
const [customStatusText, setCustomStatusText] = useState('')
34+
const [customStatusEmoji, setCustomStatusEmoji] = useState('')
3235
const { data: slackStatus, isLoading: slackStatusLoading } = useSlackStatus()
36+
const updatePreferencesMutation = useUpdateSlackPreferences()
37+
38+
// Load current preferences when dialog opens
39+
useEffect(() => {
40+
if (showDialog && slackStatus?.preferences) {
41+
setCustomStatusText(slackStatus.preferences.custom_status_text || '')
42+
setCustomStatusEmoji(slackStatus.preferences.custom_status_emoji || '')
43+
}
44+
}, [showDialog, slackStatus?.preferences])
3345

3446
const handleSlackToggle = async () => {
3547
if (!user) {
@@ -60,6 +72,31 @@ export function SlackFocusToggle({ slackSettings, onSlackSettingsChange }: Slack
6072
}
6173
}
6274

75+
const validateEmoji = (emoji: string): boolean => {
76+
if (!emoji) return true // Empty is valid
77+
return emoji.startsWith(':') && emoji.endsWith(':') && emoji.length > 2
78+
}
79+
80+
const handleSavePreferences = async () => {
81+
if (!validateEmoji(customStatusEmoji)) {
82+
toast.error('Emoji must be in format :emoji_name: (e.g., :brain:)')
83+
return
84+
}
85+
86+
updatePreferencesMutation.mutate({
87+
custom_status_text: customStatusText,
88+
custom_status_emoji: customStatusEmoji
89+
}, {
90+
onSuccess: () => {
91+
toast.success('Slack preferences updated')
92+
setShowDialog(false)
93+
},
94+
onError: (error) => {
95+
logAndToastError('Failed to update Slack preferences', error)
96+
}
97+
})
98+
}
99+
63100
const navigateToSettings = () => {
64101
setShowDialog(false)
65102
// Navigate to settings page - you may need to adjust this based on your routing
@@ -128,6 +165,45 @@ export function SlackFocusToggle({ slackSettings, onSlackSettingsChange }: Slack
128165
/>
129166
</div>
130167

168+
<div className="space-y-4">
169+
<div className="space-y-2">
170+
<label htmlFor="custom-status-text" className="text-sm font-medium">Custom Status Text</label>
171+
<Input
172+
id="custom-status-text"
173+
value={customStatusText}
174+
onChange={(e) => setCustomStatusText(e.target.value)}
175+
placeholder="e.g., In a focus session"
176+
maxLength={100}
177+
/>
178+
<div className="text-xs text-muted-foreground">
179+
Status message shown during focus sessions
180+
</div>
181+
</div>
182+
183+
<div className="space-y-2">
184+
<label htmlFor="custom-status-emoji" className="text-sm font-medium">Custom Status Emoji</label>
185+
<Input
186+
id="custom-status-emoji"
187+
value={customStatusEmoji}
188+
onChange={(e) => setCustomStatusEmoji(e.target.value)}
189+
placeholder="e.g., :brain:"
190+
maxLength={50}
191+
className={!validateEmoji(customStatusEmoji) ? 'border-red-500' : ''}
192+
/>
193+
<div className="text-xs text-muted-foreground">
194+
Emoji in format :emoji_name: (e.g., :brain:, :rocket:)
195+
</div>
196+
</div>
197+
198+
<Button
199+
onClick={handleSavePreferences}
200+
className="w-full"
201+
disabled={updatePreferencesMutation.isPending}
202+
>
203+
{updatePreferencesMutation.isPending ? 'Saving...' : 'Save Preferences'}
204+
</Button>
205+
</div>
206+
131207
<div className="pt-4 border-t">
132208
<Button
133209
variant="outline"

0 commit comments

Comments
 (0)