Skip to content

Commit

Permalink
Optomistic memo update (#168)
Browse files Browse the repository at this point in the history
  • Loading branch information
samdatkins authored Dec 24, 2024
1 parent aabf840 commit 9ca0298
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 70 deletions.
88 changes: 88 additions & 0 deletions frontend/src/components/SongPreviewCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useState, useEffect } from "react";
import { Card, Flex, Heading, Icon, Text } from "@chakra-ui/react";
import { FaFlag, FaRegFlag } from "react-icons/fa";
import { toggleSongMemoPending } from "../services/songs";

export default function SongPreviewCard({
entry,
isHighlighted,
onUpdate,
toast,
}) {
const [optimisticMemo, setOptimisticMemo] = useState(entry.song.song_memo);

// Sync local state with incoming props
useEffect(() => {
setOptimisticMemo(entry.song.song_memo);
}, [entry.song.song_memo]);

const handleToggleMemo = async () => {
const previousMemo = optimisticMemo;
const hasSongMemo = optimisticMemo !== null;
const hasPendingSongMemo = hasSongMemo && optimisticMemo.text === "pending";

// Optimistically update the memo
setOptimisticMemo(
hasPendingSongMemo
? null // Remove memo if toggling off
: { text: "pending" } // Add pending memo if toggling on
);

try {
const result = await toggleSongMemoPending(entry.song.id);
if (typeof result === "string") {
throw new Error("Failed to toggle memo");
}
onUpdate(); // Refresh parent data after a successful update
} catch (error) {
// Revert to previous state on failure
setOptimisticMemo(previousMemo);
toast({
title: "Error",
description: "Could not toggle pending memo",
status: "error",
duration: 9000,
isClosable: true,
});
}
};

const hasSongMemo = optimisticMemo !== null;
const hasPendingSongMemo = hasSongMemo && optimisticMemo.text === "pending";
const songMemoText = hasPendingSongMemo
? "Memo pending"
: hasSongMemo
? optimisticMemo.text
: "No memo";

return (
<Card
p="4"
m="4"
borderRadius="lg"
bgColor={!isHighlighted ? "gray.100" : ""}
minH={isHighlighted ? "30vh" : "16vh"}
>
<Flex direction={"row"} justifyContent={"space-between"}>
<Heading size="md" mb="4">
"{entry.song.title}" by {entry.song.artist}
</Heading>
{(!hasSongMemo || hasPendingSongMemo) && (
<Icon
as={hasPendingSongMemo ? FaFlag : FaRegFlag}
color="blue.300"
fontSize={"48px"}
onClick={handleToggleMemo}
/>
)}
</Flex>
{hasSongMemo && !hasPendingSongMemo ? (
<Text fontSize="xl">{songMemoText}</Text>
) : (
<Text fontSize="xl" fontStyle="italic">
{songMemoText}
</Text>
)}
</Card>
);
}
88 changes: 18 additions & 70 deletions frontend/src/components/SongbookPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import {
Card,
Container,
Flex,
Heading,
Skeleton,
Stack,
Text,
useToast,
} from "@chakra-ui/react";
import { getSongbookDetails, toggleSongMemoPending } from "../services/songs";

import { getSongbookDetails } from "../services/songs";
import { useAsync } from "react-async-hook";
import { useParams } from "react-router-dom";
import { useInterval } from "usehooks-ts";
import { Icon } from "@chakra-ui/icons";
import { FaFlag, FaRegFlag } from "react-icons/fa";
import { isSongbookActive } from "../helpers/time";
import SongPreviewCard from "./SongPreviewCard";

const POLL_INTERVAL = 5 * 1000;

export default function SongbookPreview() {
Expand All @@ -25,14 +22,14 @@ export default function SongbookPreview() {
const asyncSongbookDetails = useAsync(getSongbookDetails, [sessionKey], {
setLoading: (state) => ({ ...state, loading: true }),
});
const songbook = asyncSongbookDetails?.result?.data;

const songPreviewList = isSongbookActive(asyncSongbookDetails?.result?.data)
? asyncSongbookDetails?.result?.data?.song_entries.slice(
asyncSongbookDetails?.result?.data?.current_song_position - 1,
asyncSongbookDetails?.result?.data?.current_song_position + 3
const songbook = asyncSongbookDetails?.result?.data;
const songPreviewList = isSongbookActive(songbook)
? songbook?.song_entries.slice(
songbook.current_song_position - 1,
songbook.current_song_position + 3
)
: asyncSongbookDetails?.result?.data.song_entries;
: songbook?.song_entries;

useInterval(() => {
if (!asyncSongbookDetails.loading) {
Expand All @@ -55,64 +52,15 @@ export default function SongbookPreview() {
</Stack>
) : (
<Flex flexDirection="column" justifyContent="space-between">
{(songPreviewList || []).map((entry, idx) => {
const isHighlighted = idx === 0;
const hasSongMemo = entry.song.song_memo !== null;
const hasPendingSongMemo =
hasSongMemo && entry.song.song_memo.text === "pending";
const songMemoText = hasPendingSongMemo
? "Memo pending"
: hasSongMemo
? entry.song.song_memo.text
: "No memo";

return (
<Card
key={entry.id}
p="4"
m="4"
borderRadius="lg"
bgColor={!isHighlighted ? "gray.100" : ""}
height={isHighlighted ? "30vh" : "16vh"}
>
<Flex direction={"row"} justifyContent={"space-between"}>
<Heading size="lg" mb="4">
"{entry.song.title}" by {entry.song.artist}
</Heading>
{(!hasSongMemo || hasPendingSongMemo) && (
<Icon
as={hasPendingSongMemo ? FaFlag : FaRegFlag}
color="blue.300"
fontSize={"48px"}
onClick={async () => {
const result = await toggleSongMemoPending(
entry.song.id
);
if (typeof result === "string") {
toast({
title: "Error",
description: "Could not toggle pending memo",
status: "error",
duration: 9000,
isClosable: true,
});
} else {
asyncSongbookDetails.execute(sessionKey);
}
}}
/>
)}
</Flex>
{hasSongMemo && !hasPendingSongMemo ? (
<Text fontSize="xl">{songMemoText}</Text>
) : (
<Text fontSize="xl" fontStyle="italic">
{songMemoText}
</Text>
)}
</Card>
);
})}
{(songPreviewList || []).map((entry, idx) => (
<SongPreviewCard
key={entry.id}
entry={entry}
isHighlighted={idx === 0}
onUpdate={() => asyncSongbookDetails.execute(sessionKey)} // Refresh data after update
toast={toast}
/>
))}
</Flex>
)}
</Container>
Expand Down

0 comments on commit 9ca0298

Please sign in to comment.