Skip to content

Commit 14b45a4

Browse files
authored
refactor: extract profile url logic (JhaSourav07#2240)
## Description Moves PROFILE_URL and BASE_ORIGIN out of hooks/useShareActions.ts into a centralised utils/urls.ts module, exposing: - getOrigin() — resolves the site origin with SSR fallback (window.location.origin → NEXT_PUBLIC_SITE_URL → https://commitpulse.vercel.app) - getDashboardUrl(username) — builds the full dashboard profile URL - All 8 call sites in useShareActions.ts updated to use the new helpers. Unit tests cover all three fallback paths for both functions. **Note: The PROFILE_URL function and BASE_ORIGIN are defined at the top of hooks/useShareActions.ts (not ShareSheet.tsx as the issue says).** Fixes JhaSourav07#356 ## 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 ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [ ] 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): ...`). - [x] 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. - [x] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [x] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
2 parents a48d605 + f45896e commit 14b45a4

3 files changed

Lines changed: 90 additions & 14 deletions

File tree

hooks/useShareActions.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,10 @@
22
import { useState, useRef, useEffect } from 'react';
33
import { toPng, toCanvas } from 'html-to-image';
44
import type { DashboardExportData } from '@/types/dashboard';
5+
import { getDashboardUrl, getOrigin } from '@/utils/urls';
56

67
type OptionState = 'idle' | 'loading' | 'success' | 'error';
78

8-
const BASE_ORIGIN =
9-
(typeof window !== 'undefined' ? window.location.origin : null) ??
10-
process.env.NEXT_PUBLIC_SITE_URL ??
11-
'https://commitpulse.vercel.app';
12-
const PROFILE_URL = (username: string) => `${BASE_ORIGIN}/dashboard/${username}`;
13-
149
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
1510
const XLINK_NAMESPACE = 'http://www.w3.org/1999/xlink';
1611
const UNSAFE_SVG_ELEMENTS = new Set([
@@ -48,7 +43,7 @@ function sanitizeFilenameSegment(value: string): string {
4843
}
4944

5045
function buildStreakSvgUrl(username: string): string {
51-
const url = new URL('/api/streak', BASE_ORIGIN);
46+
const url = new URL('/api/streak', getOrigin());
5247
url.searchParams.set('user', sanitizeUsernameForUrl(username));
5348
return url.toString();
5449
}
@@ -147,7 +142,7 @@ export function useShareActions(
147142
const handleCopyLink = async (): Promise<boolean> => {
148143
setOptionState('copy', 'loading');
149144
try {
150-
await navigator.clipboard.writeText(PROFILE_URL(username));
145+
await navigator.clipboard.writeText(getDashboardUrl(username));
151146
setOptionState('copy', 'success');
152147
setTimeout(() => onClose(), 800);
153148
return true;
@@ -158,20 +153,20 @@ export function useShareActions(
158153
};
159154

160155
const handleTwitter = () => {
161-
const url = PROFILE_URL(username);
156+
const url = getDashboardUrl(username);
162157
const text = encodeURIComponent(`Check out my GitHub commit pulse on CommitPulse 🚀\n${url}`);
163158
window.open(`https://twitter.com/intent/tweet?text=${text}`, '_blank', 'noopener');
164159
onClose();
165160
};
166161

167162
const handleLinkedIn = () => {
168-
const url = encodeURIComponent(PROFILE_URL(username));
163+
const url = encodeURIComponent(getDashboardUrl(username));
169164
window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${url}`, '_blank', 'noopener');
170165
onClose();
171166
};
172167

173168
const handleReddit = () => {
174-
const url = encodeURIComponent(PROFILE_URL(username));
169+
const url = encodeURIComponent(getDashboardUrl(username));
175170
const title = encodeURIComponent('Check out my CommitPulse dashboard 🚀');
176171
window.open(
177172
`https://www.reddit.com/submit?url=${url}&title=${title}`,
@@ -356,7 +351,7 @@ export function useShareActions(
356351
const rows: Array<Array<string | number>> = [
357352
['field', 'value'],
358353
['username', username],
359-
['profileUrl', PROFILE_URL(username)],
354+
['profileUrl', getDashboardUrl(username)],
360355
['exportedAt', exportedAt],
361356
['totalContributions', exportData.stats.totalContributions],
362357
['currentStreak', exportData.stats.currentStreak],
@@ -388,7 +383,7 @@ export function useShareActions(
388383

389384
const payload = {
390385
username,
391-
profileUrl: PROFILE_URL(username),
386+
profileUrl: getDashboardUrl(username),
392387
exportedAt: new Date().toISOString(),
393388
totalContributions: exportData.stats.totalContributions,
394389
currentStreak: exportData.stats.currentStreak,
@@ -422,7 +417,7 @@ export function useShareActions(
422417
await navigator.share({
423418
title: `${username}'s Commit Pulse`,
424419
text: `Check out my GitHub commit pulse on CommitPulse 🚀`,
425-
url: PROFILE_URL(username),
420+
url: getDashboardUrl(username),
426421
});
427422
setOptionState('native', 'success');
428423
setTimeout(() => onClose(), 800);

utils/urls.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { describe, it, expect, vi, afterEach } from 'vitest';
2+
import { getDashboardUrl, getOrigin } from './urls';
3+
4+
afterEach(() => {
5+
vi.unstubAllGlobals();
6+
delete process.env.NEXT_PUBLIC_SITE_URL;
7+
});
8+
9+
describe('getOrigin', () => {
10+
it('returns window.location.origin when window is defined', () => {
11+
vi.stubGlobal('window', { location: { origin: 'https://example.com' } });
12+
13+
expect(getOrigin()).toBe('https://example.com');
14+
});
15+
16+
it('falls back to NEXT_PUBLIC_SITE_URL when window is undefined', () => {
17+
vi.stubGlobal('window', undefined);
18+
process.env.NEXT_PUBLIC_SITE_URL = 'https://staging.commitpulse.app';
19+
20+
expect(getOrigin()).toBe('https://staging.commitpulse.app');
21+
});
22+
23+
it('falls back to the hardcoded origin when both window and NEXT_PUBLIC_SITE_URL are absent', () => {
24+
vi.stubGlobal('window', undefined);
25+
26+
expect(getOrigin()).toBe('https://commitpulse.vercel.app');
27+
});
28+
29+
it('treats an empty NEXT_PUBLIC_SITE_URL as absent and falls back to the hardcoded origin', () => {
30+
vi.stubGlobal('window', undefined);
31+
process.env.NEXT_PUBLIC_SITE_URL = '';
32+
33+
expect(getOrigin()).toBe('https://commitpulse.vercel.app');
34+
});
35+
36+
it('treats a whitespace-only NEXT_PUBLIC_SITE_URL as absent and falls back to the hardcoded origin', () => {
37+
vi.stubGlobal('window', undefined);
38+
process.env.NEXT_PUBLIC_SITE_URL = ' ';
39+
40+
expect(getOrigin()).toBe('https://commitpulse.vercel.app');
41+
});
42+
});
43+
44+
describe('getDashboardUrl', () => {
45+
it('returns the correct dashboard URL using window.location.origin', () => {
46+
vi.stubGlobal('window', { location: { origin: 'https://example.com' } });
47+
48+
expect(getDashboardUrl('octocat')).toBe('https://example.com/dashboard/octocat');
49+
});
50+
51+
it('falls back to NEXT_PUBLIC_SITE_URL when window is undefined', () => {
52+
vi.stubGlobal('window', undefined);
53+
process.env.NEXT_PUBLIC_SITE_URL = 'https://staging.commitpulse.app';
54+
55+
expect(getDashboardUrl('octocat')).toBe('https://staging.commitpulse.app/dashboard/octocat');
56+
});
57+
58+
it('falls back to the hardcoded origin when both window and NEXT_PUBLIC_SITE_URL are absent', () => {
59+
vi.stubGlobal('window', undefined);
60+
61+
expect(getDashboardUrl('octocat')).toBe('https://commitpulse.vercel.app/dashboard/octocat');
62+
});
63+
64+
it('includes the username in the path', () => {
65+
vi.stubGlobal('window', { location: { origin: 'https://example.com' } });
66+
67+
expect(getDashboardUrl('torvalds')).toBe('https://example.com/dashboard/torvalds');
68+
});
69+
});

utils/urls.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const FALLBACK_ORIGIN = 'https://commitpulse.vercel.app';
2+
3+
export function getOrigin(): string {
4+
const envOrigin = process.env.NEXT_PUBLIC_SITE_URL?.trim() || null;
5+
return (
6+
(typeof window !== 'undefined' ? window.location.origin : null) ?? envOrigin ?? FALLBACK_ORIGIN
7+
);
8+
}
9+
10+
export function getDashboardUrl(username: string): string {
11+
return `${getOrigin()}/dashboard/${encodeURIComponent(username)}`;
12+
}

0 commit comments

Comments
 (0)