|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import { useMemo } from "react"; |
| 3 | +import { useMemo, useState, useRef } from "react"; |
4 | 4 | import Link from "next/link"; |
5 | 5 | import type { Route } from "next"; |
6 | | -import { Check, Copy, ExternalLink, Trophy } from "lucide-react"; |
| 6 | +import { Check, Copy, ExternalLink, Trophy, Share2, XCircle } from "lucide-react"; |
7 | 7 | import { useSearchParams } from "next/navigation"; |
8 | 8 | import { useClipboardCopy } from "@/hooks"; |
9 | 9 | import { Avatar } from "@/components/layout/avatar"; |
@@ -63,6 +63,34 @@ export function ResultDashboard({ |
63 | 63 | const { t } = useTranslation(); |
64 | 64 | const searchParams = useSearchParams(); |
65 | 65 | const { copied, copy } = useClipboardCopy(); |
| 66 | + const { copy: copyShare } = useClipboardCopy(); |
| 67 | + const [shareState, setShareState] = useState<"idle" | "success" | "error">("idle"); |
| 68 | + const shareTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| 69 | + |
| 70 | + const setShareFeedback = (state: "success" | "error") => { |
| 71 | + setShareState(state); |
| 72 | + if (shareTimerRef.current) clearTimeout(shareTimerRef.current); |
| 73 | + shareTimerRef.current = setTimeout(() => setShareState("idle"), 2000); |
| 74 | + }; |
| 75 | + |
| 76 | + const handleShare = async () => { |
| 77 | + const url = window.location.href; |
| 78 | + try { |
| 79 | + if (navigator.share) { |
| 80 | + await navigator.share({ url }); |
| 81 | + setShareFeedback("success"); |
| 82 | + } else { |
| 83 | + const success = await copyShare(url); |
| 84 | + setShareFeedback(success ? "success" : "error"); |
| 85 | + } |
| 86 | + } catch (err: unknown) { |
| 87 | + if (err instanceof Error && err.name === "AbortError") { |
| 88 | + return; |
| 89 | + } |
| 90 | + setShareFeedback("error"); |
| 91 | + } |
| 92 | + }; |
| 93 | + |
66 | 94 | const methodologyHref = useMemo(() => { |
67 | 95 | const query = searchParams.toString(); |
68 | 96 | return query ? `/scoring-methodology?${query}` : "/scoring-methodology"; |
@@ -454,7 +482,31 @@ export function ResultDashboard({ |
454 | 482 | </Card> |
455 | 483 | ) : null} |
456 | 484 |
|
457 | | - <div className="flex justify-end"> |
| 485 | + <div className="flex justify-end gap-2"> |
| 486 | + <Button |
| 487 | + variant="secondary" |
| 488 | + size="sm" |
| 489 | + onClick={handleShare} |
| 490 | + className="flex items-center gap-2" |
| 491 | + aria-label={t("results.shareAria")} |
| 492 | + > |
| 493 | + {shareState === "success" ? ( |
| 494 | + <> |
| 495 | + <Check className="h-4 w-4 text-green-500" /> |
| 496 | + <span className="text-green-500">{t("results.shareSuccess")}</span> |
| 497 | + </> |
| 498 | + ) : shareState === "error" ? ( |
| 499 | + <> |
| 500 | + <XCircle className="h-4 w-4 text-destructive" /> |
| 501 | + <span className="text-destructive">{t("results.shareError")}</span> |
| 502 | + </> |
| 503 | + ) : ( |
| 504 | + <> |
| 505 | + <Share2 className="h-4 w-4" /> |
| 506 | + {t("results.share")} |
| 507 | + </> |
| 508 | + )} |
| 509 | + </Button> |
458 | 510 | <Button |
459 | 511 | variant="secondary" |
460 | 512 | size="sm" |
|
0 commit comments