diff --git a/app/generator/GeneratorClient.tsx b/app/generator/GeneratorClient.tsx index 9644d1775..fa33be4a0 100644 --- a/app/generator/GeneratorClient.tsx +++ b/app/generator/GeneratorClient.tsx @@ -82,6 +82,13 @@ export function GeneratorClient() { }); }; + const handleApplyPreset = (presetState: Partial) => { + setState((prevState) => ({ + ...prevState, + ...presetState, + })); + }; + return (
@@ -113,6 +120,7 @@ export function GeneratorClient() { onArticlesPlatformChange={(v) => setState((s) => ({ ...s, articlesPlatform: v }))} onArticlesUsernameChange={(v) => setState((s) => ({ ...s, articlesUsername: v }))} onApplyImport={handleApplyImport} + onApplyPreset={handleApplyPreset} />
diff --git a/app/generator/components/EditorPanel.presets-and-reset.test.tsx b/app/generator/components/EditorPanel.presets-and-reset.test.tsx new file mode 100644 index 000000000..936ba68a3 --- /dev/null +++ b/app/generator/components/EditorPanel.presets-and-reset.test.tsx @@ -0,0 +1,107 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { EditorPanel } from './EditorPanel'; +import type { GeneratorState } from '../types'; + +const mockState: GeneratorState = { + name: 'Original Name', + description: 'Original Description', + selectedTechs: ['react', 'nextjs'], + selectedSocials: ['github'], + socialLinks: { github: 'https://github.com/user' }, + githubUsername: 'user', + showCommitPulse: true, + commitPulseAccent: '10b981', + showSnakeGraph: true, + showPacmanGraph: false, + graphPlacement: 'bottom', + showRepoSpotlight: false, + spotlightRepo: '', + showArticles: false, + articlesPlatform: 'devto', + articlesUsername: '', +}; + +describe('EditorPanel Section Reset & Profile Presets', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders Profile Presets section and options', () => { + render( + + ); + + expect(screen.getByText('Profile Presets')).toBeInTheDocument(); + expect(screen.getByText('Full-Stack Developer')).toBeInTheDocument(); + expect(screen.getByText('Open Source Maintainer')).toBeInTheDocument(); + expect(screen.getByText('Data Scientist & AI Engineer')).toBeInTheDocument(); + expect(screen.getByText('Frontend Specialist & UI Engineer')).toBeInTheDocument(); + }); + + it('calls onApplyPreset when a preset button is clicked', () => { + const onApplyPresetMock = vi.fn(); + render( + + ); + + fireEvent.click(screen.getByText('Full-Stack Developer')); + + expect(onApplyPresetMock).toHaveBeenCalledWith( + expect.objectContaining({ + name: "Hi 👋, I'm a Full-Stack Developer", + showCommitPulse: true, + }) + ); + }); + + it('triggers onReset when SectionCard reset button is clicked', () => { + const onNameChangeMock = vi.fn(); + render( + + ); + + const nameResetBtn = screen.getByTitle('Reset Name Section'); + expect(nameResetBtn).toBeInTheDocument(); + + fireEvent.click(nameResetBtn); + + expect(onNameChangeMock).toHaveBeenCalledWith(''); + }); +}); diff --git a/app/generator/components/EditorPanel.tsx b/app/generator/components/EditorPanel.tsx index c6d03b9e8..4f58da562 100644 --- a/app/generator/components/EditorPanel.tsx +++ b/app/generator/components/EditorPanel.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState } from 'react'; +import { Sparkles } from 'lucide-react'; import { NameSection } from './sections/NameSection'; import { DescriptionSection } from './sections/DescriptionSection'; import { TechnologiesSection } from './sections/TechnologiesSection'; @@ -11,6 +12,7 @@ import { ContributionGraphSection } from './sections/ContributionGraphSection'; import { ArticlesSection } from './sections/ArticlesSection'; import { GitHubImportModal } from './GitHubImportModal'; import { FaGithub } from 'react-icons/fa'; +import { PROFILE_PRESETS } from '../data/presets'; import type { GeneratorState } from '../types'; import type { ImportedData } from '../utils/githubMapper'; @@ -38,6 +40,7 @@ export interface EditorPanelProps { onArticlesPlatformChange?: (v: 'devto' | 'hashnode') => void; onArticlesUsernameChange?: (v: string) => void; onApplyImport: (data: ImportedData) => void; + onApplyPreset?: (presetState: Partial) => void; } export function EditorPanel({ @@ -59,6 +62,7 @@ export function EditorPanel({ onArticlesPlatformChange = () => {}, onArticlesUsernameChange = () => {}, onApplyImport, + onApplyPreset, }: EditorPanelProps) { const [isImportModalOpen, setIsImportModalOpen] = useState(false); @@ -69,6 +73,7 @@ export function EditorPanel({ className="flex flex-col gap-4" onSubmit={(e) => e.preventDefault()} > + {/* GitHub Import */} + {/* Profile Presets Selector */} +
+
+
+ +

+ Profile Presets +

+
+ + 1-click template setup + +
+
+ {PROFILE_PRESETS.map((preset) => ( + + ))} +
+
+ setIsImportModalOpen(false)} onApply={onApplyImport} /> - - - + onNameChange('')} /> + onDescriptionChange('')} + /> + onTechsChange([])} + /> onSocialsChange([])} /> { + onShowCommitPulseChange(false); + onCommitPulseAccentChange(''); + }} /> { + onShowSnakeGraphChange(false); + onShowPacmanGraphChange(false); + onGraphPlacementChange('bottom'); + }} /> { + onShowRepoSpotlightChange(false); + onSpotlightRepoChange(''); + }} /> { + onShowArticlesChange(false); + onArticlesPlatformChange('devto'); + onArticlesUsernameChange(''); + }} /> ); diff --git a/app/generator/components/SectionCard.tsx b/app/generator/components/SectionCard.tsx index 783461612..c4ff0a1b9 100644 --- a/app/generator/components/SectionCard.tsx +++ b/app/generator/components/SectionCard.tsx @@ -1,7 +1,7 @@ 'use client'; import { type ReactNode, useState, useId } from 'react'; -import { ChevronDown } from 'lucide-react'; +import { ChevronDown, RotateCcw } from 'lucide-react'; interface SectionCardProps { title: string; @@ -10,6 +10,7 @@ interface SectionCardProps { children: ReactNode; defaultOpen?: boolean; badge?: number; + onReset?: () => void; } export function SectionCard({ @@ -19,6 +20,7 @@ export function SectionCard({ children, defaultOpen = true, badge, + onReset, }: SectionCardProps) { const [open, setOpen] = useState(defaultOpen); const contentId = useId(); @@ -58,6 +60,27 @@ export function SectionCard({

)}
+ {onReset && ( + { + e.stopPropagation(); + onReset(); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.stopPropagation(); + onReset(); + } + }} + className="p-1 rounded-lg text-gray-400 hover:text-rose-500 hover:bg-rose-500/10 dark:hover:bg-rose-500/20 transition-colors flex-shrink-0 mr-1 cursor-pointer" + > + + + )} { children: React.ReactNode; defaultOpen?: boolean; badge?: number; + onReset?: () => void; }>(); }); diff --git a/app/generator/components/sections/ArticlesSection.tsx b/app/generator/components/sections/ArticlesSection.tsx index 7c9883ea1..73d69d362 100644 --- a/app/generator/components/sections/ArticlesSection.tsx +++ b/app/generator/components/sections/ArticlesSection.tsx @@ -12,6 +12,7 @@ export interface ArticlesSectionProps { onShowArticlesChange: (v: boolean) => void; onArticlesPlatformChange: (v: 'devto' | 'hashnode') => void; onArticlesUsernameChange: (v: string) => void; + onReset?: () => void; } export function ArticlesSection({ @@ -21,6 +22,7 @@ export function ArticlesSection({ onShowArticlesChange, onArticlesPlatformChange, onArticlesUsernameChange, + onReset, }: ArticlesSectionProps) { const safeUsername = articlesUsername || ''; const trimmed = safeUsername.trim(); @@ -53,6 +55,7 @@ export function ArticlesSection({ description="Display your most recent blog posts dynamically" defaultOpen={true} badge={badgeCount} + onReset={onReset} >
diff --git a/app/generator/components/sections/CommitPulseSection.tsx b/app/generator/components/sections/CommitPulseSection.tsx index 46968142c..0da3dc480 100644 --- a/app/generator/components/sections/CommitPulseSection.tsx +++ b/app/generator/components/sections/CommitPulseSection.tsx @@ -33,6 +33,7 @@ export interface CommitPulseSectionProps { onGithubUsernameChange: (v: string) => void; onShowCommitPulseChange: (v: boolean) => void; onCommitPulseAccentChange: (v: string) => void; + onReset?: () => void; } const BADGE_BASE = 'https://commitpulse.vercel.app/api/streak'; @@ -78,6 +79,7 @@ export function CommitPulseSection({ onGithubUsernameChange, onShowCommitPulseChange, onCommitPulseAccentChange, + onReset, }: CommitPulseSectionProps) { const safeUsername = githubUsername || ''; const safeAccent = commitPulseAccent || ''; @@ -193,6 +195,7 @@ export function CommitPulseSection({ description="Embed your live 3D contribution streak in the README" defaultOpen={true} badge={badgeCount} + onReset={onReset} >
diff --git a/app/generator/components/sections/ContributionGraphSection.tsx b/app/generator/components/sections/ContributionGraphSection.tsx index f7cd5fc33..4df98ce48 100644 --- a/app/generator/components/sections/ContributionGraphSection.tsx +++ b/app/generator/components/sections/ContributionGraphSection.tsx @@ -36,6 +36,7 @@ export interface ContributionGraphSectionProps { onShowSnakeGraphChange: (v: boolean) => void; onShowPacmanGraphChange: (v: boolean) => void; onGraphPlacementChange: (v: 'top' | 'middle' | 'bottom') => void; + onReset?: () => void; } export function ContributionGraphSection({ @@ -47,6 +48,7 @@ export function ContributionGraphSection({ onShowSnakeGraphChange, onShowPacmanGraphChange, onGraphPlacementChange, + onReset, }: ContributionGraphSectionProps) { const safeUsername = githubUsername || ''; const trimmed = safeUsername.trim(); @@ -126,6 +128,7 @@ export function ContributionGraphSection({ description="Add animated Snake/Pacman contribution graphs to your README" defaultOpen={false} badge={activeCount} + onReset={onReset} >
{/* Toggle Snake Graph */} diff --git a/app/generator/components/sections/DescriptionSection.tsx b/app/generator/components/sections/DescriptionSection.tsx index 46e110173..d17e76f68 100644 --- a/app/generator/components/sections/DescriptionSection.tsx +++ b/app/generator/components/sections/DescriptionSection.tsx @@ -5,11 +5,12 @@ import { SectionCard, FieldLabel } from '../SectionCard'; interface DescriptionSectionProps { value: string; onChange: (v: string) => void; + onReset?: () => void; } const CHAR_LIMIT = 280; -export function DescriptionSection({ value, onChange }: DescriptionSectionProps) { +export function DescriptionSection({ value, onChange, onReset }: DescriptionSectionProps) { const safeValue = value || ''; const remaining = CHAR_LIMIT - safeValue.length; const isNearLimit = remaining < 40; @@ -20,6 +21,7 @@ export function DescriptionSection({ value, onChange }: DescriptionSectionProps) title="Description" description="A short bio or tagline about yourself" defaultOpen + onReset={onReset} > Bio / Tagline