Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | 3x 2x 413x 3x 1x 1x 316x 316x | /**
* Public-user lookup hooks.
*
* Lazy-loads the public projection of a user from the CUMS public endpoint:
* GET /api/v1/public/users/{username}
*
* Skip-Auth: public endpoint — does NOT attach the Cognito token. A stale ID
* token would otherwise produce a 401 and trigger the apiClient interceptor's
* `/login` redirect (mirrors publicOrganizerService).
*
* 24-hour stale time: public-user projections change rarely; avoid repeated refetches.
*
* Story 11.C.1 originally introduced this for portrait lookup (`useUserPortrait`).
* Epic 11 bug fix 2026-05-19 generalised to `usePublicUser` so the kanban card +
* drawer header can resolve the linked User (firstName/lastName/portrait) once
* `speaker_pool.username` is populated by the CONTACTED→READY promote flow.
*/
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import apiClient from '@/services/api/apiClient';
export interface PublicUserResponse {
username: string;
firstName?: string | null;
lastName?: string | null;
profilePictureUrl?: string | null;
}
async function fetchPublicUser(username: string): Promise<PublicUserResponse> {
const response = await apiClient.get<PublicUserResponse>(`/public/users/${username}`, {
headers: { 'Skip-Auth': 'true' },
});
return response.data;
}
/**
* Fetch the full public projection (firstName, lastName, profilePictureUrl) for a
* user by username. Returns React Query state so callers can show a loading
* placeholder while the user resolves.
*
* @param username Username (the meaningful ID on `speaker_pool.username` /
* `session_users.username`)
* @param enabled Set to false until the component is in/near the viewport
*/
export function usePublicUser(username: string | undefined, enabled: boolean = true) {
return useQuery({
queryKey: ['public-user', username],
queryFn: () => fetchPublicUser(username!),
enabled: !!username && enabled,
staleTime: 24 * 60 * 60 * 1000, // 24 hours — public projections change rarely
gcTime: 30 * 60 * 1000,
// Retry transient 5xx (e.g. Fargate-spot replacement, ~5 min windows) but not 404
// (legitimate persistent miss — non-existent users). Without this the 24h
// staleTime would cache transient failures for the whole browser session.
retry: (failureCount, error) => {
Eif (axios.isAxiosError(error) && error.response?.status === 404) {
return false;
}
return failureCount < 2;
},
});
}
/**
* Backward-compat thin wrapper: returns only the profile picture URL. Prefer
* `usePublicUser` for new code that also needs firstName/lastName.
*
* @param username Username (from session_users)
* @param enabled Set to false until the component is in/near the viewport
*/
export function useUserPortrait(username: string | undefined, enabled: boolean = true) {
const query = usePublicUser(username, enabled);
// Re-projection: callers (e.g. SpeakerDisplay) destructure `{ data }`. Returning a
// narrowed object instead of `{ ...query, data: ... }` avoids cast issues with React
// Query's discriminated-union result types.
return {
data: query.data?.profilePictureUrl ?? null,
isLoading: query.isLoading,
isError: query.isError,
error: query.error,
};
}
|