Skip to content
Open
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
20 changes: 15 additions & 5 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { motion } from 'framer-motion';
import { Download } from 'lucide-react';
import { profileSchema, linksSchema, socialSchema } from '@/lib/validations';
import { profileSchema, linksSchema, socialSchema, defaultStarHistoryConfig } from '@/lib/validations';
import { DEFAULT_DATA, DEFAULT_LINK, DEFAULT_SOCIAL } from '@/constants/defaults';
import { initialSkillState } from '@/constants/skills';
import { BasicInfoSection } from '@/components/sections/basic-info-section';
Expand Down Expand Up @@ -74,15 +74,23 @@ export default function GeneratorPage() {
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved'>('idle');
const [hasInitialized, setHasInitialized] = useState(false);

// Create default profile data with Star History config
const defaultProfileData = useMemo(() => ({
...DEFAULT_DATA,
starHistory: false,
starHistoryConfig: defaultStarHistoryConfig
}), []);

const {
register: registerProfile,
formState: { errors: profileErrors },
watch: watchProfile,
reset: resetProfile,
trigger: triggerProfile,
setValue: setValueProfile,
} = useForm<ProfileFormData>({
resolver: zodResolver(profileSchema),
defaultValues: savedData?.profile ? { ...DEFAULT_DATA, ...savedData.profile } : DEFAULT_DATA,
defaultValues: savedData?.profile ? { ...defaultProfileData, ...savedData.profile } : defaultProfileData,
mode: 'onChange',
});

Expand Down Expand Up @@ -267,7 +275,7 @@ export default function GeneratorPage() {
variant: 'warning',
onConfirm: () => {
clearFormData();
resetProfile(DEFAULT_DATA);
resetProfile(defaultProfileData);
resetLinks(DEFAULT_LINK);
resetSocial(DEFAULT_SOCIAL);
setSkills(initialSkillState);
Expand All @@ -276,7 +284,7 @@ export default function GeneratorPage() {
showSuccess('All data cleared successfully', 'Form has been reset to default values');
},
});
}, [showConfirm, resetProfile, resetLinks, resetSocial, setSkills, showSuccess]);
}, [showConfirm, resetProfile, defaultProfileData, resetLinks, resetSocial, setSkills, showSuccess]);

const handleDownloadJSON = () => {
const data = {
Expand Down Expand Up @@ -316,7 +324,7 @@ export default function GeneratorPage() {

// Validate and import data
if (imported.profile) {
resetProfile({ ...DEFAULT_DATA, ...imported.profile } as ProfileFormData);
resetProfile({ ...defaultProfileData, ...imported.profile } as ProfileFormData);
}
if (imported.links) {
resetLinks({ ...DEFAULT_LINK, ...imported.links } as LinksFormData);
Expand Down Expand Up @@ -560,6 +568,8 @@ export default function GeneratorPage() {
selectedSkills={skills}
onSkillChange={handleSkillChange}
registerProfile={registerProfile}
watchProfile={watchProfile}
setValueProfile={setValueProfile}
/>
</Suspense>
)}
Expand Down
14 changes: 13 additions & 1 deletion src/components/sections/skills-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

import { useState, useMemo, useEffect } from 'react';
import { Info } from 'lucide-react';
import { UseFormRegister } from 'react-hook-form';
import { UseFormRegister, UseFormWatch, UseFormSetValue } from 'react-hook-form';
import { FormCheckbox } from '@/components/forms/form-checkbox';
import { FormInput } from '@/components/forms/form-input';
import { Select } from '@/components/ui/select';
import { CollapsibleSection } from '@/components/ui/collapsible-section';
import { StarHistory } from './star-history';
import { categorizedSkills, categories } from '@/constants/skills';
import { getSkillIconUrl } from '@/lib/markdown-generator';
import type { ProfileFormData } from '@/lib/validations';
Expand All @@ -15,12 +16,16 @@ interface SkillsSectionProps {
selectedSkills: Record<string, boolean>;
onSkillChange: (skill: string, checked: boolean) => void;
registerProfile: UseFormRegister<ProfileFormData>;
watchProfile: UseFormWatch<ProfileFormData>;
setValueProfile: UseFormSetValue<ProfileFormData>;
}

export function SkillsSection({
selectedSkills,
onSkillChange,
registerProfile,
watchProfile,
setValueProfile,
}: SkillsSectionProps) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string>('all');
Expand Down Expand Up @@ -259,6 +264,13 @@ export function SkillsSection({
)}
</div>
</div>

{/* Star History Charts */}
<StarHistory
register={registerProfile}
watch={watchProfile}
setValue={setValueProfile}
/>
</div>
);
}
244 changes: 244 additions & 0 deletions src/components/sections/star-history.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
'use client';

import { useState, useEffect } from 'react';
import { UseFormRegister, UseFormWatch, UseFormSetValue } from 'react-hook-form';
import { FormCheckbox } from '@/components/forms/form-checkbox';
import { FormInput } from '@/components/forms/form-input';
import type { ProfileFormData } from '@/lib/validations';
import { generateStarHistoryURL, parseRepos, validateRepos } from '@/lib/star-history';

interface StarHistoryProps {
register: UseFormRegister<ProfileFormData>;
watch: UseFormWatch<ProfileFormData>;
setValue: UseFormSetValue<ProfileFormData>;
}


export function StarHistory({ register, watch, setValue }: StarHistoryProps) {
const [reposInput, setReposInput] = useState<string>('');
const [previewUrl, setPreviewUrl] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [errors, setErrors] = useState<string[]>([]);

// Watch form values
const starHistoryEnabled = watch('starHistory');
const starHistoryConfig = watch('starHistoryConfig');
const repos = starHistoryConfig?.repos || [];
const chartType = starHistoryConfig?.chartType || 'Date';
const theme = starHistoryConfig?.theme || 'auto';

// Initialize repos input only once when component mounts or repos change from empty
useEffect(() => {
// Only set reposInput if it's empty and we have repos
if (repos.length > 0 && reposInput === '') {
setReposInput(repos.join(', '));
}
}, [repos]); // Intentionally exclude reposInput from dependencies to prevent infinite loops when updating its value; note this creates a hidden dependency issue if reposInput changes independently.

// Update preview when config changes
const updatePreview = useCallback(() => {
// ...original updatePreview logic here...
}, [repos, chartType, theme]);

useEffect(() => {
updatePreview();
}, [updatePreview]);

// Add this useEffect to sync the states
useEffect(() => {
const configEnabled = configData?.enabled;

// If they're out of sync, fix it
if (mainEnabled !== configEnabled) {
setValue('starHistoryConfig.enabled', mainEnabled, { shouldValidate: true });
}
}, [watch('starHistory'), configData?.enabled, setValue]);

const updatePreview = async () => {
if (!starHistoryEnabled || repos.length === 0) {
setPreviewUrl('');
return;
}

setLoading(true);
try {
const effectiveTheme = theme === 'auto' ? 'light' : theme;
const url = generateStarHistoryURL({
repos: repos,
type: chartType,
theme: effectiveTheme as 'light' | 'dark'
});
setPreviewUrl(url);
} catch (error) {
console.error('Error generating preview:', error);
setErrors(['Failed to generate preview']);
} finally {
setLoading(false);
}
};

const handleReposChange = (value: string) => {
setReposInput(value);

// Parse repositories
const parsedRepos = parseRepos(value);

// Validate
const validation = validateRepos(parsedRepos);
setErrors(validation.errors);

if (validation.valid) {
setValue('starHistoryConfig.repos', parsedRepos, { shouldValidate: true });
}
else {
// Clear repos if invalid
setValue('starHistoryConfig.repos', [], { shouldValidate: true });
}
};

const handleChartTypeChange = (type: 'Date' | 'Timeline') => {
setValue('starHistoryConfig.chartType', type, { shouldValidate: true });
};

const handleThemeChange = (theme: 'light' | 'dark' | 'auto') => {
setValue('starHistoryConfig.theme', theme, { shouldValidate: true });
};

return (
<div className="border-border mt-6 border-t pt-6">
<div className={`rounded-lg p-4 transition-all ${starHistoryEnabled ? 'bg-accent/50' : 'bg-muted/30'}`}>
<h4 className="mb-2 flex items-center gap-2 text-sm font-semibold">
<span>⭐</span>
<span>Star History Charts</span>
</h4>

<FormCheckbox
{...register('starHistory')}
id="starHistory"
label="Show Star History chart on profile"
/>

{starHistoryEnabled && (
<div className="mt-4 space-y-4">
{/* Repositories Input */}
<div>
<label className="block text-sm font-medium mb-2">
Repositories (comma-separated):
</label>
<FormInput
id="starHistoryRepos"
value={reposInput}
onChange={(e) => handleReposChange(e.target.value)}
placeholder="facebook/react, vuejs/vue, microsoft/vscode"
helperText="πŸ’‘ Enter repositories in 'owner/repo' format. Example: facebook/react, vuejs/vue"
/>
{errors.length > 0 && (
<div className="text-red-500 text-xs mt-2 space-y-1">
{errors.map((error, index) => (
<div key={index}>β€’ {error}</div>
))}
</div>
)}
</div>

{/* Chart Type */}
<div>
<label className="block text-sm font-medium mb-2">Chart Type:</label>
<div className="flex flex-wrap gap-4">
<label className="flex items-center">
<input
type="radio"
name="chartType"
value="Date"
checked={chartType === 'Date'}
onChange={() => handleChartTypeChange('Date')}
className="mr-2"
/>
Date
</label>
<label className="flex items-center">
<input
type="radio"
name="chartType"
value="Timeline"
checked={chartType === 'Timeline'}
onChange={() => handleChartTypeChange('Timeline')}
className="mr-2"
/>
Timeline
</label>
</div>
</div>

{/* Theme */}
<div>
<label className="block text-sm font-medium mb-2">Theme:</label>
<div className="flex flex-wrap gap-4">
<label className="flex items-center">
<input
type="radio"
name="theme"
value="light"
checked={theme === 'light'}
onChange={() => handleThemeChange('light')}
className="mr-2"
/>
Light
</label>
<label className="flex items-center">
<input
type="radio"
name="theme"
value="dark"
checked={theme === 'dark'}
onChange={() => handleThemeChange('dark')}
className="mr-2"
/>
Dark
</label>
<label className="flex items-center">
<input
type="radio"
name="theme"
value="auto"
checked={theme === 'auto'}
onChange={() => handleThemeChange('auto')}
className="mr-2"
/>
Auto (match profile)
</label>
</div>
</div>

{/* Preview */}
<div>
<label className="block text-sm font-medium mb-2">Preview:</label>
<div className="border rounded p-4 bg-gray-50 dark:bg-gray-900 min-h-[200px] flex items-center justify-center">
{loading ? (
<div className="text-gray-500">Loading preview...</div>
) : previewUrl ? (
<img
src={previewUrl}
alt="Star History Preview"
className="max-w-full h-auto"
onError={() => setErrors(['Failed to load preview. Check repository names.'])}
/>
) : (
<div className="text-gray-500">Enter repositories to see preview</div>
)}
</div>
</div>

{/* Help Text */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded p-3">
<p className="text-blue-800 dark:text-blue-300 text-sm">
<strong>Tip:</strong> The Star History chart shows the growth of GitHub stars over time.
Perfect for showcasing project popularity and growth trends.
</p>
</div>
</div>
)}
</div>
</div>
);
}
10 changes: 10 additions & 0 deletions src/lib/markdown-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
SupportFormData,
} from './validations';
import { DEFAULT_PREFIX } from '@/constants/defaults';
import { generateStarHistoryMarkdown } from './star-history';

interface GenerateMarkdownOptions {
profile: Partial<ProfileFormData>;
Expand Down Expand Up @@ -350,6 +351,15 @@ export function generateMarkdown(options: GenerateMarkdownOptions): string {
markdown += `<p align="left"> <a href="https://twitter.com/${social.twitter}" target="blank"><img src="https://img.shields.io/twitter/follow/${social.twitter}?logo=twitter&style=for-the-badge" alt="${social.twitter}" /></a> </p>\n\n`;
}

// Star History Chart
if (profile.starHistory && profile.starHistoryConfig?.enabled) {
const starHistoryMarkdown = generateStarHistoryMarkdown(profile.starHistoryConfig);
if (starHistoryMarkdown) {
markdown += `<h3 align="left">⭐ Star History</h3>\n\n`;
markdown += `${starHistoryMarkdown}\n\n`;
}
}

// About sections
const aboutSections = [
{
Expand Down
Loading