Skip to content

Commit a782840

Browse files
committed
feat: fundraising share on x
1 parent 2b3fd63 commit a782840

File tree

5 files changed

+214
-14
lines changed

5 files changed

+214
-14
lines changed

frontend/public/Logo.png

38.3 KB
Loading

frontend/public/coin.png

1.59 MB
Loading
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
"use client";
2+
import { Share2 } from "lucide-react";
3+
import React, { useState } from "react";
4+
5+
interface PoolData {
6+
amountRaised: string;
7+
remainingAmount: string;
8+
targetAmount: string;
9+
fundraiserId: string;
10+
}
11+
12+
export const XShareButtonWithPoolImage = ({
13+
poolData,
14+
}: {
15+
poolData: PoolData;
16+
}) => {
17+
const [showModal, setShowModal] = useState(false);
18+
19+
const loadImage = (src: string): Promise<HTMLImageElement> => {
20+
return new Promise((resolve, reject) => {
21+
const img = new Image();
22+
img.crossOrigin = "anonymous";
23+
img.onload = () => resolve(img);
24+
img.onerror = () => reject(new Error(`Failed to load image at ${src}`));
25+
img.src = src;
26+
});
27+
};
28+
29+
const generateAndDownload = async () => {
30+
const canvas = document.createElement("canvas");
31+
canvas.width = 1200;
32+
canvas.height = 1200;
33+
const ctx = canvas.getContext("2d")!;
34+
35+
// 1. Background
36+
ctx.fillStyle = "#070A11";
37+
ctx.fillRect(0, 0, 1200, 1200);
38+
39+
// 2. Background Coin
40+
try {
41+
const coinImg = await loadImage("/coin.png");
42+
ctx.globalAlpha = 0.6;
43+
ctx.drawImage(
44+
coinImg,
45+
100,
46+
380,
47+
1000,
48+
1000 * (coinImg.height / coinImg.width)
49+
);
50+
ctx.globalAlpha = 1.0;
51+
} catch (e) {
52+
console.error("Background coin failed to load", e);
53+
}
54+
55+
// 3. Logo Badge & Logo Image
56+
const badgeX = 80,
57+
badgeY = 420,
58+
badgeW = 280,
59+
badgeH = 75;
60+
61+
// Draw the pill background
62+
ctx.fillStyle = "rgba(255, 255, 255, 0.05)";
63+
ctx.beginPath();
64+
ctx.roundRect(badgeX, badgeY, badgeW, badgeH, 40);
65+
ctx.fill();
66+
ctx.strokeStyle = "rgba(255, 255, 255, 0.2)";
67+
ctx.stroke();
68+
69+
try {
70+
// FIX: Load and draw the logo image inside the badge
71+
const logoImg = await loadImage("/logo.png");
72+
// Adjust size (50x50) and position within the badge
73+
ctx.drawImage(logoImg, badgeX + 15, badgeY + 12, 50, 50);
74+
} catch (e) {
75+
console.error("Logo image failed to load", e);
76+
}
77+
78+
// Draw "PAYMESH" Text
79+
ctx.fillStyle = "white";
80+
ctx.font = "bold 32px Arial";
81+
ctx.textAlign = "left"; // Ensure alignment
82+
ctx.fillText("PAYMESH", badgeX + 75, badgeY + 48);
83+
84+
// 4. Stats Rendering
85+
const drawStat = (
86+
x: number,
87+
label: string,
88+
value: string,
89+
color: string
90+
) => {
91+
ctx.textAlign = "left";
92+
ctx.fillStyle = "#8398AD";
93+
ctx.font = "bold 24px Arial";
94+
ctx.fillText(label, x, 650);
95+
ctx.fillStyle = color;
96+
ctx.font = "bold 30px Arial";
97+
ctx.fillText(value, x, 700);
98+
};
99+
100+
drawStat(80, "Amount Raised", poolData.amountRaised, "#BCC0C5");
101+
drawStat(480, "Remaining Amount", poolData.remainingAmount, "#BCC0C5");
102+
drawStat(880, "Target Amount", poolData.targetAmount, "#92FFB0");
103+
104+
// 5. Bottom Text
105+
ctx.fillStyle = "white";
106+
ctx.font = "900 100px Impact, Arial Black, sans-serif";
107+
ctx.textAlign = "center";
108+
ctx.fillText("A LITTLE SUPPORT CAN", 600, 950);
109+
ctx.fillText("MAKE A BIG DIFFERENCE.", 600, 1070);
110+
111+
// 6. Download Trigger
112+
canvas.toBlob((blob) => {
113+
if (blob) {
114+
const url = URL.createObjectURL(blob);
115+
const a = document.createElement("a");
116+
a.href = url;
117+
a.download = `paymesh-share.png`;
118+
a.click();
119+
URL.revokeObjectURL(url);
120+
setShowModal(true);
121+
}
122+
});
123+
};
124+
125+
const handlePostToX = () => {
126+
const fundraiserUrl = `https://paymesh.app/fundraiser/${poolData.fundraiserId}`;
127+
const defaultText = encodeURIComponent(
128+
`Every contribution counts! 🚀\n\n` +
129+
`I just joined the pool on @paymesh_ to support this initiative on @StarknetFndn. \n\n` +
130+
`Current progress: ${poolData.amountRaised} / ${poolData.targetAmount}\n\n` +
131+
`Join me and make an impact here: ${fundraiserUrl}`
132+
);
133+
const xUrl = `https://twitter.com/intent/tweet?text=${defaultText}`;
134+
window.open(xUrl, "_blank");
135+
setShowModal(false);
136+
};
137+
138+
return (
139+
<>
140+
<button
141+
className="flex items-center gap-2 bg-white rounded-full px-3 py-1.5 cursor-pointer hover:bg-gray-100 transition-colors shrink-0"
142+
onClick={generateAndDownload}
143+
>
144+
<span className="text-[#030407] text-sm font-medium">Share</span>
145+
<Share2 className="w-4 h-4 text-[#030407]" />
146+
</button>
147+
148+
{showModal && (
149+
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
150+
<div className="bg-[#070A11] border border-white/20 p-8 rounded-3xl max-w-md w-full text-center shadow-2xl">
151+
<h2 className="text-white text-2xl font-bold mb-4">
152+
Image Downloaded
153+
</h2>
154+
<p className="text-[#8398AD] mb-6 leading-relaxed">
155+
To share on X, simply click the button below. When the window
156+
opens,
157+
<strong> attach the image you just downloaded</strong> from your
158+
files.
159+
</p>
160+
<div className="flex flex-col gap-3">
161+
<button
162+
onClick={handlePostToX}
163+
className="bg-[#575EB7] text-[#DDDDDD] font-bold py-3 rounded-full hover:brightness-110 transition-all"
164+
>
165+
Open X & Post
166+
</button>
167+
<button
168+
onClick={() => setShowModal(false)}
169+
className="text-white/50 text-sm hover:text-white transition-all"
170+
>
171+
Cancel
172+
</button>
173+
</div>
174+
</div>
175+
</div>
176+
)}
177+
</>
178+
);
179+
};

frontend/src/app/fundraiser/[id]/page.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import Loading from "@/components/Loading";
2323
import ContributeModal from "./components/ContributeModal";
2424
import { MyCleanQrCode } from "@/components/qr-code";
2525
import { useGetPoolAddress } from "@/hooks/useContractInteraction";
26+
import { XShareButtonWithPoolImage } from "./components/share-btn";
27+
import { formatAmountUsd } from "@/utils/currency-formatter";
2628

2729
const FundraiseDetails = () => {
2830
const router = useRouter();
@@ -153,7 +155,12 @@ const FundraiseDetails = () => {
153155
setIsSubmitting(false);
154156
}
155157
};
156-
158+
const samplePoolData = {
159+
amountRaised: `${formatAmountUsd(+amountRaised)} USD`,
160+
remainingAmount: `${formatAmountUsd(+targetAmount - +amountRaised)} USD`,
161+
targetAmount: `${formatAmountUsd(+targetAmount)} USD`,
162+
fundraiserId: fetchFundraiseDetails?.crowd_funding?.pool_address ?? "",
163+
};
157164
return (
158165
<div className="space-y-6 my-16 max-w-sit-screen px-5 mx-auto">
159166
{fetchFundraiseDetails?.crowd_funding?.is_complete && (
@@ -167,22 +174,19 @@ const FundraiseDetails = () => {
167174

168175
<button
169176
onClick={() => router.push("/fundraiser")}
170-
className="flex items-center cursor-pointer bg-[#FFFFFF0D] border border-[#FFFFFF1A] gap-2 text-[#DFDADA] hover:text-[#DFDFE0] py-3 px-4 rounded-4xl hover:bg-[#232542] mb-12"
177+
className="flex items-center cursor-pointer bg-[#FFFFFF0D] border border-[#FFFFFF1A] gap-2 text-[#DFDADA] hover:text-[#DFDFE0] py-3 px-4 rounded-4xl hover:bg-moon-blue mb-12"
171178
>
172179
<ArrowLeft className="w-4 h-4" />
173180
Back to Fundraisers
174181
</button>
175182

176-
<div className="bg-[#FFFFFF05] border border-[#232542] rounded-lg">
177-
<div className="flex flex-col md:flex-row items-center justify-between gap-4 border-b border-[#232542] py-4 px-6">
183+
<div className="bg-[#FFFFFF05] border border-moon-blue rounded-lg">
184+
<div className="flex flex-col md:flex-row items-center justify-between gap-4 border-b border-moon-blue py-4 px-6">
178185
<div className="flex items-center justify-between w-full md:w-auto gap-4">
179186
<h2 className="text-[#E2E2E2] font-semibold text-base leading-tight truncate max-w-[200px] sm:max-w-md">
180187
{fetchFundraiseDetails?.crowd_funding?.name}
181188
</h2>
182-
<button className="flex items-center gap-2 bg-white rounded-full px-3 py-1.5 cursor-pointer hover:bg-gray-100 transition-colors shrink-0">
183-
<span className="text-[#030407] text-sm">Share</span>
184-
<Share2 className="w-4 h-4 text-[#030407]" />
185-
</button>
189+
<XShareButtonWithPoolImage poolData={samplePoolData} />
186190
</div>
187191

188192
<div className="flex items-center justify-between md:justify-end space-x-2 bg-[#0C121D] py-2 px-5 rounded-full w-full md:w-auto max-w-full">
@@ -220,7 +224,7 @@ const FundraiseDetails = () => {
220224

221225
<div className="px-6 py-4 flex flex-col xl:flex-row items-center justify-between gap-6">
222226
<div className="grid grid-cols-1 sm:grid-cols-2 lg:flex lg:items-center gap-4 w-full xl:w-auto">
223-
<div className="flex items-center justify-between sm:justify-start space-x-2 border border-[#232542] w-full sm:w-fit rounded-full py-2.5 px-4">
227+
<div className="flex items-center justify-between sm:justify-start space-x-2 border border-moon-blue w-full sm:w-fit rounded-full py-2.5 px-4">
224228
<h3 className="text-[#8398AD] border-r border-[#8398AD] pr-2">
225229
Amount Raised
226230
</h3>
@@ -229,7 +233,7 @@ const FundraiseDetails = () => {
229233
</span>
230234
</div>
231235

232-
<div className="flex items-center justify-between sm:justify-start space-x-2 border border-[#232542] w-full sm:w-fit rounded-full py-2.5 px-4">
236+
<div className="flex items-center justify-between sm:justify-start space-x-2 border border-moon-blue w-full sm:w-fit rounded-full py-2.5 px-4">
233237
<h3 className="text-[#8398AD] border-r border-[#8398AD] pr-2">
234238
Target Amount
235239
</h3>
@@ -296,8 +300,8 @@ const FundraiseDetails = () => {
296300
</div>
297301

298302
<div className="grid grid-cols-1 lg:grid-cols-24 gap-4">
299-
<div className="lg:col-span-19 py-6 bg-[#FFFFFF05] border border-[#232542] rounded-lg flex flex-col h-full">
300-
<h2 className="text-[#E2E2E2] font-semibold border-b px-6 border-[#232542] pb-4 text-base">
303+
<div className="lg:col-span-19 py-6 bg-[#FFFFFF05] border border-moon-blue rounded-lg flex flex-col h-full">
304+
<h2 className="text-[#E2E2E2] font-semibold border-b px-6 border-moon-blue pb-4 text-base">
301305
Description
302306
</h2>
303307

@@ -308,8 +312,8 @@ const FundraiseDetails = () => {
308312
</div>
309313
</div>
310314

311-
<div className="lg:col-span-5 py-6 bg-[#FFFFFF05] border border-[#232542] rounded-lg flex flex-col h-full">
312-
<h2 className="text-[#E2E2E2] px-6 pb-4 border-b border-[#232542] text-center font-semibold text-base">
315+
<div className="lg:col-span-5 py-6 bg-[#FFFFFF05] border border-moon-blue rounded-lg flex flex-col h-full">
316+
<h2 className="text-[#E2E2E2] px-6 pb-4 border-b border-moon-blue text-center font-semibold text-base">
313317
Scan to fund address
314318
</h2>
315319

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
type UsdFormatOptions = {
2+
decimals?: number;
3+
};
4+
5+
export function formatAmountUsd(
6+
amount: number,
7+
options: UsdFormatOptions = {}
8+
): string {
9+
const {decimals = 0 } = options;
10+
11+
const formatted = new Intl.NumberFormat("en-US", {
12+
minimumFractionDigits: decimals,
13+
maximumFractionDigits: decimals,
14+
}).format(amount);
15+
16+
return `$${formatted}`;
17+
}

0 commit comments

Comments
 (0)