Skip to content

Commit 8fd1d75

Browse files
authored
Merge pull request #371 from raizo07/feat/Share-Raffle
feat: Implement 'Share Raffle' feature
2 parents f0c47a6 + d256dcb commit 8fd1d75

2 files changed

Lines changed: 146 additions & 26 deletions

File tree

client/src/components/ShareRaffle.tsx

Lines changed: 142 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,169 @@
1+
import { useCallback, useMemo, useState } from "react";
12
import Union from "../assets/Union.png";
2-
import { Facebook, Link, Send, Twitter } from "lucide-react";
3+
import { Link2, Share2, Twitter } from "lucide-react";
4+
import { toast } from "sonner";
5+
6+
type ShareSource = "twitter" | "telegram" | "native" | "copy";
7+
8+
type ShareRaffleProps = {
9+
raffleId: number;
10+
title: string;
11+
};
12+
13+
function buildRaffleShareUrl(raffleId: number, utmSource?: ShareSource): string {
14+
if (typeof window === "undefined") {
15+
return "";
16+
}
17+
const url = new URL(`${window.location.origin}/raffles/${raffleId}`);
18+
url.searchParams.set("utm_medium", "social");
19+
url.searchParams.set("utm_campaign", "raffle_share");
20+
if (utmSource) {
21+
url.searchParams.set("utm_source", utmSource);
22+
}
23+
return url.toString();
24+
}
25+
26+
function TelegramIcon({ className }: { className?: string }) {
27+
return (
28+
<svg
29+
className={className}
30+
viewBox="0 0 24 24"
31+
fill="currentColor"
32+
aria-hidden
33+
>
34+
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
35+
</svg>
36+
);
37+
}
38+
39+
const iconButtonClass =
40+
"inline-flex items-center justify-center rounded-full bg-[#090E1F] p-2.5 text-[#00E6CC] ring-1 ring-[#00E6CC]/20 transition hover:bg-[#00E6CC]/10 hover:ring-[#00E6CC]/40 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#00E6CC]";
41+
42+
const ShareRaffle = ({ raffleId, title }: ShareRaffleProps) => {
43+
const [copied, setCopied] = useState(false);
44+
45+
const shareBlurb = useMemo(
46+
() => `Check out this raffle: ${title}`,
47+
[title],
48+
);
49+
50+
const twitterHref = useMemo(() => {
51+
const url = buildRaffleShareUrl(raffleId, "twitter");
52+
const params = new URLSearchParams({
53+
text: shareBlurb,
54+
url: url,
55+
});
56+
return `https://twitter.com/intent/tweet?${params.toString()}`;
57+
}, [raffleId, shareBlurb]);
58+
59+
const telegramHref = useMemo(() => {
60+
const url = buildRaffleShareUrl(raffleId, "telegram");
61+
const params = new URLSearchParams({
62+
url,
63+
text: shareBlurb,
64+
});
65+
return `https://t.me/share/url?${params.toString()}`;
66+
}, [raffleId, shareBlurb]);
67+
68+
const copyHref = useMemo(
69+
() => buildRaffleShareUrl(raffleId, "copy"),
70+
[raffleId],
71+
);
72+
73+
const nativeShareUrl = useMemo(
74+
() => buildRaffleShareUrl(raffleId, "native"),
75+
[raffleId],
76+
);
77+
78+
const canWebShare =
79+
typeof navigator !== "undefined" &&
80+
typeof navigator.share === "function";
81+
82+
const handleNativeShare = useCallback(async () => {
83+
if (!canWebShare) return;
84+
try {
85+
await navigator.share({
86+
title,
87+
text: shareBlurb,
88+
url: nativeShareUrl,
89+
});
90+
} catch (err) {
91+
const name = err instanceof DOMException ? err.name : "";
92+
if (name === "AbortError") return;
93+
toast.error("Sharing was cancelled or failed.");
94+
}
95+
}, [canWebShare, nativeShareUrl, shareBlurb, title]);
96+
97+
const handleCopyLink = useCallback(async () => {
98+
try {
99+
await navigator.clipboard.writeText(copyHref);
100+
setCopied(true);
101+
toast.success("Link copied to clipboard");
102+
window.setTimeout(() => setCopied(false), 2000);
103+
} catch {
104+
toast.error("Could not copy link");
105+
}
106+
}, [copyHref]);
3107

4-
const ShareRaffle = () => {
5108
return (
6109
<div className="bg-white dark:bg-[#11172E] rounded-3xl flex flex-col md:flex-row justify-between items-center border border-gray-200 dark:border-[#1F263F] mt-8 overflow-hidden">
7-
{/* Left Section */}
8-
<div className="p-8 flex-1">
110+
<div className="p-8 flex-1 w-full">
9111
<h3 className="text-[22px] font-semibold text-gray-900 dark:text-white">
10112
Share This Raffle
11113
</h3>
12114
<p className="mt-3 text-gray-600 dark:text-[#9CA3AF] text-sm md:text-base">
13115
Invite your friends to join and increase the excitement!
14116
</p>
15117

16-
{/* Social Links */}
17-
<div className="mt-5 flex space-x-3">
18-
<a
19-
href="#"
20-
className="p-2 rounded-full bg-[#090E1F] hover:bg-[#00E6CC]/10 transition"
21-
>
22-
<Twitter className="w-5 h-5" color="#00E6CC" />
23-
</a>
118+
<div className="mt-5 flex flex-wrap items-center gap-3">
119+
{canWebShare ? (
120+
<button
121+
type="button"
122+
className={`${iconButtonClass} gap-2 px-4 py-2 rounded-full text-sm font-medium`}
123+
onClick={handleNativeShare}
124+
aria-label="Share using your device"
125+
>
126+
<Share2 className="h-5 w-5 shrink-0" />
127+
<span className="text-gray-100">Share</span>
128+
</button>
129+
) : null}
130+
24131
<a
25-
href="#"
26-
className="p-2 rounded-full bg-[#090E1F] hover:bg-[#00E6CC]/10 transition"
132+
href={twitterHref}
133+
target="_blank"
134+
rel="noopener noreferrer"
135+
className={iconButtonClass}
136+
aria-label="Share on X (Twitter)"
27137
>
28-
<Facebook className="w-5 h-5" color="#00E6CC" />
138+
<Twitter className="h-5 w-5" />
29139
</a>
30140
<a
31-
href="#"
32-
className="p-2 rounded-full bg-[#090E1F] hover:bg-[#00E6CC]/10 transition"
141+
href={telegramHref}
142+
target="_blank"
143+
rel="noopener noreferrer"
144+
className={iconButtonClass}
145+
aria-label="Share on Telegram"
33146
>
34-
<Send className="w-5 h-5" color="#00E6CC" />
147+
<TelegramIcon className="h-5 w-5" />
35148
</a>
36-
<a
37-
href="#"
38-
className="p-2 rounded-full bg-[#090E1F] hover:bg-[#00E6CC]/10 transition"
149+
<button
150+
type="button"
151+
className={iconButtonClass}
152+
onClick={handleCopyLink}
153+
aria-label="Copy raffle link"
39154
>
40-
<Link className="w-5 h-5" color="#00E6CC" />
41-
</a>
155+
<Link2 className="h-5 w-5" />
156+
{copied ? (
157+
<span className="sr-only">Copied</span>
158+
) : null}
159+
</button>
42160
</div>
43161
</div>
44162

45-
{/* Right Section (Image) */}
46163
<div className="md:flex-shrink-0 hidden w-full md:w-auto ">
47164
<img
48165
src={Union}
49-
alt="union"
166+
alt=""
50167
className="w-full md:w-auto object-contain"
51168
/>
52169
</div>

client/src/pages/RaffleDetails.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,10 @@ const RaffleDetails = () => {
172172
</div>
173173

174174
<VerifiedBadge />
175-
<ShareRaffle />
175+
<ShareRaffle
176+
raffleId={raffle.id}
177+
title={raffle.metadata?.title || raffle.description || "Raffle"}
178+
/>
176179
</div>
177180
);
178181
};

0 commit comments

Comments
 (0)