From 00ded5d1c9e5bdf8f49e3fc082cdacc4e3150e1e Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Wed, 11 Oct 2023 00:58:00 -0600 Subject: [PATCH] 3.1.5 --- app/(client)/layout.tsx | 9 +- app/(client)/room/[id]/VoteUI.tsx | 312 +++++++++++++++++++----------- package.json | 18 +- pnpm-lock.yaml | 301 +++++++++++++++------------- 4 files changed, 387 insertions(+), 253 deletions(-) diff --git a/app/(client)/layout.tsx b/app/(client)/layout.tsx index 6972022..242a6c0 100644 --- a/app/(client)/layout.tsx +++ b/app/(client)/layout.tsx @@ -2,6 +2,9 @@ import { AblyProvider } from "ably/react"; import * as Ably from "ably"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const queryClient = new QueryClient(); export default function RootLayout({ children, @@ -12,5 +15,9 @@ export default function RootLayout({ authUrl: "/api/internal/ably", }); - return {children}; + return ( + + {children}{" "} + + ); } diff --git a/app/(client)/room/[id]/VoteUI.tsx b/app/(client)/room/[id]/VoteUI.tsx index 9408a25..5182ea7 100644 --- a/app/(client)/room/[id]/VoteUI.tsx +++ b/app/(client)/room/[id]/VoteUI.tsx @@ -3,8 +3,7 @@ import { EventTypes } from "@/_utils/types"; import Image from "next/image"; import { useEffect, useState } from "react"; - -import { experimental_useOptimistic } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import LoadingIndicator from "@/_components/LoadingIndicator"; import type { PresenceItem, RoomResponse, VoteResponse } from "@/_utils/types"; @@ -29,105 +28,134 @@ import { RiVipCrownFill } from "react-icons/ri"; import NoRoomUI from "./NoRoomUI"; const VoteUI = () => { + // State + // ================================= const params = useParams(); const roomId = params?.id as string; const { user } = useUser(); - const [storyNameText, setStoryNameText] = useState(""); const [roomScale, setRoomScale] = useState(""); const [copied, setCopied] = useState(false); - const [roomFromDb, setRoomFromDb] = useState(); - const [votesFromDb, setVotesFromDb] = useState(undefined); + const queryClient = useQueryClient(); - const [optimisticVotes, setOptimisticVotes] = - experimental_useOptimistic(votesFromDb); + const { data: roomFromDb } = useQuery({ + queryKey: ["room"], + queryFn: getRoomHandler, + }); - const getRoomHandler = () => { - fetch(`/api/internal/room/${roomId}`, { + const { data: votesFromDb } = useQuery({ + queryKey: ["votes"], + queryFn: getVotesHandler, + }); + + const { mutate: setVote } = useMutation({ + mutationFn: setVoteHandler, + // When mutate is called: + onMutate: async (newVote) => { + // Cancel any outgoing refetches + // (so they don't overwrite our optimistic update) + await queryClient.cancelQueries({ queryKey: ["votes"] }); + + // Snapshot the previous value + const previousVotes = queryClient.getQueryData(["votes"]); + + // Optimistically update to the new value + queryClient.setQueryData(["votes"], (old) => + old?.map((vote) => { + if (vote.userId === user?.id) { + return { + ...vote, + value: newVote, + }; + } else { + return vote; + } + }) + ); + + // Return a context object with the snapshotted value + return { previousVotes }; + }, + // If the mutation fails, + // use the context returned from onMutate to roll back + onError: (err, newTodo, context) => { + queryClient.setQueryData(["votes"], context?.previousVotes); + }, + // Always refetch after error or success: + onSettled: () => { + void queryClient.invalidateQueries({ queryKey: ["votes"] }); + }, + }); + + const { mutate: setRoom } = useMutation({ + mutationFn: setRoomHandler, + // When mutate is called: + onMutate: async (data: { + visible: boolean; + reset: boolean; + log: boolean; + }) => { + // Cancel any outgoing refetches + // (so they don't overwrite our optimistic update) + await queryClient.cancelQueries({ queryKey: ["room"] }); + + // Snapshot the previous value + const previousRoom = queryClient.getQueryData(["room"]); + + // Optimistically update to the new value + queryClient.setQueryData(["room"], (old) => { + return old?.created_at || old?.id || old?.userId + ? { + roomName: old?.roomName, + created_at: old?.created_at, + id: old?.id, + userId: old?.userId, + logs: old?.logs, + storyName: storyNameText, + visible: data.visible, + scale: roomScale, + reset: data.reset, + log: data.log, + } + : old; + }); + + // Return a context object with the snapshotted value + return { previousRoom }; + }, + // If the mutation fails, + // use the context returned from onMutate to roll back + onError: (err, newTodo, context) => { + queryClient.setQueryData(["room"], context?.previousRoom); + }, + // Always refetch after error or success: + onSettled: () => { + void queryClient.invalidateQueries({ queryKey: ["room"] }); + }, + }); + + // Handlers + // ================================= + async function getRoomHandler() { + const response = await fetch(`/api/internal/room/${roomId}`, { cache: "no-cache", method: "GET", - }) - .then(async (response) => { - const dbRoom = (await response.json()) as RoomResponse; - setRoomFromDb(dbRoom); - }) - .catch(() => { - setRoomFromDb(null); - }); - }; + }); - const getVotesHandler = async () => { + return (await response.json()) as RoomResponse; + } + + async function getVotesHandler() { const dbVotesResponse = await fetch(`/api/internal/room/${roomId}/votes`, { cache: "no-cache", method: "GET", }); const dbVotes = (await dbVotesResponse.json()) as VoteResponse; - setVotesFromDb(dbVotes); - }; - - useChannel( - { - channelName: `${env.NEXT_PUBLIC_APP_ENV}-${roomId}`, - }, - ({ name }: { name: string }) => { - if (name === EventTypes.ROOM_UPDATE) { - void getVotesHandler(); - void getRoomHandler(); - } else if (name === EventTypes.VOTE_UPDATE) { - void getVotesHandler(); - } - } - ); - - const { presenceData } = usePresence( - `${env.NEXT_PUBLIC_APP_ENV}-${roomId}`, - { - name: (user?.fullName ?? user?.username) || "", - image: user?.imageUrl || "", - client_id: user?.id || "unknown", - isAdmin: isAdmin(user?.publicMetadata), - isVIP: isVIP(user?.publicMetadata), - } - ); - - // Init Story name - useEffect(() => { - if (roomFromDb) { - setStoryNameText(roomFromDb.storyName || ""); - setRoomScale(roomFromDb.scale || "ERROR"); - } else { - void getRoomHandler(); - void getVotesHandler(); - } - }, [roomFromDb, roomId, user]); - - // Helper functions - const getVoteForCurrentUser = () => { - if (roomFromDb) { - return ( - optimisticVotes && - optimisticVotes.find((vote) => vote.userId === user?.id) - ); - } else { - return null; - } - }; - - const setVoteHandler = async (value: string) => { - const newVotes = optimisticVotes?.map((vote) => { - if (vote.userId === user?.id) { - return { - ...vote, - value, - }; - } else { - return vote; - } - }); - - setOptimisticVotes(newVotes); + return dbVotes; + } + async function setVoteHandler(value: string) { if (roomFromDb) { await fetch(`/api/internal/room/${roomId}/vote`, { cache: "no-cache", @@ -137,30 +165,42 @@ const VoteUI = () => { }), }); } - }; + } - const setRoomHandler = async ( - visible: boolean, - reset = false, - log = false - ) => { + async function setRoomHandler(data: { + visible: boolean; + reset: boolean | undefined; + log: boolean | undefined; + }) { if (roomFromDb) { await fetch(`/api/internal/room/${roomId}`, { cache: "no-cache", method: "PUT", body: JSON.stringify({ name: storyNameText, - visible, + visible: data.visible, scale: roomScale, - reset, - log, + reset: data.reset ? data.reset : false, + log: data.log ? data.log : false, }), }); } + } + + // Helpers + // ================================= + const getVoteForCurrentUser = () => { + if (roomFromDb) { + return ( + votesFromDb && votesFromDb.find((vote) => vote.userId === user?.id) + ); + } else { + return null; + } }; const downloadLogs = () => { - if (roomFromDb && optimisticVotes) { + if (roomFromDb && votesFromDb) { const jsonObject = roomFromDb?.logs .map((item) => { return { @@ -169,7 +209,7 @@ const VoteUI = () => { userId: item.userId, roomId: item.roomId, roomName: item.roomName, - storyName: item.storyName, + topicName: item.storyName, scale: item.scale, votes: item.votes, }; @@ -180,9 +220,9 @@ const VoteUI = () => { userId: roomFromDb.userId, roomId: roomFromDb.id, roomName: roomFromDb.roomName, - storyName: storyNameText, + topicName: storyNameText, scale: roomScale, - votes: optimisticVotes.map((vote) => { + votes: votesFromDb.map((vote) => { return { value: vote.value, }; @@ -232,6 +272,45 @@ const VoteUI = () => { } }; + // Hooks + // ================================= + useChannel( + { + channelName: `${env.NEXT_PUBLIC_APP_ENV}-${roomId}`, + }, + ({ name }: { name: string }) => { + if (name === EventTypes.ROOM_UPDATE) { + void queryClient.invalidateQueries({ queryKey: ["votes"] }); + void queryClient.invalidateQueries({ queryKey: ["room"] }); + } else if (name === EventTypes.VOTE_UPDATE) { + void queryClient.invalidateQueries({ queryKey: ["votes"] }); + } + } + ); + + const { presenceData } = usePresence( + `${env.NEXT_PUBLIC_APP_ENV}-${roomId}`, + { + name: (user?.fullName ?? user?.username) || "", + image: user?.imageUrl || "", + client_id: user?.id || "unknown", + isAdmin: isAdmin(user?.publicMetadata), + isVIP: isVIP(user?.publicMetadata), + } + ); + + useEffect(() => { + if (roomFromDb) { + setStoryNameText(roomFromDb.storyName || ""); + setRoomScale(roomFromDb.scale || "ERROR"); + } else { + void getRoomHandler(); + void getVotesHandler(); + } + }, [roomFromDb, roomId, user]); + + // UI + // ================================= // Room is loading if (roomFromDb === undefined) { return ; @@ -260,7 +339,7 @@ const VoteUI = () => {

- Story: {roomFromDb.storyName} + Topic: {roomFromDb.storyName}

    @@ -319,10 +398,10 @@ const VoteUI = () => {

    {roomFromDb && - optimisticVotes && + votesFromDb && voteString( roomFromDb.visible, - optimisticVotes, + votesFromDb, presenceItem.data )} @@ -340,7 +419,7 @@ const VoteUI = () => { ? "btn btn-active btn-primary" : "btn" }`} - onClick={() => void setVoteHandler(scaleItem)} + onClick={() => void setVote(scaleItem)} > {scaleItem} @@ -372,11 +451,11 @@ const VoteUI = () => { }} /> - + { @@ -388,7 +467,11 @@ const VoteUI = () => {
    - {optimisticVotes && + {votesFromDb && (roomFromDb.logs.length > 0 || - optimisticVotes.length > 0) && ( + votesFromDb.length > 0) && (