A personal media tracker built on the AT Protocol opnshelf.xyz
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: refactor authentication handling in mobile and web components to utilize TanStack Query, enhancing user session management and API interactions

+133 -228
+18 -17
apps/mobile/src/components/HeaderRight.tsx
··· 1 1 import { useNavigation } from '@react-navigation/native'; 2 2 import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; 3 - import { useQuery, useQueryClient } from '@tanstack/react-query'; 4 - import { getAuthUser, logout } from '@opnshelf/api'; 3 + import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'; 4 + import { authControllerMeOptions, authControllerLogoutMutation } from '@opnshelf/api'; 5 5 import { Ionicons } from '@expo/vector-icons'; 6 6 import { ActivityIndicator, Image, TouchableOpacity, View } from 'react-native'; 7 - import { useState } from 'react'; 8 7 import type { RootStackParamList } from '../navigation'; 9 8 10 9 type NavigationProp = NativeStackNavigationProp<RootStackParamList>; ··· 12 11 export function HeaderRight() { 13 12 const navigation = useNavigation<NavigationProp>(); 14 13 const queryClient = useQueryClient(); 15 - const [isLoggingOut, setIsLoggingOut] = useState(false); 16 14 15 + // Fetch auth state using generated TanStack Query hook 17 16 const { data: user, isLoading } = useQuery({ 18 - queryKey: ['auth', 'me'], 19 - queryFn: getAuthUser, 17 + ...authControllerMeOptions(), 20 18 staleTime: 5 * 60 * 1000, 21 19 retry: false, 22 20 }); 23 21 24 - const handleLogout = async () => { 25 - setIsLoggingOut(true); 26 - try { 27 - await logout(); 22 + // Logout mutation using generated TanStack Query hook 23 + const logoutMutation = useMutation({ 24 + ...authControllerLogoutMutation(), 25 + onSuccess: () => { 28 26 queryClient.invalidateQueries({ queryKey: ['auth'] }); 29 27 queryClient.invalidateQueries({ queryKey: ['shelf'] }); 30 28 navigation.reset({ 31 29 index: 0, 32 30 routes: [{ name: 'Home' }], 33 31 }); 34 - } catch (error) { 32 + }, 33 + onError: (error) => { 35 34 console.error('Logout failed:', error); 36 - } finally { 37 - setIsLoggingOut(false); 38 - } 35 + }, 36 + }); 37 + 38 + const handleLogout = async () => { 39 + await logoutMutation.mutateAsync({}); 39 40 }; 40 41 41 42 if (isLoading) { ··· 71 72 {/* User avatar/logout */} 72 73 <TouchableOpacity 73 74 onPress={handleLogout} 74 - disabled={isLoggingOut} 75 + disabled={logoutMutation.isPending} 75 76 activeOpacity={0.7} 76 77 > 77 - {isLoggingOut ? ( 78 + {logoutMutation.isPending ? ( 78 79 <ActivityIndicator size="small" color="#a855f7" /> 79 80 ) : user.avatar ? ( 80 81 <Image 81 - source={{ uri: user.avatar }} 82 + source={{ uri: String(user.avatar) }} 82 83 className="w-8 h-8 rounded-full" 83 84 /> 84 85 ) : (
+5 -6
apps/mobile/src/screens/HomeScreen.tsx
··· 1 1 import type { NativeStackScreenProps } from '@react-navigation/native-stack'; 2 2 import { useQuery } from '@tanstack/react-query'; 3 - import { getAuthUser } from '@opnshelf/api'; 3 + import { authControllerMeOptions } from '@opnshelf/api'; 4 4 import { Ionicons } from '@expo/vector-icons'; 5 5 import { Image, Text, TouchableOpacity, View } from 'react-native'; 6 6 import type { RootStackParamList } from '../navigation'; ··· 8 8 type Props = NativeStackScreenProps<RootStackParamList, 'Home'>; 9 9 10 10 export function HomeScreen({ navigation }: Props) { 11 - // Check auth state 11 + // Check auth state using generated TanStack Query hook 12 12 const { data: user } = useQuery({ 13 - queryKey: ['auth', 'me'], 14 - queryFn: getAuthUser, 13 + ...authControllerMeOptions(), 15 14 staleTime: 5 * 60 * 1000, 16 15 retry: false, 17 16 }); ··· 32 31 <View className="flex-row items-center gap-3 mb-6 bg-gray-900 py-3 px-4 rounded-lg border border-gray-800"> 33 32 {user.avatar ? ( 34 33 <Image 35 - source={{ uri: user.avatar }} 34 + source={{ uri: String(user.avatar) }} 36 35 className="w-10 h-10 rounded-full" 37 36 /> 38 37 ) : ( ··· 42 41 )} 43 42 <View> 44 43 <Text className="text-gray-50 font-semibold"> 45 - {user.displayName || user.handle} 44 + {user.displayName ? String(user.displayName) : user.handle} 46 45 </Text> 47 46 <Text className="text-gray-500 text-sm">@{user.handle}</Text> 48 47 </View>
+3 -4
apps/mobile/src/screens/LoginScreen.tsx
··· 11 11 View, 12 12 } from 'react-native'; 13 13 import * as WebBrowser from 'expo-web-browser'; 14 - import { getAuthUser, getLoginUrl } from '@opnshelf/api'; 14 + import { authControllerMeOptions, getLoginUrl } from '@opnshelf/api'; 15 15 import type { RootStackParamList } from '../navigation'; 16 16 17 17 type Props = NativeStackScreenProps<RootStackParamList, 'Login'>; ··· 21 21 const [isSubmitting, setIsSubmitting] = useState(false); 22 22 const { error, redirect, reason } = route.params ?? {}; 23 23 24 - // Check if user is already logged in 24 + // Check if user is already logged in using generated TanStack Query hook 25 25 const { data: user, isLoading: isAuthLoading } = useQuery({ 26 - queryKey: ['auth', 'me'], 27 - queryFn: getAuthUser, 26 + ...authControllerMeOptions(), 28 27 staleTime: 5 * 60 * 1000, 29 28 retry: false, 30 29 });
+22 -20
apps/mobile/src/screens/SearchScreen.tsx
··· 1 1 import type { NativeStackScreenProps } from '@react-navigation/native-stack'; 2 2 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; 3 3 import { 4 - getAuthUser, 5 - getUserMovies, 6 - searchMovies, 7 - markMovieWatched, 8 - unmarkMovieWatched, 4 + authControllerMeOptions, 5 + moviesControllerGetUserMoviesOptions, 6 + moviesControllerSearchMoviesOptions, 7 + moviesControllerMarkWatchedMutation, 8 + moviesControllerUnmarkWatchedMutation, 9 9 } from '@opnshelf/api'; 10 10 import { Ionicons } from '@expo/vector-icons'; 11 11 import { ··· 110 110 }; 111 111 }, [query, searchQuery]); 112 112 113 - // Auth state 113 + // Auth state using generated TanStack Query hook 114 114 const { data: user } = useQuery({ 115 - queryKey: ['auth', 'me'], 116 - queryFn: getAuthUser, 115 + ...authControllerMeOptions(), 117 116 staleTime: 5 * 60 * 1000, 118 117 retry: false, 119 118 }); 120 119 121 - // User's tracked movies 120 + // User's tracked movies using generated TanStack Query hook 122 121 const { data: trackedMovies } = useQuery({ 123 - queryKey: ['shelf', user?.did], 124 - queryFn: () => getUserMovies(user!.did), 122 + ...moviesControllerGetUserMoviesOptions({ 123 + path: { userDid: user?.did || '' }, 124 + }), 125 125 enabled: !!user?.did, 126 126 }); 127 127 ··· 130 130 trackedMovies?.map((t: { movieId: string }) => t.movieId) ?? [], 131 131 ); 132 132 133 - // Mutations for marking/unmarking movies 133 + // Mutations for marking/unmarking movies using generated TanStack Query hooks 134 134 const markMutation = useMutation({ 135 - mutationFn: markMovieWatched, 135 + ...moviesControllerMarkWatchedMutation(), 136 136 onSuccess: () => { 137 137 queryClient.invalidateQueries({ queryKey: ['shelf'] }); 138 138 }, 139 139 }); 140 140 141 141 const unmarkMutation = useMutation({ 142 - mutationFn: unmarkMovieWatched, 142 + ...moviesControllerUnmarkWatchedMutation(), 143 143 onSuccess: () => { 144 144 queryClient.invalidateQueries({ queryKey: ['shelf'] }); 145 145 }, 146 146 }); 147 147 148 + // Search movies using generated TanStack Query hook 148 149 const { data, isLoading, error } = useQuery({ 149 - queryKey: ['search', searchQuery], 150 - queryFn: () => searchMovies(searchQuery), 150 + ...moviesControllerSearchMoviesOptions({ 151 + query: { query: searchQuery }, 152 + }), 151 153 enabled: searchQuery.length > 0, 152 154 }); 153 155 ··· 160 162 navigation.navigate('Login', { redirect: 'Search' }); 161 163 return; 162 164 } 163 - markMutation.mutate(movieId); 165 + markMutation.mutate({ body: { movieId } }); 164 166 }, 165 167 [user, navigation, markMutation], 166 168 ); 167 169 168 170 const handleUnmarkWatched = useCallback( 169 171 (movieId: string) => { 170 - unmarkMutation.mutate(movieId); 172 + unmarkMutation.mutate({ path: { movieId } }); 171 173 }, 172 174 [unmarkMutation], 173 175 ); ··· 177 179 const movieId = String(item.id); 178 180 const isWatched = watchedMovieIds.has(movieId); 179 181 const isLoading = 180 - (markMutation.isPending && markMutation.variables === movieId) || 181 - (unmarkMutation.isPending && unmarkMutation.variables === movieId); 182 + (markMutation.isPending && markMutation.variables?.body?.movieId === movieId) || 183 + (unmarkMutation.isPending && unmarkMutation.variables?.path?.movieId === movieId); 182 184 183 185 return ( 184 186 <MovieCard
+13 -13
apps/mobile/src/screens/ShelfScreen.tsx
··· 1 1 import type { NativeStackScreenProps } from '@react-navigation/native-stack'; 2 2 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; 3 - import { getAuthUser, getUserMovies, unmarkMovieWatched } from '@opnshelf/api'; 3 + import { authControllerMeOptions, moviesControllerGetUserMoviesOptions, moviesControllerUnmarkWatchedMutation } from '@opnshelf/api'; 4 4 import { Ionicons } from '@expo/vector-icons'; 5 5 import { 6 6 ActivityIndicator, ··· 22 22 movieId: string; 23 23 movie: { 24 24 title: string; 25 - posterPath: string | null; 26 - releaseYear: number | null; 25 + posterPath?: string; 26 + releaseYear?: number; 27 27 }; 28 28 }; 29 29 ··· 76 76 export function ShelfScreen({ navigation }: Props) { 77 77 const queryClient = useQueryClient(); 78 78 79 - // Fetch auth state 79 + // Fetch auth state using generated TanStack Query hook 80 80 const { data: user, isLoading: isAuthLoading } = useQuery({ 81 - queryKey: ['auth', 'me'], 82 - queryFn: getAuthUser, 81 + ...authControllerMeOptions(), 83 82 staleTime: 5 * 60 * 1000, 84 83 retry: false, 85 84 }); 86 85 87 - // Fetch user's tracked movies 86 + // Fetch user's tracked movies using generated TanStack Query hook 88 87 const { data: trackedMovies, isLoading: isMoviesLoading } = useQuery({ 89 - queryKey: ['shelf', user?.did], 90 - queryFn: () => getUserMovies(user!.did), 88 + ...moviesControllerGetUserMoviesOptions({ 89 + path: { userDid: user?.did || '' }, 90 + }), 91 91 enabled: !!user?.did, 92 92 }); 93 93 94 - // Mutation for removing from shelf 94 + // Mutation for removing from shelf using generated TanStack Query hook 95 95 const unmarkMutation = useMutation({ 96 - mutationFn: unmarkMovieWatched, 96 + ...moviesControllerUnmarkWatchedMutation(), 97 97 onSuccess: () => { 98 98 queryClient.invalidateQueries({ queryKey: ['shelf'] }); 99 99 }, ··· 103 103 ({ item }: { item: TrackedMovie }) => ( 104 104 <MovieCard 105 105 tracked={item} 106 - onRemove={() => unmarkMutation.mutate(item.movieId)} 107 - isRemoving={unmarkMutation.isPending && unmarkMutation.variables === item.movieId} 106 + onRemove={() => unmarkMutation.mutate({ path: { movieId: item.movieId } })} 107 + isRemoving={unmarkMutation.isPending && unmarkMutation.variables?.path?.movieId === item.movieId} 108 108 /> 109 109 ), 110 110 [unmarkMutation],
+2 -1
apps/mobile/tsconfig.json
··· 1 1 { 2 2 "extends": "expo/tsconfig.base", 3 3 "compilerOptions": { 4 - "strict": true 4 + "strict": true, 5 + "lib": ["ES2020", "DOM"] 5 6 } 6 7 }
+29 -21
apps/web/src/components/Header.tsx
··· 1 1 import { Link, useNavigate } from '@tanstack/react-router' 2 2 import { useState } from 'react' 3 - import { useQuery, useQueryClient } from '@tanstack/react-query' 3 + import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query' 4 4 import { Film, Home, Menu, Search, X, LogIn, LogOut, User, BookOpen } from 'lucide-react' 5 - import { getAuthUser, logout } from '@opnshelf/api' 5 + import { authControllerMeOptions, authControllerLogoutMutation } from '@opnshelf/api' 6 6 7 7 export default function Header() { 8 8 const [isOpen, setIsOpen] = useState(false) 9 9 const queryClient = useQueryClient() 10 10 const navigate = useNavigate() 11 11 12 - // Fetch auth state 12 + // Fetch auth state using generated TanStack Query hook 13 13 const { data: user, isLoading: isAuthLoading } = useQuery({ 14 - queryKey: ['auth', 'me'], 15 - queryFn: getAuthUser, 14 + ...authControllerMeOptions(), 16 15 staleTime: 5 * 60 * 1000, // 5 minutes 17 16 retry: false, 18 17 }) 19 18 19 + // Logout mutation using generated TanStack Query hook 20 + const logoutMutation = useMutation({ 21 + ...authControllerLogoutMutation(), 22 + onSuccess: () => { 23 + queryClient.invalidateQueries({ queryKey: ['auth'] }) 24 + }, 25 + }) 26 + 20 27 const handleLogout = async () => { 21 - await logout() 22 - queryClient.invalidateQueries({ queryKey: ['auth'] }) 28 + await logoutMutation.mutateAsync({}) 23 29 } 24 30 25 31 const handleLogin = () => { ··· 58 64 <Home size={18} /> 59 65 <span className="font-medium">Home</span> 60 66 </Link> 61 - <Link 62 - to="/search" 63 - search={{ q: '' }} 64 - className="flex items-center gap-2 px-4 py-2 rounded-lg hover:bg-gray-800 transition-colors text-gray-300 hover:text-white" 65 - > 66 - <Search size={18} /> 67 - <span className="font-medium">Search</span> 68 - </Link> 69 67 {user && ( 70 68 <Link 71 69 to="/shelf" ··· 79 77 <span className="font-medium">My Shelf</span> 80 78 </Link> 81 79 )} 80 + <Link 81 + to="/search" 82 + search={{ q: '' }} 83 + className="flex items-center gap-2 px-4 py-2 rounded-lg hover:bg-gray-800 transition-colors text-gray-300 hover:text-white" 84 + > 85 + <Search size={18} /> 86 + <span className="font-medium">Search</span> 87 + </Link> 82 88 83 89 {/* Auth section */} 84 90 <div className="ml-4 pl-4 border-l border-gray-700"> ··· 88 94 <div className="flex items-center gap-3"> 89 95 {user.avatar ? ( 90 96 <img 91 - src={user.avatar} 92 - alt={user.displayName || user.handle} 97 + src={String(user.avatar)} 98 + alt={String(user.displayName || user.handle)} 93 99 className="w-8 h-8 rounded-full" 94 100 /> 95 101 ) : ( ··· 98 104 </div> 99 105 )} 100 106 <span className="text-sm text-gray-300"> 101 - {user.displayName || `@${user.handle}`} 107 + {user.displayName ? String(user.displayName) : `@${user.handle}`} 102 108 </span> 103 109 <button 104 110 type="button" 105 111 onClick={handleLogout} 112 + disabled={logoutMutation.isPending} 106 113 className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-gray-800 transition-colors text-gray-300 hover:text-white text-sm" 107 114 title="Sign out" 108 115 > ··· 204 211 <div className="flex items-center gap-3"> 205 212 {user.avatar ? ( 206 213 <img 207 - src={user.avatar} 208 - alt={user.displayName || user.handle} 214 + src={String(user.avatar)} 215 + alt={String(user.displayName || user.handle)} 209 216 className="w-10 h-10 rounded-full" 210 217 /> 211 218 ) : ( ··· 214 221 </div> 215 222 )} 216 223 <div> 217 - <div className="font-medium">{user.displayName || user.handle}</div> 224 + <div className="font-medium">{user.displayName ? String(user.displayName) : user.handle}</div> 218 225 <div className="text-sm text-gray-400">@{user.handle}</div> 219 226 </div> 220 227 </div> ··· 224 231 handleLogout() 225 232 setIsOpen(false) 226 233 }} 234 + disabled={logoutMutation.isPending} 227 235 className="w-full flex items-center justify-center gap-2 px-4 py-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors text-gray-300" 228 236 > 229 237 <LogOut size={18} />
+3 -4
apps/web/src/routes/login.tsx
··· 2 2 import { useState, useEffect, useId } from 'react'; 3 3 import { useQuery } from '@tanstack/react-query'; 4 4 import { Film, LogIn, AlertCircle } from 'lucide-react'; 5 - import { getAuthUser, getLoginUrl } from '@opnshelf/api'; 5 + import { authControllerMeOptions, getLoginUrl } from '@opnshelf/api'; 6 6 import { z } from 'zod'; 7 7 import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; 8 8 ··· 24 24 const { error, redirect, reason } = Route.useSearch(); 25 25 const handleId = useId(); 26 26 27 - // Check if user is already logged in 27 + // Check if user is already logged in using generated TanStack Query hook 28 28 const { data: user, isLoading: isAuthLoading } = useQuery({ 29 - queryKey: ['auth', 'me'], 30 - queryFn: getAuthUser, 29 + ...authControllerMeOptions(), 31 30 staleTime: 5 * 60 * 1000, 32 31 retry: false, 33 32 });
+19 -17
apps/web/src/routes/search.tsx
··· 1 1 import { createFileRoute, useNavigate } from '@tanstack/react-router'; 2 2 import { useState, useEffect, useRef, useMemo } from 'react'; 3 3 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; 4 - import { searchMovies, getAuthUser, getUserMovies, markMovieWatched, unmarkMovieWatched } from '@opnshelf/api'; 4 + import { authControllerMeOptions, moviesControllerGetUserMoviesOptions, moviesControllerSearchMoviesOptions, moviesControllerMarkWatchedMutation, moviesControllerUnmarkWatchedMutation } from '@opnshelf/api'; 5 5 import { Search, Check, Plus } from 'lucide-react'; 6 6 7 7 export const Route = createFileRoute('/search')({ ··· 20 20 const [query, setQuery] = useState(searchQuery); 21 21 const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); 22 22 23 - // Fetch auth state 23 + // Fetch auth state using generated TanStack Query hook 24 24 const { data: user } = useQuery({ 25 - queryKey: ['auth', 'me'], 26 - queryFn: getAuthUser, 25 + ...authControllerMeOptions(), 27 26 staleTime: 5 * 60 * 1000, 28 27 retry: false, 29 28 }); 30 29 31 - // Fetch user's tracked movies when logged in 30 + // Fetch user's tracked movies when logged in using generated TanStack Query hook 32 31 const { data: trackedMovies } = useQuery({ 33 - queryKey: ['shelf', user?.did], 34 - queryFn: () => getUserMovies(user?.did), 32 + ...moviesControllerGetUserMoviesOptions({ 33 + path: { userDid: user?.did || '' }, 34 + }), 35 35 enabled: !!user?.did, 36 36 }); 37 37 38 38 // Build a set of watched movie IDs for fast lookup 39 39 const watchedMovieIds = useMemo(() => { 40 40 if (!trackedMovies) return new Set<string>(); 41 - return new Set(trackedMovies.map((m) => m.movieId)); 41 + return new Set(trackedMovies.map((m: { movieId: string }) => m.movieId)); 42 42 }, [trackedMovies]); 43 43 44 - // Mutation for marking as watched 44 + // Mutation for marking as watched using generated TanStack Query hook 45 45 const markMutation = useMutation({ 46 - mutationFn: markMovieWatched, 46 + ...moviesControllerMarkWatchedMutation(), 47 47 onSuccess: () => { 48 48 queryClient.invalidateQueries({ queryKey: ['shelf'] }); 49 49 }, 50 50 }); 51 51 52 - // Mutation for unmarking as watched 52 + // Mutation for unmarking as watched using generated TanStack Query hook 53 53 const unmarkMutation = useMutation({ 54 - mutationFn: unmarkMovieWatched, 54 + ...moviesControllerUnmarkWatchedMutation(), 55 55 onSuccess: () => { 56 56 queryClient.invalidateQueries({ queryKey: ['shelf'] }); 57 57 }, ··· 82 82 }; 83 83 }, [query, searchQuery, navigate]); 84 84 85 + // Search movies using generated TanStack Query hook 85 86 const { data, isLoading, error } = useQuery({ 86 - queryKey: ['search', searchQuery], 87 - queryFn: () => searchMovies(searchQuery), 87 + ...moviesControllerSearchMoviesOptions({ 88 + query: { query: searchQuery }, 89 + }), 88 90 enabled: searchQuery.length > 0, 89 91 }); 90 92 ··· 152 154 type="button" 153 155 onClick={() => { 154 156 if (isWatched) { 155 - unmarkMutation.mutate(movieId); 157 + unmarkMutation.mutate({ path: { movieId } }); 156 158 } else { 157 - markMutation.mutate(movieId); 159 + markMutation.mutate({ body: { movieId } }); 158 160 } 159 161 }} 160 162 disabled={isPending} ··· 196 198 </div> 197 199 </div> 198 200 ); 199 - } 201 + }
+10 -10
apps/web/src/routes/shelf.tsx
··· 1 1 import { createFileRoute, Link } from '@tanstack/react-router'; 2 2 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; 3 - import { getAuthUser, getUserMovies, unmarkMovieWatched } from '@opnshelf/api'; 3 + import { authControllerMeOptions, moviesControllerGetUserMoviesOptions, moviesControllerUnmarkWatchedMutation } from '@opnshelf/api'; 4 4 import { BookOpen, Trash2, LogIn } from 'lucide-react'; 5 5 6 6 export const Route = createFileRoute('/shelf')({ ··· 10 10 function ShelfPage() { 11 11 const queryClient = useQueryClient(); 12 12 13 - // Fetch auth state 13 + // Fetch auth state using generated TanStack Query hook 14 14 const { data: user, isLoading: isAuthLoading } = useQuery({ 15 - queryKey: ['auth', 'me'], 16 - queryFn: getAuthUser, 15 + ...authControllerMeOptions(), 17 16 staleTime: 5 * 60 * 1000, 18 17 retry: false, 19 18 }); 20 19 21 - // Fetch user's tracked movies 20 + // Fetch user's tracked movies using generated TanStack Query hook 22 21 const { data: trackedMovies, isLoading: isMoviesLoading } = useQuery({ 23 - queryKey: ['shelf', user?.did], 24 - queryFn: () => getUserMovies(user?.did), 22 + ...moviesControllerGetUserMoviesOptions({ 23 + path: { userDid: user?.did || '' }, 24 + }), 25 25 enabled: !!user?.did, 26 26 }); 27 27 28 - // Mutation for removing from shelf 28 + // Mutation for removing from shelf using generated TanStack Query hook 29 29 const unmarkMutation = useMutation({ 30 - mutationFn: unmarkMovieWatched, 30 + ...moviesControllerUnmarkWatchedMutation(), 31 31 onSuccess: () => { 32 32 queryClient.invalidateQueries({ queryKey: ['shelf'] }); 33 33 }, ··· 107 107 )} 108 108 <button 109 109 type="button" 110 - onClick={() => unmarkMutation.mutate(tracked.movieId)} 110 + onClick={() => unmarkMutation.mutate({ path: { movieId: tracked.movieId } })} 111 111 disabled={unmarkMutation.isPending} 112 112 className="absolute top-2 right-2 p-2 bg-red-600 hover:bg-red-700 rounded-full opacity-0 group-hover:opacity-100 transition-opacity disabled:opacity-50" 113 113 title="Remove from shelf"
+1 -44
packages/api/src/client.ts
··· 69 69 avatar: string | null; 70 70 } 71 71 72 - // Auth functions 73 - export async function getAuthUser(): Promise<AuthUser | null> { 74 - try { 75 - const headers: HeadersInit = {}; 76 - if (sessionToken) { 77 - headers['Authorization'] = `Bearer ${sessionToken}`; 78 - } 79 - 80 - const response = await fetch(`${baseUrl}/auth/me`, { 81 - credentials: 'include', 82 - headers, 83 - }); 84 - 85 - if (response.status === 401) { 86 - onUnauthorized?.(); 87 - } 88 - 89 - if (!response.ok) { 90 - return null; 91 - } 92 - 93 - return response.json(); 94 - } catch { 95 - return null; 96 - } 97 - } 98 - 72 + // Simple URL helper for login (not an API call) 99 73 export function getLoginUrl(handle?: string): string { 100 74 const params = handle ? `?handle=${encodeURIComponent(handle)}` : ''; 101 75 return `${baseUrl}/auth/login${params}`; 102 76 } 103 - 104 - export async function logout(): Promise<void> { 105 - const headers: HeadersInit = {}; 106 - if (sessionToken) { 107 - headers['Authorization'] = `Bearer ${sessionToken}`; 108 - } 109 - 110 - await fetch(`${baseUrl}/auth/logout`, { 111 - method: 'POST', 112 - credentials: 'include', 113 - headers, 114 - }); 115 - 116 - // Clear session token on logout 117 - sessionToken = null; 118 - updateClientConfig(); 119 - }
+6 -1
packages/api/src/generated/core/queryKeySerializer.gen.ts
··· 57 57 * Turns URLSearchParams into a sorted JSON object for deterministic keys. 58 58 */ 59 59 const serializeSearchParams = (params: URLSearchParams): JsonValue => { 60 - const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); 60 + const entries: Array<[string, string]> = []; 61 + params.forEach((value, key) => { 62 + entries.push([key, value]); 63 + }); 64 + entries.sort((a, b) => a[0].localeCompare(b[0])); 65 + 61 66 const result: Record<string, JsonValue> = {}; 62 67 63 68 for (const [key, value] of entries) {
+2 -70
packages/api/src/index.ts
··· 14 14 setOnUnauthorized, 15 15 setSessionToken, 16 16 getSessionToken, 17 - getAuthUser, 17 + configureApiClient, 18 18 getLoginUrl, 19 - logout, 20 - configureApiClient, 19 + type AuthUser, 21 20 } from './client'; 22 - 23 - // Backward compatibility wrappers for old API functions 24 - import { 25 - moviesControllerSearchMovies, 26 - moviesControllerGetUserMovies, 27 - moviesControllerMarkWatched, 28 - moviesControllerUnmarkWatched, 29 - moviesControllerGetMovieDetails, 30 - } from './generated/sdk.gen'; 31 - 32 - /** 33 - * @deprecated Use moviesControllerSearchMovies with TanStack Query instead 34 - */ 35 - export async function searchMovies(query: string) { 36 - const { data } = await moviesControllerSearchMovies({ 37 - query: { query }, 38 - throwOnError: true, 39 - }); 40 - return data; 41 - } 42 - 43 - /** 44 - * @deprecated Use moviesControllerGetUserMovies with TanStack Query instead 45 - */ 46 - export async function getUserMovies(userDid: string | undefined) { 47 - if (!userDid) { 48 - return []; 49 - } 50 - const { data } = await moviesControllerGetUserMovies({ 51 - path: { userDid }, 52 - throwOnError: true, 53 - }); 54 - return data; 55 - } 56 - 57 - /** 58 - * @deprecated Use moviesControllerMarkWatched with TanStack Query instead 59 - */ 60 - export async function markMovieWatched(movieId: string) { 61 - const { data } = await moviesControllerMarkWatched({ 62 - body: { movieId }, 63 - throwOnError: true, 64 - }); 65 - return data; 66 - } 67 - 68 - /** 69 - * @deprecated Use moviesControllerUnmarkWatched with TanStack Query instead 70 - */ 71 - export async function unmarkMovieWatched(movieId: string) { 72 - const { data } = await moviesControllerUnmarkWatched({ 73 - path: { movieId }, 74 - throwOnError: true, 75 - }); 76 - return data; 77 - } 78 - 79 - /** 80 - * @deprecated Use moviesControllerGetMovieDetails with TanStack Query instead 81 - */ 82 - export async function getMovieDetails(movieId: string) { 83 - const { data } = await moviesControllerGetMovieDetails({ 84 - path: { movieId }, 85 - throwOnError: true, 86 - }); 87 - return data; 88 - }