feat: migrate components to React Query hooks

- Add comprehensive API hooks for all operations
- Migrate rooms page to use React Query mutations
- Update transcript title component to use mutation hook
- Refactor share/privacy component with proper error handling
- Remove direct API client usage in favor of hooks
This commit is contained in:
2025-08-28 00:07:47 -06:00
parent 68c161ee7e
commit 55f83cf5f4
4 changed files with 385 additions and 71 deletions

View File

@@ -15,9 +15,15 @@ import {
useDisclosure, useDisclosure,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useApi from "../../lib/useApi";
import useRoomList from "./useRoomList"; import useRoomList from "./useRoomList";
import { ApiError, Room } from "../../lib/api-types"; import { ApiError, Room } from "../../lib/api-types";
import {
useRoomCreate,
useRoomUpdate,
useRoomDelete,
useZulipStreams,
useZulipTopics,
} from "../../lib/api-hooks";
import { RoomList } from "./_components/RoomList"; import { RoomList } from "./_components/RoomList";
import { PaginationPage } from "../browse/_components/Pagination"; import { PaginationPage } from "../browse/_components/Pagination";
@@ -75,64 +81,42 @@ export default function RoomsList() {
const [room, setRoom] = useState(roomInitialState); const [room, setRoom] = useState(roomInitialState);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [editRoomId, setEditRoomId] = useState(""); const [editRoomId, setEditRoomId] = useState("");
const api = useApi();
// TODO seems to be no setPage calls // TODO seems to be no setPage calls
const [page, setPage] = useState<number>(1); const [page, setPage] = useState<number>(1);
const { loading, response, refetch } = useRoomList(PaginationPage(page)); const { loading, response, refetch } = useRoomList(PaginationPage(page));
const [streams, setStreams] = useState<Stream[]>([]);
const [topics, setTopics] = useState<Topic[]>([]);
const [nameError, setNameError] = useState(""); const [nameError, setNameError] = useState("");
const [linkCopied, setLinkCopied] = useState(""); const [linkCopied, setLinkCopied] = useState("");
interface Stream { const [selectedStreamId, setSelectedStreamId] = useState<number | null>(null);
stream_id: number;
name: string;
}
// React Query hooks
const createRoomMutation = useRoomCreate();
const updateRoomMutation = useRoomUpdate();
const deleteRoomMutation = useRoomDelete();
const { data: streams = [] } = useZulipStreams();
const { data: topics = [] } = useZulipTopics(selectedStreamId);
interface Topic { interface Topic {
name: string; name: string;
} }
// Update selected stream ID when zulip stream changes
useEffect(() => { useEffect(() => {
const fetchZulipStreams = async () => { if (room.zulipStream && streams.length > 0) {
if (!api) return; const selectedStream = streams.find(
(s: any) => s.name === room.zulipStream,
try { );
const response = await api.v1ZulipGetStreams();
setStreams(response);
} catch (error) {
console.error("Error fetching Zulip streams:", error);
}
};
if (room.zulipAutoPost) {
fetchZulipStreams();
}
}, [room.zulipAutoPost, !api]);
useEffect(() => {
const fetchZulipTopics = async () => {
if (!api || !room.zulipStream) return;
try {
const selectedStream = streams.find((s) => s.name === room.zulipStream);
if (selectedStream) { if (selectedStream) {
const response = await api.v1ZulipGetTopics({ setSelectedStreamId((selectedStream as any).stream_id);
streamId: selectedStream.stream_id,
});
setTopics(response);
} }
} catch (error) { } else {
console.error("Error fetching Zulip topics:", error); setSelectedStreamId(null);
} }
}; }, [room.zulipStream, streams]);
fetchZulipTopics(); const streamOptions: SelectOption[] = streams.map((stream: any) => {
}, [room.zulipStream, streams, api]);
const streamOptions: SelectOption[] = streams.map((stream) => {
return { label: stream.name, value: stream.name }; return { label: stream.name, value: stream.name };
}); });
const topicOptions: SelectOption[] = topics.map((topic) => ({ const topicOptions: SelectOption[] = topics.map((topic: any) => ({
label: topic.name, label: topic.name,
value: topic.name, value: topic.name,
})); }));
@@ -175,13 +159,15 @@ export default function RoomsList() {
}; };
if (isEditing) { if (isEditing) {
await api?.v1RoomsUpdate({ await updateRoomMutation.mutateAsync({
roomId: editRoomId, params: {
requestBody: roomData, path: { room_id: editRoomId },
},
body: roomData,
}); });
} else { } else {
await api?.v1RoomsCreate({ await createRoomMutation.mutateAsync({
requestBody: roomData, body: roomData,
}); });
} }
@@ -226,8 +212,10 @@ export default function RoomsList() {
const handleDeleteRoom = async (roomId: string) => { const handleDeleteRoom = async (roomId: string) => {
try { try {
await api?.v1RoomsDelete({ await deleteRoomMutation.mutateAsync({
roomId, params: {
path: { room_id: roomId },
},
}); });
refetch(); refetch();
} catch (err) { } catch (err) {

View File

@@ -19,7 +19,7 @@ import {
createListCollection, createListCollection,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { LuShare2 } from "react-icons/lu"; import { LuShare2 } from "react-icons/lu";
import useApi from "../../lib/useApi"; import { useTranscriptUpdate } from "../../lib/api-hooks";
import useSessionUser from "../../lib/useSessionUser"; import useSessionUser from "../../lib/useSessionUser";
import { CustomSession } from "../../lib/types"; import { CustomSession } from "../../lib/types";
import ShareLink from "./shareLink"; import ShareLink from "./shareLink";
@@ -54,12 +54,9 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
); );
const [shareLoading, setShareLoading] = useState(false); const [shareLoading, setShareLoading] = useState(false);
const requireLogin = featureEnabled("requireLogin"); const requireLogin = featureEnabled("requireLogin");
const api = useApi(); const updateTranscriptMutation = useTranscriptUpdate();
const updateShareMode = async (selectedValue: string) => { const updateShareMode = async (selectedValue: string) => {
if (!api)
throw new Error("ShareLink's API should always be ready at this point");
const selectedOption = shareOptionsData.find( const selectedOption = shareOptionsData.find(
(option) => option.value === selectedValue, (option) => option.value === selectedValue,
); );
@@ -71,16 +68,23 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
share_mode: selectedValue as "public" | "semi-private" | "private", share_mode: selectedValue as "public" | "semi-private" | "private",
}; };
const updatedTranscript = await api.v1TranscriptUpdate({ try {
transcriptId: props.transcriptResponse.id, const updatedTranscript = await updateTranscriptMutation.mutateAsync({
requestBody, params: {
path: { transcript_id: props.transcriptResponse.id },
},
body: requestBody,
}); });
setShareMode( setShareMode(
shareOptionsData.find( shareOptionsData.find(
(option) => option.value === updatedTranscript.share_mode, (option) => option.value === (updatedTranscript as any).share_mode,
) || shareOptionsData[0], ) || shareOptionsData[0],
); );
} catch (err) {
console.error("Failed to update share mode:", err);
} finally {
setShareLoading(false); setShareLoading(false);
}
}; };
const userId = useSessionUser().id; const userId = useSessionUser().id;

View File

@@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { UpdateTranscript } from "../../lib/api-types"; import { UpdateTranscript } from "../../lib/api-types";
import useApi from "../../lib/useApi"; import { useTranscriptUpdate } from "../../lib/api-hooks";
import { Heading, IconButton, Input, Flex, Spacer } from "@chakra-ui/react"; import { Heading, IconButton, Input, Flex, Spacer } from "@chakra-ui/react";
import { LuPen } from "react-icons/lu"; import { LuPen } from "react-icons/lu";
@@ -14,24 +14,27 @@ const TranscriptTitle = (props: TranscriptTitle) => {
const [displayedTitle, setDisplayedTitle] = useState(props.title); const [displayedTitle, setDisplayedTitle] = useState(props.title);
const [preEditTitle, setPreEditTitle] = useState(props.title); const [preEditTitle, setPreEditTitle] = useState(props.title);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const api = useApi(); const updateTranscriptMutation = useTranscriptUpdate();
const updateTitle = async (newTitle: string, transcriptId: string) => { const updateTitle = async (newTitle: string, transcriptId: string) => {
if (!api) return;
try { try {
const requestBody: UpdateTranscript = { const requestBody: UpdateTranscript = {
title: newTitle, title: newTitle,
}; };
const updatedTranscript = await api?.v1TranscriptUpdate({ await updateTranscriptMutation.mutateAsync({
transcriptId, params: {
requestBody, path: { transcript_id: transcriptId },
},
body: requestBody,
}); });
if (props.onUpdate) { if (props.onUpdate) {
props.onUpdate(newTitle); props.onUpdate(newTitle);
} }
console.log("Updated transcript:", updatedTranscript); console.log("Updated transcript title:", newTitle);
} catch (err) { } catch (err) {
console.error("Failed to update transcript:", err); console.error("Failed to update transcript:", err);
// Revert title on error
setDisplayedTitle(preEditTitle);
} }
}; };

View File

@@ -193,3 +193,322 @@ export function useZulipTopics(streamId: number | null) {
}, },
); );
} }
// Transcript mutations
export function useTranscriptUpdate() {
const { setError } = useError();
const queryClient = useQueryClient();
return $api.useMutation("patch", "/v1/transcripts/{transcript_id}", {
onSuccess: (data, variables) => {
// Invalidate and refetch transcript data
queryClient.invalidateQueries({
queryKey: $api.queryOptions("get", "/v1/transcripts/{transcript_id}", {
params: {
path: { transcript_id: variables.params.path.transcript_id },
},
}).queryKey,
});
},
onError: (error) => {
setError(error as Error, "There was an error updating the transcript");
},
});
}
export function useTranscriptPostToZulip() {
const { setError } = useError();
return $api.useMutation("post", "/v1/transcripts/{transcript_id}/zulip", {
onError: (error) => {
setError(error as Error, "There was an error posting to Zulip");
},
});
}
export function useTranscriptUploadAudio() {
const { setError } = useError();
const queryClient = useQueryClient();
return $api.useMutation(
"post",
"/v1/transcripts/{transcript_id}/record/upload",
{
onSuccess: (data, variables) => {
// Invalidate transcript to refresh status
queryClient.invalidateQueries({
queryKey: $api.queryOptions(
"get",
"/v1/transcripts/{transcript_id}",
{
params: {
path: { transcript_id: variables.params.path.transcript_id },
},
},
).queryKey,
});
},
onError: (error) => {
setError(error as Error, "There was an error uploading the audio file");
},
},
);
}
// Transcript queries
export function useTranscriptWaveform(transcriptId: string | null) {
const { setError } = useError();
return $api.useQuery(
"get",
"/v1/transcripts/{transcript_id}/audio/waveform",
{
params: {
path: { transcript_id: transcriptId || "" },
},
},
{
enabled: !!transcriptId,
onError: (error) => {
setError(error as Error, "There was an error fetching the waveform");
},
},
);
}
export function useTranscriptMP3(transcriptId: string | null) {
const { setError } = useError();
return $api.useQuery(
"get",
"/v1/transcripts/{transcript_id}/audio/mp3",
{
params: {
path: { transcript_id: transcriptId || "" },
},
},
{
enabled: !!transcriptId,
onError: (error) => {
setError(error as Error, "There was an error fetching the MP3");
},
},
);
}
export function useTranscriptTopics(transcriptId: string | null) {
const { setError } = useError();
return $api.useQuery(
"get",
"/v1/transcripts/{transcript_id}/topics",
{
params: {
path: { transcript_id: transcriptId || "" },
},
},
{
enabled: !!transcriptId,
onError: (error) => {
setError(error as Error, "There was an error fetching topics");
},
},
);
}
export function useTranscriptTopicsWithWords(transcriptId: string | null) {
const { setError } = useError();
return $api.useQuery(
"get",
"/v1/transcripts/{transcript_id}/topics/with-words",
{
params: {
path: { transcript_id: transcriptId || "" },
},
},
{
enabled: !!transcriptId,
onError: (error) => {
setError(
error as Error,
"There was an error fetching topics with words",
);
},
},
);
}
// Participant operations
export function useTranscriptParticipants(transcriptId: string | null) {
const { setError } = useError();
return $api.useQuery(
"get",
"/v1/transcripts/{transcript_id}/participants",
{
params: {
path: { transcript_id: transcriptId || "" },
},
},
{
enabled: !!transcriptId,
onError: (error) => {
setError(error as Error, "There was an error fetching participants");
},
},
);
}
export function useTranscriptParticipantUpdate() {
const { setError } = useError();
const queryClient = useQueryClient();
return $api.useMutation(
"patch",
"/v1/transcripts/{transcript_id}/participants/{participant_id}",
{
onSuccess: (data, variables) => {
// Invalidate participants list
queryClient.invalidateQueries({
queryKey: $api.queryOptions(
"get",
"/v1/transcripts/{transcript_id}/participants",
{
params: {
path: { transcript_id: variables.params.path.transcript_id },
},
},
).queryKey,
});
},
onError: (error) => {
setError(error as Error, "There was an error updating the participant");
},
},
);
}
export function useTranscriptSpeakerAssign() {
const { setError } = useError();
const queryClient = useQueryClient();
return $api.useMutation(
"post",
"/v1/transcripts/{transcript_id}/speaker/assign",
{
onSuccess: (data, variables) => {
// Invalidate transcript and participants
queryClient.invalidateQueries({
queryKey: $api.queryOptions(
"get",
"/v1/transcripts/{transcript_id}",
{
params: {
path: { transcript_id: variables.params.path.transcript_id },
},
},
).queryKey,
});
queryClient.invalidateQueries({
queryKey: $api.queryOptions(
"get",
"/v1/transcripts/{transcript_id}/participants",
{
params: {
path: { transcript_id: variables.params.path.transcript_id },
},
},
).queryKey,
});
},
onError: (error) => {
setError(error as Error, "There was an error assigning the speaker");
},
},
);
}
export function useTranscriptSpeakerMerge() {
const { setError } = useError();
const queryClient = useQueryClient();
return $api.useMutation(
"post",
"/v1/transcripts/{transcript_id}/speaker/merge",
{
onSuccess: (data, variables) => {
// Invalidate transcript and participants
queryClient.invalidateQueries({
queryKey: $api.queryOptions(
"get",
"/v1/transcripts/{transcript_id}",
{
params: {
path: { transcript_id: variables.params.path.transcript_id },
},
},
).queryKey,
});
queryClient.invalidateQueries({
queryKey: $api.queryOptions(
"get",
"/v1/transcripts/{transcript_id}/participants",
{
params: {
path: { transcript_id: variables.params.path.transcript_id },
},
},
).queryKey,
});
},
onError: (error) => {
setError(error as Error, "There was an error merging speakers");
},
},
);
}
// Meeting operations
export function useMeetingAudioConsent() {
const { setError } = useError();
return $api.useMutation("post", "/v1/meetings/{meeting_id}/consent", {
onError: (error) => {
setError(error as Error, "There was an error recording consent");
},
});
}
// WebRTC operations
export function useTranscriptWebRTC() {
const { setError } = useError();
return $api.useMutation(
"post",
"/v1/transcripts/{transcript_id}/record/webrtc",
{
onError: (error) => {
setError(error as Error, "There was an error with WebRTC connection");
},
},
);
}
// Transcript creation
export function useTranscriptCreate() {
const { setError } = useError();
const queryClient = useQueryClient();
return $api.useMutation("post", "/v1/transcripts", {
onSuccess: () => {
// Invalidate transcripts list
queryClient.invalidateQueries({
queryKey: $api.queryOptions("get", "/v1/transcripts/search").queryKey,
});
},
onError: (error) => {
setError(error as Error, "There was an error creating the transcript");
},
});
}