From 8651e8df0f8c17e20bfab88d6ce4902294f97da6 Mon Sep 17 00:00:00 2001 From: gnezim Date: Fri, 17 Apr 2026 01:29:31 +0300 Subject: [PATCH] Add SharePanel component with social links and copy-to-clipboard --- .../BoardDetailsHeader/SharePanel.test.tsx | 55 ++++++++++++++++ .../BoardDetailsHeader/SharePanel.tsx | 65 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 src/features/online-board/components/BoardDetailsHeader/SharePanel.test.tsx create mode 100644 src/features/online-board/components/BoardDetailsHeader/SharePanel.tsx diff --git a/src/features/online-board/components/BoardDetailsHeader/SharePanel.test.tsx b/src/features/online-board/components/BoardDetailsHeader/SharePanel.test.tsx new file mode 100644 index 00000000..ab89e7ea --- /dev/null +++ b/src/features/online-board/components/BoardDetailsHeader/SharePanel.test.tsx @@ -0,0 +1,55 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { SharePanel } from "./SharePanel.js"; + +vi.mock("@/i18n/provider.js", () => ({ + useTranslation: () => ({ t: (k: string) => k }), +})); + +describe("SharePanel", () => { + let writeTextMock: ReturnType; + + beforeEach(() => { + writeTextMock = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { + value: { writeText: writeTextMock }, + writable: true, + configurable: true, + }); + }); + + it("renders 3 social links for en locale (no Weibo)", () => { + render( {}} />); + expect(screen.getByTestId("share-facebook")).toBeTruthy(); + expect(screen.getByTestId("share-vk")).toBeTruthy(); + expect(screen.getByTestId("share-twitter")).toBeTruthy(); + expect(screen.queryByTestId("share-weibo")).toBeNull(); + }); + + it("renders 4 social links for zh locale (includes Weibo)", () => { + render( {}} />); + expect(screen.getByTestId("share-facebook")).toBeTruthy(); + expect(screen.getByTestId("share-vk")).toBeTruthy(); + expect(screen.getByTestId("share-twitter")).toBeTruthy(); + expect(screen.getByTestId("share-weibo")).toBeTruthy(); + }); + + it("Facebook href contains encoded URL", () => { + render( {}} />); + const link = screen.getByTestId("share-facebook") as HTMLAnchorElement; + expect(link.href).toContain(encodeURIComponent("https://example.com/flight?a=1")); + }); + + it("copy button calls navigator.clipboard.writeText and onClose", async () => { + const onClose = vi.fn(); + render(); + fireEvent.click(screen.getByTestId("share-copy")); + await waitFor(() => { + expect(writeTextMock).toHaveBeenCalledWith("https://example.com/flight"); + }); + await waitFor(() => { + expect(onClose).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/features/online-board/components/BoardDetailsHeader/SharePanel.tsx b/src/features/online-board/components/BoardDetailsHeader/SharePanel.tsx new file mode 100644 index 00000000..2cdd1be6 --- /dev/null +++ b/src/features/online-board/components/BoardDetailsHeader/SharePanel.tsx @@ -0,0 +1,65 @@ +import type { FC } from "react"; +import { useTranslation } from "@/i18n/provider.js"; +import "./actions.scss"; + +export interface SharePanelProps { + url: string; + locale: string; + onClose: () => void; +} + +export const SharePanel: FC = ({ url, locale, onClose }) => { + const { t } = useTranslation(); + const encoded = encodeURIComponent(url); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(url); + onClose(); + } catch { + // silent + } + }; + + return ( +
+ + Facebook + + + VK + + + Twitter + + {locale === "zh" && ( + + Weibo + + )} + +
+ ); +};