Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 4 additions & 20 deletions src/app/brampton/vote/2026/pledge/[slug]/SharedPledgeClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import dynamic from "next/dynamic";
import Link from "next/link";
import { useState } from "react";
import { ArrowLeft, ArrowRight, Check, Link2 } from "lucide-react";
import { getElection } from "@/lib/elections/registry";
import { usePledgeShare } from "@/lib/elections/use-pledge-share";
import stampImage from "../brampton-stamp.png";

const ELECTION = getElection("brampton-2026");
Expand All @@ -21,25 +21,9 @@ const StampScene = dynamic(() => import("@/components/elections/StampScene"), {
});

export default function SharedPledgeClient({ name }: { name: string }) {
const [copied, setCopied] = useState(false);

async function share() {
const url = window.location.href;
if (navigator.share) {
try {
await navigator.share({
title: `${name} pledged to vote — Brampton 2026`,
url,
});
return;
} catch {
// fall through to the clipboard if the user dismissed the sheet
}
}
await navigator.clipboard.writeText(url);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
const { copied, share } = usePledgeShare(
`${name} pledged to vote — Brampton 2026`,
);

return (
/* On small screens the copy would sit on top of the stamp, so the three
Expand Down
24 changes: 4 additions & 20 deletions src/app/hamilton/vote/2026/pledge/[slug]/SharedPledgeClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import dynamic from "next/dynamic";
import Link from "next/link";
import { useState } from "react";
import { ArrowLeft, ArrowRight, Check, Link2 } from "lucide-react";
import { getElection } from "@/lib/elections/registry";
import { usePledgeShare } from "@/lib/elections/use-pledge-share";
import stampImage from "../hamilton-stamp.png";

const ELECTION = getElection("hamilton-2026");
Expand All @@ -21,25 +21,9 @@ const StampScene = dynamic(() => import("@/components/elections/StampScene"), {
});

export default function SharedPledgeClient({ name }: { name: string }) {
const [copied, setCopied] = useState(false);

async function share() {
const url = window.location.href;
if (navigator.share) {
try {
await navigator.share({
title: `${name} pledged to vote — Hamilton 2026`,
url,
});
return;
} catch {
// fall through to the clipboard if the user dismissed the sheet
}
}
await navigator.clipboard.writeText(url);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
const { copied, share } = usePledgeShare(
`${name} pledged to vote — Hamilton 2026`,
);

return (
<div className="relative h-[calc(100dvh-20px)] min-h-[480px] border-2 border-dark bg-[#efe4da] overflow-clip">
Expand Down
24 changes: 4 additions & 20 deletions src/app/ottawa/vote/2026/pledge/[slug]/SharedPledgeClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import dynamic from "next/dynamic";
import Link from "next/link";
import { useState } from "react";
import { ArrowLeft, ArrowRight, Check, Link2 } from "lucide-react";
import { getElection } from "@/lib/elections/registry";
import { usePledgeShare } from "@/lib/elections/use-pledge-share";
import stampImage from "../ottawa-stamp.png";

const ELECTION = getElection("ottawa-2026");
Expand All @@ -21,25 +21,9 @@ const StampScene = dynamic(() => import("@/components/elections/StampScene"), {
});

export default function SharedPledgeClient({ name }: { name: string }) {
const [copied, setCopied] = useState(false);

async function share() {
const url = window.location.href;
if (navigator.share) {
try {
await navigator.share({
title: `${name} pledged to vote — Ottawa 2026`,
url,
});
return;
} catch {
// fall through to the clipboard if the user dismissed the sheet
}
}
await navigator.clipboard.writeText(url);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
const { copied, share } = usePledgeShare(
`${name} pledged to vote — Ottawa 2026`,
);

return (
<div className="relative h-[calc(100dvh-20px)] min-h-[480px] border-2 border-dark bg-[#efe4da] overflow-clip">
Expand Down
24 changes: 4 additions & 20 deletions src/app/toronto/vote/2026/pledge/[slug]/SharedPledgeClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import dynamic from "next/dynamic";
import Link from "next/link";
import { useState } from "react";
import { ArrowLeft, ArrowRight, Check, Link2 } from "lucide-react";
import { usePledgeShare } from "@/lib/elections/use-pledge-share";
import stampImage from "../toronto-stamp.png";

const StampScene = dynamic(() => import("@/components/elections/StampScene"), {
Expand All @@ -18,25 +18,9 @@ const StampScene = dynamic(() => import("@/components/elections/StampScene"), {
});

export default function SharedPledgeClient({ name }: { name: string }) {
const [copied, setCopied] = useState(false);

async function share() {
const url = window.location.href;
if (navigator.share) {
try {
await navigator.share({
title: `${name} pledged to vote — Toronto 2026`,
url,
});
return;
} catch {
// fall through to the clipboard if the user dismissed the sheet
}
}
await navigator.clipboard.writeText(url);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
const { copied, share } = usePledgeShare(
`${name} pledged to vote — Toronto 2026`,
);

return (
<div className="theme-election bg-bg text-dark">
Expand Down
48 changes: 48 additions & 0 deletions src/lib/elections/use-pledge-share.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client";

import { useState } from "react";
import { toast } from "sonner";

/**
* Share the current pledge page, and report failure instead of failing
* silently. Every region's pledge page uses this, so the share-sheet handling
* and the clipboard guard live in one place.
*
* The order is: try the native share sheet, then fall back to the clipboard.
* Two failures need care:
* - The user dismisses the share sheet. That rejects with `AbortError`. Treat
* it as done, because the user chose to cancel — and skip the clipboard,
* because the transient user activation is already gone and the write would
* reject with `NotAllowedError`.
* - The clipboard write rejects for any reason. Show a toast, so the button
* no longer sits on "Share this pledge" with nothing to explain the miss.
*/
export function usePledgeShare(title: string) {
const [copied, setCopied] = useState(false);

async function share() {
const url = window.location.href;

if (navigator.share) {
try {
await navigator.share({ title, url });
return;
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") {
return;
}
// Any other share failure falls through to the clipboard.
}
}

try {
await navigator.clipboard.writeText(url);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
toast.error("Could not copy the link. Copy it from the address bar.");
}
}

return { copied, share };
}
Loading