Skip to content

Commit 8c60b5a

Browse files
authored
refactor(dashboard): extract share action handlers into useShareActions hook (JhaSourav07#630)
## Description Extracts the inlined share handler functions and state from `ShareSheet.tsx` into a new `useShareActions` custom hook to improve readability and maintainability. Fixes JhaSourav07#333 ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview N/A - This is a logic refactor only; no visual changes. ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [ ] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have started the repo. - [x] I have made sure that i have only one commit to merge in this PR. - [ ] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [ ] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
2 parents cbaa93f + e042842 commit 8c60b5a

2 files changed

Lines changed: 215 additions & 185 deletions

File tree

components/dashboard/ShareSheet.tsx

Lines changed: 17 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useState, useEffect, useRef } from 'react';
3+
import { useEffect, useRef } from 'react';
44
import { motion, AnimatePresence } from 'framer-motion';
55
import {
66
Check,
@@ -13,10 +13,9 @@ import {
1313
Smartphone,
1414
X,
1515
} from 'lucide-react';
16-
import { toPng } from 'html-to-image';
1716
import type { DashboardExportData } from '@/types/dashboard';
17+
import { useShareActions } from '@/hooks/useShareActions';
1818

19-
// Inline branded icons (Twitter/X brand, LinkedIn brand)
2019
const XBrandIcon = ({ size = 18, className = '' }: { size?: number; className?: string }) => (
2120
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" className={className}>
2221
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.744l7.73-8.835L1.254 2.25H8.08l4.259 5.63L18.244 2.25Zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
@@ -42,191 +41,34 @@ interface ShareSheetProps {
4241
exportData: DashboardExportData;
4342
}
4443

45-
type OptionState = 'idle' | 'loading' | 'success' | 'error';
46-
47-
const PROFILE_URL = (username: string) =>
48-
typeof window !== 'undefined'
49-
? `${window.location.origin}/dashboard/${username}`
50-
: `https://commitpulse.vercel.app/dashboard/${username}`;
51-
5244
export default function ShareSheet({ username, isOpen, onClose, exportData }: ShareSheetProps) {
53-
const [states, setStates] = useState<Record<string, OptionState>>({});
5445
const overlayRef = useRef<HTMLDivElement>(null);
46+
const {
47+
states,
48+
handleCopyLink,
49+
handleTwitter,
50+
handleLinkedIn,
51+
handleReddit,
52+
handleDownloadPNG,
53+
handleDownloadSVG,
54+
handleCopyMarkdown,
55+
handleDownloadJSON,
56+
handleNativeShare,
57+
} = useShareActions(username, exportData, onClose);
5558

56-
// Close on Escape
5759
useEffect(() => {
5860
const handler = (e: KeyboardEvent) => e.key === 'Escape' && onClose();
5961
window.addEventListener('keydown', handler);
6062
return () => window.removeEventListener('keydown', handler);
6163
}, [onClose]);
6264

63-
// Prevent scroll when open
6465
useEffect(() => {
6566
document.body.style.overflow = isOpen ? 'hidden' : '';
6667
return () => {
6768
document.body.style.overflow = '';
6869
};
6970
}, [isOpen]);
7071

71-
const setOptionState = (key: string, state: OptionState) => {
72-
setStates((prev) => ({ ...prev, [key]: state }));
73-
if (state === 'success' || state === 'error') {
74-
setTimeout(() => setStates((prev) => ({ ...prev, [key]: 'idle' })), 2500);
75-
}
76-
};
77-
78-
/* ── Option handlers ─────────────────────────── */
79-
80-
const handleCopyLink = async () => {
81-
setOptionState('copy', 'loading');
82-
try {
83-
await navigator.clipboard.writeText(PROFILE_URL(username));
84-
setOptionState('copy', 'success');
85-
setTimeout(() => onClose(), 800);
86-
} catch {
87-
setOptionState('copy', 'error');
88-
}
89-
};
90-
91-
const handleTwitter = () => {
92-
const url = PROFILE_URL(username);
93-
const text = encodeURIComponent(`Check out my GitHub commit pulse on CommitPulse 🚀\n${url}`);
94-
window.open(`https://twitter.com/intent/tweet?text=${text}`, '_blank', 'noopener');
95-
onClose();
96-
};
97-
98-
const handleLinkedIn = () => {
99-
const url = encodeURIComponent(PROFILE_URL(username));
100-
window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${url}`, '_blank', 'noopener');
101-
onClose();
102-
};
103-
104-
const handleReddit = () => {
105-
const url = encodeURIComponent(PROFILE_URL(username));
106-
const title = encodeURIComponent('Check out my CommitPulse dashboard 🚀');
107-
108-
window.open(`https://www.reddit.com/submit?url=${url}&title=${title}`, '_blank');
109-
110-
onClose();
111-
};
112-
113-
const handleDownloadPNG = async () => {
114-
setOptionState('png', 'loading');
115-
try {
116-
// Target the whole dashboard wrapper; fall back to body
117-
const node =
118-
document.getElementById('dashboard-root') ??
119-
document.querySelector<HTMLElement>('[data-dashboard]') ??
120-
document.body;
121-
122-
const dataUrl = await toPng(node, {
123-
quality: 0.95,
124-
pixelRatio: 2,
125-
backgroundColor: '#050505',
126-
filter: (el) => {
127-
// Exclude the share sheet itself and the generate button from the capture
128-
if (el instanceof HTMLElement) {
129-
if (el.id === 'share-sheet-overlay') return false;
130-
if (el.id === 'generate-dashboard-btn') return false;
131-
}
132-
return true;
133-
},
134-
});
135-
136-
const link = document.createElement('a');
137-
link.download = `${username}-commitpulse.png`;
138-
link.href = dataUrl;
139-
link.click();
140-
setOptionState('png', 'success');
141-
} catch {
142-
setOptionState('png', 'error');
143-
}
144-
};
145-
const handleDownloadSVG = async () => {
146-
setOptionState('svg', 'loading');
147-
try {
148-
const response = await fetch(`/api/streak?user=${encodeURIComponent(username)}`);
149-
if (!response.ok) throw new Error('Failed to fetch SVG');
150-
const svgText = await response.text();
151-
const blob = new Blob([svgText], { type: 'image/svg+xml' });
152-
const url = URL.createObjectURL(blob);
153-
const link = document.createElement('a');
154-
link.download = `${username}-commitpulse.svg`;
155-
link.href = url;
156-
link.click();
157-
URL.revokeObjectURL(url);
158-
setOptionState('svg', 'success');
159-
} catch {
160-
setOptionState('svg', 'error');
161-
}
162-
};
163-
164-
const handleCopyMarkdown = async () => {
165-
setOptionState('markdown', 'loading');
166-
try {
167-
const markdown = `![CommitPulse](${window.location.origin}/api/streak?user=${encodeURIComponent(username)})`;
168-
await navigator.clipboard.writeText(markdown);
169-
setOptionState('markdown', 'success');
170-
setTimeout(() => onClose(), 800);
171-
} catch {
172-
setOptionState('markdown', 'error');
173-
}
174-
};
175-
176-
const handleDownloadJSON = () => {
177-
setOptionState('json', 'loading');
178-
try {
179-
const payload = {
180-
username,
181-
profileUrl: PROFILE_URL(username),
182-
exportedAt: new Date().toISOString(),
183-
currentStreak: exportData.stats.currentStreak,
184-
longestStreak: exportData.stats.peakStreak,
185-
totalContributions: exportData.stats.totalContributions,
186-
topLanguages: exportData.languages,
187-
};
188-
const blob = new Blob([JSON.stringify(payload, null, 2)], {
189-
type: 'application/json',
190-
});
191-
const url = URL.createObjectURL(blob);
192-
const link = document.createElement('a');
193-
link.download = `commitpulse-${username}.json`;
194-
link.href = url;
195-
link.click();
196-
URL.revokeObjectURL(url);
197-
setOptionState('json', 'success');
198-
} catch {
199-
setOptionState('json', 'error');
200-
}
201-
};
202-
203-
const handleNativeShare = async () => {
204-
if (!('share' in navigator)) {
205-
// Graceful fallback: just copy the link
206-
await handleCopyLink();
207-
return;
208-
}
209-
setOptionState('native', 'loading');
210-
try {
211-
await navigator.share({
212-
title: `${username}'s Commit Pulse`,
213-
text: `Check out my GitHub contribution pulse — streaks, insights, and more.`,
214-
url: PROFILE_URL(username),
215-
});
216-
setOptionState('native', 'success');
217-
setTimeout(() => onClose(), 600);
218-
} catch (err) {
219-
// User cancelled — not an error worth showing
220-
if (err instanceof Error && err.name !== 'AbortError') {
221-
setOptionState('native', 'error');
222-
} else {
223-
setOptionState('native', 'idle');
224-
}
225-
}
226-
};
227-
228-
/* ── Option config ───────────────────────────── */
229-
23072
const options = [
23173
{
23274
key: 'copy',
@@ -255,7 +97,6 @@ export default function ShareSheet({ username, isOpen, onClose, exportData }: Sh
25597
glow: 'rgba(37,99,235,0.35)',
25698
action: handleLinkedIn,
25799
},
258-
259100
{
260101
key: 'markdown',
261102
icon: Code,
@@ -309,20 +150,19 @@ export default function ShareSheet({ username, isOpen, onClose, exportData }: Sh
309150
},
310151
{
311152
key: 'reddit',
153+
icon: RedditIcon,
312154
label: 'Reddit',
313155
description: 'Share on Reddit',
314-
icon: RedditIcon,
315-
action: handleReddit,
316156
gradient: 'from-orange-500 to-orange-700',
317157
glow: 'rgba(249,115,22,0.35)',
158+
action: handleReddit,
318159
},
319160
];
320161

321162
return (
322163
<AnimatePresence>
323164
{isOpen && (
324165
<>
325-
{/* Backdrop */}
326166
<motion.div
327167
id="share-sheet-overlay"
328168
ref={overlayRef}
@@ -333,7 +173,6 @@ export default function ShareSheet({ username, isOpen, onClose, exportData }: Sh
333173
onClick={onClose}
334174
className="fixed inset-0 z-50 bg-black/60 backdrop-blur-[2px] flex items-center justify-center p-4"
335175
>
336-
{/* Panel */}
337176
<motion.div
338177
initial={{ opacity: 0, y: 16, scale: 0.98 }}
339178
animate={{ opacity: 1, y: 0, scale: 1 }}
@@ -342,8 +181,7 @@ export default function ShareSheet({ username, isOpen, onClose, exportData }: Sh
342181
className="relative w-full max-w-sm"
343182
onClick={(e) => e.stopPropagation()}
344183
>
345-
<div className="rounded-xl bg-white/60 dark:bg-white/[0.05] dark:bg-white/[0.05] backdrop-blur-xl border border-black/10 dark:border-white/10 shadow-[0_24px_64px_rgba(0,0,0,0.18)] dark:shadow-[0_24px_64px_rgba(0,0,0,0.7)] overflow-hidden">
346-
{/* Header */}
184+
<div className="rounded-xl bg-white/60 dark:bg-white/[0.05] backdrop-blur-xl border border-black/10 dark:border-white/10 shadow-[0_24px_64px_rgba(0,0,0,0.18)] dark:shadow-[0_24px_64px_rgba(0,0,0,0.7)] overflow-hidden">
347185
<div className="flex items-center justify-between px-5 pt-5 pb-4 border-b border-white/10">
348186
<div>
349187
<h2 className="text-sm font-semibold text-gray-900 dark:text-white tracking-tight">
@@ -359,13 +197,10 @@ export default function ShareSheet({ username, isOpen, onClose, exportData }: Sh
359197
<X size={14} className="text-gray-500 dark:text-white/45" />
360198
</button>
361199
</div>
362-
363-
{/* Options */}
364200
<div className="flex flex-col p-3 gap-1">
365201
{options.map((opt, idx) => {
366202
const state = states[opt.key] ?? 'idle';
367203
const Icon = opt.icon;
368-
369204
return (
370205
<motion.button
371206
key={opt.key}
@@ -376,7 +211,6 @@ export default function ShareSheet({ username, isOpen, onClose, exportData }: Sh
376211
disabled={state === 'loading'}
377212
className="group flex items-center gap-3 w-full px-3 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-white/[0.06] border border-transparent hover:border-white/10 transition-all duration-200 text-left disabled:opacity-40 disabled:cursor-not-allowed"
378213
>
379-
{/* Icon box */}
380214
<div className="flex-shrink-0 w-8 h-8 rounded-lg bg-gray-100 dark:bg-white/[0.04] border border-[rgba(255,255,255,0.08)] flex items-center justify-center">
381215
{state === 'loading' ? (
382216
<Loader2
@@ -392,8 +226,6 @@ export default function ShareSheet({ username, isOpen, onClose, exportData }: Sh
392226
/>
393227
)}
394228
</div>
395-
396-
{/* Label */}
397229
<div className="flex flex-col min-w-0">
398230
<span className="text-sm text-gray-900 dark:text-white font-medium leading-tight">
399231
{state === 'success'

0 commit comments

Comments
 (0)