Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions app/customize/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { useCallback, useEffect, useRef, useState, type ReactElement } from 'react';
import { validateGitHubUsername } from '@/lib/validations';
import Link from 'next/link';
import { motion } from 'framer-motion';
import { ControlsPanel } from './components/ControlsPanel';
Expand Down Expand Up @@ -121,6 +122,12 @@ export default function CustomizePage(): ReactElement {
setSvgState('idle');
return;
}
if (!validateGitHubUsername(trimmedUsername)) {
setSvgContent('');
setSvgState('error');
setErrorMessage("That doesn't look like a valid GitHub username");
return;
}

setSvgState('loading');
const controller = new AbortController();
Expand Down Expand Up @@ -192,7 +199,7 @@ export default function CustomizePage(): ReactElement {
});

return () => controller.abort();
}, [previewSrc, hasUsername]);
}, [previewSrc, hasUsername, trimmedUsername]);

const exportSnippet = getExportSnippet(exportFormat, queryString);

Expand Down Expand Up @@ -395,6 +402,14 @@ export default function CustomizePage(): ReactElement {
Loading preview...
</div>
)}
{svgState === 'error' &&
errorMessage === "That doesn't look like a valid GitHub username" && (
<div className="flex flex-col items-center justify-center gap-2 text-center py-8">
<p className="text-sm font-semibold text-red-500 dark:text-red-400">
{errorMessage}
</p>
</div>
)}
{svgState === 'error' && errorMessage === 'GitHub user not found' && (
<div className="flex flex-col items-center justify-center gap-4 py-12 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-3xl border border-red-500/20 bg-red-500/10 shadow-inner">
Expand Down Expand Up @@ -422,16 +437,18 @@ export default function CustomizePage(): ReactElement {
</div>
</div>
)}
{svgState === 'error' && errorMessage !== 'GitHub user not found' && (
<div className="flex flex-col items-center justify-center gap-2 text-center py-8">
<p className="text-sm font-semibold text-red-500 dark:text-red-400">
Failed to load badge
</p>
<p className="text-xs text-gray-500 dark:text-white/45">
The API may be unavailable. Please try again.
</p>
</div>
)}
{svgState === 'error' &&
errorMessage !== 'GitHub user not found' &&
errorMessage !== "That doesn't look like a valid GitHub username" && (
<div className="flex flex-col items-center justify-center gap-2 text-center py-8">
<p className="text-sm font-semibold text-red-500 dark:text-red-400">
Failed to load badge
</p>
<p className="text-xs text-gray-500 dark:text-white/45">
The API may be unavailable. Please try again.
</p>
</div>
)}
{svgState === 'loaded' && svgContent && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
Expand All @@ -446,8 +463,8 @@ export default function CustomizePage(): ReactElement {
)}
</div>
) : (
<div className="relative z-10 flex w-full max-w-xl flex-col items-center justify-center rounded-[1.25rem] border border-dashed border-black/10 bg-gray-100/80 backdrop-blur-md dark:border-white/10 dark:bg-white/[0.03] px-6 py-12 text-center">
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl border border-black/10 bg-gray-100/80 dark:border-white/10 dark:bg-white/[0.04] text-gray-500 dark:text-emerald-300/70">
<div className="relative z-10 flex w-full max-w-xl flex-col items-center justify-center rounded-[1.25rem] border border-dashed border-black/10 bg-gray-100/80 backdrop-blur-md dark:border-white/10 dark:bg-white/3 px-6 py-12 text-center">
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl border border-black/10 bg-gray-100/80 dark:border-white/10 dark:bg-white/4 text-gray-500 dark:text-emerald-300/70">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
Expand Down
14 changes: 0 additions & 14 deletions lib/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
buildCommitClock,
clearGitHubApiCacheForTests,
GITHUB_CACHE_TTL_MS,
validateGitHubUsername,
cacheKey,
displayName,
fetchOrgMembers,
Expand Down Expand Up @@ -1690,19 +1689,6 @@ describe('displayName', () => {
});
});

describe('validateGitHubUsername', () => {
it('returns true for a valid username', () => {
expect(validateGitHubUsername('valid-username-123')).toBe(true);
});

it('returns false for a too long username', () => {
expect(validateGitHubUsername('a'.repeat(40))).toBe(false);
});

it('returns false for a username with underscore', () => {
expect(validateGitHubUsername('invalid_username')).toBe(false);
});
});
describe('cacheKey', () => {
it('creates key without year', () => {
expect(cacheKey('profile', 'DeepSikha')).toBe('profile:deepsikha');
Expand Down
4 changes: 0 additions & 4 deletions lib/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,6 @@ const getHeaders = () => ({
'Content-Type': 'application/json',
});

export function validateGitHubUsername(username: string): boolean {
return /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i.test(username);
}

export function displayName(profile: GitHubUserProfile): string {
if (typeof profile.name === 'string' && profile.name.trim() !== '') return profile.name;
return profile.login;
Expand Down
37 changes: 36 additions & 1 deletion lib/validations.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { describe, expect, it } from 'vitest';
import { githubParamsSchema, ogParamsSchema, streakParamsSchema } from './validations';
import {
githubParamsSchema,
ogParamsSchema,
streakParamsSchema,
validateGitHubUsername,
} from './validations';

describe('streakParamsSchema — grace fallback behavior', () => {
it('accepts "0" as a valid grace value', () => {
Expand Down Expand Up @@ -40,6 +45,36 @@ describe('streakParamsSchema — grace fallback behavior', () => {
});
});

describe('validateGitHubUsername', () => {
it('returns true for a valid username', () => {
expect(validateGitHubUsername('valid-username-123')).toBe(true);
});

it('returns false for a too long username', () => {
expect(validateGitHubUsername('a'.repeat(40))).toBe(false);
});

it('returns false for a username with underscore', () => {
expect(validateGitHubUsername('invalid_username')).toBe(false);
});

it('returns false for empty string', () => {
expect(validateGitHubUsername('')).toBe(false);
});

it('returns false for leading hyphen', () => {
expect(validateGitHubUsername('-octocat')).toBe(false);
});

it('returns false for trailing hyphen', () => {
expect(validateGitHubUsername('octocat-')).toBe(false);
});

it('returns false for consecutive hyphens', () => {
expect(validateGitHubUsername('octo--cat')).toBe(false);
});
});

describe('githubParamsSchema', () => {
it('should pass when username is valid', () => {
const result = githubParamsSchema.safeParse({
Expand Down
4 changes: 4 additions & 0 deletions lib/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export function toDimensionValue(val?: string): number | undefined {
return val === undefined ? undefined : Number(val);
}

export function validateGitHubUsername(username: string): boolean {
return /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i.test(username);
}

function dimensionParam(name: string, min: number, max: number) {
return z
.string()
Expand Down
Loading