Repository: deriv-com/champion-trader
Pull request #96: Design discrepancies 6: Favorite MarketSelector Toast Notification
Author: login: aum-deriv
Created: 2025-03-20T12:03:59Z | Merged: 2025-03-20T12:17:16Z

## Summary by Sourcery

Updates the favorite market selector toast notification to match the design specifications. This includes using new icons, adjusting the toast's styling, and ensuring it displays correctly in both light and dark themes.

Enhancements:
- Updates the styling of the favorite market selector toast notification to match the design specifications, including icon updates and theme adjustments.
- Allows passing custom class names to the Toast component.


Diff:
diff --git a/src/components/MarketSelector/MarketSelectorList.tsx b/src/components/MarketSelector/MarketSelectorList.tsx
index fb358ef8..1fdf6377 100644
--- a/src/components/MarketSelector/MarketSelectorList.tsx
+++ b/src/components/MarketSelector/MarketSelectorList.tsx
@@ -1,5 +1,5 @@
 import React, { useState } from "react";
-import { Search, X, Star } from "lucide-react";
+import { Search, X } from "lucide-react";
 import { useBottomSheetStore } from "@/stores/bottomSheetStore";
 import { useTradeStore } from "@/stores/tradeStore";
 import { useMarketStore } from "@/stores/marketStore";
@@ -10,7 +10,7 @@ import { marketData, MarketInfo, marketTitles, marketTypeMap } from "./marketSel
 import { MarketIcon } from "./MarketIcon";
 import { ScrollableTabs } from "@/components/ui/scrollable-tabs";
 import { useDeviceDetection } from "@/hooks/useDeviceDetection";
-import { StandaloneStarRegularIcon } from "@deriv/quill-icons";
+import { StandaloneStarFillIcon, StandaloneStarRegularIcon } from "@deriv/quill-icons";
 
 interface MarketSelectorListProps {
     onDragDown?: () => void;
@@ -21,6 +21,7 @@ export const MarketSelectorList: React.FC<MarketSelectorListProps> = () => {
     const [activeTab, setActiveTab] = useState("all");
     const [searchQuery, setSearchQuery] = useState("");
     const { isMobile } = useDeviceDetection();
+    const { theme } = useMainLayoutStore();
     const [favorites, setFavorites] = useState<Set<string>>(() => {
         const savedFavorites = localStorage.getItem("market-favorites");
         return savedFavorites ? new Set(JSON.parse(savedFavorites)) : new Set();
@@ -42,30 +43,31 @@ export const MarketSelectorList: React.FC<MarketSelectorListProps> = () => {
 
             if (isAdding) {
                 newFavorites.add(symbol);
-                toast({
-                    content: (
-                        <div className="flex items-center gap-3 bg-theme-text text-theme-bg p-4 rounded-lg">
-                            <Star className="w-6 h-6 fill-yellow-400 text-yellow-400" />
-                            <span className="text-base">Added to favourites</span>
-                        </div>
-                    ),
-                    variant: "default",
-                    duration: 2000,
-                    position: "bottom-center",
-                });
             } else {
                 newFavorites.delete(symbol);
-                toast({
-                    content: (
-                        <div className="flex items-center gap-3 bg-theme-text text-theme-bg p-4 rounded-lg">
-                            <Star className="w-6 h-6 fill-yellow-400 text-yellow-400" />
-                            <span className="text-base">Removed from favourites</span>
-                        </div>
-                    ),
-                    duration: 2000,
-                    position: "bottom-center",
-                });
             }
+
+            toast({
+                className: "max-w-full w-[334px]",
+                content: (
+                    <div className="w-full h-12 flex items-center gap-3 bg-theme-text text-theme-bg p-2 pl-4 rounded-lg">
+                        {isAdding ? (
+                            <StandaloneStarFillIcon fill="#F7C60B" iconSize="sm" />
+                        ) : (
+                            <StandaloneStarRegularIcon
+                                fill={theme === "light" ? "#ffffff" : "#000000B8"}
+                                iconSize="sm"
+                            />
+                        )}
+                        <span className="text-sm">
+                            {isAdding ? "Added" : "Removed"} from favourites
+                        </span>
+                    </div>
+                ),
+                duration: 2000,
+                position: "bottom-center",
+            });
+
             return newFavorites;
         });
     };
@@ -179,11 +181,7 @@ export const MarketSelectorList: React.FC<MarketSelectorListProps> = () => {
                 {/* Empty state for favourites tab */}
                 {activeTab === "favourites" && favorites.size === 0 && !searchQuery && (
                     <div className="flex flex-col items-center justify-center h-full min-h-[400px]">
-                        <StandaloneStarRegularIcon
-                            fill="#D1D5DB"
-                            iconSize="2xl"
-                            className=" mb-4"
-                        />
+                        <StandaloneStarFillIcon fill="#D1D5DB" iconSize="2xl" className=" mb-4" />
                         <h3 className="text-lg font-semibold text-gray-500 mb-2">No favourites</h3>
                         <p className="text-sm text-gray-400">
                             Your favourite markets will appear here.
@@ -238,25 +236,27 @@ export const MarketSelectorList: React.FC<MarketSelectorListProps> = () => {
                                                 e.stopPropagation();
                                                 toggleFavorite(market.symbol)(e);
                                             }}
-                                            className={`
-                              ${
-                                  favorites.has(market.symbol)
-                                      ? "text-yellow-400"
-                                      : selectedMarket?.symbol === market.symbol
-                                        ? "text-theme-bg"
-                                        : "text-theme-muted"
-                              }
-                            `}
+                                            data-testid="star-icon"
                                         >
-                                            <Star
-                                                className={`w-5 h-5 ${
-                                                    favorites.has(market.symbol)
-                                                        ? "fill-yellow-400"
-                                                        : selectedMarket?.symbol === market.symbol
-                                                          ? "stroke-theme-bg"
-                                                          : ""
-                                                }`}
-                                            />
+                                            {favorites.has(market.symbol) ? (
+                                                <StandaloneStarFillIcon
+                                                    fill="#F7C60B"
+                                                    iconSize="sm"
+                                                />
+                                            ) : (
+                                                <StandaloneStarRegularIcon
+                                                    fill={
+                                                        selectedMarket?.symbol === market.symbol
+                                                            ? theme === "light"
+                                                                ? "#ffffff"
+                                                                : "#000000B8"
+                                                            : theme === "light"
+                                                              ? "#000000B8"
+                                                              : "#ffffff"
+                                                    }
+                                                    iconSize="sm"
+                                                />
+                                            )}
                                         </button>
                                     </div>
                                 ))}
diff --git a/src/components/MarketSelector/__tests__/MarketSelectorList.test.tsx b/src/components/MarketSelector/__tests__/MarketSelectorList.test.tsx
index 0481b3a2..cdf61e77 100644
--- a/src/components/MarketSelector/__tests__/MarketSelectorList.test.tsx
+++ b/src/components/MarketSelector/__tests__/MarketSelectorList.test.tsx
@@ -76,7 +76,6 @@ jest.mock("../marketSelectorStub", () => ({
 jest.mock("lucide-react", () => ({
     Search: () => <div data-testid="search-icon">Search</div>,
     X: () => <div data-testid="clear-icon">Clear</div>,
-    Star: () => <div data-testid="star-icon">Star</div>,
 }));
 
 // Mock localStorage
@@ -202,7 +201,7 @@ describe("MarketSelectorList", () => {
             it("toggles favorites and updates localStorage", () => {
                 render(<MarketSelectorList />);
 
-                const starButton = screen.getAllByTestId("star-icon")[1].parentElement;
+                const starButton = screen.getAllByTestId("star-icon")[0];
                 fireEvent.click(starButton!);
 
                 expect(mockLocalStorage.setItem).toHaveBeenCalledWith(
@@ -211,7 +210,7 @@ describe("MarketSelectorList", () => {
                 );
                 expect(mockToast).toHaveBeenCalledWith(
                     expect.objectContaining({
-                        variant: "default",
+                        className: "max-w-full w-[334px]",
                         duration: 2000,
                         position: "bottom-center",
                     })
diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx
index 618af15f..c90e97e1 100644
--- a/src/components/ui/toast.tsx
+++ b/src/components/ui/toast.tsx
@@ -5,6 +5,7 @@ type ToastPosition = "bottom-left" | "bottom-center" | "bottom-right" | "top-cen
 type ToastVariant = "success" | "error" | "warning" | "info" | "default";
 
 export interface ToastProps {
+    className?: string;
     content: React.ReactNode;
     variant?: ToastVariant;
     onClose: () => void;
@@ -13,6 +14,7 @@ export interface ToastProps {
 }
 
 export const Toast = ({
+    className,
     content,
     variant,
     onClose,
@@ -30,7 +32,7 @@ export const Toast = ({
     return (
         <div
             className={cn(
-                "fixed z-[99999] px-4 py-3 rounded-md shadow-lg",
+                "fixed z-[99999] rounded-md shadow-lg",
                 "w-[100%] max-w-[320px]", // Set minimum width and allow content to wrap
                 // Position classes
                 {
@@ -43,7 +45,7 @@ export const Toast = ({
                 {
                     "animate-in fade-in slide-in-from-bottom-4": position.startsWith("bottom"),
                     "animate-in fade-in slide-in-from-top-4": position.startsWith("top"),
-                }
+                },
                 // Variant-specific styling
                 // {
                 //     "bg-green-500 text-white": variant === "success",
@@ -51,7 +53,8 @@ export const Toast = ({
                 //     "bg-amber-500 text-white": variant === "warning",
                 //     "bg-blue-500 text-white": variant === "info",
                 //     "bg-gray-700 text-white": variant === "default",
-                // }
+                // },
+                className
             )}
             role="alert"
         >
diff --git a/src/stores/toastStore.tsx b/src/stores/toastStore.tsx
index e189a8ba..aee8ed70 100644
--- a/src/stores/toastStore.tsx
+++ b/src/stores/toastStore.tsx
@@ -7,6 +7,7 @@ type ToastPosition = "bottom-left" | "bottom-center" | "bottom-right" | "top-cen
 type ToastVariant = "success" | "error" | "warning" | "info" | "default";
 
 interface ToastOptions {
+    className?: string;
     content: React.ReactNode;
     variant?: ToastVariant;
     duration?: number;
@@ -35,6 +36,7 @@ export const ToastProvider: FC = (): JSX.Element | null => {
 
     return (
         <Toast
+            className={options.className}
             content={options.content}
             variant={options.variant}
             duration={options.duration}


Reviews:
user: login: sourcery-ai[bot]
body: Hey @aum-deriv - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- Consider using a more specific and descriptive name for the `className` prop in the `ToastProps` interface to avoid potential conflicts.
- The test update seems to target the first star icon, ensure this aligns with the intended functionality and the correct element is being targeted.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟢 **General issues**: all looks good
- 🟢 **Security**: all looks good
- 🟡 **Testing**: 2 issues found
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-20T12:04:58Z

Review comments:
diff_hunk: @@ -202,7 +201,7 @@ describe("MarketSelectorList", () => {
             it("toggles favorites and updates localStorage", () => {
                 render(<MarketSelectorList />);
 
-                const starButton = screen.getAllByTestId("star-icon")[1].parentElement;
+                const starButton = screen.getAllByTestId("star-icon")[0];
path: src/components/MarketSelector/__tests__/MarketSelectorList.test.tsx
user: login: sourcery-ai[bot]
body: **suggestion (testing):** Test the toast content

The test should assert the content of the toast notification to ensure it displays the correct message ('Added to favorites' or 'Removed from favorites'). Additionally, consider testing the icon displayed within the toast.

Suggested implementation:

```typescript
import { render, screen, fireEvent, waitFor } from "@testing-library/react";

```

```typescript
            it("toggles favorites and updates localStorage", async () => {

```

• Confirm that the application shows a toast with a data-testid="toast-icon" for the icon.
• Tailor the regular expression in getByText if the toast messages vary.
created_at: 2025-03-20T12:04:58Z
author_association: NONE
diff_hunk: @@ -211,7 +210,7 @@ describe("MarketSelectorList", () => {
                 );
                 expect(mockToast).toHaveBeenCalledWith(
                     expect.objectContaining({
-                        variant: "default",
+                        className: "max-w-full w-[334px]",
                         duration: 2000,
                         position: "bottom-center",
                     })
path: src/components/MarketSelector/__tests__/MarketSelectorList.test.tsx
user: login: sourcery-ai[bot]
body: **suggestion (testing):** Test different themes

Add tests to verify the toast notification behavior and appearance with different themes (light and dark) to ensure consistent styling.

Suggested implementation:

```typescript
import { render, screen, fireEvent } from "@testing-library/react";
import { ThemeProvider } from "path/to/ThemeProvider"; // adjust the import path as per your codebase

```

```typescript
                expect(mockToast).toHaveBeenCalledWith(
                    expect.objectContaining({
                        className: "max-w-full w-[334px]",
                        duration: 2000,
                        position: "bottom-center",
                    })

```

```typescript
            });

            // New tests for toast notification behavior with different themes
            describe("Toast notification appearance with different themes", () => {
                const themes = [
                    { name: "light", expectedClassName: "max-w-full w-[334px] light-theme-class" },
                    { name: "dark", expectedClassName: "max-w-full w-[334px] dark-theme-class" }
                ];

                themes.forEach(({ name, expectedClassName }) => {
                    it(`should display toast with correct styling in ${name} theme`, () => {
                        render(
                            <ThemeProvider theme={name}>
                                <MarketSelectorList />
                            </ThemeProvider>
                        );
                        const starButton = screen.getAllByTestId("star-icon")[0];
                        fireEvent.click(starButton!);
                        expect(mockToast).toHaveBeenCalledWith(
                            expect.objectContaining({
                                className: expectedClassName,
                                duration: 2000,
                                position: "bottom-center"
                            })
                        );
                    });
                });
            });

```

Ensure that:
1. The "ThemeProvider" used here is imported from your project's theme context or UI library as applicable.
2. The expected classNames ("max-w-full w-[334px] light-theme-class" and "max-w-full w-[334px] dark-theme-class") match the actual styles applied in your codebase. Adjust them if necessary.
3. The tests are placed within the proper context in your file so that they don't conflict with existing tests.
created_at: 2025-03-20T12:04:58Z
author_association: NONE
diff_hunk: @@ -41,31 +43,31 @@ export const MarketSelectorList: React.FC<MarketSelectorListProps> = () => {
 
             if (isAdding) {
                 newFavorites.add(symbol);
-                toast({
-                    content: (
-                        <div className="flex items-center gap-3 bg-theme-text text-theme-bg p-4 rounded-lg">
-                            <Star className="w-6 h-6 fill-yellow-400 text-yellow-400" />
-                            <span className="text-base">Added to favourites</span>
-                        </div>
-                    ),
-                    variant: "default",
-                    duration: 2000,
-                    position: "bottom-center",
-                });
             } else {
                 newFavorites.delete(symbol);
-                toast({
-                    content: (
-                        <div className="flex items-center gap-3 bg-theme-text text-theme-bg p-4 rounded-lg">
-                            <Star className="w-6 h-6 fill-yellow-400 text-yellow-400" />
-                            <span className="text-base">Removed from favourites</span>
-                        </div>
-                    ),
-                    variant: "default",
-                    duration: 2000,
-                    position: "bottom-center",
-                });
             }
path: src/components/MarketSelector/MarketSelectorList.tsx
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider extracting the toast content and icon logic into a separate helper function to improve readability by reducing nested ternary operators in the JSX code.

```markdown
Consider moving the inline toast content and icon logic into separate helper functions or components. This will reduce the nesting of ternary operators in JSX. For example, extract a helper for rendering the toast content:

// New helper function
const renderFavoriteToastContent = (isAdding: boolean, theme: string) => (
  <div className="w-full h-12 flex items-center gap-3 bg-theme-text text-theme-bg p-2 pl-4 rounded-lg">
    {isAdding ? (
      <StandaloneStarFillIcon fill="#F7C60B" iconSize="sm" />
    ) : (
      <StandaloneStarRegularIcon
        fill={theme === "light" ? "#ffffff" : "#000000B8"}
        iconSize="sm"
      />
    )}
    <span className="text-sm">
      {isAdding ? "Added" : "Removed"} from favourites
    </span>
  </div>
);

// In your toggleFavorite method, simplify the toast call:
const toggleFavorite = (symbol: string) => (e: React.MouseEvent) => {
  e.stopPropagation();
  e.preventDefault();
  setFavorites((prev) => {
    const newFavorites = new Set(prev);
    const isAdding = !newFavorites.has(symbol);

    if (isAdding) {
      newFavorites.add(symbol);
    } else {
      newFavorites.delete(symbol);
    }

    toast({
      className: "max-w-full w-[334px]",
      content: renderFavoriteToastContent(isAdding, theme),
      duration: 2000,
      position: "bottom-center",
    });

    return newFavorites;
  });
};
```

Using this approach, all changes are preserved while improving readability by isolating the inline conditional JSX logic.
created_at: 2025-03-20T12:04:58Z
author_association: NONE

PR comments:
user: login: devloai[bot]
created_at: 2025-03-20T12:04:01Z
body: Unable to perform a code review. You have run out of credits 😔
author_association: NONE
user: login: sourcery-ai[bot]
created_at: 2025-03-20T12:04:02Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide by Sourcery

This pull request updates the favorite market selector toast notification to align with the latest design specifications. It replaces the previous toast with a new design that includes updated icons, styling, and logic for handling different themes and favorite states.

_No diagrams generated as the changes look simple and do not need a visual representation._

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Updated the favorite market selector toast notification to match the design specifications. | <ul><li>Replaced the `Star` component with `StandaloneStarFillIcon` and `StandaloneStarRegularIcon` from `@deriv/quill-icons`.</li><li>Adjusted the toast notification's styling to match the design, including icon, text, and background colors.</li><li>Added logic to change the star icon based on whether the market is being added to or removed from favorites.</li><li>Added logic to change the star icon color based on the theme (light or dark).</li><li>Added a `className` prop to the `Toast` component to allow for custom styling.</li><li>Set a fixed width for the toast notification.</li><li>Updated the test to reflect the changes in the toast notification's styling and content.</li></ul> | `src/components/MarketSelector/MarketSelectorList.tsx`<br/>`src/components/ui/toast.tsx`<br/>`src/components/MarketSelector/__tests__/MarketSelectorList.test.tsx`<br/>`src/stores/toastStore.tsx` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!
- **Generate a plan of action for an issue:** Comment `@sourcery-ai plan` on
  an issue to generate a plan of action for it.

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: NONE
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-03-20T12:04:14Z
body: ## Deploying champion-trader with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>6e4741a</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://6c51a8e6.champion-trader.pages.dev'>https://6c51a8e6.champion-trader.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://aum-design-discrepancies-6.champion-trader.pages.dev'>https://aum-design-discrepancies-6.champion-trader.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/a88ddb4a494e6f543194cd549214286d/pages/view/champion-trader/6c51a8e6-8d82-4c3b-a7cb-f72ae04720f1)

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-20T12:14:14Z
body: <h1>Dependency Review</h1>
✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.<h2>OpenSSF Scorecard</h2>
<table><tr><th>Package</th><th>Version</th><th>Score</th><th>Details</th></tr>
</table><h2>Scanned Manifest Files</h2>


<!-- dependency-review-pr-comment-marker -->
author_association: NONE