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
8 changes: 8 additions & 0 deletions app/generator/GeneratorClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ export function GeneratorClient() {
});
};

const handleApplyPreset = (presetState: Partial<GeneratorState>) => {
setState((prevState) => ({
...prevState,
...presetState,
}));
};

return (
<div className="flex flex-col lg:flex-row gap-5 xl:gap-6 items-start w-full">
<div className="w-full lg:w-[44%] xl:w-[42%] flex-shrink-0">
Expand Down Expand Up @@ -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}
/>
</div>

Expand Down
107 changes: 107 additions & 0 deletions app/generator/components/EditorPanel.presets-and-reset.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<EditorPanel
state={mockState}
onNameChange={vi.fn()}
onDescriptionChange={vi.fn()}
onTechsChange={vi.fn()}
onSocialsChange={vi.fn()}
onSocialLinkChange={vi.fn()}
onGithubUsernameChange={vi.fn()}
onShowCommitPulseChange={vi.fn()}
onCommitPulseAccentChange={vi.fn()}
onApplyImport={vi.fn()}
onApplyPreset={vi.fn()}
/>
);

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(
<EditorPanel
state={mockState}
onNameChange={vi.fn()}
onDescriptionChange={vi.fn()}
onTechsChange={vi.fn()}
onSocialsChange={vi.fn()}
onSocialLinkChange={vi.fn()}
onGithubUsernameChange={vi.fn()}
onShowCommitPulseChange={vi.fn()}
onCommitPulseAccentChange={vi.fn()}
onApplyImport={vi.fn()}
onApplyPreset={onApplyPresetMock}
/>
);

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(
<EditorPanel
state={mockState}
onNameChange={onNameChangeMock}
onDescriptionChange={vi.fn()}
onTechsChange={vi.fn()}
onSocialsChange={vi.fn()}
onSocialLinkChange={vi.fn()}
onGithubUsernameChange={vi.fn()}
onShowCommitPulseChange={vi.fn()}
onCommitPulseAccentChange={vi.fn()}
onApplyImport={vi.fn()}
onApplyPreset={vi.fn()}
/>
);

const nameResetBtn = screen.getByTitle('Reset Name Section');
expect(nameResetBtn).toBeInTheDocument();

fireEvent.click(nameResetBtn);

expect(onNameChangeMock).toHaveBeenCalledWith('');
});
});
76 changes: 73 additions & 3 deletions app/generator/components/EditorPanel.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

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

export function EditorPanel({
Expand All @@ -59,6 +62,7 @@ export function EditorPanel({
onArticlesPlatformChange = () => {},
onArticlesUsernameChange = () => {},
onApplyImport,
onApplyPreset,
}: EditorPanelProps) {
const [isImportModalOpen, setIsImportModalOpen] = useState(false);

Expand All @@ -69,6 +73,7 @@ export function EditorPanel({
className="flex flex-col gap-4"
onSubmit={(e) => e.preventDefault()}
>
{/* GitHub Import */}
<button
type="button"
onClick={() => setIsImportModalOpen(true)}
Expand All @@ -81,20 +86,67 @@ export function EditorPanel({
</span>
</button>

{/* Profile Presets Selector */}
<div className="p-4 rounded-2xl bg-white dark:bg-[#111111] border border-gray-200 dark:border-white/10 shadow-sm">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Sparkles size={16} className="text-emerald-500" />
<h3 className="text-xs font-bold text-gray-900 dark:text-white uppercase tracking-wider">
Profile Presets
</h3>
</div>
<span className="text-[10px] text-gray-400 dark:text-white/40">
1-click template setup
</span>
</div>
<div className="grid grid-cols-2 gap-2 mt-3">
{PROFILE_PRESETS.map((preset) => (
<button
key={preset.id}
type="button"
onClick={() => onApplyPreset?.(preset.state)}
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"
>
<div className="flex items-center justify-between w-full mb-1">
<span className="text-sm select-none">{preset.icon}</span>
<span className="text-[9px] font-bold px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
{preset.badge}
</span>
</div>
<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">
{preset.name}
</span>
<span className="text-[10px] text-gray-500 dark:text-white/40 truncate mt-0.5">
{preset.description}
</span>
</button>
))}
</div>
</div>

<GitHubImportModal
isOpen={isImportModalOpen}
onClose={() => setIsImportModalOpen(false)}
onApply={onApplyImport}
/>

<NameSection value={state.name} onChange={onNameChange} />
<DescriptionSection value={state.description} onChange={onDescriptionChange} />
<TechnologiesSection selected={state.selectedTechs} onChange={onTechsChange} />
<NameSection value={state.name} onChange={onNameChange} onReset={() => onNameChange('')} />
<DescriptionSection
value={state.description}
onChange={onDescriptionChange}
onReset={() => onDescriptionChange('')}
/>
<TechnologiesSection
selected={state.selectedTechs}
onChange={onTechsChange}
onReset={() => onTechsChange([])}
/>
<SocialsSection
selected={state.selectedSocials}
socialLinks={state.socialLinks}
onSelectedChange={onSocialsChange}
onLinkChange={onSocialLinkChange}
onReset={() => onSocialsChange([])}
/>
<CommitPulseSection
githubUsername={state.githubUsername}
Expand All @@ -103,6 +155,10 @@ export function EditorPanel({
onGithubUsernameChange={onGithubUsernameChange}
onShowCommitPulseChange={onShowCommitPulseChange}
onCommitPulseAccentChange={onCommitPulseAccentChange}
onReset={() => {
onShowCommitPulseChange(false);
onCommitPulseAccentChange('');
}}
/>
<ContributionGraphSection
githubUsername={state.githubUsername}
Expand All @@ -113,6 +169,11 @@ export function EditorPanel({
onShowSnakeGraphChange={onShowSnakeGraphChange}
onShowPacmanGraphChange={onShowPacmanGraphChange}
onGraphPlacementChange={onGraphPlacementChange}
onReset={() => {
onShowSnakeGraphChange(false);
onShowPacmanGraphChange(false);
onGraphPlacementChange('bottom');
}}
/>
<RepoSpotlightSection
githubUsername={state.githubUsername}
Expand All @@ -121,6 +182,10 @@ export function EditorPanel({
commitPulseAccent={state.commitPulseAccent}
onShowRepoSpotlightChange={onShowRepoSpotlightChange}
onSpotlightRepoChange={onSpotlightRepoChange}
onReset={() => {
onShowRepoSpotlightChange(false);
onSpotlightRepoChange('');
}}
/>
<ArticlesSection
showArticles={state.showArticles ?? false}
Expand All @@ -129,6 +194,11 @@ export function EditorPanel({
onShowArticlesChange={onShowArticlesChange}
onArticlesPlatformChange={onArticlesPlatformChange}
onArticlesUsernameChange={onArticlesUsernameChange}
onReset={() => {
onShowArticlesChange(false);
onArticlesPlatformChange('devto');
onArticlesUsernameChange('');
}}
/>
</form>
);
Expand Down
25 changes: 24 additions & 1 deletion app/generator/components/SectionCard.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,6 +10,7 @@ interface SectionCardProps {
children: ReactNode;
defaultOpen?: boolean;
badge?: number;
onReset?: () => void;
}

export function SectionCard({
Expand All @@ -19,6 +20,7 @@ export function SectionCard({
children,
defaultOpen = true,
badge,
onReset,
}: SectionCardProps) {
const [open, setOpen] = useState(defaultOpen);
const contentId = useId();
Expand Down Expand Up @@ -58,6 +60,27 @@ export function SectionCard({
</p>
)}
</div>
{onReset && (
<span
role="button"
tabIndex={0}
aria-label={`Reset ${title} section`}
title={`Reset ${title} Section`}
onClick={(e) => {
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"
>
<RotateCcw size={13} />
</span>
)}
<ChevronDown
size={14}
className={`text-gray-400 dark:text-white/30 flex-shrink-0 transition-transform duration-200 ${
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('SectionCard Type Compiler Validation', () => {
children: React.ReactNode;
defaultOpen?: boolean;
badge?: number;
onReset?: () => void;
}>();
});

Expand Down
3 changes: 3 additions & 0 deletions app/generator/components/sections/ArticlesSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -21,6 +22,7 @@ export function ArticlesSection({
onShowArticlesChange,
onArticlesPlatformChange,
onArticlesUsernameChange,
onReset,
}: ArticlesSectionProps) {
const safeUsername = articlesUsername || '';
const trimmed = safeUsername.trim();
Expand Down Expand Up @@ -53,6 +55,7 @@ export function ArticlesSection({
description="Display your most recent blog posts dynamically"
defaultOpen={true}
badge={badgeCount}
onReset={onReset}
>
<div className="flex items-start justify-between mb-5">
<div>
Expand Down
3 changes: 3 additions & 0 deletions app/generator/components/sections/CommitPulseSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -78,6 +79,7 @@ export function CommitPulseSection({
onGithubUsernameChange,
onShowCommitPulseChange,
onCommitPulseAccentChange,
onReset,
}: CommitPulseSectionProps) {
const safeUsername = githubUsername || '';
const safeAccent = commitPulseAccent || '';
Expand Down Expand Up @@ -193,6 +195,7 @@ export function CommitPulseSection({
description="Embed your live 3D contribution streak in the README"
defaultOpen={true}
badge={badgeCount}
onReset={onReset}
>
<div className="flex items-center justify-between mb-5">
<div>
Expand Down
Loading
Loading