Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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>
);
}
248 changes: 248 additions & 0 deletions src/components/sections/star-history.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
'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 { CollapsibleSection } from '@/components/ui/collapsible-section';
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) {
// Add this debug to check form values - use different variable names
const mainEnabled = watch('starHistory');
const configData = watch('starHistoryConfig');
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]); // Remove reposInput from dependencies

// Update preview when config changes
useEffect(() => {
updatePreview();
}, [repos, chartType, theme]);

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

console.log('πŸ” Step 6 - Syncing states:', { mainEnabled, configEnabled });

// If they're out of sync, fix it
if (mainEnabled !== configEnabled) {
console.log('πŸ”„ Fixing sync issue');
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
Loading