From 8024d1199d1a4154b492b4ac556f84494c32714e Mon Sep 17 00:00:00 2001 From: Paul Hovley Date: Mon, 11 Aug 2025 13:31:25 -0600 Subject: [PATCH] Enable updating slack status/emoji --- src/CLAUDE.md | 45 ++++++++++++++-- src/api/hooks/useSlack.ts | 16 +++++- src/components/SlackFocusToggle.tsx | 80 ++++++++++++++++++++++++++++- 3 files changed, 134 insertions(+), 7 deletions(-) diff --git a/src/CLAUDE.md b/src/CLAUDE.md index f14ba685..5cfcac39 100644 --- a/src/CLAUDE.md +++ b/src/CLAUDE.md @@ -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 @@ -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 @@ -238,13 +247,25 @@ 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
Loading...
if (error) return
Error loading sessions
@@ -252,13 +273,31 @@ export default function FlowSessionList() { return (
{sessions?.map(session => ( -
{session.objective}
+
+ {session.objective} + +
))}
) } ``` +**❌ 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) diff --git a/src/api/hooks/useSlack.ts b/src/api/hooks/useSlack.ts index 8f7074da..f1874c05 100644 --- a/src/api/hooks/useSlack.ts +++ b/src/api/hooks/useSlack.ts @@ -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, @@ -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() }) + }, + }) +} diff --git a/src/components/SlackFocusToggle.tsx b/src/components/SlackFocusToggle.tsx index 37b26cef..377124b5 100644 --- a/src/components/SlackFocusToggle.tsx +++ b/src/components/SlackFocusToggle.tsx @@ -1,7 +1,8 @@ -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, @@ -9,7 +10,7 @@ import { 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' @@ -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) { @@ -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 @@ -128,6 +165,45 @@ export function SlackFocusToggle({ slackSettings, onSlackSettingsChange }: Slack /> +
+
+ + setCustomStatusText(e.target.value)} + placeholder="e.g., In a focus session" + maxLength={100} + /> +
+ Status message shown during focus sessions +
+
+ +
+ + setCustomStatusEmoji(e.target.value)} + placeholder="e.g., :brain:" + maxLength={50} + className={!validateEmoji(customStatusEmoji) ? 'border-red-500' : ''} + /> +
+ Emoji in format :emoji_name: (e.g., :brain:, :rocket:) +
+
+ + +
+