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
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ src/
- **React Router**: Use future flags `v7_startTransition` and `v7_relativeSplatPath` in BrowserRouter to prepare for v7 upgrade
- **Component Extraction**: When a section of JSX + logic becomes substantial (>30 lines) or reusable, extract it into a separate component. Place in appropriate directory: page-specific components in `components/PageName/`, reusable ones in `components/`
- **Forms**: ALL forms must use react-hook-form with proper validation. Never use plain HTML forms or manual state management for form inputs. Use @hookform/resolvers for validation schemas when needed.
- **Long Components**: Break long components (>150 lines) into smaller focused pieces. Follow the FilterSortControls pattern of primary controls + expandable sections.

### Important Notes

Expand All @@ -126,3 +127,7 @@ src/
- try to use mutation.mutate(variables, {onSuccess, onError}) instead of try{await mutation.mutateAsync(variables)}catch(err){}

- don't add comments unless really necessary

## Git Workflow

- **Auto-commit Rule**: For every user message that requests code changes, automatically commit the changes after implementation with an appropriate commit message
150 changes: 150 additions & 0 deletions MUTATE_ASYNC_CONVERSION_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# MutateAsync to Mutate Conversion Plan

## Overview

This document outlines the plan to convert all `mutateAsync` usage to regular `mutate` calls throughout the codebase, following the project convention that prefers `mutation.mutate(variables, {onSuccess, onError})` over `try{await mutation.mutateAsync(variables)}catch(err){}`.

## Current Status

Found **15 files** with `mutateAsync` usage across **16 instances**:

## Files to Convert

### 1. **Offline Operations** (2 files)
- `src/hooks/useOfflineVoting.ts:34`
- `src/hooks/useOfflineNotes.ts:26`
- `src/hooks/useOfflineNotes.ts:41`

### 2. **Admin Components** (8 files)
- `src/components/Admin/FestivalManagementTable.tsx:45`
- `src/components/Admin/FestivalLogoDialog.tsx:45`
- `src/components/Admin/FestivalLogoDialog.tsx:78`
- `src/components/Admin/FestivalEditionManagement.tsx:171`
- `src/components/Admin/FestivalEditionManagement.tsx:176`
- `src/components/Admin/FestivalDialog.tsx:143`
- `src/components/Admin/FestivalDialog.tsx:148`
- `src/components/Admin/StageManagement.tsx:87`
- `src/components/Admin/StageManagement.tsx:92`
- `src/components/Admin/StageManagement.tsx:117`
- `src/components/Admin/SetFormDialog.tsx:174`
- `src/components/Admin/SetFormDialog.tsx:180`
- `src/components/Admin/SetFormDialog.tsx:193`
- `src/components/Admin/SetFormDialog.tsx:204`

### 3. **Pages** (1 file)
- `src/pages/GroupDetail.tsx:268`

### 4. **Utilities** (2 files)
- `src/components/Index/useInviteValidation.ts:67`
- `src/hooks/queries/knowledge/useKnowledge.ts:147`

## Conversion Patterns

### Pattern 1: Simple Try-Catch → onSuccess/onError

**Before:**
```typescript
try {
await mutation.mutateAsync(variables);
// success logic
} catch (error) {
// error handling
}
```

**After:**
```typescript
mutation.mutate(variables, {
onSuccess: () => {
// success logic
},
onError: (error) => {
// error handling
}
});
```

### Pattern 2: Complex Sequential Operations

**Before:**
```typescript
try {
const result = await mutation1.mutateAsync(data1);
await mutation2.mutateAsync({ id: result.id, ...data2 });
// success logic
} catch (error) {
// error handling
}
```

**After:**
```typescript
mutation1.mutate(data1, {
onSuccess: (result) => {
mutation2.mutate({ id: result.id, ...data2 }, {
onSuccess: () => {
// success logic
},
onError: (error) => {
// error handling
}
});
},
onError: (error) => {
// error handling
}
});
```

### Pattern 3: Multiple Sequential Mutations

For files like `SetFormDialog.tsx` with multiple sequential mutations, we'll need to chain them using nested `onSuccess` callbacks or create helper functions.

## Conversion Priority

### **High Priority** (Simple conversions)
1. `useOfflineVoting.ts` - Single mutation
2. `useOfflineNotes.ts` - Two simple mutations
3. `FestivalManagementTable.tsx` - Single deletion
4. `StageManagement.tsx` - Create/Update/Delete operations
5. `GroupDetail.tsx` - Single remove member operation
6. `useInviteValidation.ts` - Single invite mutation
7. `useKnowledge.ts` - Single toggle mutation

### **Medium Priority** (Moderate complexity)
8. `FestivalLogoDialog.tsx` - Upload then update operations
9. `FestivalEditionManagement.tsx` - Create/Update with form reset
10. `FestivalDialog.tsx` - Create/Update with cleanup

### **Low Priority** (Complex sequential operations)
11. `SetFormDialog.tsx` - Multiple chained mutations with loops

## Benefits of Conversion

1. **Consistency** - Aligns with project conventions
2. **Better Error Handling** - Centralized in mutation hooks
3. **Cleaner Code** - No try-catch blocks needed
4. **Loading States** - Automatic `isPending` management
5. **Success Handling** - Cleaner success callback patterns

## Implementation Steps

1. **Phase 1**: Convert simple single-mutation files (High Priority)
2. **Phase 2**: Convert moderate complexity files (Medium Priority)
3. **Phase 3**: Refactor complex sequential operations (Low Priority)
4. **Phase 4**: Test all conversions and ensure functionality remains identical

## Testing Strategy

- Verify each conversion maintains identical behavior
- Test error scenarios to ensure proper error handling
- Confirm loading states work correctly
- Check that success/cleanup logic executes as expected

## Completion Criteria

- [ ] All 16 `mutateAsync` instances converted to `mutate`
- [ ] No remaining `mutateAsync` usage in codebase
- [ ] All functionality verified to work identically
- [ ] Consistent error handling patterns across all mutations
- [ ] Code follows project conventions for mutation usage
162 changes: 27 additions & 135 deletions src/components/AppHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { ReactNode } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { ReactNode, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { TooltipProvider } from "@/components/ui/tooltip";
import { Music, Heart, LogIn } from "lucide-react";
import { useIsMobile } from "@/hooks/use-mobile";
import { Navigation } from "./AppHeader/Navigation";
import { UserMenu } from "./AppHeader/UserMenu";
import { AdminActions } from "./AppHeader/AdminActions";
import { useAuth } from "@/contexts/AuthContext";
import { useScrollVisibility } from "@/hooks/useScrollVisibility";
import { TopBar } from "./AppHeader/TopBar";
import { TitleSection } from "./AppHeader/TitleSection";

interface AppHeaderProps {
// Navigation
Expand All @@ -17,12 +13,9 @@ interface AppHeaderProps {

// Page content
title?: string;
logoUrl?: string | null;
subtitle?: string;
description?: string;
logoUrl?: string | null;

// Actions
actions?: ReactNode;

// Navigation options
showGroupsButton?: boolean;
Expand All @@ -35,140 +28,39 @@ export function AppHeader({
showBackButton = false,
backLabel = "Back",
title,
subtitle,
description,
// subtitle,
// description,
logoUrl,
actions,
// actions,
showGroupsButton = false,
children,
// children,
}: AppHeaderProps) {
const navigate = useNavigate();
const { user, profile, signOut, showAuthDialog } = useAuth();
const isMobile = useIsMobile();

// Track visibility of title section for festival context in top bar
const titleRef = useRef<HTMLDivElement>(null);
const isTitleVisible = useScrollVisibility(titleRef);

function handleBackClick() {
navigate(-1);
}

function getGreeting() {
const hour = new Date().getHours();
if (hour < 12) return "Good morning";
if (hour < 18) return "Good afternoon";
return "Good evening";
}

const displayName =
profile?.username || user?.email?.split("@")[0] || "there";

return (
<TooltipProvider>
<div className="mb-8">
{/* Top Bar - App Branding, User Identity & Navigation */}
<div className="flex items-center justify-between mb-6 pb-6 border-b border-purple-400/20">
{/* Left Side - Branding */}
<div className="flex items-center gap-4">
<Link to="/">
<div className="flex items-center gap-3">
<Music className="h-6 w-6 text-purple-400" />
<h1 className="text-2xl font-bold text-white">UpLine</h1>
</div>
</Link>

{/* User Greeting - Desktop Only */}
{user && !isMobile && (
<div className="flex items-center gap-3 pl-4 border-l border-purple-400/20">
<span className="text-purple-200 text-sm">
{getGreeting()}, {displayName}! 🎶
</span>
</div>
)}
</div>

{/* Right Side - Navigation & User Actions */}
<div className="flex items-center gap-4">
{/* Navigation Buttons */}
<Navigation
showBackButton={showBackButton}
backLabel={backLabel}
showGroupsButton={showGroupsButton}
isMobile={isMobile}
onBackClick={handleBackClick}
/>

{/* Admin Actions */}
{user && (
<div className="flex items-center gap-3">
<AdminActions userId={user.id} isMobile={isMobile} />
</div>
)}

{/* Authentication - User Menu or Sign In */}
<div className="flex items-center">
{user ? (
<UserMenu
user={user}
profile={profile || undefined}
onSignOut={signOut}
isMobile={isMobile}
/>
) : (
<Button
onClick={() => showAuthDialog()}
size={isMobile ? "sm" : "default"}
className="bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-full px-6"
>
<LogIn className="h-4 w-4" />
<span className={isMobile ? "ml-1" : "ml-2"}>
{isMobile ? "Sign In" : "Sign In / Sign Up"}
</span>
</Button>
)}
</div>
</div>
<div>
<TopBar
showBackButton={showBackButton}
backLabel={backLabel}
showGroupsButton={showGroupsButton}
onBackClick={handleBackClick}
isTitleVisible={isTitleVisible}
logoUrl={logoUrl}
title={title}
/>

<div className="pt-16 md:pt-20" ref={titleRef}>
<TitleSection title={title} logoUrl={logoUrl} />
</div>

{/* Page Content Section */}
{(title || subtitle || description || children) && (
<div className="text-center space-y-4">
{title && (
<div className="flex items-center justify-center gap-3 mb-6">
{logoUrl ? (
<img
src={logoUrl}
alt={`${title} logo`}
className="h-40 w-auto max-w-sm object-contain rounded"
/>
) : (
<>
<Music className="h-8 w-8 text-purple-400 animate-pulse" />
<h2 className="text-4xl font-bold text-white tracking-tight">
{title}
</h2>
<Heart className="h-8 w-8 text-pink-400 animate-pulse" />
</>
)}
</div>
)}

{subtitle && (
<p className="text-xl text-purple-200 font-medium mb-4">
{subtitle}
</p>
)}

{description && (
<p className="text-purple-300 mb-6 max-w-2xl mx-auto leading-relaxed">
{description}
</p>
)}

{actions && (
<div className="flex justify-center mb-8">{actions}</div>
)}

{children}
</div>
)}
</div>
</TooltipProvider>
);
Expand Down
Loading
Loading