Repository: justaddcl/sanbi
Pull request #58: [SWY-105] Add favorite song action
Author: login: justaddcl
Created: 2025-04-13T14:31:09Z | Merged: 2025-04-13T22:21:49Z

## Background
This PR is to add the ability for a user to favorite/unfavorite a song on the song details page.

| Song not favorited | Song is favorited | 
| ------------------ | ----------------- |
| ![SWY-105__after--not-favorite](https://github.com/user-attachments/assets/64c4e135-3339-429a-a4c3-e5daee7d3033) | ![SWY-105__after--favorite](https://github.com/user-attachments/assets/687045b7-49c0-45b3-8ee2-b81854c31d26) |

## What's changed
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

- **New Features**
  - Users can now mark or unmark songs as favorites directly from the song details page, with immediate visual feedback and notifications.
- **Chores**
  - Enhanced the system to record the timestamp when a song is favorited.
  - Updated sample song data to support the new favorite tracking functionality, including a new property for favorite status.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

## How to test
### Test Steps

1. **Navigate to a Song Details Page**
   - Open any existing song's details page
   - Verify the heart icon button is visible in the top-right area of the page (only visible on medium screens and above, "md:flex")

2. **Check Initial State**
   - Observe the heart icon's appearance:
     - If the song is not favorited, the heart icon should appear as an outline (regular weight)
     - If the song is already favorited, the heart icon should appear filled

3. **Toggle Favorite Status: Favoriting a Song**
   - If the song is not favorited (outline heart):
     - Click the heart button
     - Verify a loading toast appears with the message "Favoriting song..."
     - Once completed, verify a success toast appears with the message "Song favorited"
     - Confirm the heart icon changes from outline to filled
  
4. **Toggle Favorite Status: Unfavoriting a Song**
   - If the song is favorited (filled heart):
     - Click the heart button
     - Verify a loading toast appears with the message "Unfavoriting song..."
     - Once completed, verify a success toast appears with the message "Song unfavorited"
     - Confirm the heart icon changes from filled to outline

5. **Error Handling**
   - If possible, simulate an error condition (this may require network throttling or disconnection)
   - Click the heart button
   - Verify an error toast appears with appropriate error message

6. **Disabled State**
   - Rapidly click the heart button
   - Verify the button becomes disabled during the pending state of the mutation


Diff:
diff --git a/src/lib/types/zod.ts b/src/lib/types/zod.ts
index 51ddeb88..a09fc0b7 100644
--- a/src/lib/types/zod.ts
+++ b/src/lib/types/zod.ts
@@ -103,6 +103,9 @@ export const songUpdateNameSchema = songIdSchema.extend({
 export const songUpdatePreferredKeySchema = songIdSchema.extend({
   preferredKey: z.enum(songKeys),
 });
+export const songUpdateFavoriteSchema = songIdSchema.extend({
+  isFavorite: z.boolean(),
+});
 
 /**
  * Set section type schemas
diff --git a/src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx b/src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx
index 84d5123d..55d57f4b 100644
--- a/src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx
+++ b/src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx
@@ -1,14 +1,17 @@
 "use client";
 
+import { api } from "@/trpc/react";
 import { HStack } from "@components/HStack";
 import { Button } from "@components/ui/button";
+import { type UserData } from "@lib/types/api";
 import { SongActionsMenu } from "@modules/songs/components/SongActionsMenu";
 import { Heart, Plus } from "@phosphor-icons/react";
 import { type AppRouter } from "@server/api/root";
 import { type inferProcedureOutput } from "@trpc/server";
+import { useRouter } from "next/navigation";
 import React, { useState } from "react";
-import { SongDetailsPageName } from "../SongDetailsPageSongName/SongDetailsPageSongName";
-import { type UserData } from "@lib/types/api";
+import { toast } from "sonner";
+import { SongDetailsPageName } from "@modules/songs/components";
 
 export type SongDetailsPageHeaderProps = {
   song: inferProcedureOutput<AppRouter["song"]["get"]>;
@@ -21,6 +24,41 @@ export const SongDetailsPageHeader: React.FC<SongDetailsPageHeaderProps> = ({
 }) => {
   const [isEditingName, setIsEditingName] = useState<boolean>(false);
 
+  const router = useRouter();
+
+  const updateSongFavoriteStatusMutation =
+    api.song.updateFavoriteStatus.useMutation();
+
+  const updateSongFavoriteStatus = () => {
+    const toastId = toast.loading(
+      `${song.favoritedAt ? "Unfavoriting" : "Favoriting"} song...`,
+    );
+    // TODO: for future enhancement - add optimistic update?
+
+    updateSongFavoriteStatusMutation.mutate(
+      {
+        organizationId: userMembership.organizationId,
+        songId: song.id,
+        isFavorite: !song.favoritedAt,
+      },
+      {
+        onSuccess() {
+          toast.success(
+            `Song ${song.favoritedAt ? "unfavorited" : "favorited"}`,
+            { id: toastId },
+          );
+          router.refresh();
+        },
+        onError(updateError) {
+          toast.error(
+            `Song could not be ${song.favoritedAt ? "unfavorited" : "favorited"}: ${updateError.message}`,
+            { id: toastId },
+          );
+        },
+      },
+    );
+  };
+
   return (
     <HStack className="justify-between gap-4">
       <SongDetailsPageName
@@ -30,8 +68,13 @@ export const SongDetailsPageHeader: React.FC<SongDetailsPageHeaderProps> = ({
         setIsEditing={setIsEditingName}
       />
       <HStack className="items-start gap-2">
-        <Button variant="outline" className="hidden md:flex">
-          <Heart />
+        <Button
+          variant="outline"
+          onClick={updateSongFavoriteStatus}
+          disabled={updateSongFavoriteStatusMutation.isPending}
+          size="sm"
+        >
+          <Heart weight={song.favoritedAt ? "fill" : "regular"} />
         </Button>
         <Button className="hidden md:flex">
           <Plus /> Add to a set
diff --git a/src/server/api/routers/song.ts b/src/server/api/routers/song.ts
index 925655d6..c4cad1b6 100644
--- a/src/server/api/routers/song.ts
+++ b/src/server/api/routers/song.ts
@@ -7,6 +7,7 @@ import {
   searchSongSchema,
   songGetLastPlayInstanceSchema,
   songGetPlayHistorySchema,
+  songUpdateFavoriteSchema,
   songUpdateNameSchema,
   songUpdatePreferredKeySchema,
   unarchiveSongSchema,
@@ -121,6 +122,7 @@ export const songRouter = createTRPCRouter({
         notes,
         isArchived,
         createdBy,
+        favoritedAt: null,
       };
 
       console.log(`🤖 - [song/create] - new song`, newSong);
@@ -504,4 +506,55 @@ export const songRouter = createTRPCRouter({
         };
       });
     }),
+
+  updateFavoriteStatus: organizationProcedure
+    .input(songUpdateFavoriteSchema)
+    .mutation(async ({ ctx, input }) => {
+      console.log(
+        `🤖 - [song/updateFavoriteStatus] - attempting to update favorite status for ${input.songId}:`,
+        { mutationInput: { ...input } },
+      );
+
+      return await ctx.db.transaction(async (updateTransaction) => {
+        const songToUpdate = await updateTransaction.query.songs.findFirst({
+          where: eq(songs.id, input.songId),
+        });
+
+        if (!songToUpdate) {
+          console.error(
+            `🤖 - [song/updateFavoriteStatus] - could not find song ${input.songId}`,
+          );
+          throw new TRPCError({
+            code: "NOT_FOUND",
+            message: "Could not find song",
+          });
+        }
+
+        if (
+          songToUpdate.organizationId !== ctx.user.membership.organizationId
+        ) {
+          console.error(
+            `🤖 - [song/updateFavoriteStatus] - user ${ctx.user.id} is not authorized to update song ${input.songId}`,
+          );
+          throw new TRPCError({
+            code: "FORBIDDEN",
+            message: "User is not authorized to update song",
+          });
+        }
+
+        const favoritedAt = input.isFavorite ? new Date() : null;
+
+        const [updatedSong] = await updateTransaction
+          .update(songs)
+          .set({ favoritedAt })
+          .where(eq(songs.id, input.songId))
+          .returning();
+
+        return {
+          success: true,
+          updatedSong,
+          mutationInput: { ...input },
+        };
+      });
+    }),
 });
diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts
index 47c82257..55ad8259 100644
--- a/src/server/db/schema.ts
+++ b/src/server/db/schema.ts
@@ -149,6 +149,7 @@ export const songs = createTable(
       .references(() => organizations.id)
       .notNull(),
     isArchived: boolean("is_archived").notNull(),
+    favoritedAt: timestamp("favorited_at"),
     updatedAt,
   },
   (songsTable) => {
diff --git a/src/server/db/seed.ts b/src/server/db/seed.ts
index e7f61cec..a9c32f6b 100644
--- a/src/server/db/seed.ts
+++ b/src/server/db/seed.ts
@@ -201,6 +201,7 @@ const seed = async () => {
       createdBy: user.id,
       organizationId,
       isArchived: false,
+      favoritedAt: null,
     },
     {
       name: "Such An Awesome God",
@@ -208,6 +209,7 @@ const seed = async () => {
       createdBy: user.id,
       organizationId,
       isArchived: false,
+      favoritedAt: null,
     },
     {
       name: "I Love You Lord/What A Beautiful Name (mash up)",
@@ -217,6 +219,7 @@ const seed = async () => {
       createdBy: user.id,
       organizationId,
       isArchived: false,
+      favoritedAt: null,
     },
     {
       name: "Refuge",
@@ -224,6 +227,7 @@ const seed = async () => {
       createdBy: user.id,
       organizationId,
       isArchived: false,
+      favoritedAt: null,
     },
     {
       name: "Son Of Suffering",
@@ -232,6 +236,7 @@ const seed = async () => {
       createdBy: user.id,
       organizationId,
       isArchived: false,
+      favoritedAt: null,
     },
     {
       name: "Draw Me Close To You",
@@ -239,6 +244,7 @@ const seed = async () => {
       createdBy: user.id,
       organizationId,
       isArchived: false,
+      favoritedAt: null,
     },
     {
       name: "Romans 2:4",
@@ -246,6 +252,7 @@ const seed = async () => {
       createdBy: user.id,
       organizationId,
       isArchived: false,
+      favoritedAt: null,
     },
     {
       name: "Only Jesus",
@@ -254,6 +261,7 @@ const seed = async () => {
       createdBy: user.id,
       organizationId,
       isArchived: false,
+      favoritedAt: null,
     },
     {
       name: "God Over Everything",
@@ -261,6 +269,7 @@ const seed = async () => {
       createdBy: user.id,
       organizationId,
       isArchived: false,
+      favoritedAt: null,
     },
   ];
 


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 102a23f048c512f3ca5efe5072dd8eb1878da55b and 64097917621ac7f413a1ad57937801f55815ad04.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `src/lib/types/zod.ts` (1 hunks)
* `src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx` (3 hunks)
* `src/server/api/routers/song.ts` (3 hunks)
* `src/server/db/schema.ts` (1 hunks)
* `src/server/db/seed.ts` (4 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>src/server/api/routers/song.ts (3)</summary><blockquote>

<details>
<summary>src/server/api/trpc.ts (1)</summary>

* `organizationProcedure` (116-162)

</details>
<details>
<summary>src/lib/types/zod.ts (1)</summary>

* `songUpdateFavoriteSchema` (106-108)

</details>
<details>
<summary>src/server/db/schema.ts (1)</summary>

* `songs` (134-162)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx</summary>

[error] 34-34: Avoid redundant double-negation.

It is not necessary to use double-negation when a value will already be coerced to a boolean.
Unsafe fix: Remove redundant double-negation


(lint/complexity/noExtraBooleanCast)

---

[error] 41-41: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
 Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with


(lint/complexity/noUselessTernary)

---

[error] 46-46: Avoid redundant double-negation.

It is not necessary to use double-negation when a value will already be coerced to a boolean.
Unsafe fix: Remove redundant double-negation


(lint/complexity/noExtraBooleanCast)

---

[error] 53-53: Avoid redundant double-negation.

It is not necessary to use double-negation when a value will already be coerced to a boolean.
Unsafe fix: Remove redundant double-negation


(lint/complexity/noExtraBooleanCast)

---

[error] 76-76: Avoid redundant double-negation.

It is not necessary to use double-negation when a value will already be coerced to a boolean.
Unsafe fix: Remove redundant double-negation


(lint/complexity/noExtraBooleanCast)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>src/server/db/schema.ts (1)</summary>

`152-152`: **Structure looks good!**

The addition of the `favoritedAt` timestamp field to the `songs` table is well-designed. It's appropriately nullable since not all songs will be favorited, and follows the established naming conventions in the schema.

</details>
<details>
<summary>src/lib/types/zod.ts (1)</summary>

`106-108`: **Schema is properly structured**

The new schema correctly extends the base `songIdSchema` and adds the boolean `isFavorite` field. This follows the pattern established with other update schemas in the file.

</details>
<details>
<summary>src/server/db/seed.ts (1)</summary>

`204-204`: **Consistent field initialization across seed data**

The `favoritedAt` field is properly initialized to `null` across all song objects in the seed data, which aligns with the schema definition where the field is nullable.



Also applies to: 212-212, 222-222, 230-230, 239-239, 247-247, 255-255, 264-264, 272-272

</details>
<details>
<summary>src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx (1)</summary>

`29-31`: **Good mutation setup**

The mutation hook is properly initialized using the new API endpoint.

</details>
<details>
<summary>src/server/api/routers/song.ts (4)</summary>

`10-10`: **Import added appropriately**

The `songUpdateFavoriteSchema` import is correctly added in alphabetical order with the other schema imports.

---

`125-125`: **Correct initialization of favoritedAt field**

The `favoritedAt` field is properly initialized to `null` in the `newSong` object, which aligns with the database schema definition where this is an optional timestamp field.

---

`510-559`: **Well-implemented favorite status mutation**

The implementation of the `updateFavoriteStatus` mutation follows the established patterns in the codebase and includes proper error handling, authorization checks, and transaction management.

I particularly like:
- The consistent error handling for both "not found" and "forbidden" cases
- Setting `favoritedAt` to the current date when favoriting and to null when unfavoriting
- The structured response that matches other mutations in this file

---

`545-545`: <details>
<summary>❓ Verification inconclusive</summary>

**Consider adding timezone handling for consistency**

The current date is created without specifying a timezone. For consistency with other timestamps in the application, consider using the same date creation method that's used elsewhere.


```diff
-const favoritedAt = input.isFavorite ? new Date() : null;
+const favoritedAt = input.isFavorite ? new Date() : null;
```

You can verify if other parts of the codebase use a specific format by running:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check how Date objects are created in the codebase
rg "new Date\(" --type ts
```

Length of output: 2281

---



</details>

**Timezone Consistency in Date Creation**

In `src/server/api/routers/song.ts` (line 545), the code assigns:

```ts
const favoritedAt = input.isFavorite ? new Date() : null;
```

Notice that elsewhere in the repository—such as in other parts of this file where dates are compared using `new Date().toLocaleDateString("en-CA")`—a specific locale is applied to enforce a consistent date format. This discrepancy suggests that the timezone handling and date creation approach may be inconsistent across the application.

**Suggested actions:**
- **Determine the intent:** Confirm whether `favoritedAt` is meant to store a full timestamp (in which case a raw `new Date()` might be acceptable if handled later) or if it should adhere to a standardized, localized format (e.g., using `.toLocaleDateString("en-CA")`).
- **Standardize date creation:** If consistency is required, consider refactoring this line to use the same timezone/date formatting method used in other parts of the codebase or centralize date creation via a helper function.
- **Review impact:** Verify that any change preserves the intended behavior for timestamp comparisons and storage across the system.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-13T14:34:35Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between df78f93418250893a9bfa63e68b34871f59b1067 and e7be3422bd770173c4053f0a9a4dd9beed1531d4.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx` (3 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx</summary>

[error] 41-41: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
 Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with


(lint/complexity/noUselessTernary)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx (2)</summary>

`32-59`: **Well-implemented song favorite functionality with proper user feedback**

The implementation of the favorite/unfavorite feature is well-structured with:
- Clear loading states with toast notifications
- Proper success and error handling
- Page refresh to update the UI after a successful mutation

This provides good user feedback throughout the interaction flow.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 41-41: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
 Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with


(lint/complexity/noUselessTernary)

</details>

</details>

---

`70-77`: **Good UI implementation with state-based visual feedback**

The button implementation effectively:
- Shows the current favorite status using the Heart icon weight
- Disables the button during the pending mutation to prevent multiple clicks
- Provides visual feedback to users on the current state

This creates a good user experience with clear visual indicators.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-13T15:25:45Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e7be3422bd770173c4053f0a9a4dd9beed1531d4 and 88d0b44b910e4e1b1af5a9ec661e273228dab31b.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx` (3 hunks)

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx (3)</summary>

`3-14`: **Well-organized imports for the new functionality.**

The imports are appropriately structured for the new favorite song feature, including the necessary TRPC API client, router hook, and toast notifications.

---

`27-30`: **Good approach for state management and API integration.**

The router instance is properly initialized for page refreshes, and the mutation hook is cleanly set up as a separate variable, making the code more readable.

---

`70-76`: **Clean button implementation with appropriate visual feedback.**

The button correctly handles the favorite/unfavorite action with appropriate states:
1. Visual feedback through the Heart icon's weight
2. Disabled state during pending mutations to prevent duplicate requests
3. Consistent sizing with the "outline" variant for proper styling

This implementation aligns well with the PR objectives for adding a favorite song action.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-13T15:44:17Z

Review comments:
diff_hunk: @@ -1,14 +1,17 @@
 "use client";
 
+import { api } from "@/trpc/react";
 import { HStack } from "@components/HStack";
 import { Button } from "@components/ui/button";
+import { type UserData } from "@lib/types/api";
 import { SongActionsMenu } from "@modules/songs/components/SongActionsMenu";
 import { Heart, Plus } from "@phosphor-icons/react";
 import { type AppRouter } from "@server/api/root";
 import { type inferProcedureOutput } from "@trpc/server";
+import { useRouter } from "next/navigation";
 import React, { useState } from "react";
-import { SongDetailsPageName } from "../SongDetailsPageSongName/SongDetailsPageSongName";
-import { type UserData } from "@lib/types/api";
+import { toast } from "sonner";
+import { SongDetailsPageName } from "@modules/songs/components/";
path: src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Check import syntax**

The import statement has an unusual trailing slash. While this might work in your project configuration, it's an uncommon pattern.


```diff
-import { SongDetailsPageName } from "@modules/songs/components/";
+import { SongDetailsPageName } from "@modules/songs/components";
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import { SongDetailsPageName } from "@modules/songs/components";
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit df78f93
created_at: 2025-04-13T14:34:34Z
author_association: NONE
diff_hunk: @@ -30,8 +67,13 @@ export const SongDetailsPageHeader: React.FC<SongDetailsPageHeaderProps> = ({
         setIsEditing={setIsEditingName}
       />
       <HStack className="items-start gap-2">
-        <Button variant="outline" className="hidden md:flex">
-          <Heart />
+        <Button
+          variant="outline"
+          className="hidden md:flex"
+          onClick={updateSongFavoriteStatus}
+          disabled={updateSongFavoriteStatusMutation.isPending}
+        >
+          <Heart weight={!!song.favoritedAt ? "fill" : "regular"} />
         </Button>
path: src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Clean button implementation with appropriate states**

The button correctly displays the favorite status using the Heart icon weight and is disabled during mutation. The `hidden md:flex` class hides the button on mobile, which might be intentional for your mobile layout.



For the Heart icon weight, you can simplify the double negation:

```diff
-<Heart weight={!!song.favoritedAt ? "fill" : "regular"} />
+<Heart weight={song.favoritedAt ? "fill" : "regular"} />
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        <Button
          variant="outline"
          className="hidden md:flex"
          onClick={updateSongFavoriteStatus}
          disabled={updateSongFavoriteStatusMutation.isPending}
        >
          <Heart weight={song.favoritedAt ? "fill" : "regular"} />
        </Button>
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 76-76: Avoid redundant double-negation.

It is not necessary to use double-negation when a value will already be coerced to a boolean.
Unsafe fix: Remove redundant double-negation


(lint/complexity/noExtraBooleanCast)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit df78f93
created_at: 2025-04-13T14:34:34Z
author_association: NONE
diff_hunk: @@ -21,6 +24,40 @@ export const SongDetailsPageHeader: React.FC<SongDetailsPageHeaderProps> = ({
 }) => {
   const [isEditingName, setIsEditingName] = useState<boolean>(false);
 
+  const router = useRouter();
+
+  const updateSongFavoriteStatusMutation =
+    api.song.updateFavoriteStatus.useMutation();
+
+  const updateSongFavoriteStatus = () => {
+    const toastId = toast.loading(
+      `${!!song.favoritedAt ? "Unfavoriting" : "Favoriting"} song...`,
+    );
+
+    updateSongFavoriteStatusMutation.mutate(
+      {
+        organizationId: userMembership.organizationId,
+        songId: song.id,
+        isFavorite: song.favoritedAt ? false : true,
+      },
+      {
+        onSuccess() {
+          toast.success(
+            `Song ${!!song.favoritedAt ? "unfavorited" : "favorited"}`,
+            { id: toastId },
+          );
+          router.refresh();
+        },
+        onError(updateError) {
+          toast.error(
+            `Song could not be ${!!song.favoritedAt ? "unfavorited" : "favorited"}: ${updateError.message}`,
+            { id: toastId },
+          );
+        },
+      },
+    );
+  };
path: src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Simplify boolean logic and improve error handling**

The function is well-structured with loading, success, and error states, but there are some simplifications that could be made to the boolean logic.


```diff
  const updateSongFavoriteStatus = () => {
    const toastId = toast.loading(
-     `${!!song.favoritedAt ? "Unfavoriting" : "Favoriting"} song...`,
+     `${song.favoritedAt ? "Unfavoriting" : "Favoriting"} song...`,
    );

    updateSongFavoriteStatusMutation.mutate(
      {
        organizationId: userMembership.organizationId,
        songId: song.id,
-       isFavorite: song.favoritedAt ? false : true,
+       isFavorite: !song.favoritedAt,
      },
      {
        onSuccess() {
          toast.success(
-           `Song ${!!song.favoritedAt ? "unfavorited" : "favorited"}`,
+           `Song ${song.favoritedAt ? "unfavorited" : "favorited"}`,
            { id: toastId },
          );
          router.refresh();
        },
        onError(updateError) {
          toast.error(
-           `Song could not be ${!!song.favoritedAt ? "unfavorited" : "favorited"}: ${updateError.message}`,
+           `Song could not be ${song.favoritedAt ? "unfavorited" : "favorited"}: ${updateError.message}`,
            { id: toastId },
          );
        },
      },
    );
  };
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const updateSongFavoriteStatus = () => {
    const toastId = toast.loading(
      `${song.favoritedAt ? "Unfavoriting" : "Favoriting"} song...`,
    );

    updateSongFavoriteStatusMutation.mutate(
      {
        organizationId: userMembership.organizationId,
        songId: song.id,
        isFavorite: !song.favoritedAt,
      },
      {
        onSuccess() {
          toast.success(
            `Song ${song.favoritedAt ? "unfavorited" : "favorited"}`,
            { id: toastId },
          );
          router.refresh();
        },
        onError(updateError) {
          toast.error(
            `Song could not be ${song.favoritedAt ? "unfavorited" : "favorited"}: ${updateError.message}`,
            { id: toastId },
          );
        },
      },
    );
  };
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 34-34: Avoid redundant double-negation.

It is not necessary to use double-negation when a value will already be coerced to a boolean.
Unsafe fix: Remove redundant double-negation


(lint/complexity/noExtraBooleanCast)

---

[error] 41-41: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
 Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with


(lint/complexity/noUselessTernary)

---

[error] 46-46: Avoid redundant double-negation.

It is not necessary to use double-negation when a value will already be coerced to a boolean.
Unsafe fix: Remove redundant double-negation


(lint/complexity/noExtraBooleanCast)

---

[error] 53-53: Avoid redundant double-negation.

It is not necessary to use double-negation when a value will already be coerced to a boolean.
Unsafe fix: Remove redundant double-negation


(lint/complexity/noExtraBooleanCast)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit df78f93
created_at: 2025-04-13T14:34:34Z
author_association: NONE
diff_hunk: @@ -21,6 +24,40 @@ export const SongDetailsPageHeader: React.FC<SongDetailsPageHeaderProps> = ({
 }) => {
   const [isEditingName, setIsEditingName] = useState<boolean>(false);
 
+  const router = useRouter();
+
+  const updateSongFavoriteStatusMutation =
+    api.song.updateFavoriteStatus.useMutation();
+
+  const updateSongFavoriteStatus = () => {
+    const toastId = toast.loading(
+      `${song.favoritedAt ? "Unfavoriting" : "Favoriting"} song...`,
+    );
+
+    updateSongFavoriteStatusMutation.mutate(
+      {
+        organizationId: userMembership.organizationId,
+        songId: song.id,
+        isFavorite: song.favoritedAt ? false : true,
path: src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Simplify boolean logic for better readability**

The ternary expression can be simplified to improve readability and maintainability.

```diff
-        isFavorite: song.favoritedAt ? false : true,
+        isFavorite: !song.favoritedAt,
```

This change maintains the same logic but is more concise and directly expresses the intent.



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        isFavorite: !song.favoritedAt,
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 41-41: Unnecessary use of boolean literals in conditional expression.

Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
 Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with


(lint/complexity/noUselessTernary)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 88d0b44
created_at: 2025-04-13T15:25:44Z
author_association: NONE
diff_hunk: @@ -21,6 +24,40 @@ export const SongDetailsPageHeader: React.FC<SongDetailsPageHeaderProps> = ({
 }) => {
   const [isEditingName, setIsEditingName] = useState<boolean>(false);
 
+  const router = useRouter();
+
+  const updateSongFavoriteStatusMutation =
+    api.song.updateFavoriteStatus.useMutation();
+
+  const updateSongFavoriteStatus = () => {
+    const toastId = toast.loading(
+      `${song.favoritedAt ? "Unfavoriting" : "Favoriting"} song...`,
+    );
+
+    updateSongFavoriteStatusMutation.mutate(
+      {
+        organizationId: userMembership.organizationId,
+        songId: song.id,
+        isFavorite: !song.favoritedAt,
+      },
+      {
+        onSuccess() {
+          toast.success(
+            `Song ${song.favoritedAt ? "unfavorited" : "favorited"}`,
+            { id: toastId },
+          );
+          router.refresh();
+        },
+        onError(updateError) {
+          toast.error(
+            `Song could not be ${song.favoritedAt ? "unfavorited" : "favorited"}: ${updateError.message}`,
+            { id: toastId },
+          );
+        },
+      },
+    );
+  };
path: src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Well-implemented favorite song functionality with proper user feedback.**

The implementation handles the favorite/unfavorite action with appropriate loading, success, and error states through toast notifications. The page refresh ensures the UI stays in sync with the backend state.

Consider adding optimistic UI updates in the future to improve perceived performance, especially for users with slower connections.


```diff
  const updateSongFavoriteStatus = () => {
    const toastId = toast.loading(
      `${song.favoritedAt ? "Unfavoriting" : "Favoriting"} song...`,
    );
+   // For future enhancement - optimistic update
+   // queryClient.setQueryData(['song', song.id], {
+   //   ...song,
+   //   favoritedAt: song.favoritedAt ? null : new Date().toISOString(),
+   // });

    updateSongFavoriteStatusMutation.mutate(
      /* existing code */
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const updateSongFavoriteStatus = () => {
    const toastId = toast.loading(
      `${song.favoritedAt ? "Unfavoriting" : "Favoriting"} song...`,
    );
    // For future enhancement - optimistic update
    // queryClient.setQueryData(['song', song.id], {
    //   ...song,
    //   favoritedAt: song.favoritedAt ? null : new Date().toISOString(),
    // });
  
    updateSongFavoriteStatusMutation.mutate(
      {
        organizationId: userMembership.organizationId,
        songId: song.id,
        isFavorite: !song.favoritedAt,
      },
      {
        onSuccess() {
          toast.success(
            `Song ${song.favoritedAt ? "unfavorited" : "favorited"}`,
            { id: toastId },
          );
          router.refresh();
        },
        onError(updateError) {
          toast.error(
            `Song could not be ${song.favoritedAt ? "unfavorited" : "favorited"}: ${updateError.message}`,
            { id: toastId },
          );
        },
      },
    );
  };
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-13T15:44:15Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-04-13T14:31:14Z
body: [vc]: #mfkgXdwt3H0LmRSUEoc+svoHzl0qSTL5RJvJs8xXjec=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJzYW5iaSIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJzYW5iaS1naXQtc3d5LTEwNS1mYXZvcml0ZS1zb25nLWFjdGlvbi1qdXN0YWRkY2xzLXByb2plY3RzLnZlcmNlbC5hcHAifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2p1c3RhZGRjbHMtcHJvamVjdHMvc2FuYmkvRzZpQTJKTnM2OXNDRTUzQWZyYkxDdk1DR3VSNiIsInByZXZpZXdVcmwiOiJzYW5iaS1naXQtc3d5LTEwNS1mYXZvcml0ZS1zb25nLWFjdGlvbi1qdXN0YWRkY2xzLXByb2plY3RzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJyb290RGlyZWN0b3J5IjpudWxsfV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **sanbi** | ✅ Ready ([Inspect](https://vercel.com/justaddcls-projects/sanbi/G6iA2JNs69sCE53AfrbLCvMCGuR6)) | [Visit Preview](https://sanbi-git-swy-105-favorite-song-action-justaddcls-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/sanbi-git-swy-105-favorite-song-action-justaddcls-projects.vercel.app?via=pr-comment-feedback-link) | Apr 13, 2025 5:20pm |


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-04-13T14:31:16Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request introduces functionality to update a song’s favorite status. A new Zod schema is added to represent the favorite state, and a corresponding API mutation is implemented. The UI component is updated with a new handler that triggers the mutation and provides user feedback. Additionally, the backend database schema and seed data are modified to include a field for tracking when a song is favorited.

## Changes

| Files | Change Summary |
|-------|----------------|
| `src/lib/types/zod.ts`, `src/server/api/routers/song.ts` | Added new schema `songUpdateFavoriteSchema` and implemented the `updateFavoriteStatus` mutation to handle song favorite status updates. |
| `src/modules/songs/components/SongDetailsPageHeader/…/SongDetailsPageHeader.tsx` | Integrated a new function and mutation for updating the song’s favorite status in the UI with loading and toast notifications and conditional icon rendering. |
| `src/server/db/schema.ts`, `src/server/db/seed.ts` | Introduced the `favoritedAt` field to the songs table schema and seed data to track when a song is marked as favorite. |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant UI as SongDetailsPageHeader
    participant API as songRouter
    participant DB as Database

    UI->>API: Trigger updateSongFavoriteStatus(input)
    API->>DB: Retrieve song by songId
    DB-->>API: Return song data
    alt Song exists & Authorized
        API->>DB: Update favoritedAt (set timestamp or null)
        DB-->>API: Return updated song
        API-->>UI: Success response
    else Error Case
        API-->>UI: Return error (NOT_FOUND/FORBIDDEN)
    end
    UI->>UI: Display toast notification and refresh page
```

## Assessment against linked issues

| Objective                                     | Addressed | Explanation |
|-----------------------------------------------|:---------:|-------------|
| SWY-105: Add/remove song from favorites       | ✅        |             |

## Poem
> I'm a hopping rabbit with a code-filled cheer,  
> Updating favorites, now the songs are dear.  
> Carrots and bytes blend in a joyful array,  
> As the heart icon beats in a marvelous way.  
> With every hop, the code sings—hip, hip, hooray!

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡💬 Agentic Chat (Pro Plan, General Availability)</summary>
> 
> - We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 88d0b44b910e4e1b1af5a9ec661e273228dab31b and a7fcc0a6bccd207ab6ada99e1b3ba5164facbbf1.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx` (3 hunks)

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx (3)</summary>

`3-3`: **Well-structured imports for the new functionality**

The imports are correctly added to support the new favorite song feature:
- TRPC API client for mutations
- Next.js router for refreshing after state changes
- Toast notifications for user feedback
- Type definitions for proper type safety




Also applies to: 6-6, 11-11, 13-14

---

`27-31`: **Good setup of React hooks for the favorite functionality**

The component properly sets up the necessary hooks:
- Router hook for refreshing the page after state changes
- TRPC mutation hook for updating the favorite status

---

`71-78`: **Clean button implementation with appropriate states**

The button is well-implemented:
- Uses appropriate styling with the outline variant
- Correctly toggles the heart icon based on favorite status
- Properly disables during mutation to prevent multiple submissions
- Uses the small size for consistency with the design

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=justaddcl/sanbi&utm_content=58):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: justaddcl
created_at: 2025-04-13T14:35:14Z
body: @coderabbitai How would you suggest I test the changes in this PR? Please make sure that the instructions are appropriately thorough, but are only kept to the scope of the changes in this PR and where it can be tested by looking or interacting with the UI.
author_association: OWNER
user: login: coderabbitai[bot]
created_at: 2025-04-13T14:38:23Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
> [!TIP]
> For best results, initiate chat on the files or code changes.

<details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Looking at the SongDetailsPageHeader component to get more details about the favorite button implementation
cat src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx
```

Length of output: 2872

---
</details>

@justaddcl Here are thorough testing instructions for the favorite song action changes in this PR:

## Testing Instructions for Song Favorite Feature

### Prerequisites
- Access to the application with a valid user account
- Access to a song details page

### Test Steps

1. **Navigate to a Song Details Page**
   - Open any existing song's details page
   - Verify the heart icon button is visible in the top-right area of the page (only visible on medium screens and above, "md:flex")

2. **Check Initial State**
   - Observe the heart icon's appearance:
     - If the song is not favorited, the heart icon should appear as an outline (regular weight)
     - If the song is already favorited, the heart icon should appear filled

3. **Toggle Favorite Status: Favoriting a Song**
   - If the song is not favorited (outline heart):
     - Click the heart button
     - Verify a loading toast appears with the message "Favoriting song..."
     - Once completed, verify a success toast appears with the message "Song favorited"
     - Confirm the heart icon changes from outline to filled
  
4. **Toggle Favorite Status: Unfavoriting a Song**
   - If the song is favorited (filled heart):
     - Click the heart button
     - Verify a loading toast appears with the message "Unfavoriting song..."
     - Once completed, verify a success toast appears with the message "Song unfavorited"
     - Confirm the heart icon changes from filled to outline

5. **Error Handling**
   - If possible, simulate an error condition (this may require network throttling or disconnection)
   - Click the heart button
   - Verify an error toast appears with appropriate error message

6. **Disabled State**
   - Rapidly click the heart button
   - Verify the button becomes disabled during the pending state of the mutation

Note: The "Add to a set" button is visible but not part of this PR's functionality.



<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Looking at the SongDetailsPageHeader component to get more details about the favorite button implementation
cat src/modules/songs/components/SongDetailsPage/SongDetailsPageHeader/SongDetailsPageHeader.tsx
```

Length of output: 2872

---



</details>

@justaddcl Here are thorough testing instructions for the favorite song action changes in this PR:

## Testing Instructions for Song Favorite Feature

### Prerequisites
- Access to the application with a valid user account
- Access to a song details page

### Test Steps

1. **Navigate to a Song Details Page**
   - Open any existing song's details page
   - Verify the heart icon button is visible in the top-right area of the page (only visible on medium screens and above, "md:flex")

2. **Check Initial State**
   - Observe the heart icon's appearance:
     - If the song is not favorited, the heart icon should appear as an outline (regular weight)
     - If the song is already favorited, the heart icon should appear filled

3. **Toggle Favorite Status: Favoriting a Song**
   - If the song is not favorited (outline heart):
     - Click the heart button
     - Verify a loading toast appears with the message "Favoriting song..."
     - Once completed, verify a success toast appears with the message "Song favorited"
     - Confirm the heart icon changes from outline to filled
  
4. **Toggle Favorite Status: Unfavoriting a Song**
   - If the song is favorited (filled heart):
     - Click the heart button
     - Verify a loading toast appears with the message "Unfavoriting song..."
     - Once completed, verify a success toast appears with the message "Song unfavorited"
     - Confirm the heart icon changes from filled to outline

5. **Error Handling**
   - If possible, simulate an error condition (this may require network throttling or disconnection)
   - Click the heart button
   - Verify an error toast appears with appropriate error message

6. **Disabled State**
   - Rapidly click the heart button
   - Verify the button becomes disabled during the pending state of the mutation

Note: The "Add to a set" button is visible but not part of this PR's functionality.</answer></rawResChunk> -->
author_association: NONE