forked from PinSpace-Org/GistPin
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathShareCardGenerator.tsx
More file actions
183 lines (160 loc) · 5.91 KB
/
Copy pathShareCardGenerator.tsx
File metadata and controls
183 lines (160 loc) · 5.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
'use client';
import { useRef, useState } from 'react';
type Platform = 'twitter' | 'linkedin';
const PLATFORM_SIZES: Record<Platform, { width: number; height: number; label: string }> = {
twitter: { width: 1200, height: 628, label: 'Twitter / X (1200×628)' },
linkedin: { width: 1200, height: 627, label: 'LinkedIn (1200×627)' },
};
export interface ShareCardData {
title: string;
metric: string;
value: string;
subtitle?: string;
}
interface ShareCardGeneratorProps {
data?: ShareCardData;
}
const DEFAULT_DATA: ShareCardData = {
title: 'GistPin Analytics',
metric: 'Total Gists',
value: '24,891',
subtitle: 'Last 30 days · gistpin.io',
};
export default function ShareCardGenerator({ data = DEFAULT_DATA }: ShareCardGeneratorProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [platform, setPlatform] = useState<Platform>('twitter');
const [cardData, setCardData] = useState<ShareCardData>(data);
const [downloading, setDownloading] = useState(false);
const { width, height } = PLATFORM_SIZES[platform];
const scale = 400 / width; // preview scale
function drawCard(ctx: CanvasRenderingContext2D, w: number, h: number) {
// Background gradient
const grad = ctx.createLinearGradient(0, 0, w, h);
grad.addColorStop(0, '#1e1b4b');
grad.addColorStop(1, '#312e81');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
// Branded header bar
ctx.fillStyle = '#6366f1';
ctx.fillRect(0, 0, w, h * 0.08);
// Logo / brand text
ctx.fillStyle = '#ffffff';
ctx.font = `bold ${h * 0.055}px Inter, sans-serif`;
ctx.fillText('GistPin', w * 0.05, h * 0.065);
// Main metric value
ctx.fillStyle = '#a5b4fc';
ctx.font = `${h * 0.07}px Inter, sans-serif`;
ctx.fillText(cardData.metric, w * 0.05, h * 0.42);
ctx.fillStyle = '#ffffff';
ctx.font = `bold ${h * 0.22}px Inter, sans-serif`;
ctx.fillText(cardData.value, w * 0.05, h * 0.72);
// Title
ctx.fillStyle = '#c7d2fe';
ctx.font = `${h * 0.055}px Inter, sans-serif`;
ctx.fillText(cardData.title, w * 0.05, h * 0.85);
// Subtitle
if (cardData.subtitle) {
ctx.fillStyle = '#818cf8';
ctx.font = `${h * 0.04}px Inter, sans-serif`;
ctx.fillText(cardData.subtitle, w * 0.05, h * 0.93);
}
// Decorative circle
ctx.beginPath();
ctx.arc(w * 0.88, h * 0.5, h * 0.3, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(99,102,241,0.15)';
ctx.fill();
}
function renderPreview() {
const canvas = canvasRef.current;
if (!canvas) return;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) return;
drawCard(ctx, width, height);
}
function handleDownload() {
setDownloading(true);
const offscreen = document.createElement('canvas');
offscreen.width = width;
offscreen.height = height;
const ctx = offscreen.getContext('2d');
if (!ctx) { setDownloading(false); return; }
drawCard(ctx, width, height);
offscreen.toBlob((blob) => {
if (!blob) { setDownloading(false); return; }
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `gistpin-share-${platform}.png`;
a.click();
URL.revokeObjectURL(url);
setDownloading(false);
}, 'image/png');
}
// Render preview on mount / change
if (typeof window !== 'undefined') {
setTimeout(renderPreview, 0);
}
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Controls */}
<div className="rounded-lg border bg-white dark:bg-gray-900 p-5 shadow-sm space-y-4">
<h2 className="font-semibold">Card Settings</h2>
<div>
<label className="block text-sm text-gray-600 dark:text-gray-400 mb-1">Platform</label>
<select
value={platform}
onChange={(e) => setPlatform(e.target.value as Platform)}
className="w-full border rounded-lg px-3 py-2 text-sm bg-transparent"
>
{(Object.keys(PLATFORM_SIZES) as Platform[]).map((p) => (
<option key={p} value={p}>
{PLATFORM_SIZES[p].label}
</option>
))}
</select>
</div>
{(
[
{ key: 'title', label: 'Title' },
{ key: 'metric', label: 'Metric Label' },
{ key: 'value', label: 'Value' },
{ key: 'subtitle', label: 'Subtitle' },
] as { key: keyof ShareCardData; label: string }[]
).map(({ key, label }) => (
<div key={key}>
<label className="block text-sm text-gray-600 dark:text-gray-400 mb-1">{label}</label>
<input
value={cardData[key] ?? ''}
onChange={(e) => setCardData((prev) => ({ ...prev, [key]: e.target.value }))}
className="w-full border rounded-lg px-3 py-2 text-sm bg-transparent"
/>
</div>
))}
<button
onClick={handleDownload}
disabled={downloading}
className="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700 disabled:opacity-50 transition-colors"
>
{downloading ? 'Generating…' : 'Download PNG'}
</button>
</div>
{/* Preview */}
<div className="rounded-lg border bg-white dark:bg-gray-900 p-5 shadow-sm">
<h2 className="font-semibold mb-3">Preview</h2>
<div
style={{ width: 400, height: Math.round(height * scale), overflow: 'hidden', borderRadius: 8 }}
>
<canvas
ref={canvasRef}
style={{ width: 400, height: Math.round(height * scale) }}
/>
</div>
<p className="text-xs text-gray-400 mt-2">{PLATFORM_SIZES[platform].label}</p>
</div>
</div>
</div>
);
}