Social Profiles
Connect your social media and coding platforms
{/* Instructions Banner */}
Enter usernames only, not full URLs
Just provide your username or handle for each platform. We'll automatically generate
the correct URLs.
✓
Correct: johndoe
✗
Incorrect: https://twitter.com/johndoe
{socialPlatforms.map(({ key, label, icon, placeholder }) => (
))}
{/* Twitter Badge Option - Show always with disabled state and hint */}
🐦
Twitter Enhancement
{!watch('twitter') && (
Enter your Twitter username above to enable this feature
)}
);
}
================================================
FILE: src/components/ui/accessibility-menu.tsx
================================================
'use client';
import { useState } from 'react';
import { Settings } from 'lucide-react';
import { Select } from '@/components/ui/select';
import { useThemeStore } from '@/lib/store';
export function AccessibilityMenu() {
const [isOpen, setIsOpen] = useState(false);
const { accessibility, setAccessibility } = useThemeStore();
// Font size options for the select component
const fontSizeOptions = [
{ value: 'small', label: 'Small' },
{ value: 'medium', label: 'Medium (Default)' },
{ value: 'large', label: 'Large' },
];
return (
{isOpen && (
<>
{/* Backdrop */}
setIsOpen(false)} aria-hidden="true" />
{/* Menu */}
Accessibility Settings
These settings are saved locally and persist across sessions.
>
)}
);
}
================================================
FILE: src/components/ui/buy-me-coffee.tsx
================================================
'use client';
import { useEffect } from 'react';
export function BuyMeACoffeeWidget() {
useEffect(() => {
const script = document.createElement('script');
script.setAttribute('data-name', 'BMC-Widget');
script.src = 'https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js';
script.setAttribute('data-id', 'rahuldkjain');
script.setAttribute('data-description', 'Support rahuldkjain on Buy me a coffee!');
script.setAttribute('data-message', '');
script.setAttribute('data-color', '#ffdd00');
script.setAttribute('data-position', 'Right');
script.setAttribute('data-x_margin', '18');
script.setAttribute('data-y_margin', '18');
script.async = true;
script.onload = function () {
const event = new CustomEvent('DOMContentLoaded', {
bubbles: true,
cancelable: true,
});
window.dispatchEvent(event);
};
document.head.appendChild(script);
return () => {
document.head.removeChild(script);
const widget = document.getElementById('bmc-wbtn');
if (widget) {
document.body.removeChild(widget);
}
};
}, []);
return null;
}
================================================
FILE: src/components/ui/collapsible-section.tsx
================================================
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ChevronDown } from 'lucide-react';
interface CollapsibleSectionProps {
title: string;
description?: string;
icon?: string;
defaultOpen?: boolean;
children: React.ReactNode;
}
export function CollapsibleSection({
title,
description,
icon,
defaultOpen = false,
children,
}: CollapsibleSectionProps) {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
{isOpen && (
{children}
)}
);
}
================================================
FILE: src/components/ui/confirm-dialog.tsx
================================================
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { AlertTriangle, X } from 'lucide-react';
interface ConfirmDialogProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
message: string;
confirmText?: string;
cancelText?: string;
variant?: 'danger' | 'warning' | 'info';
}
export function ConfirmDialog({
isOpen,
onClose,
onConfirm,
title,
message,
confirmText = 'Confirm',
cancelText = 'Cancel',
variant = 'danger',
}: ConfirmDialogProps) {
const handleConfirm = () => {
onConfirm();
onClose();
};
const variantStyles = {
danger: {
icon: 'text-red-600',
confirmButton: 'bg-red-600 hover:bg-red-700 text-white',
backdrop: 'bg-red-50',
},
warning: {
icon: 'text-amber-600',
confirmButton: 'bg-amber-600 hover:bg-amber-700 text-white',
backdrop: 'bg-amber-50',
},
info: {
icon: 'text-blue-600',
confirmButton: 'bg-blue-600 hover:bg-blue-700 text-white',
backdrop: 'bg-blue-50',
},
};
const styles = variantStyles[variant];
return (
{isOpen && (
<>
{/* Backdrop */}
{/* Dialog */}
{/* Header */}
{/* Actions */}
>
)}
);
}
/**
* Hook for using confirmation dialogs
*/
export function useConfirmDialog() {
const [isOpen, setIsOpen] = useState(false);
const [config, setConfig] = useState<
Omit
>({
title: '',
message: '',
});
const [confirmCallback, setConfirmCallback] = useState<(() => void) | null>(null);
const showConfirm = (
options: Omit & {
onConfirm: () => void;
}
) => {
const { onConfirm, ...rest } = options;
setConfig(rest);
setConfirmCallback(() => onConfirm);
setIsOpen(true);
};
const closeDialog = () => {
setIsOpen(false);
// Clear callback after a small delay to prevent race conditions
setTimeout(() => {
setConfirmCallback(null);
}, 100);
};
const handleConfirm = () => {
if (confirmCallback) {
confirmCallback();
}
closeDialog();
};
const ConfirmDialogComponent = () => (
);
return {
showConfirm,
ConfirmDialog: ConfirmDialogComponent,
};
}
================================================
FILE: src/components/ui/cookie-consent.tsx
================================================
'use client';
import { motion, AnimatePresence } from 'framer-motion';
import { Cookie, ShieldCheck } from 'lucide-react';
import { useConsent } from '@/hooks/use-consent';
export function CookieConsent() {
const { showBanner, acceptConsent, rejectConsent } = useConsent();
return (
{showBanner && (
<>
{/* Backdrop */}
{/* Consent Banner */}
{/* Header */}
We use cookies to improve your experience
We use Google Analytics to understand how you interact with our site and improve
your experience. Your data is anonymized and we don't track personal
information.
{/* Privacy Info */}
Privacy-friendly tracking
- • IP addresses are anonymized
- • No personal data is collected
- • You can opt-out anytime
{/* Actions */}
{/* Learn More Link */}
>
)}
);
}
/**
* Privacy Settings Component (for settings page or footer)
*/
export function PrivacySettings() {
const { status, resetConsent } = useConsent();
if (status === 'pending') {
return null;
}
return (
Analytics Cookies
Status: {status === 'accepted' ? 'Enabled' : 'Disabled'}
);
}
================================================
FILE: src/components/ui/github-stats.tsx
================================================
'use client';
import { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { useErrorToast } from '@/components/ui/toast';
interface GitHubStats {
stars: number;
forks: number;
}
export function GitHubStats() {
const [stats, setStats] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(false);
const errorToast = useErrorToast();
useEffect(() => {
// Smart request logic like original
const shouldRequestStats = () => {
const isFirstRequest = stats === null;
const isVisible = typeof window !== 'undefined' && document.visibilityState === 'visible';
const hasFocus = typeof window !== 'undefined' && document.hasFocus();
return isFirstRequest || (isVisible && hasFocus);
};
const fetchStats = async () => {
if (!shouldRequestStats()) return;
try {
const response = await fetch(
'https://api.github.com/repos/rahuldkjain/github-profile-readme-generator'
);
if (!response.ok) {
if (response.status === 403) {
const rateLimitRemaining = response.headers.get('X-RateLimit-Remaining');
if (rateLimitRemaining === '0') {
console.warn('GitHub API rate limit exceeded for stats');
setError(true);
setIsLoading(false);
return;
}
}
throw new Error(`Failed to fetch stats (${response.status})`);
}
const data = await response.json();
setStats({
stars: data.stargazers_count || 0,
forks: data.forks_count || 0,
});
setIsLoading(false);
} catch (err) {
console.error('Error fetching GitHub stats:', err);
setError(true);
setIsLoading(false);
// Only show error toast on first load, not on periodic refreshes
if (stats === null) {
errorToast(
'Failed to load GitHub stats',
"Unable to fetch repository statistics. This won't affect the generator functionality.",
{
label: 'Retry',
onClick: fetchStats,
}
);
}
}
};
// Initial fetch
fetchStats();
// Periodic refresh like original (every minute)
const interval = setInterval(fetchStats, 60000);
return () => clearInterval(interval);
}, []); // Remove stats dependency to prevent infinite loop
if (error || isLoading) {
return null; // Don't show anything if loading or error
}
if (!stats) {
return null;
}
return (
{/* Star Button - Styled like original */}
Stars
{stats.stars.toLocaleString()}
{/* Fork Button - Styled like original */}
Forks
{stats.forks.toLocaleString()}
);
}
================================================
FILE: src/components/ui/markdown-preview.tsx
================================================
'use client';
import { useState, memo, useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import { trackFileExported } from '@/lib/analytics';
interface MarkdownPreviewProps {
markdown: string;
title?: string;
}
export const MarkdownPreview = memo(function MarkdownPreview({
markdown,
title = 'Preview',
}: MarkdownPreviewProps) {
const [viewMode, setViewMode] = useState<'preview' | 'markdown'>('preview');
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(markdown);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
// Track copy event
trackFileExported('copy', 'markdown');
} catch (err) {
console.error('Failed to copy:', err);
}
};
const handleDownload = () => {
const blob = new Blob([markdown], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'README.md';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// Track download event
trackFileExported('download', 'markdown');
};
// Memoize custom components to prevent ReactMarkdown re-renders
const markdownComponents = useMemo(
() => ({
// Custom rendering to maintain alignment and styling
p: ({ children }: any) => {children}
,
h1: ({ children }: any) => {children}
,
h3: ({ children }: any) => {children}
,
img: ({ src, alt, width }: any) => {
// Skill icons have width=40, resize them responsively
if (width === '40' || width === 40) {
return (
);
}
// Other images (badges, stats, etc.) keep their original size
return
;
},
a: ({ href, children }: any) => (
{children}
),
}),
[] // Empty deps - components never change
);
return (
{/* Header with actions - Aligned layout */}
{title}
{/* View Mode Toggle - With border */}
{/* Action buttons - Aligned to end */}
{/* Copy Button */}
{/* Download Button */}
{/* Content */}
{/* Copy success announcement for screen readers */}
{copied && (
Markdown copied to clipboard successfully
)}
{viewMode === 'preview' ? (
{markdown}
) : (
{markdown}
)}
);
});
================================================
FILE: src/components/ui/select.tsx
================================================
'use client';
import { Fragment, forwardRef } from 'react';
import {
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
Transition,
} from '@headlessui/react';
import { ChevronDown, Check } from 'lucide-react';
export interface SelectOption {
value: string;
label: string;
}
export interface SelectProps {
label?: string;
error?: string;
helperText?: string;
placeholder?: string;
options: SelectOption[];
value?: string;
onChange?: (value: string) => void;
disabled?: boolean;
required?: boolean;
id?: string;
className?: string;
}
export const Select = forwardRef(
(
{
label,
error,
helperText,
placeholder = 'Select an option',
options,
value,
onChange,
disabled = false,
required = false,
id,
className = '',
},
ref
) => {
const selectedOption = options.find((option) => option.value === value);
return (
{label && (
)}
{selectedOption ? selectedOption.label : placeholder}
{options.map((option) => (
`relative cursor-default py-2 pr-4 pl-10 transition-colors select-none ${
active ? 'bg-accent text-accent-foreground' : ''
}`
}
value={option.value}
>
{({ selected }) => (
<>
{option.label}
{selected ? (
) : null}
>
)}
))}
{error && (
{error}
)}
{helperText && !error &&
{helperText}
}
);
}
);
Select.displayName = 'Select';
================================================
FILE: src/components/ui/theme-toggle.tsx
================================================
'use client';
import { useEffect, useState } from 'react';
import { Sun, Moon, Monitor } from 'lucide-react';
import { useTheme } from '@/hooks/use-theme';
import type { ThemeMode } from '@/types/theme';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
// Prevent hydration mismatch by only rendering after mount
useEffect(() => {
setMounted(true);
}, []);
const handleToggle = () => {
const modes: ThemeMode[] = ['light', 'dark', 'system'];
const currentIndex = modes.indexOf(theme);
const nextIndex = (currentIndex + 1) % modes.length;
setTheme(modes[nextIndex]);
};
const getIcon = () => {
switch (theme) {
case 'light':
return ;
case 'dark':
return ;
case 'system':
return ;
}
};
// Render a skeleton button during SSR to prevent layout shift
if (!mounted) {
return (
);
}
return (
);
}
================================================
FILE: src/components/ui/toast.tsx
================================================
'use client';
import { createContext, useContext, useState, useCallback, ReactNode } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { CheckCircle, XCircle, AlertTriangle, Info, X, Loader2 } from 'lucide-react';
interface Toast {
id: string;
type: 'success' | 'error' | 'warning' | 'info' | 'loading';
title: string;
description?: string;
duration?: number;
action?: {
label: string;
onClick: () => void;
};
dismissible?: boolean;
}
interface ToastContextType {
toasts: Toast[];
addToast: (toast: Omit) => void;
removeToast: (id: string) => void;
updateToast: (id: string, updates: Partial) => void;
promise: (
promise: Promise,
options: {
loading: string;
success: string | ((data: T) => string);
error: string | ((error: any) => string);
}
) => Promise;
}
const ToastContext = createContext(undefined);
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState([]);
const addToast = useCallback((toast: Omit) => {
const id =
typeof crypto !== 'undefined' && crypto.randomUUID
? crypto.randomUUID()
: Math.random().toString(36).substring(2, 11);
const duration = toast.duration ?? (toast.type === 'loading' ? 0 : 5000);
const dismissible = toast.dismissible ?? true;
const newToast: Toast = {
...toast,
id,
duration,
dismissible,
};
setToasts((prev) => [...prev, newToast]);
// Auto-remove toast after duration (except loading toasts)
if (duration > 0 && toast.type !== 'loading') {
setTimeout(() => {
removeToast(id);
}, duration);
}
return id;
}, []);
const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
}, []);
const updateToast = useCallback((id: string, updates: Partial) => {
setToasts((prev) => prev.map((toast) => (toast.id === id ? { ...toast, ...updates } : toast)));
}, []);
const promiseToast = useCallback(
async (
promise: Promise,
options: {
loading: string;
success: string | ((data: T) => string);
error: string | ((error: any) => string);
}
): Promise => {
const loadingId = addToast({
type: 'loading',
title: options.loading,
duration: 0,
dismissible: false,
});
try {
const data = await promise;
// Remove loading toast and show success
removeToast(loadingId);
addToast({
type: 'success',
title: typeof options.success === 'function' ? options.success(data) : options.success,
duration: 4000,
});
return data;
} catch (error) {
// Remove loading toast and show error
removeToast(loadingId);
addToast({
type: 'error',
title: typeof options.error === 'function' ? options.error(error) : options.error,
duration: 6000,
});
throw error;
}
},
[addToast, removeToast]
);
return (
{children}
);
}
export function useToast() {
const context = useContext(ToastContext);
if (context === undefined) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
}
function ToastContainer() {
const { toasts, removeToast } = useToast();
return (
{toasts.map((toast) => (
))}
);
}
function ToastItem({ toast, onRemove }: { toast: Toast; onRemove: (id: string) => void }) {
const getToastStyles = () => {
switch (toast.type) {
case 'success':
return 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900 dark:border-green-700 dark:text-green-100';
case 'error':
return 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900 dark:border-red-700 dark:text-red-100';
case 'warning':
return 'bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900 dark:border-yellow-700 dark:text-yellow-100';
case 'info':
return 'bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-900 dark:border-blue-700 dark:text-blue-100';
case 'loading':
return 'bg-gray-50 border-gray-200 text-gray-800 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100';
default:
return 'bg-card border-border text-foreground';
}
};
const getIcon = () => {
switch (toast.type) {
case 'success':
return ;
case 'error':
return ;
case 'warning':
return ;
case 'info':
return ;
case 'loading':
return ;
}
};
return (
{getIcon()}
{toast.title}
{toast.description &&
{toast.description}
}
{toast.action && (
)}
{toast.dismissible !== false && (
)}
);
}
// Convenience hooks for different toast types
export function useSuccessToast() {
const { addToast } = useToast();
return useCallback(
(title: string, description?: string) => {
addToast({ type: 'success', title, description });
},
[addToast]
);
}
export function useErrorToast() {
const { addToast } = useToast();
return useCallback(
(title: string, description?: string, action?: Toast['action']) => {
addToast({ type: 'error', title, description, action, duration: 8000 });
},
[addToast]
);
}
export function useWarningToast() {
const { addToast } = useToast();
return useCallback(
(title: string, description?: string) => {
addToast({ type: 'warning', title, description });
},
[addToast]
);
}
export function useInfoToast() {
const { addToast } = useToast();
return useCallback(
(title: string, description?: string) => {
addToast({ type: 'info', title, description });
},
[addToast]
);
}
================================================
FILE: src/constants/defaults.ts
================================================
import type {
ProfilePrefix,
ProfileData,
ProfileLinks,
SocialLinks,
SupportLinks,
} from '@/types/profile';
export const DEFAULT_PREFIX: ProfilePrefix = {
title: "Hi 👋, I'm",
currentWork: "🔭 I'm currently working on",
currentLearn: "🌱 I'm currently learning",
collaborateOn: "👯 I'm looking to collaborate on",
helpWith: "🤝 I'm looking for help with",
ama: '💬 Ask me about',
contact: '📫 How to reach me',
resume: '📄 Know about my experiences',
funFact: '⚡ Fun fact',
portfolio: '👨💻 All of my projects are available at',
blog: '📝 I regularly write articles on',
};
export const DEFAULT_DATA: ProfileData = {
title: '',
subtitle: '',
currentWork: '',
currentLearn: '',
collaborateOn: '',
helpWith: '',
ama: '',
contact: '',
funFact: '',
visitorsBadge: false,
badgeStyle: 'flat',
badgeColor: '0e75b6',
badgeLabel: 'Profile views',
githubProfileTrophy: false,
githubStats: false,
githubStatsOptions: {
theme: '',
titleColor: '',
textColor: '',
bgColor: '',
hideBorder: false,
cacheSeconds: null,
locale: 'en',
},
topLanguages: false,
topLanguagesOptions: {
theme: '',
titleColor: '',
textColor: '',
bgColor: '',
hideBorder: false,
cacheSeconds: null,
locale: 'en',
},
streakStats: false,
streakStatsOptions: {
theme: '',
},
devDynamicBlogs: false,
mediumDynamicBlogs: false,
rssDynamicBlogs: false,
};
export const DEFAULT_LINK: ProfileLinks = {
currentWork: '',
collaborateOn: '',
helpWith: '',
portfolio: '',
blog: '',
resume: '',
};
export const DEFAULT_SOCIAL: SocialLinks = {
github: '',
dev: '',
linkedin: '',
codepen: '',
stackoverflow: '',
kaggle: '',
codesandbox: '',
fb: '',
instagram: '',
twitter: '',
dribbble: '',
behance: '',
medium: '',
youtube: '',
codechef: '',
hackerrank: '',
codeforces: '',
leetcode: '',
topcoder: '',
hackerearth: '',
geeks_for_geeks: '',
discord: '',
rssurl: '',
twitterBadge: false,
};
export const DEFAULT_SUPPORT: SupportLinks = {
buyMeACoffee: '',
};
================================================
FILE: src/constants/skills.ts
================================================
import type { CategorizedSkills, SkillState } from '@/types/skills';
export const categorizedSkills: CategorizedSkills = {
language: {
title: 'Programming Languages',
skills: [
'c',
'cplusplus',
'csharp',
'go',
'java',
'javascript',
'typescript',
'php',
'perl',
'ruby',
'scala',
'python',
'swift',
'objectivec',
'clojure',
'rust',
'haskell',
'coffeescript',
'elixir',
'erlang',
],
},
frontend_dev: {
title: 'Frontend Development',
skills: [
'vuejs',
'react',
'svelte',
'angularjs',
'angular',
'backbonejs',
'bootstrap',
'vuetify',
'css3',
'html5',
'pug',
'gulp',
'sass',
'redux',
'webpack',
'babel',
'tailwind',
'materialize',
'bulma',
'gtk',
'qt',
'ember',
],
},
backend_dev: {
title: 'Backend Development',
skills: [
'nodejs',
'spring',
'express',
'graphql',
'kafka',
'solr',
'rabbitMQ',
'hadoop',
'nginx',
'openresty',
'nestjs',
],
},
mobile_dev: {
title: 'Mobile App Development',
skills: [
'android',
'flutter',
'dart',
'kotlin',
'nativescript',
'xamarin',
'reactnative',
'ionic',
'apachecordova',
],
},
ai: {
title: 'AI/ML',
skills: [
// Core ML Frameworks
'tensorflow',
'pytorch',
'keras',
'scikit_learn',
'opencv',
// Data Science
'pandas',
'numpy',
'matplotlib',
'seaborn',
'jupyter',
'anaconda',
// Modern AI Tools
'langchain',
'huggingface',
'ollama',
// ML Ops & Deployment
'mlflow',
'streamlit',
'fastapi',
'gradio',
],
},
database: {
title: 'Database',
skills: [
'mongodb',
'mysql',
'postgresql',
'redis',
'oracle',
'cassandra',
'couchdb',
'hive',
'realm',
'mariadb',
'cockroachdb',
'elasticsearch',
'sqlite',
'mssql',
],
},
data_visualization: {
title: 'Data Visualization',
skills: ['d3js', 'chartjs', 'canvasjs', 'kibana', 'grafana'],
},
devops: {
title: 'Devops',
skills: [
'aws',
'docker',
'jenkins',
'gcp',
'kubernetes',
'bash',
'azure',
'vagrant',
'circleci',
'travisci',
],
},
baas: {
title: 'Backend as a Service(BaaS)',
skills: ['firebase', 'appwrite', 'amplify', 'heroku'],
},
framework: {
title: 'Framework',
skills: [
'django',
'dotnet',
'electron',
'symfony',
'laravel',
'codeigniter',
'rails',
'flask',
'quasar',
],
},
testing: {
title: 'Testing',
skills: ['cypress', 'selenium', 'jest', 'mocha', 'puppeteer', 'karma', 'jasmine'],
},
software: {
title: 'Software',
skills: [
'illustrator',
'photoshop',
'xd',
'figma',
'blender',
'sketch',
'invision',
'framer',
'matlab',
'postman',
],
},
static_site_generator: {
title: 'Static Site Generators',
skills: [
'gatsby',
'gridsome',
'hugo',
'jekyll',
'nextjs',
'nuxtjs',
'11ty',
'scully',
'sculpin',
'vuepress',
'hexo',
'middleman',
],
},
game_engines: {
title: 'Game Engines',
skills: ['unity', 'unreal'],
},
automation: {
title: 'Automation',
skills: ['zapier', 'ifttt'],
},
other: {
title: 'Other',
skills: ['linux', 'git', 'arduino'],
},
};
// Get all skills as a flat array
const skillsArray = Object.keys(categorizedSkills).map((key) => categorizedSkills[key].skills);
export const skills = skillsArray.flat().sort();
// Initialize skill state
export const initialSkillState: SkillState = {};
skills.forEach((skill) => {
initialSkillState[skill] = false;
});
// Export categories
export const categories = Object.keys(categorizedSkills);
================================================
FILE: src/hooks/use-consent.ts
================================================
'use client';
import { useState, useEffect } from 'react';
export type ConsentStatus = 'pending' | 'accepted' | 'rejected';
interface ConsentState {
status: ConsentStatus;
showBanner: boolean;
acceptConsent: () => void;
rejectConsent: () => void;
resetConsent: () => void;
}
/**
* Hook for managing GDPR cookie consent
*/
export function useConsent(): ConsentState {
const [status, setStatus] = useState('pending');
const [showBanner, setShowBanner] = useState(false);
// Load consent status from localStorage on mount
useEffect(() => {
if (typeof window === 'undefined') return;
try {
const savedConsent = localStorage.getItem('analytics-consent');
const consentTimestamp = localStorage.getItem('analytics-consent-timestamp');
if (savedConsent && consentTimestamp) {
// Check if consent is still valid (30 days)
const consentDate = new Date(consentTimestamp);
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
if (consentDate > thirtyDaysAgo) {
setStatus(savedConsent as ConsentStatus);
setShowBanner(false);
} else {
// Consent expired, show banner again
setStatus('pending');
setShowBanner(true);
}
} else {
// No consent found, show banner
setStatus('pending');
setShowBanner(true);
}
} catch (error) {
console.warn('Failed to load consent status:', error);
setStatus('pending');
setShowBanner(true);
}
}, []);
const acceptConsent = () => {
try {
localStorage.setItem('analytics-consent', 'accepted');
localStorage.setItem('analytics-consent-timestamp', new Date().toISOString());
setStatus('accepted');
setShowBanner(false);
// Reload page to initialize analytics
if (typeof window !== 'undefined') {
window.location.reload();
}
} catch (error) {
console.warn('Failed to save consent:', error);
}
};
const rejectConsent = () => {
try {
localStorage.setItem('analytics-consent', 'rejected');
localStorage.setItem('analytics-consent-timestamp', new Date().toISOString());
setStatus('rejected');
setShowBanner(false);
} catch (error) {
console.warn('Failed to save consent rejection:', error);
}
};
const resetConsent = () => {
try {
localStorage.removeItem('analytics-consent');
localStorage.removeItem('analytics-consent-timestamp');
setStatus('pending');
setShowBanner(true);
} catch (error) {
console.warn('Failed to reset consent:', error);
}
};
return {
status,
showBanner,
acceptConsent,
rejectConsent,
resetConsent,
};
}
================================================
FILE: src/hooks/use-local-storage.ts
================================================
'use client';
import { useState, useEffect, useCallback } from 'react';
export function useLocalStorage(key: string, initialValue: T) {
// State to store our value
const [storedValue, setStoredValue] = useState(initialValue);
const [isLoaded, setIsLoaded] = useState(false);
// Load initial value from localStorage
useEffect(() => {
try {
const item = window.localStorage.getItem(key);
if (item) {
setStoredValue(JSON.parse(item));
}
} catch (error) {
console.error(`Error loading ${key} from localStorage:`, error);
} finally {
setIsLoaded(true);
}
}, [key]);
// Return a wrapped version of useState's setter function that persists the new value to localStorage
const setValue = useCallback(
(value: T | ((val: T) => T)) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error saving ${key} to localStorage:`, error);
}
},
[key, storedValue]
);
const clearValue = useCallback(() => {
try {
window.localStorage.removeItem(key);
setStoredValue(initialValue);
} catch (error) {
console.error(`Error clearing ${key} from localStorage:`, error);
}
}, [key, initialValue]);
return [storedValue, setValue, clearValue, isLoaded] as const;
}
================================================
FILE: src/hooks/use-theme.ts
================================================
'use client';
import { useEffect } from 'react';
import { useThemeStore } from '@/lib/store';
import type { ThemeMode } from '@/types/theme';
export function useTheme() {
const { mode, resolvedTheme, setMode, setResolvedTheme } = useThemeStore();
useEffect(() => {
const root = document.documentElement;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const updateTheme = () => {
const isDark = mode === 'dark' || (mode === 'system' && mediaQuery.matches);
const newTheme = isDark ? 'dark' : 'light';
// Only update if theme actually changed
if (resolvedTheme !== newTheme) {
setResolvedTheme(newTheme);
// Temporarily disable transitions for instant theme switch
root.classList.add('theme-switching');
// Use requestAnimationFrame for smoother transitions
requestAnimationFrame(() => {
// Remove both classes first to avoid conflicts
root.classList.remove('dark', 'light');
// Add the new theme class
root.classList.add(newTheme);
// Re-enable transitions after a short delay (matching CSS transition duration)
setTimeout(() => {
root.classList.remove('theme-switching');
}, 25); // Half of 100ms for optimal timing
});
}
};
updateTheme();
const listener = () => {
if (mode === 'system') {
updateTheme();
}
};
mediaQuery.addEventListener('change', listener);
return () => mediaQuery.removeEventListener('change', listener);
}, [mode, resolvedTheme, setResolvedTheme]);
const setTheme = (newMode: ThemeMode) => {
setMode(newMode);
};
return {
theme: mode,
resolvedTheme,
setTheme,
};
}
================================================
FILE: src/lib/analytics.ts
================================================
'use client';
// Analytics utility for GA4 custom event tracking
// Only tracks events if user has given consent
declare global {
interface Window {
gtag?: (...args: unknown[]) => void;
}
}
/**
* Check if analytics consent has been given
*/
export function hasAnalyticsConsent(): boolean {
if (typeof window === 'undefined') return false;
try {
const consent = localStorage.getItem('analytics-consent');
return consent === 'accepted';
} catch {
return false;
}
}
/**
* Send custom event to GA4 if consent is given
*/
function trackEvent(eventName: string, parameters?: Record) {
// Only track if consent is given and gtag is available
if (!hasAnalyticsConsent() || typeof window === 'undefined' || !window.gtag) {
return;
}
try {
window.gtag('event', eventName, {
// Add default parameters
timestamp: new Date().toISOString(),
page_location: window.location.href,
page_title: document.title,
// Add custom parameters
...parameters,
});
} catch (error) {
console.warn('Analytics tracking failed:', error);
}
}
/**
* Track GitHub auto-fill usage
*/
export function trackGitHubAutofill(username?: string) {
trackEvent('github_autofill_used', {
event_category: 'engagement',
event_label: 'github_integration',
has_username: !!username,
// Don't track actual username for privacy
});
}
/**
* Track README generation completion
*/
export function trackReadmeGenerated(data?: {
hasSkills?: boolean;
hasSocial?: boolean;
hasLinks?: boolean;
stepCount?: number;
}) {
trackEvent('readme_generated', {
event_category: 'conversion',
event_label: 'readme_completion',
value: 1, // Conversion value
has_skills: data?.hasSkills || false,
has_social: data?.hasSocial || false,
has_links: data?.hasLinks || false,
step_count: data?.stepCount || 0,
});
}
/**
* Track file export/download actions
*/
export function trackFileExported(action: 'copy' | 'download' | 'json_export', format?: string) {
trackEvent('file_exported', {
event_category: 'engagement',
event_label: 'file_export',
export_action: action,
file_format: format || 'markdown',
value: action === 'download' ? 2 : 1, // Downloads are more valuable
});
}
/**
* Initialize analytics with consent
*/
export function initializeAnalytics() {
if (!hasAnalyticsConsent()) {
return;
}
// Configure GA4 for privacy
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('config', process.env.NEXT_PUBLIC_GA_ID || '', {
// Privacy-friendly configuration
anonymize_ip: true,
allow_google_signals: false,
allow_ad_personalization_signals: false,
// Custom configuration
page_title: document.title,
page_location: window.location.href,
});
}
}
/**
* Disable analytics tracking (for opt-out)
*/
export function disableAnalytics() {
if (typeof window !== 'undefined') {
// Set GA4 opt-out flag
const gaId = process.env.NEXT_PUBLIC_GA_ID;
if (gaId) {
(window as unknown as Record)[`ga-disable-${gaId}`] = true;
}
// Remove consent
try {
localStorage.removeItem('analytics-consent');
} catch {
// Ignore localStorage errors
}
}
}
================================================
FILE: src/lib/asset-path.ts
================================================
/**
* Get the correct asset path with basePath for GitHub Pages
* Uses NEXT_PUBLIC_BASE_PATH environment variable if set
*/
export function getAssetPath(path: string): string {
// Ensure path starts with /
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
// Use NEXT_PUBLIC_BASE_PATH if set, otherwise detect based on build
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
return `${basePath}${normalizedPath}`;
}
================================================
FILE: src/lib/github-api.ts
================================================
interface GitHubUser {
login: string;
name: string | null;
bio: string | null;
location: string | null;
blog: string | null;
twitter_username: string | null;
public_repos: number;
followers: number;
following: number;
company: string | null;
email: string | null;
}
interface GitHubRepo {
name: string;
language: string | null;
stargazers_count: number;
description: string | null;
}
export interface GitHubUserData {
username: string;
name: string;
bio: string;
location: string;
blog: string;
twitter: string;
email: string;
topLanguages: string[];
totalRepos: number;
totalStars: number;
}
export interface GitHubApiError {
message: string;
type: 'rate_limit' | 'not_found' | 'network' | 'unknown';
retryAfter?: number;
}
export async function fetchGitHubUser(username: string): Promise {
try {
// Fetch user data
const userResponse = await fetch(`https://api.github.com/users/${username}`);
if (!userResponse.ok) {
// Handle specific error cases
if (userResponse.status === 403) {
const rateLimitRemaining = userResponse.headers.get('X-RateLimit-Remaining');
const rateLimitReset = userResponse.headers.get('X-RateLimit-Reset');
if (rateLimitRemaining === '0') {
const resetTime = rateLimitReset ? new Date(parseInt(rateLimitReset) * 1000) : null;
const retryAfter = resetTime
? Math.ceil((resetTime.getTime() - Date.now()) / 1000 / 60)
: null;
const error: GitHubApiError = {
message: `GitHub API rate limit exceeded. ${retryAfter ? `Try again in ${retryAfter} minutes.` : 'Please try again later.'}`,
type: 'rate_limit',
retryAfter: retryAfter || undefined,
};
throw error;
}
}
if (userResponse.status === 404) {
const error: GitHubApiError = {
message: `GitHub user "${username}" not found. Please check the username.`,
type: 'not_found',
};
throw error;
}
// Generic error for other status codes
const error: GitHubApiError = {
message: `Failed to fetch GitHub user data (${userResponse.status}). Please try again.`,
type: 'unknown',
};
throw error;
}
const user: GitHubUser = await userResponse.json();
// Fetch user repos to analyze languages
const reposResponse = await fetch(
`https://api.github.com/users/${username}/repos?sort=updated&per_page=100`
);
let repos: GitHubRepo[] = [];
if (reposResponse.ok) {
repos = await reposResponse.json();
} else if (reposResponse.status === 403) {
// Rate limit hit on repos endpoint, continue with user data only
console.warn('GitHub API rate limit hit for repos endpoint, continuing with basic user data');
}
// Analyze top languages
const languageCounts: Record = {};
let totalStars = 0;
repos.forEach((repo) => {
if (repo.language) {
languageCounts[repo.language] = (languageCounts[repo.language] || 0) + 1;
}
totalStars += repo.stargazers_count;
});
// Sort languages by frequency
const topLanguages = Object.entries(languageCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([lang]) => lang.toLowerCase());
return {
username: user.login,
name: user.name || user.login,
bio: user.bio || '',
location: user.location || '',
blog: user.blog || '',
twitter: user.twitter_username || '',
email: user.email || '',
topLanguages,
totalRepos: user.public_repos,
totalStars,
};
} catch (error) {
console.error('Error fetching GitHub user:', error);
// Re-throw GitHubApiError for proper handling
if (error && typeof error === 'object' && 'type' in error) {
throw error as GitHubApiError;
}
// Network or other errors
const networkError: GitHubApiError = {
message: 'Network error occurred. Please check your connection and try again.',
type: 'network',
};
throw networkError;
}
}
// Map GitHub languages to skill IDs in our system
export function mapLanguageToSkills(language: string): string[] {
const languageMap: Record = {
javascript: ['javascript', 'nodejs', 'react', 'express'],
typescript: ['typescript', 'nodejs', 'react'],
python: ['python', 'django', 'flask'],
java: ['java', 'spring'],
go: ['go'],
rust: ['rust'],
ruby: ['ruby', 'rails'],
php: ['php', 'laravel'],
swift: ['swift'],
kotlin: ['kotlin', 'android'],
csharp: ['csharp', 'dotnet'],
cpp: ['cplusplus'],
c: ['c'],
scala: ['scala'],
html: ['html5', 'css3'],
css: ['css3', 'sass'],
};
return languageMap[language] || [language];
}
// Generate smart subtitle based on user data
export function generateSmartSubtitle(userData: GitHubUserData): string {
const { topLanguages, totalRepos } = userData;
if (topLanguages.length === 0) {
return 'A passionate developer from around the world';
}
const primaryLang = topLanguages[0];
const langDisplay = primaryLang.charAt(0).toUpperCase() + primaryLang.slice(1);
if (totalRepos < 5) {
return `A budding ${langDisplay} developer`;
} else if (totalRepos < 20) {
return `A passionate ${langDisplay} developer`;
} else if (totalRepos < 50) {
return `An experienced ${langDisplay} developer`;
} else {
return `A seasoned ${langDisplay} developer`;
}
}
================================================
FILE: src/lib/markdown-generator.ts
================================================
import type {
ProfileFormData,
LinksFormData,
SocialFormData,
SupportFormData,
} from './validations';
import { DEFAULT_PREFIX } from '@/constants/defaults';
interface GenerateMarkdownOptions {
profile: Partial;
links: Partial;
social: Partial;
support: Partial;
skills: Record;
}
const socialPlatformUrls: Record string> = {
github: (u) => `https://github.com/${u}`,
linkedin: (u) => `https://linkedin.com/in/${u}`,
twitter: (u) => `https://twitter.com/${u}`,
dev: (u) => `https://dev.to/${u}`,
stackoverflow: (u) => `https://stackoverflow.com/users/${u}`,
kaggle: (u) => `https://kaggle.com/${u}`,
fb: (u) => `https://fb.com/${u}`,
instagram: (u) => `https://instagram.com/${u}`,
dribbble: (u) => `https://dribbble.com/${u}`,
behance: (u) => `https://behance.net/${u}`,
medium: (u) => `https://medium.com/${u}`,
youtube: (u) => `https://youtube.com/${u}`,
codepen: (u) => `https://codepen.io/${u}`,
codesandbox: (u) => `https://codesandbox.io/${u}`,
leetcode: (u) => `https://leetcode.com/${u}`,
hackerrank: (u) => `https://hackerrank.com/${u}`,
codeforces: (u) => `https://codeforces.com/profile/${u}`,
codechef: (u) => `https://codechef.com/users/${u}`,
topcoder: (u) => `https://topcoder.com/members/${u}`,
hackerearth: (u) => `https://hackerearth.com/${u}`,
geeks_for_geeks: (u) => `https://auth.geeksforgeeks.org/user/${u}`,
discord: (u) => `https://discord.gg/${u}`,
};
const socialIcons: Record = {
github: 'github.svg',
linkedin: 'linked-in-alt.svg',
twitter: 'twitter.svg',
dev: 'devto.svg',
stackoverflow: 'stack-overflow.svg',
kaggle: 'kaggle.svg',
fb: 'facebook.svg',
instagram: 'instagram.svg',
dribbble: 'dribbble.svg',
behance: 'behance.svg',
medium: 'medium.svg',
youtube: 'youtube.svg',
codepen: 'codepen.svg',
codesandbox: 'codesandbox.svg',
leetcode: 'leet-code.svg',
hackerrank: 'hackerrank.svg',
codeforces: 'codeforces.svg',
codechef: 'codechef.svg',
topcoder: 'topcoder.svg',
hackerearth: 'hackerearth.svg',
geeks_for_geeks: 'geeks-for-geeks.svg',
discord: 'discord.svg',
};
// Generate skill icon URL - uses skillicons.dev for consistent dark mode support
export function getSkillIconUrl(skill: string): string {
// Skills that use simple-icons for better brand colors and dark mode support
// Using colors that work in both light and dark modes
const simpleIconsFallback: Record = {
// DevOps
circleci: 'circleci/555', // CircleCI in medium gray (visible in both modes)
travisci: 'travisci', // Travis CI uses teal brand color (works in both modes)
// Modern AI/ML Tools
langchain: 'langchain/1C3C3C', // LangChain dark color
huggingface: 'huggingface', // HuggingFace brand color
ollama: 'ollama', // Ollama brand color
mlflow: 'mlflow/0194E2', // MLflow blue
streamlit: 'streamlit/FF4B4B', // Streamlit red
gradio: 'gradio/FF7C00', // Gradio orange
// Frontend
backbonejs: 'backbonedotjs/0071B5', // Backbone.js blue
// Mobile
nativescript: 'nativescript/3655FF', // NativeScript blue
apachecordova: 'apachecordova/E8E8E8', // Apache Cordova gray
// Backend
solr: 'apachesolr/D9411E', // Apache Solr red
// Database
cockroachdb: 'cockroachlabs', // CockroachDB official
hive: 'apachehive/FDEE21', // Apache Hive yellow
// Data Visualization
chartjs: 'chartdotjs/FF6384', // Chart.js pink
// Testing
puppeteer: 'puppeteer/40B5A4', // Puppeteer teal
// Software/Design
framer: 'framer', // Framer brand color
invision: 'invision/FF3366', // InVision pink
// Static Site Generators
'11ty': 'eleventy', // Eleventy brand color
hexo: 'hexo/0E83CD', // Hexo blue
gridsome: 'gridsome', // Gridsome brand color
// Automation
zapier: 'zapier/FF4A00', // Zapier orange
ifttt: 'ifttt', // IFTTT brand color
};
// Check if skill needs simple-icons fallback
if (simpleIconsFallback[skill]) {
const parts = simpleIconsFallback[skill].split('/');
const iconName = parts[0];
const color = parts[1] || ''; // Use brand color if specified
return `https://cdn.simpleicons.org/${iconName}${color ? `/${color}` : ''}`;
}
// Skills that need devicon fallback (not available on skillicons.dev)
const deviconFallback: Record = {
// Database
oracle: 'oracle/oracle-original',
realm: 'realm/realm-original',
couchdb: 'couchdb/couchdb-original',
mssql: 'microsoftsqlserver/microsoftsqlserver-plain',
mariadb: 'mysql/mysql-original-wordmark',
// Mobile
xamarin: 'dot-net/dot-net-plain',
ionic: 'ionic/ionic-original',
// Programming Languages
erlang: 'erlang/erlang-original',
// Frontend
bulma: 'bulma/bulma-plain',
materialize: 'materialui/materialui-original',
// Backend
openresty: 'nginx/nginx-original',
hadoop: 'hadoop/hadoop-original',
// AI/ML - Core Data Science
keras: 'keras/keras-original',
numpy: 'numpy/numpy-original',
matplotlib: 'matplotlib/matplotlib-original',
jupyter: 'jupyter/jupyter-original-wordmark',
pandas: 'pandas/pandas-original',
seaborn: 'python/python-original',
// Data Visualization
canvasjs: 'javascript/javascript-original', // No official icon available
kibana: 'kibana/kibana-original',
// DevOps
vagrant: 'vagrant/vagrant-original',
// BaaS
amplify: 'amazonwebservices/amazonwebservices-plain-wordmark',
// Framework
codeigniter: 'codeigniter/codeigniter-plain',
quasar: 'quasar/quasar-plain',
// Testing
mocha: 'mocha/mocha-plain',
karma: 'karma/karma-original',
jasmine: 'jasmine/jasmine-original',
// Software
sketch: 'sketch/sketch-original',
// Static Site Generators
hugo: 'hugo/hugo-original',
sculpin: 'php/php-original',
vuepress: 'vuejs/vuejs-original', // VuePress doesn't have official icon, using Vue
jekyll: 'jekyll/jekyll-original',
middleman: 'ruby/ruby-original',
// Angular variant
scully: 'angularjs/angularjs-original',
};
// Check if skill needs devicon fallback
if (deviconFallback[skill]) {
return `https://cdn.jsdelivr.net/gh/devicons/devicon/icons/${deviconFallback[skill]}.svg`;
}
// Map skill names to skillicons identifiers
const skillIconsMap: Record = {
// Programming Languages
c: 'c',
cplusplus: 'cpp',
csharp: 'cs',
go: 'go',
java: 'java',
javascript: 'js',
typescript: 'ts',
php: 'php',
perl: 'perl',
ruby: 'ruby',
scala: 'scala',
python: 'py',
swift: 'swift',
objectivec: 'apple',
clojure: 'clojure',
rust: 'rust',
haskell: 'haskell',
coffeescript: 'coffeescript',
elixir: 'elixir',
erlang: 'erlang',
// Frontend
vuejs: 'vue',
react: 'react',
svelte: 'svelte',
angularjs: 'angular',
angular: 'angular',
backbonejs: 'backbone',
bootstrap: 'bootstrap',
vuetify: 'vuetify',
css3: 'css',
html5: 'html',
pug: 'pug',
gulp: 'gulp',
sass: 'sass',
redux: 'redux',
webpack: 'webpack',
babel: 'babel',
tailwind: 'tailwind',
bulma: 'bulma',
gtk: 'gtk',
qt: 'qt',
ember: 'ember',
// Backend
nodejs: 'nodejs',
spring: 'spring',
express: 'express',
graphql: 'graphql',
kafka: 'kafka',
rabbitmq: 'rabbitmq',
rabbitMQ: 'rabbitmq', // Handle capital MQ variant
hadoop: 'hadoop',
nginx: 'nginx',
nestjs: 'nestjs',
// Mobile
android: 'androidstudio',
flutter: 'flutter',
dart: 'dart',
kotlin: 'kotlin',
reactnative: 'react',
ionic: 'ionic',
// AI/ML
tensorflow: 'tensorflow',
pytorch: 'pytorch',
opencv: 'opencv',
scikit_learn: 'scikitlearn',
anaconda: 'anaconda',
fastapi: 'fastapi',
// Database
mongodb: 'mongodb',
mysql: 'mysql',
postgresql: 'postgres',
redis: 'redis',
cassandra: 'cassandra',
elasticsearch: 'elasticsearch',
sqlite: 'sqlite',
// Data Visualization
d3js: 'd3',
grafana: 'grafana',
// DevOps
aws: 'aws',
docker: 'docker',
jenkins: 'jenkins',
gcp: 'gcp',
kubernetes: 'kubernetes',
bash: 'bash',
azure: 'azure',
vagrant: 'vagrant',
circleci: 'circleci',
travisci: 'travis',
// BaaS
firebase: 'firebase',
appwrite: 'appwrite',
heroku: 'heroku',
// Framework
django: 'django',
dotnet: 'dotnet',
electron: 'electron',
symfony: 'symfony',
laravel: 'laravel',
codeigniter: 'codeigniter',
rails: 'rails',
flask: 'flask',
quasar: 'quasar',
// Testing
cypress: 'cypress',
selenium: 'selenium',
jest: 'jest',
mocha: 'mocha',
puppeteer: 'puppeteer',
karma: 'karma',
jasmine: 'jasmine',
// Software
illustrator: 'illustrator',
photoshop: 'photoshop',
xd: 'xd',
figma: 'figma',
blender: 'blender',
sketch: 'sketch',
invision: 'invision',
framer: 'framer',
matlab: 'matlab',
postman: 'postman',
// Static Site Generators
gatsby: 'gatsby',
hugo: 'hugo',
jekyll: 'jekyll',
nextjs: 'nextjs',
nuxtjs: 'nuxtjs',
'11ty': 'eleventy',
hexo: 'hexo',
// Game Engines
unity: 'unity',
unreal: 'unreal',
// Automation
zapier: 'zapier',
ifttt: 'ifttt',
// Other
linux: 'linux',
git: 'git',
arduino: 'arduino',
};
const iconName = skillIconsMap[skill] || skill;
// Use skillicons.dev which provides dark-mode compatible icons
// This service automatically handles dark mode and provides consistent styling
return `https://skillicons.dev/icons?i=${iconName}`;
}
export function generateMarkdown(options: GenerateMarkdownOptions): string {
const { profile, links, social, support, skills } = options;
let markdown = '';
// Title and Subtitle
if (profile.title) {
markdown += `# ${DEFAULT_PREFIX.title} ${profile.title}\n\n`;
}
if (profile.subtitle) {
markdown += `### ${profile.subtitle}\n\n`;
}
// Visitor Badge
if (profile.visitorsBadge && social.github) {
markdown += `
\n\n`;
}
// GitHub Trophy
if (profile.githubProfileTrophy && social.github) {
markdown += `
\n\n`;
}
// Twitter Badge
if (social.twitterBadge && social.twitter) {
markdown += `
\n\n`;
}
// About sections
const aboutSections = [
{
key: 'currentWork',
value: profile.currentWork,
prefix: DEFAULT_PREFIX.currentWork,
link: links.currentWork,
},
{ key: 'currentLearn', value: profile.currentLearn, prefix: DEFAULT_PREFIX.currentLearn },
{
key: 'collaborateOn',
value: profile.collaborateOn,
prefix: DEFAULT_PREFIX.collaborateOn,
link: links.collaborateOn,
},
{
key: 'helpWith',
value: profile.helpWith,
prefix: DEFAULT_PREFIX.helpWith,
link: links.helpWith,
},
{ key: 'ama', value: profile.ama, prefix: DEFAULT_PREFIX.ama },
{ key: 'contact', value: profile.contact, prefix: DEFAULT_PREFIX.contact },
{ key: 'funFact', value: profile.funFact, prefix: DEFAULT_PREFIX.funFact },
];
aboutSections.forEach(({ value, prefix, link }) => {
if (value) {
if (link) {
markdown += `- ${prefix} **[${value}](${link})**\n\n`;
} else {
markdown += `- ${prefix} **${value}**\n\n`;
}
}
});
// Portfolio
if (links.portfolio) {
markdown += `- ${DEFAULT_PREFIX.portfolio} **[${links.portfolio}](${links.portfolio})**\n\n`;
}
// Blog
if (links.blog) {
markdown += `- ${DEFAULT_PREFIX.blog} **[${links.blog}](${links.blog})**\n\n`;
}
// Resume
if (links.resume) {
markdown += `- ${DEFAULT_PREFIX.resume} **[${links.resume}](${links.resume})**\n\n`;
}
// Social Connect
const socialLinks = Object.entries(social).filter(
([key, value]) =>
key !== 'twitterBadge' && value && typeof value === 'string' && value.trim() !== ''
);
if (socialLinks.length > 0) {
markdown += `Connect with me:
\n`;
markdown += `\n`;
socialLinks.forEach(([platform, username]) => {
const icon = socialIcons[platform];
const url = socialPlatformUrls[platform];
if (icon && url && username) {
markdown += `
\n`;
}
});
markdown += `
\n\n`;
}
// Skills
const selectedSkills = Object.entries(skills).filter(([_, selected]) => selected);
if (selectedSkills.length > 0) {
markdown += `Languages and Tools:
\n`;
markdown += ``;
selectedSkills.forEach(([skill]) => {
const iconUrl = getSkillIconUrl(skill);
markdown += `
`;
});
markdown += `
\n\n`;
}
// Support
if (support.buyMeACoffee) {
markdown += `Support:
\n`;
markdown += ` 
\n\n`;
}
// GitHub Stats
if (profile.githubStats && social.github) {
markdown += `
\n\n`;
markdown += ` 
\n\n`;
}
// Streak Stats
if (profile.streakStats && social.github) {
markdown += `
\n\n`;
}
return markdown;
}
export function generateTitle(profile: Partial): string {
return profile.title || 'My GitHub Profile';
}
================================================
FILE: src/lib/storage.ts
================================================
import type { ProfileFormData, LinksFormData, SocialFormData, SupportFormData } from './validations';
export interface SavedFormData {
profile: Partial;
links: Partial;
social: Partial;
support: Partial;
skills: Record;
lastSaved: string;
}
const STORAGE_KEY = 'github-profile-generator';
export function saveFormData(data: SavedFormData): void {
try {
const dataToSave = {
...data,
lastSaved: new Date().toISOString(),
};
console.log('💾 storage.ts - saveFormData called with:', dataToSave);
const jsonString = JSON.stringify(dataToSave);
console.log('💾 storage.ts - JSON string length:', jsonString.length);
localStorage.setItem(STORAGE_KEY, jsonString);
console.log('✅ storage.ts - Successfully saved to localStorage');
} catch (error) {
console.error('❌ storage.ts - Error saving form data:', error);
}
}
export function loadFormData(): SavedFormData | null {
try {
console.log('📂 storage.ts - loadFormData called');
const saved = localStorage.getItem(STORAGE_KEY);
console.log('📂 storage.ts - Raw data from localStorage:', saved ? `${saved.length} bytes` : 'null');
if (saved) {
const parsed = JSON.parse(saved);
console.log('✅ storage.ts - Successfully parsed data:', parsed);
return parsed;
}
console.log('⚠️ storage.ts - No data found in localStorage');
} catch (error) {
console.error('❌ storage.ts - Error loading form data:', error);
}
return null;
}
export function clearFormData(): void {
try {
localStorage.removeItem(STORAGE_KEY);
} catch (error) {
console.error('Error clearing form data:', error);
}
}
export function hasFormData(): boolean {
try {
const hasData = localStorage.getItem(STORAGE_KEY) !== null;
console.log('🔍 storage.ts - hasFormData:', hasData);
return hasData;
} catch (error) {
console.error('❌ storage.ts - Error checking form data:', error);
return false;
}
}
================================================
FILE: src/lib/store.ts
================================================
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { ThemeMode, ResolvedTheme, AccessibilitySettings } from '@/types/theme';
interface ThemeState {
mode: ThemeMode;
resolvedTheme: ResolvedTheme;
accessibility: AccessibilitySettings;
setMode: (mode: ThemeMode) => void;
setResolvedTheme: (theme: ResolvedTheme) => void;
setAccessibility: (settings: Partial) => void;
}
export const useThemeStore = create()(
persist(
(set) => ({
mode: 'system',
resolvedTheme: 'light',
accessibility: {
highContrast: false,
fontSize: 'medium',
reducedMotion: false,
},
setMode: (mode) => set({ mode }),
setResolvedTheme: (resolvedTheme) => set({ resolvedTheme }),
setAccessibility: (settings) =>
set((state) => ({
accessibility: { ...state.accessibility, ...settings },
})),
}),
{
name: 'theme-storage',
partialize: (state) => ({ mode: state.mode, accessibility: state.accessibility }),
}
)
);
================================================
FILE: src/lib/validations.ts
================================================
import { z } from 'zod';
// Profile validation schema
export const profileSchema = z.object({
// Basic Information
title: z.string().min(1, 'Name is required').max(100, 'Name is too long'),
subtitle: z.string().max(200, 'Subtitle is too long'),
// About sections
currentWork: z.string().max(200),
currentLearn: z.string().max(200),
collaborateOn: z.string().max(200),
helpWith: z.string().max(200),
ama: z.string().max(200),
contact: z.string().email('Invalid email').or(z.string().max(100)),
funFact: z.string().max(200),
// Badges
visitorsBadge: z.boolean(),
badgeStyle: z.enum(['flat', 'flat-square', 'plastic', 'for-the-badge']),
badgeColor: z.string().regex(/^[0-9a-fA-F]{6}$/, 'Invalid color hex'),
badgeLabel: z.string().max(50),
// GitHub Stats
githubProfileTrophy: z.boolean(),
githubStats: z.boolean(),
githubStatsOptions: z.object({
theme: z.string(),
titleColor: z.string(),
textColor: z.string(),
bgColor: z.string(),
hideBorder: z.boolean(),
cacheSeconds: z.number().nullable(),
locale: z.string(),
}),
// Top Languages
topLanguages: z.boolean(),
topLanguagesOptions: z.object({
theme: z.string(),
titleColor: z.string(),
textColor: z.string(),
bgColor: z.string(),
hideBorder: z.boolean(),
cacheSeconds: z.number().nullable(),
locale: z.string(),
}),
// Streak Stats
streakStats: z.boolean(),
streakStatsOptions: z.object({
theme: z.string(),
}),
// Blog Integration
devDynamicBlogs: z.boolean(),
mediumDynamicBlogs: z.boolean(),
rssDynamicBlogs: z.boolean(),
});
// Links validation schema
export const linksSchema = z.object({
currentWork: z.string().url('Invalid URL').or(z.literal('')),
collaborateOn: z.string().url('Invalid URL').or(z.literal('')),
helpWith: z.string().url('Invalid URL').or(z.literal('')),
portfolio: z.string().url('Invalid URL').or(z.literal('')),
blog: z.string().url('Invalid URL').or(z.literal('')),
resume: z.string().url('Invalid URL').or(z.literal('')),
});
// Social links validation schema
export const socialSchema = z.object({
github: z.string().max(100),
dev: z.string().max(100),
linkedin: z.string().max(100),
codepen: z.string().max(100),
stackoverflow: z.string().max(100),
kaggle: z.string().max(100),
codesandbox: z.string().max(100),
fb: z.string().max(100),
instagram: z.string().max(100),
twitter: z.string().max(100),
dribbble: z.string().max(100),
behance: z.string().max(100),
medium: z.string().max(100),
youtube: z.string().max(100),
codechef: z.string().max(100),
hackerrank: z.string().max(100),
codeforces: z.string().max(100),
leetcode: z.string().max(100),
topcoder: z.string().max(100),
hackerearth: z.string().max(100),
geeks_for_geeks: z.string().max(100),
discord: z.string().max(100),
rssurl: z.string().url('Invalid URL').or(z.literal('')),
// Twitter Badge Enhancement
twitterBadge: z.boolean(),
});
// Support validation schema
export const supportSchema = z.object({
buyMeACoffee: z.string().max(100),
});
// Complete form schema
export const completeFormSchema = z.object({
profile: profileSchema,
links: linksSchema,
social: socialSchema,
support: supportSchema,
skills: z.record(z.string(), z.boolean()),
});
export type ProfileFormData = z.infer;
export type LinksFormData = z.infer;
export type SocialFormData = z.infer;
export type SupportFormData = z.infer;
export type CompleteFormData = z.infer;
================================================
FILE: src/markdown-pages/about.md
================================================
---
slug: '/about'
date: '2019-05-04'
title: '👨💻 About'
---
**GitHub Profile README Generator** is an OSS(Open Source Software) that provides a cool interface to generate GitHub profile README in markdown.
The tool aims to provide hassle-free experience to add trending addons like profile **visitors count**, **github-stats**, **dynamic blog posts** etc.
The profile should be neat and minimal to give a clear overview of the work. Non-uniform icons, too much content, too much images/gifs distracts visitors to see your actual work.
To solve this, GitHub Profile README Generator came into existence.
So many developers contributed to the project and made it more awesome to use. You can contribute too to make it grow even further.
### Contributors 🙏
List of the developers who contributed to the project. A big shout out for them.
## How do I create a profile README?
The profile README is created by creating a new repository that’s the same name as your username. For example, my GitHub username is **rahuldkjain** so I created a new repository with the name **rahuldkjain**. Note: at the time of this writing, in order to access the profile README feature, the letter-casing must match your GitHub username.
1. Create a new repository with the same name (including casing) as your GitHub username: https://github.com/new
2. Create a README.md file inside the new repo with content (text, GIFs, images, emojis, etc.)
3. Commit your fancy new README!
- If you're on GitHub's web interface you can choose to commit directly to the repo's main branch (i.e., master or main) which will make it immediately visible on your profile)
4. Push changes to GitHub (if you made changes locally i.e., on your computer and not github.com)
## How to use?
Tired of editing profile README(.md) to add new features like visitors-count badge, github-stats etc?
Don't worry. Keep calm, fill the form and let the tool do the work for you

## Why visitors count keeps on increasing?
So many users raised an issue that the counter keeps on increasing everytime the page reloads.
Well it is visitors count not "unique" visitors count. The goal of the addon is to provide a good stat of how well the github profile is doing.
Proper use or misuse of the addon is the sole responsibility of the user. The developer of the addon is working on it to fix this issue.
================================================
FILE: src/markdown-pages/addons.md
================================================
---
slug: '/addons'
date: '2019-05-04'
title: '🚀 Addons'
---
GitHub Profile README Generator tool uses few open-source addons developed by other developers. Including such features makes the tool useful. The developers of this tool is very grateful to use these awesome addons.
## [GitHub README Stats](https://github.com/anuraghazra/github-readme-stats)
⚡️ Dynamically generated stats for your github readmes
#### GitHub Stats Card
#### Top Skills Card
Developed by [Anurag Hazra](https://github.com/anuraghazra).
You can customize the theme too. See how to customize yours [here](https://github.com/anuraghazra/github-readme-stats)
## [GitHub Readme Streak Stats](https://github.com/DenverCoder1/github-readme-streak-stats)
Stay motivated while contributing to open source by displaying your current contribution streak

Developed by by [Jonah Lawrence](https://github.com/DenverCoder1).
See how to customize the theme [here](https://github.com/DenverCoder1/github-readme-streak-stats)
## [GitHub Profile Views Counter](https://github.com/antonkomarev/github-profile-views-counter)
It counts how many times your GitHub profile has been viewed. Free cloud micro-service.

Developed by by [Anton Komarev](https://github.com/antonkomarev).
You can customize the color, label and style too. See how to customize [here](https://github.com/antonkomarev/github-profile-views-counter)
## [Dynamic Latest Blog Posts](https://github.com/gautamkrishnar/blog-post-workflow)
Show your latest blog posts from any sources(like dev(.)to, medium etc) or StackOverflow activity on your GitHub profile/project readme automatically using the RSS feed.
Developed by [Gautam Krishna R](https://github.com/gautamkrishnar)
### How to use
- Go to your repository
- Add the following section to your **README.md** file, you can give whatever title you want. Just make sure that you use **** in your readme. The workflow will replace this comment with the actual blog post list:
```markdown
# Blog posts
```
- Create a folder named `.github` and create `workflows` folder inside it if it doesn't exist.
- Create a new file named `blog-post-workflow.yml` with the following contents inside the workflows folder:
```yaml
name: Latest blog post workflow
on:
schedule:
# Runs every hour
- cron: '0 * * * *'
jobs:
update-readme-with-blog:
name: Update this repo's README with latest blog posts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: gautamkrishnar/blog-post-workflow@master
with:
feed_list: 'https://dev.to/feed/rahuldkjain, https://medium.com/feed/@rahuldkjain'
```
- Replace the above url list with your own rss feed urls. See [popular-sources](#popular-sources) for a list of common RSS feed urls.
- Commit and wait for it to run
To know more, check out the [official github repository](https://github.com/gautamkrishnar/blog-post-workflow)
================================================
FILE: src/markdown-pages/support.md
================================================
---
slug: '/support'
date: '2019-05-04'
title: '💵 Support OSS'
---
> Think of giving not as a duty but as a privilege --John D. Rockefeller Hr.
🚀 GitHub Profile README Generator tool is free and will always be free. Numerous developers has put their time and efforts to make this tool more powerful. However, these developers are doing their full time job along with open-source contributions.
You can come forward to support the developers by making small donations. You will never know what this support mean to them. If you find the tool really helpful, then it will be very grateful to support the tool 🙇.
## Social Support 🤝
Let the world know how you feel using this tool. Share with others on twitter.
## Sponsors 🙇
- [Scott C Wilson](https://github.com/scottcwilson) donated the first ever grant to this tool. A big thanks to him.
- [Max Schmitt](https://github.com/mxschmitt) loved the tool and showed the support with his donation. Thanks a lot.
================================================
FILE: src/styles/tailwind.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind @screens;
/* Additional vertical padding used by kbd tag. */
.py-05 {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
}
.markdown {
@apply text-gray-900 leading-normal break-words;
}
.markdown > * + * {
@apply mt-0 mb-4;
}
.markdown li + li {
@apply mt-1;
}
.markdown li > p + p {
@apply mt-6;
}
.markdown strong {
@apply font-semibold;
}
.markdown a {
@apply text-blue-600 font-semibold;
}
.markdown strong a {
@apply font-bold;
}
.markdown h1 {
@apply leading-tight border-b text-4xl font-semibold mb-4 mt-6 pb-2;
}
.markdown h2 {
@apply leading-tight border-b text-2xl font-semibold mb-4 mt-6 pb-2;
}
.markdown h3 {
@apply leading-snug text-lg font-semibold mb-4 mt-6;
}
.markdown h4 {
@apply leading-none text-base font-semibold mb-4 mt-6;
}
.markdown h5 {
@apply leading-tight text-sm font-semibold mb-4 mt-6;
}
.markdown h6 {
@apply leading-tight text-sm font-semibold text-gray-600 mb-4 mt-6;
}
.markdown blockquote {
@apply text-base border-l-4 border-gray-300 pl-4 pr-4 text-gray-600;
}
.markdown code {
@apply font-body text-sm inline bg-gray-200 rounded px-1 py-05;
}
.markdown pre {
@apply bg-gray-900 text-white overflow-scroll rounded p-4;
}
.markdown pre code {
@apply block p-0 rounded-none text-white;
}
.markdown ul {
@apply text-base pl-8 list-disc;
}
.markdown ol {
@apply text-base pl-8 list-decimal;
}
.markdown kbd {
@apply text-xs inline-block rounded border px-1 py-05 align-middle font-normal font-body shadow;
}
.markdown table {
@apply text-base border-gray-600;
}
.markdown th {
@apply border py-1 px-3;
}
.markdown td {
@apply border py-1 px-3;
}
/* Override pygments style background color. */
.markdown .highlight pre {
@apply bg-gray-100 !important;
}
================================================
FILE: src/test/setup.ts
================================================
import '@testing-library/jest-dom';
// Add custom matchers or global test setup here
================================================
FILE: src/types/profile.ts
================================================
// Profile data types
export interface ProfilePrefix {
title: string;
currentWork: string;
currentLearn: string;
collaborateOn: string;
helpWith: string;
ama: string;
contact: string;
resume: string;
funFact: string;
portfolio: string;
blog: string;
}
export interface GithubStatsOptions {
theme: string;
titleColor: string;
textColor: string;
bgColor: string;
hideBorder: boolean;
cacheSeconds: number | null;
locale: string;
}
export interface TopLanguagesOptions {
theme: string;
titleColor: string;
textColor: string;
bgColor: string;
hideBorder: boolean;
cacheSeconds: number | null;
locale: string;
}
export interface StreakStatsOptions {
theme: string;
}
export interface ProfileData {
title: string;
subtitle: string;
currentWork: string;
currentLearn: string;
collaborateOn: string;
helpWith: string;
ama: string;
contact: string;
funFact: string;
visitorsBadge: boolean;
badgeStyle: 'flat' | 'flat-square' | 'plastic' | 'for-the-badge';
badgeColor: string;
badgeLabel: string;
githubProfileTrophy: boolean;
githubStats: boolean;
githubStatsOptions: GithubStatsOptions;
topLanguages: boolean;
topLanguagesOptions: TopLanguagesOptions;
streakStats: boolean;
streakStatsOptions: StreakStatsOptions;
devDynamicBlogs: boolean;
mediumDynamicBlogs: boolean;
rssDynamicBlogs: boolean;
}
export interface ProfileLinks {
currentWork: string;
collaborateOn: string;
helpWith: string;
portfolio: string;
blog: string;
resume: string;
}
export interface SocialLinks {
github: string;
dev: string;
linkedin: string;
codepen: string;
stackoverflow: string;
kaggle: string;
codesandbox: string;
fb: string;
instagram: string;
twitter: string;
dribbble: string;
behance: string;
medium: string;
youtube: string;
codechef: string;
hackerrank: string;
codeforces: string;
leetcode: string;
topcoder: string;
hackerearth: string;
geeks_for_geeks: string;
discord: string;
rssurl: string;
twitterBadge: boolean;
}
export interface SupportLinks {
buyMeACoffee: string;
}
export interface Skills {
[key: string]: boolean;
}
================================================
FILE: src/types/skills.ts
================================================
export interface SkillCategory {
title: string;
skills: string[];
}
export interface CategorizedSkills {
[key: string]: SkillCategory;
}
export interface SkillIcons {
[key: string]: string;
}
export interface SkillWebsites {
[key: string]: string;
}
export type SkillState = Record;
================================================
FILE: src/types/theme.ts
================================================
export type ThemeMode = 'light' | 'dark' | 'system';
export type ResolvedTheme = 'light' | 'dark';
export interface AccessibilitySettings {
highContrast: boolean;
fontSize: 'small' | 'medium' | 'large';
reducedMotion: boolean;
}
================================================
FILE: tailwind.config.js
================================================
module.exports = {
purge: [],
theme: {
extend: {},
fontSize: {
xxs: '.60rem',
xs: '.75rem',
sm: '.875rem',
tiny: '.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
'4xl': '2.25rem',
'5xl': '3rem',
'6xl': '4rem',
'7xl': '5rem',
},
fontFamily: {
title: ['Lato', 'sans-serif'],
body: ['Roboto Mono', 'monospace'],
},
},
variants: {},
plugins: [],
};
================================================
FILE: tailwind.config.ts
================================================
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
// Only scan the actual source files we need
'./src/app/**/*.{js,ts,jsx,tsx}',
'./src/components/**/*.{js,ts,jsx,tsx}',
'./src/lib/**/*.{js,ts,jsx,tsx}',
// Exclude heavy directories
'!./src/constants/**',
'!./node_modules/**',
'!./old-gatsby-backup/**',
'!./out/**',
],
theme: {
extend: {
fontFamily: {
sans: ['var(--font-wotfard)', 'system-ui', 'sans-serif'],
mono: ['var(--font-roboto-mono)', 'monospace'],
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
animation: {
'fade-in': 'fadeIn 0.3s ease-in-out',
'slide-in': 'slideIn 0.3s ease-out',
'bounce-in': 'bounceIn 0.6s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideIn: {
'0%': { transform: 'translateY(-10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
bounceIn: {
'0%, 20%, 40%, 60%, 80%': {
transform: 'translateY(0)',
},
'10%, 30%, 50%, 70%, 90%': {
transform: 'translateY(-10px)',
},
},
},
},
},
plugins: [require('@tailwindcss/typography')],
};
export default config;
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
================================================
FILE: vitest.config.ts
================================================
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/test/',
'**/*.config.ts',
'**/*.config.js',
'**/*.d.ts',
'**/types/',
],
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});