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

View File

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

View File

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