Skip to content

Commit 11b163e

Browse files
authored
merge: feat: Enhance README Generator with Profile Presets and Section Reset Actions (#8264)
## Description Resolves #8218. This PR introduces 1-click **Profile Preset Templates** and single-click **Section Reset** action buttons to the README Generator (`/generator`), eliminating manual input clearing and accelerating profile setup. ### Key Enhancements & Features: 1. **1-Click Profile Presets Loader**: - **Full-Stack Developer**: Prefills JavaScript, TypeScript, React, Next.js, Node.js, MongoDB, PostgreSQL, Docker, Git. - **Open Source Maintainer**: Prefills TypeScript, Go, Rust, Python, Docker, Kubernetes, GraphQL, Linux, and Repo Spotlight. - **Data Scientist & AI Engineer**: Prefills Python, PyTorch, TensorFlow, Pandas, NumPy, Jupyter, AWS, and Pacman Contribution Graph. - **Frontend Specialist & UI Engineer**: Prefills React, Vue, Next.js, TailwindCSS, Figma, Framer, Sass, and Snake Graph. 2. **Section Reset Buttons**: - Added a `RotateCcw` reset button to every `SectionCard` header (Name, Description, Technologies, Socials, CommitPulse Badge, Contribution Visualizations, Repo Spotlight, and Latest Articles). - Clicking reset clears only that specific section's inputs without collapsing the section accordion. --- ## Target Files Modified / Created: - `app/generator/data/presets.ts` [NEW] - `app/generator/components/SectionCard.tsx` - `app/generator/components/EditorPanel.tsx` - `app/generator/GeneratorClient.tsx` - `app/generator/components/sections/*.tsx` - `app/generator/components/EditorPanel.presets-and-reset.test.tsx` [NEW] --- ## Verification & Testing - ✅ **Unit Tests**: Ran `npx vitest run app/generator/components/EditorPanel.presets-and-reset.test.tsx app/generator/components/SectionCard.accessibility.test.tsx app/generator/components/EditorPanel.empty-fallback.test.tsx` — **31 / 31 tests passed**. - ✅ **TypeScript Check**: Ran `npx tsc --noEmit` — **0 errors**. - ✅ **Linting**: Passed `eslint --fix` and `prettier` pre-commit checks cleanly. --- *GSSoC 2026 Contribution*
2 parents 33ab6b2 + e8b0d55 commit 11b163e

15 files changed

Lines changed: 368 additions & 10 deletions

app/generator/GeneratorClient.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ export function GeneratorClient() {
8282
});
8383
};
8484

85+
const handleApplyPreset = (presetState: Partial<GeneratorState>) => {
86+
setState((prevState) => ({
87+
...prevState,
88+
...presetState,
89+
}));
90+
};
91+
8592
return (
8693
<div className="flex flex-col lg:flex-row gap-5 xl:gap-6 items-start w-full">
8794
<div className="w-full lg:w-[44%] xl:w-[42%] flex-shrink-0">
@@ -113,6 +120,7 @@ export function GeneratorClient() {
113120
onArticlesPlatformChange={(v) => setState((s) => ({ ...s, articlesPlatform: v }))}
114121
onArticlesUsernameChange={(v) => setState((s) => ({ ...s, articlesUsername: v }))}
115122
onApplyImport={handleApplyImport}
123+
onApplyPreset={handleApplyPreset}
116124
/>
117125
</div>
118126

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import { EditorPanel } from './EditorPanel';
4+
import type { GeneratorState } from '../types';
5+
6+
const mockState: GeneratorState = {
7+
name: 'Original Name',
8+
description: 'Original Description',
9+
selectedTechs: ['react', 'nextjs'],
10+
selectedSocials: ['github'],
11+
socialLinks: { github: 'https://github.com/user' },
12+
githubUsername: 'user',
13+
showCommitPulse: true,
14+
commitPulseAccent: '10b981',
15+
showSnakeGraph: true,
16+
showPacmanGraph: false,
17+
graphPlacement: 'bottom',
18+
showRepoSpotlight: false,
19+
spotlightRepo: '',
20+
showArticles: false,
21+
articlesPlatform: 'devto',
22+
articlesUsername: '',
23+
};
24+
25+
describe('EditorPanel Section Reset & Profile Presets', () => {
26+
beforeEach(() => {
27+
vi.clearAllMocks();
28+
});
29+
30+
it('renders Profile Presets section and options', () => {
31+
render(
32+
<EditorPanel
33+
state={mockState}
34+
onNameChange={vi.fn()}
35+
onDescriptionChange={vi.fn()}
36+
onTechsChange={vi.fn()}
37+
onSocialsChange={vi.fn()}
38+
onSocialLinkChange={vi.fn()}
39+
onGithubUsernameChange={vi.fn()}
40+
onShowCommitPulseChange={vi.fn()}
41+
onCommitPulseAccentChange={vi.fn()}
42+
onApplyImport={vi.fn()}
43+
onApplyPreset={vi.fn()}
44+
/>
45+
);
46+
47+
expect(screen.getByText('Profile Presets')).toBeInTheDocument();
48+
expect(screen.getByText('Full-Stack Developer')).toBeInTheDocument();
49+
expect(screen.getByText('Open Source Maintainer')).toBeInTheDocument();
50+
expect(screen.getByText('Data Scientist & AI Engineer')).toBeInTheDocument();
51+
expect(screen.getByText('Frontend Specialist & UI Engineer')).toBeInTheDocument();
52+
});
53+
54+
it('calls onApplyPreset when a preset button is clicked', () => {
55+
const onApplyPresetMock = vi.fn();
56+
render(
57+
<EditorPanel
58+
state={mockState}
59+
onNameChange={vi.fn()}
60+
onDescriptionChange={vi.fn()}
61+
onTechsChange={vi.fn()}
62+
onSocialsChange={vi.fn()}
63+
onSocialLinkChange={vi.fn()}
64+
onGithubUsernameChange={vi.fn()}
65+
onShowCommitPulseChange={vi.fn()}
66+
onCommitPulseAccentChange={vi.fn()}
67+
onApplyImport={vi.fn()}
68+
onApplyPreset={onApplyPresetMock}
69+
/>
70+
);
71+
72+
fireEvent.click(screen.getByText('Full-Stack Developer'));
73+
74+
expect(onApplyPresetMock).toHaveBeenCalledWith(
75+
expect.objectContaining({
76+
name: "Hi 👋, I'm a Full-Stack Developer",
77+
showCommitPulse: true,
78+
})
79+
);
80+
});
81+
82+
it('triggers onReset when SectionCard reset button is clicked', () => {
83+
const onNameChangeMock = vi.fn();
84+
render(
85+
<EditorPanel
86+
state={mockState}
87+
onNameChange={onNameChangeMock}
88+
onDescriptionChange={vi.fn()}
89+
onTechsChange={vi.fn()}
90+
onSocialsChange={vi.fn()}
91+
onSocialLinkChange={vi.fn()}
92+
onGithubUsernameChange={vi.fn()}
93+
onShowCommitPulseChange={vi.fn()}
94+
onCommitPulseAccentChange={vi.fn()}
95+
onApplyImport={vi.fn()}
96+
onApplyPreset={vi.fn()}
97+
/>
98+
);
99+
100+
const nameResetBtn = screen.getByTitle('Reset Name Section');
101+
expect(nameResetBtn).toBeInTheDocument();
102+
103+
fireEvent.click(nameResetBtn);
104+
105+
expect(onNameChangeMock).toHaveBeenCalledWith('');
106+
});
107+
});

app/generator/components/EditorPanel.tsx

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

33
import { useState } from 'react';
4+
import { Sparkles } from 'lucide-react';
45
import { NameSection } from './sections/NameSection';
56
import { DescriptionSection } from './sections/DescriptionSection';
67
import { TechnologiesSection } from './sections/TechnologiesSection';
@@ -11,6 +12,7 @@ import { ContributionGraphSection } from './sections/ContributionGraphSection';
1112
import { ArticlesSection } from './sections/ArticlesSection';
1213
import { GitHubImportModal } from './GitHubImportModal';
1314
import { FaGithub } from 'react-icons/fa';
15+
import { PROFILE_PRESETS } from '../data/presets';
1416
import type { GeneratorState } from '../types';
1517
import type { ImportedData } from '../utils/githubMapper';
1618

@@ -38,6 +40,7 @@ export interface EditorPanelProps {
3840
onArticlesPlatformChange?: (v: 'devto' | 'hashnode') => void;
3941
onArticlesUsernameChange?: (v: string) => void;
4042
onApplyImport: (data: ImportedData) => void;
43+
onApplyPreset?: (presetState: Partial<GeneratorState>) => void;
4144
}
4245

4346
export function EditorPanel({
@@ -59,6 +62,7 @@ export function EditorPanel({
5962
onArticlesPlatformChange = () => {},
6063
onArticlesUsernameChange = () => {},
6164
onApplyImport,
65+
onApplyPreset,
6266
}: EditorPanelProps) {
6367
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
6468

@@ -69,6 +73,7 @@ export function EditorPanel({
6973
className="flex flex-col gap-4"
7074
onSubmit={(e) => e.preventDefault()}
7175
>
76+
{/* GitHub Import */}
7277
<button
7378
type="button"
7479
onClick={() => setIsImportModalOpen(true)}
@@ -81,20 +86,67 @@ export function EditorPanel({
8186
</span>
8287
</button>
8388

89+
{/* Profile Presets Selector */}
90+
<div className="p-4 rounded-2xl bg-white dark:bg-[#111111] border border-gray-200 dark:border-white/10 shadow-sm">
91+
<div className="flex items-center justify-between mb-2">
92+
<div className="flex items-center gap-2">
93+
<Sparkles size={16} className="text-emerald-500" />
94+
<h3 className="text-xs font-bold text-gray-900 dark:text-white uppercase tracking-wider">
95+
Profile Presets
96+
</h3>
97+
</div>
98+
<span className="text-[10px] text-gray-400 dark:text-white/40">
99+
1-click template setup
100+
</span>
101+
</div>
102+
<div className="grid grid-cols-2 gap-2 mt-3">
103+
{PROFILE_PRESETS.map((preset) => (
104+
<button
105+
key={preset.id}
106+
type="button"
107+
onClick={() => onApplyPreset?.(preset.state)}
108+
className="flex flex-col text-left p-2.5 rounded-xl bg-gray-50 dark:bg-white/5 border border-gray-200/60 dark:border-white/5 hover:border-emerald-500/40 dark:hover:border-emerald-500/40 hover:bg-emerald-500/5 transition-all group"
109+
>
110+
<div className="flex items-center justify-between w-full mb-1">
111+
<span className="text-sm select-none">{preset.icon}</span>
112+
<span className="text-[9px] font-bold px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
113+
{preset.badge}
114+
</span>
115+
</div>
116+
<span className="text-xs font-semibold text-gray-900 dark:text-white group-hover:text-emerald-600 dark:group-hover:text-emerald-400 transition-colors">
117+
{preset.name}
118+
</span>
119+
<span className="text-[10px] text-gray-500 dark:text-white/40 truncate mt-0.5">
120+
{preset.description}
121+
</span>
122+
</button>
123+
))}
124+
</div>
125+
</div>
126+
84127
<GitHubImportModal
85128
isOpen={isImportModalOpen}
86129
onClose={() => setIsImportModalOpen(false)}
87130
onApply={onApplyImport}
88131
/>
89132

90-
<NameSection value={state.name} onChange={onNameChange} />
91-
<DescriptionSection value={state.description} onChange={onDescriptionChange} />
92-
<TechnologiesSection selected={state.selectedTechs} onChange={onTechsChange} />
133+
<NameSection value={state.name} onChange={onNameChange} onReset={() => onNameChange('')} />
134+
<DescriptionSection
135+
value={state.description}
136+
onChange={onDescriptionChange}
137+
onReset={() => onDescriptionChange('')}
138+
/>
139+
<TechnologiesSection
140+
selected={state.selectedTechs}
141+
onChange={onTechsChange}
142+
onReset={() => onTechsChange([])}
143+
/>
93144
<SocialsSection
94145
selected={state.selectedSocials}
95146
socialLinks={state.socialLinks}
96147
onSelectedChange={onSocialsChange}
97148
onLinkChange={onSocialLinkChange}
149+
onReset={() => onSocialsChange([])}
98150
/>
99151
<CommitPulseSection
100152
githubUsername={state.githubUsername}
@@ -103,6 +155,10 @@ export function EditorPanel({
103155
onGithubUsernameChange={onGithubUsernameChange}
104156
onShowCommitPulseChange={onShowCommitPulseChange}
105157
onCommitPulseAccentChange={onCommitPulseAccentChange}
158+
onReset={() => {
159+
onShowCommitPulseChange(false);
160+
onCommitPulseAccentChange('');
161+
}}
106162
/>
107163
<ContributionGraphSection
108164
githubUsername={state.githubUsername}
@@ -113,6 +169,11 @@ export function EditorPanel({
113169
onShowSnakeGraphChange={onShowSnakeGraphChange}
114170
onShowPacmanGraphChange={onShowPacmanGraphChange}
115171
onGraphPlacementChange={onGraphPlacementChange}
172+
onReset={() => {
173+
onShowSnakeGraphChange(false);
174+
onShowPacmanGraphChange(false);
175+
onGraphPlacementChange('bottom');
176+
}}
116177
/>
117178
<RepoSpotlightSection
118179
githubUsername={state.githubUsername}
@@ -121,6 +182,10 @@ export function EditorPanel({
121182
commitPulseAccent={state.commitPulseAccent}
122183
onShowRepoSpotlightChange={onShowRepoSpotlightChange}
123184
onSpotlightRepoChange={onSpotlightRepoChange}
185+
onReset={() => {
186+
onShowRepoSpotlightChange(false);
187+
onSpotlightRepoChange('');
188+
}}
124189
/>
125190
<ArticlesSection
126191
showArticles={state.showArticles ?? false}
@@ -129,6 +194,11 @@ export function EditorPanel({
129194
onShowArticlesChange={onShowArticlesChange}
130195
onArticlesPlatformChange={onArticlesPlatformChange}
131196
onArticlesUsernameChange={onArticlesUsernameChange}
197+
onReset={() => {
198+
onShowArticlesChange(false);
199+
onArticlesPlatformChange('devto');
200+
onArticlesUsernameChange('');
201+
}}
132202
/>
133203
</form>
134204
);

app/generator/components/SectionCard.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { type ReactNode, useState, useId } from 'react';
4-
import { ChevronDown } from 'lucide-react';
4+
import { ChevronDown, RotateCcw } from 'lucide-react';
55

66
interface SectionCardProps {
77
title: string;
@@ -10,6 +10,7 @@ interface SectionCardProps {
1010
children: ReactNode;
1111
defaultOpen?: boolean;
1212
badge?: number;
13+
onReset?: () => void;
1314
}
1415

1516
export function SectionCard({
@@ -19,6 +20,7 @@ export function SectionCard({
1920
children,
2021
defaultOpen = true,
2122
badge,
23+
onReset,
2224
}: SectionCardProps) {
2325
const [open, setOpen] = useState(defaultOpen);
2426
const contentId = useId();
@@ -58,6 +60,27 @@ export function SectionCard({
5860
</p>
5961
)}
6062
</div>
63+
{onReset && (
64+
<span
65+
role="button"
66+
tabIndex={0}
67+
aria-label={`Reset ${title} section`}
68+
title={`Reset ${title} Section`}
69+
onClick={(e) => {
70+
e.stopPropagation();
71+
onReset();
72+
}}
73+
onKeyDown={(e) => {
74+
if (e.key === 'Enter' || e.key === ' ') {
75+
e.stopPropagation();
76+
onReset();
77+
}
78+
}}
79+
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"
80+
>
81+
<RotateCcw size={13} />
82+
</span>
83+
)}
6184
<ChevronDown
6285
size={14}
6386
className={`text-gray-400 dark:text-white/30 flex-shrink-0 transition-transform duration-200 ${

app/generator/components/SectionCard.type-compiler.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('SectionCard Type Compiler Validation', () => {
2626
children: React.ReactNode;
2727
defaultOpen?: boolean;
2828
badge?: number;
29+
onReset?: () => void;
2930
}>();
3031
});
3132

app/generator/components/sections/ArticlesSection.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface ArticlesSectionProps {
1212
onShowArticlesChange: (v: boolean) => void;
1313
onArticlesPlatformChange: (v: 'devto' | 'hashnode') => void;
1414
onArticlesUsernameChange: (v: string) => void;
15+
onReset?: () => void;
1516
}
1617

1718
export function ArticlesSection({
@@ -21,6 +22,7 @@ export function ArticlesSection({
2122
onShowArticlesChange,
2223
onArticlesPlatformChange,
2324
onArticlesUsernameChange,
25+
onReset,
2426
}: ArticlesSectionProps) {
2527
const safeUsername = articlesUsername || '';
2628
const trimmed = safeUsername.trim();
@@ -53,6 +55,7 @@ export function ArticlesSection({
5355
description="Display your most recent blog posts dynamically"
5456
defaultOpen={true}
5557
badge={badgeCount}
58+
onReset={onReset}
5659
>
5760
<div className="flex items-start justify-between mb-5">
5861
<div>

app/generator/components/sections/CommitPulseSection.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface CommitPulseSectionProps {
3333
onGithubUsernameChange: (v: string) => void;
3434
onShowCommitPulseChange: (v: boolean) => void;
3535
onCommitPulseAccentChange: (v: string) => void;
36+
onReset?: () => void;
3637
}
3738

3839
const BADGE_BASE = 'https://commitpulse.vercel.app/api/streak';
@@ -78,6 +79,7 @@ export function CommitPulseSection({
7879
onGithubUsernameChange,
7980
onShowCommitPulseChange,
8081
onCommitPulseAccentChange,
82+
onReset,
8183
}: CommitPulseSectionProps) {
8284
const safeUsername = githubUsername || '';
8385
const safeAccent = commitPulseAccent || '';
@@ -193,6 +195,7 @@ export function CommitPulseSection({
193195
description="Embed your live 3D contribution streak in the README"
194196
defaultOpen={true}
195197
badge={badgeCount}
198+
onReset={onReset}
196199
>
197200
<div className="flex items-center justify-between mb-5">
198201
<div>

0 commit comments

Comments
 (0)