diff --git a/app/(app)/quizzes/[id]/quiz-player.tsx b/app/(app)/quizzes/[id]/quiz-player.tsx
index 7bf71f9c..1f8b2983 100644
--- a/app/(app)/quizzes/[id]/quiz-player.tsx
+++ b/app/(app)/quizzes/[id]/quiz-player.tsx
@@ -39,6 +39,7 @@ import {
type QuizResult,
} from "@/lib/quiz";
import { QuizResultView } from "@/components/quiz/quiz-result-view";
+import { CopyPageLinkButton } from "@/components/layout/copy-page-link-button";
import { cn } from "@/lib/utils";
type Stage =
@@ -170,6 +171,10 @@ export function QuizPlayer({ id }: { id: string }) {
{stage.card.target_title || stage.card.target_path}
)}
+ {/* Shares the quiz itself; the link unfurls with its own OG card. */}
+
+
+
)}
diff --git a/components/layout/copy-page-link-button.tsx b/components/layout/copy-page-link-button.tsx
new file mode 100644
index 00000000..bed29bf9
--- /dev/null
+++ b/components/layout/copy-page-link-button.tsx
@@ -0,0 +1,79 @@
+"use client";
+
+/**
+ * "Copy link to this page" — copies the full, unshortened absolute URL of
+ * the active page. Deliberately bypasses useCopyFeedback (which rewrites
+ * `?hl=` highlight URLs into short /share links); here we want the literal
+ * page URL. The write is synchronous within the click so a single tap works.
+ *
+ * Shared by the PageHeader controls row and pages with their own header
+ * (e.g. a quiz).
+ */
+
+import { useEffect, useRef, useState } from "react";
+import { Check, Link2 } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+export function CopyPageLinkButton({
+ label = "Copy link to this page",
+ className,
+}: {
+ label?: string;
+ className?: string;
+}) {
+ const [copied, setCopied] = useState(false);
+ const copyTimerRef = useRef | null>(null);
+ useEffect(() => {
+ return () => {
+ if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
+ };
+ }, []);
+
+ async function copyPageLink() {
+ const url = window.location.href;
+ try {
+ await navigator.clipboard.writeText(url);
+ } catch {
+ // Fallback for browsers without the async clipboard API.
+ const ta = document.createElement("textarea");
+ ta.value = url;
+ ta.style.position = "fixed";
+ ta.style.opacity = "0";
+ document.body.appendChild(ta);
+ ta.focus();
+ ta.select();
+ try {
+ document.execCommand("copy");
+ } catch {
+ // silent fail
+ }
+ document.body.removeChild(ta);
+ }
+ setCopied(true);
+ if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
+ copyTimerRef.current = setTimeout(() => setCopied(false), 1500);
+ }
+
+ return (
+
+ );
+}
diff --git a/components/layout/page-header.tsx b/components/layout/page-header.tsx
index feb75d12..d79168bd 100644
--- a/components/layout/page-header.tsx
+++ b/components/layout/page-header.tsx
@@ -25,8 +25,8 @@
import { useEffect, useRef, useState } from "react";
import { useRouter, usePathname } from "next/navigation";
-import { Check, Link2 } from "lucide-react";
import { cn } from "@/lib/utils";
+import { CopyPageLinkButton } from "@/components/layout/copy-page-link-button";
import { SaveOfflineButton } from "@/components/layout/save-offline-button";
import { QuizButton } from "@/components/quiz/quiz-button";
import type { OfflineDescriptor } from "@/lib/offline";
@@ -153,43 +153,6 @@ export function PageHeader({
return () => window.removeEventListener("scroll", onScroll);
}, [sticky]);
- // "Copy link to this page" — copies the full, unshortened absolute URL of
- // the active page. Deliberately bypasses useCopyFeedback (which rewrites
- // `?hl=` highlight URLs into short /share links); here we want the literal
- // page URL. The write is synchronous within the click so a single tap works.
- const [copied, setCopied] = useState(false);
- const copyTimerRef = useRef | null>(null);
- useEffect(() => {
- return () => {
- if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
- };
- }, []);
-
- async function copyPageLink() {
- const url = window.location.href;
- try {
- await navigator.clipboard.writeText(url);
- } catch {
- // Fallback for browsers without the async clipboard API.
- const ta = document.createElement("textarea");
- ta.value = url;
- ta.style.position = "fixed";
- ta.style.opacity = "0";
- document.body.appendChild(ta);
- ta.focus();
- ta.select();
- try {
- document.execCommand("copy");
- } catch {
- // silent fail
- }
- document.body.removeChild(ta);
- }
- setCopied(true);
- if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
- copyTimerRef.current = setTimeout(() => setCopied(false), 1500);
- }
-
return (
: null}
-
+
{resolvedOffline ? (
) : null}