quiz link

This commit is contained in:
gosangam
2026-10-16 03:25:08 +05:30
parent 8537eb6ce2
commit 8dd72de359
3 changed files with 86 additions and 57 deletions
+5
View File
@@ -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}
</Link>
)}
{/* Shares the quiz itself; the link unfurls with its own OG card. */}
<div className="mt-3">
<CopyPageLinkButton label="Copy quiz link" />
</div>
</header>
)}
@@ -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<ReturnType<typeof setTimeout> | 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 (
<button
type="button"
onClick={copyPageLink}
aria-label={label}
title={label}
className={cn(
"inline-flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1 text-xs transition-colors",
copied
? "text-foreground border-primary/40"
: "text-muted-foreground hover:text-foreground hover:bg-muted",
className,
)}
>
{copied ? (
<Check className="size-3.5 text-primary" />
) : (
<Link2 className="size-3.5" />
)}
{copied ? "Link copied" : label}
</button>
);
}
+2 -57
View File
@@ -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<ReturnType<typeof setTimeout> | 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 (
<header
className={cn(
@@ -228,25 +191,7 @@ export function PageHeader({
{/* First, so it reads as belonging to the title above it. Renders
nothing on pages without a published quiz. */}
{quiz && isContentPath(pathname) ? <QuizButton path={pathname} /> : null}
<button
type="button"
onClick={copyPageLink}
aria-label="Copy link to this page"
title="Copy link to this page"
className={cn(
"inline-flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1 text-xs transition-colors",
copied
? "text-foreground border-primary/40"
: "text-muted-foreground hover:text-foreground hover:bg-muted",
)}
>
{copied ? (
<Check className="size-3.5 text-primary" />
) : (
<Link2 className="size-3.5" />
)}
{copied ? "Link copied" : "Copy link to this page"}
</button>
<CopyPageLinkButton />
{resolvedOffline ? (
<SaveOfflineButton descriptor={resolvedOffline} />
) : null}