From 8da3834bf14f42233e9f3d9c277975480d3937dd Mon Sep 17 00:00:00 2001
From: Rakshak05 <159248180+Rakshak05@users.noreply.github.com>
Date: Mon, 10 Aug 2026 17:08:44 +0530
Subject: [PATCH 1/3] Resolves issue-#7814
---
app/generator/GeneratorClient.tsx | 2 +
app/generator/components/EditorPanel.tsx | 11 +-
.../sections/LayoutTemplateSection.tsx | 120 +++++
app/generator/data/presets.ts | 4 +
app/generator/types.ts | 3 +
.../readmeGenerator.layout-templates.test.ts | 121 +++++
app/generator/utils/readmeGenerator.ts | 437 ++++++++++--------
7 files changed, 500 insertions(+), 198 deletions(-)
create mode 100644 app/generator/components/sections/LayoutTemplateSection.tsx
create mode 100644 app/generator/utils/readmeGenerator.layout-templates.test.ts
diff --git a/app/generator/GeneratorClient.tsx b/app/generator/GeneratorClient.tsx
index 13886a81a..4574362cd 100644
--- a/app/generator/GeneratorClient.tsx
+++ b/app/generator/GeneratorClient.tsx
@@ -13,6 +13,7 @@ import type { GeneratorState } from './types';
import type { ImportedData } from './utils/githubMapper';
const INITIAL_STATE: GeneratorState = {
+ layoutTemplate: 'classic',
name: '',
description: '',
selectedTechs: [],
@@ -103,6 +104,7 @@ export function GeneratorClient() {
setState((s) => ({ ...s, layoutTemplate: v }))}
onNameChange={(v) => setState((s) => ({ ...s, name: v }))}
onDescriptionChange={(v) => setState((s) => ({ ...s, description: v }))}
onShowHeroImageChange={(v) => setState((s) => ({ ...s, showHeroImage: v }))}
diff --git a/app/generator/components/EditorPanel.tsx b/app/generator/components/EditorPanel.tsx
index 83a9d0368..d34aab4b2 100644
--- a/app/generator/components/EditorPanel.tsx
+++ b/app/generator/components/EditorPanel.tsx
@@ -2,6 +2,7 @@
import { useState } from 'react';
import { Sparkles } from 'lucide-react';
+import { LayoutTemplateSection } from './sections/LayoutTemplateSection';
import { NameSection } from './sections/NameSection';
import { DescriptionSection } from './sections/DescriptionSection';
import { HeroImageSection } from './sections/HeroImageSection';
@@ -14,11 +15,12 @@ import { ArticlesSection } from './sections/ArticlesSection';
import { GitHubImportModal } from './GitHubImportModal';
import { FaGithub } from 'react-icons/fa';
import { PROFILE_PRESETS } from '../data/presets';
-import type { GeneratorState, TechIconDisplay } from '../types';
+import type { GeneratorState, TechIconDisplay, LayoutTemplate } from '../types';
import type { ImportedData } from '../utils/githubMapper';
export interface EditorPanelProps {
state: GeneratorState;
+ onLayoutTemplateChange?: (v: LayoutTemplate) => void;
onNameChange: (v: string) => void;
onDescriptionChange: (v: string) => void;
onShowHeroImageChange?: (v: boolean) => void;
@@ -54,6 +56,7 @@ export interface EditorPanelProps {
export function EditorPanel({
state,
+ onLayoutTemplateChange = () => {},
onNameChange,
onDescriptionChange,
onShowHeroImageChange = () => {},
@@ -147,6 +150,12 @@ export function EditorPanel({
onApply={onApplyImport}
/>
+ onLayoutTemplateChange('classic')}
+ />
+
onNameChange('')} />
void;
+ onReset?: () => void;
+}
+
+interface TemplateOption {
+ id: LayoutTemplate;
+ name: string;
+ badge: string;
+ icon: string;
+ description: string;
+ orderPreview: string;
+}
+
+const TEMPLATE_OPTIONS: TemplateOption[] = [
+ {
+ id: 'classic',
+ name: 'Classic',
+ badge: 'Standard',
+ icon: 'ðïļ',
+ description: 'Traditional top-down layout starting with Header and Hero banner.',
+ orderPreview: 'Header â Hero â Tech â Socials â Streak â Spotlight',
+ },
+ {
+ id: 'minimalist',
+ name: 'Minimalist',
+ badge: 'Clean & Direct',
+ icon: 'ðŠķ',
+ description: 'Concise layout putting quick contact and key skills upfront.',
+ orderPreview: 'Header â Socials â Tech â Hero â Streak â Articles',
+ },
+ {
+ id: 'data-heavy',
+ name: 'Data Heavy',
+ badge: 'Stats First',
+ icon: 'ð',
+ description: 'Metrics-driven structure emphasizing contribution graphs and streak stats early.',
+ orderPreview: 'Header â Streak â Spotlight â Graphs â Tech â Socials',
+ },
+ {
+ id: 'storyteller',
+ name: 'Storyteller',
+ badge: 'Narrative',
+ icon: 'ð',
+ description:
+ 'Content and visual narrative layout featuring articles and repository spotlights.',
+ orderPreview: 'Header â Hero â Articles â Spotlight â Tech â Streak',
+ },
+];
+
+export function LayoutTemplateSection({
+ layoutTemplate = 'classic',
+ onChange,
+ onReset,
+}: LayoutTemplateSectionProps) {
+ return (
+
+ Select Layout
+
+ {TEMPLATE_OPTIONS.map((tpl) => {
+ const isSelected = layoutTemplate === tpl.id;
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/app/generator/data/presets.ts b/app/generator/data/presets.ts
index 9bf02e508..47abc78b8 100644
--- a/app/generator/data/presets.ts
+++ b/app/generator/data/presets.ts
@@ -36,6 +36,7 @@ export const PROFILE_PRESETS: ProfilePreset[] = [
showCommitPulse: true,
showSnakeGraph: true,
graphPlacement: 'bottom',
+ layoutTemplate: 'classic',
},
},
{
@@ -64,6 +65,7 @@ export const PROFILE_PRESETS: ProfilePreset[] = [
showRepoSpotlight: true,
showSnakeGraph: true,
graphPlacement: 'bottom',
+ layoutTemplate: 'storyteller',
},
},
{
@@ -90,6 +92,7 @@ export const PROFILE_PRESETS: ProfilePreset[] = [
showCommitPulse: true,
showPacmanGraph: true,
graphPlacement: 'bottom',
+ layoutTemplate: 'data-heavy',
},
},
{
@@ -117,6 +120,7 @@ export const PROFILE_PRESETS: ProfilePreset[] = [
showCommitPulse: true,
showSnakeGraph: true,
graphPlacement: 'bottom',
+ layoutTemplate: 'minimalist',
},
},
];
diff --git a/app/generator/types.ts b/app/generator/types.ts
index 0af720d7c..9e61f964d 100644
--- a/app/generator/types.ts
+++ b/app/generator/types.ts
@@ -46,7 +46,10 @@ export interface Social {
siSlug?: string;
}
+export type LayoutTemplate = 'classic' | 'minimalist' | 'data-heavy' | 'storyteller';
+
export interface GeneratorState {
+ layoutTemplate?: LayoutTemplate;
name: string;
description: string;
selectedTechs: string[];
diff --git a/app/generator/utils/readmeGenerator.layout-templates.test.ts b/app/generator/utils/readmeGenerator.layout-templates.test.ts
new file mode 100644
index 000000000..aff65cfc9
--- /dev/null
+++ b/app/generator/utils/readmeGenerator.layout-templates.test.ts
@@ -0,0 +1,121 @@
+import { describe, it, expect } from 'vitest';
+import { generateReadme } from './readmeGenerator';
+import type { GeneratorState } from '../types';
+
+const sampleState: GeneratorState = {
+ layoutTemplate: 'classic',
+ name: 'Alex Developer',
+ description: 'Full stack software architect',
+ showHeroImage: true,
+ heroImageUrl: 'https://example.com/hero.gif',
+ selectedTechs: ['typescript', 'react'],
+ selectedSocials: ['github'],
+ socialLinks: { github: 'alexdev' },
+ githubUsername: 'alexdev',
+ showCommitPulse: true,
+ commitPulseAccent: '00ff00',
+ showRepoSpotlight: true,
+ spotlightRepo: 'awesome-project',
+ showArticles: true,
+ articlesPlatform: 'devto',
+ articlesUsername: 'alexdev',
+ showSnakeGraph: true,
+ showPacmanGraph: false,
+ graphPlacement: 'bottom',
+};
+
+describe('readmeGenerator Layout Templates', () => {
+ it('generates sections in Classic order by default', () => {
+ const md = generateReadme({ ...sampleState, layoutTemplate: 'classic' });
+ const headerPos = md.indexOf("Hi, I'm Alex Developer");
+ const heroPos = md.indexOf('https://example.com/hero.gif');
+ const techPos = md.indexOf('Tech Stack');
+ const socialPos = md.indexOf('Connect With Me');
+ const streakPos = md.indexOf('GitHub Streak');
+ const spotlightPos = md.indexOf('Repository Spotlight');
+ const articlesPos = md.indexOf('Latest Articles');
+ const graphsPos = md.indexOf('Snake Contribution Graph');
+
+ expect(headerPos).toBeGreaterThan(-1);
+ expect(heroPos).toBeGreaterThan(headerPos);
+ expect(techPos).toBeGreaterThan(heroPos);
+ expect(socialPos).toBeGreaterThan(techPos);
+ expect(streakPos).toBeGreaterThan(socialPos);
+ expect(spotlightPos).toBeGreaterThan(streakPos);
+ expect(articlesPos).toBeGreaterThan(spotlightPos);
+ expect(graphsPos).toBeGreaterThan(articlesPos);
+ });
+
+ it('generates sections in Minimalist order', () => {
+ const md = generateReadme({ ...sampleState, layoutTemplate: 'minimalist' });
+ const headerPos = md.indexOf("Hi, I'm Alex Developer");
+ const socialPos = md.indexOf('Connect With Me');
+ const techPos = md.indexOf('Tech Stack');
+ const heroPos = md.indexOf('https://example.com/hero.gif');
+ const streakPos = md.indexOf('GitHub Streak');
+ const spotlightPos = md.indexOf('Repository Spotlight');
+ const articlesPos = md.indexOf('Latest Articles');
+ const graphsPos = md.indexOf('Snake Contribution Graph');
+
+ expect(headerPos).toBeLessThan(socialPos);
+ expect(socialPos).toBeLessThan(techPos);
+ expect(techPos).toBeLessThan(heroPos);
+ expect(heroPos).toBeLessThan(streakPos);
+ expect(streakPos).toBeLessThan(spotlightPos);
+ expect(spotlightPos).toBeLessThan(articlesPos);
+ expect(articlesPos).toBeLessThan(graphsPos);
+ });
+
+ it('generates sections in Data Heavy order', () => {
+ const md = generateReadme({ ...sampleState, layoutTemplate: 'data-heavy' });
+ const headerPos = md.indexOf("Hi, I'm Alex Developer");
+ const streakPos = md.indexOf('GitHub Streak');
+ const spotlightPos = md.indexOf('Repository Spotlight');
+ const graphsPos = md.indexOf('Snake Contribution Graph');
+ const techPos = md.indexOf('Tech Stack');
+ const socialPos = md.indexOf('Connect With Me');
+ const heroPos = md.indexOf('https://example.com/hero.gif');
+ const articlesPos = md.indexOf('Latest Articles');
+
+ expect(headerPos).toBeLessThan(streakPos);
+ expect(streakPos).toBeLessThan(spotlightPos);
+ expect(spotlightPos).toBeLessThan(graphsPos);
+ expect(graphsPos).toBeLessThan(techPos);
+ expect(techPos).toBeLessThan(socialPos);
+ expect(socialPos).toBeLessThan(heroPos);
+ expect(heroPos).toBeLessThan(articlesPos);
+ });
+
+ it('generates sections in Storyteller order', () => {
+ const md = generateReadme({ ...sampleState, layoutTemplate: 'storyteller' });
+ const headerPos = md.indexOf("Hi, I'm Alex Developer");
+ const heroPos = md.indexOf('https://example.com/hero.gif');
+ const articlesPos = md.indexOf('Latest Articles');
+ const spotlightPos = md.indexOf('Repository Spotlight');
+ const techPos = md.indexOf('Tech Stack');
+ const streakPos = md.indexOf('GitHub Streak');
+ const socialPos = md.indexOf('Connect With Me');
+ const graphsPos = md.indexOf('Snake Contribution Graph');
+
+ expect(headerPos).toBeLessThan(heroPos);
+ expect(heroPos).toBeLessThan(articlesPos);
+ expect(articlesPos).toBeLessThan(spotlightPos);
+ expect(spotlightPos).toBeLessThan(techPos);
+ expect(techPos).toBeLessThan(streakPos);
+ expect(streakPos).toBeLessThan(socialPos);
+ expect(socialPos).toBeLessThan(graphsPos);
+ });
+
+ it('falls back to Classic layout when layoutTemplate is missing', () => {
+ const stateWithoutLayout = { ...sampleState };
+ delete stateWithoutLayout.layoutTemplate;
+ const md = generateReadme(stateWithoutLayout);
+
+ const headerPos = md.indexOf("Hi, I'm Alex Developer");
+ const heroPos = md.indexOf('https://example.com/hero.gif');
+ const techPos = md.indexOf('Tech Stack');
+
+ expect(headerPos).toBeLessThan(heroPos);
+ expect(heroPos).toBeLessThan(techPos);
+ });
+});
diff --git a/app/generator/utils/readmeGenerator.ts b/app/generator/utils/readmeGenerator.ts
index a9180991d..2142630cf 100644
--- a/app/generator/utils/readmeGenerator.ts
+++ b/app/generator/utils/readmeGenerator.ts
@@ -61,11 +61,7 @@ function buildGraphsMarkdown(state: GeneratorState): string | null {
return graphSections.join('\n\n');
}
-export function generateReadme(state: GeneratorState): string {
- const sections: string[] = [];
- const graphsMarkdown = buildGraphsMarkdown(state);
-
- // 1. Header Section
+function buildHeaderSection(state: GeneratorState): string | null {
const name = state.name?.trim();
const description = state.description?.trim();
@@ -78,226 +74,273 @@ export function generateReadme(state: GeneratorState): string {
}
headerLines.push('');
- headerLines.push(' ');
- sections.push(headerLines.join('\n'));
+ headerLines.push('div>');
+ // Fix: close div properly
+ headerLines[headerLines.length - 1] = '';
+ return headerLines.join('\n');
} else if (description) {
- sections.push(``);
+ return ``;
}
+ return null;
+}
- // 1.5 Hero Image Section
- if (state.showHeroImage && state.heroImageUrl?.trim()) {
- const url = state.heroImageUrl.trim();
- const align = state.heroImageAlign || 'center';
- const alt = state.heroImageAlt?.trim() || 'Coding GIF';
- const width = state.heroImageWidth?.trim();
-
- const imgLines: string[] = [
- ``,
- '
', '
');
- sections.push(imgLines.join('\n'));
- }
+ const url = state.heroImageUrl.trim();
+ const align = state.heroImageAlign || 'center';
+ const alt = state.heroImageAlt?.trim() || 'Coding GIF';
+ const width = state.heroImageWidth?.trim();
- // Inject top graphs
- if (state.graphPlacement === 'top' && graphsMarkdown) {
- sections.push(graphsMarkdown);
- }
+ const imgLines: string[] = [
+ ``,
+ '
0) {
- const techLines: string[] = ['## ð ïļ Tech Stack', '', '
'];
-
- const iconDisplay = state.techIconDisplay || 'logo';
-
- const techIcons = state.selectedTechs
- .map((id) => {
- const tech = getTechById(id);
- if (!tech) return null;
-
- if (iconDisplay === 'logo-name') {
- const badgeUrl = getShieldsBadgeUrl(
- tech,
- state.techBadgeBgColor,
- state.techBadgeLogoColor
- );
- return `

`;
- }
-
- if (tech.type === 'simpleicon') {
- const slug = tech.iconUrl.split('/').pop() || id;
- const dark = `https://cdn.simpleicons.org/${slug}/ffffff`;
- const light = `https://cdn.simpleicons.org/${slug}/000000`;
- return [
- '
',
- ` `,
- `
`,
- '',
- ].join('\n');
- } else {
- return diImg(tech.iconUrl, tech.name);
- }
- })
- .filter(Boolean);
-
- techLines.push('');
- techLines.push(techIcons.join('\n \n'));
- techLines.push('');
- techLines.push('
');
- sections.push(techLines.join('\n'));
+ if (width) {
+ imgLines.push(` width="${width}"`);
}
- // Inject middle graphs
- if (state.graphPlacement === 'middle' && graphsMarkdown) {
- sections.push(graphsMarkdown);
- }
+ imgLines.push(' />', '');
+ return imgLines.join('\n');
+}
+
+function buildTechSection(state: GeneratorState): string | null {
+ if (!state.selectedTechs || state.selectedTechs.length === 0) return null;
+
+ const techLines: string[] = ['## ð ïļ Tech Stack', '', ''];
+ const iconDisplay = state.techIconDisplay || 'logo';
+
+ const techIcons = state.selectedTechs
+ .map((id) => {
+ const tech = getTechById(id);
+ if (!tech) return null;
+
+ if (iconDisplay === 'logo-name') {
+ const badgeUrl = getShieldsBadgeUrl(tech, state.techBadgeBgColor, state.techBadgeLogoColor);
+ return `

`;
+ }
+
+ if (tech.type === 'simpleicon') {
+ const slug = tech.iconUrl.split('/').pop() || id;
+ const dark = `https://cdn.simpleicons.org/${slug}/ffffff`;
+ const light = `https://cdn.simpleicons.org/${slug}/000000`;
+ return [
+ '
',
+ ` `,
+ `
`,
+ '',
+ ].join('\n');
+ } else {
+ return diImg(tech.iconUrl, tech.name);
+ }
+ })
+ .filter(Boolean);
+
+ techLines.push('');
+ techLines.push(techIcons.join('\n \n'));
+ techLines.push('');
+ techLines.push('
');
+ return techLines.join('\n');
+}
+
+function buildSocialsSection(state: GeneratorState): string | null {
+ if (!state.selectedSocials || state.selectedSocials.length === 0) return null;
- // 3. Socials Section
const activeSocials = state.selectedSocials.filter((id) => {
- const val = state.socialLinks[id];
+ const val = state.socialLinks?.[id];
if (!val?.trim()) return false;
const sanitized = sanitizeSocialUrl(id, val);
return validateSocialHandle(id, sanitized);
});
- if (activeSocials.length > 0) {
- const socialLines: string[] = ['## ð Connect With Me', '', ''];
-
- const badges = activeSocials
- .map((id) => {
- const social = getSocialById(id);
- if (!social) return null;
- const val = state.socialLinks[id] || '';
- const sanitized = sanitizeSocialUrl(id, val);
- let resolvedUrl =
- social.id === 'email'
- ? `mailto:${sanitized.replace(/^mailto:/i, '')}`
- : sanitized.startsWith('http')
- ? sanitized
- : `${social.baseUrl || ''}${sanitized}`;
-
- if (social.id !== 'email' && !/^https?:\/\//i.test(resolvedUrl)) {
- resolvedUrl = `https://${resolvedUrl}`;
- }
-
- if (social.type === 'simpleicon' && social.siSlug) {
- return [
- `
`,
- ' ',
- ` `,
- `
`,
- ' ',
- '',
- ].join('\n');
- } else {
- return [
- `
`,
- `
`,
- '',
- ].join('\n');
- }
- })
- .filter(Boolean);
-
- socialLines.push('');
- socialLines.push(badges.join('\n \n'));
- socialLines.push('');
- socialLines.push('
');
- sections.push(socialLines.join('\n'));
- }
+ if (activeSocials.length === 0) return null;
+
+ const socialLines: string[] = ['## ð Connect With Me', '', ''];
+
+ const badges = activeSocials
+ .map((id) => {
+ const social = getSocialById(id);
+ if (!social) return null;
+ const val = state.socialLinks?.[id] || '';
+ const sanitized = sanitizeSocialUrl(id, val);
+ let resolvedUrl =
+ social.id === 'email'
+ ? `mailto:${sanitized.replace(/^mailto:/i, '')}`
+ : sanitized.startsWith('http')
+ ? sanitized
+ : `${social.baseUrl || ''}${sanitized}`;
+
+ if (social.id !== 'email' && !/^https?:\/\//i.test(resolvedUrl)) {
+ resolvedUrl = `https://${resolvedUrl}`;
+ }
+
+ if (social.type === 'simpleicon' && social.siSlug) {
+ return [
+ `
`,
+ ' ',
+ ` `,
+ `
`,
+ ' ',
+ '',
+ ].join('\n');
+ } else {
+ return [
+ `
`,
+ `
`,
+ '',
+ ].join('\n');
+ }
+ })
+ .filter(Boolean);
+
+ socialLines.push('');
+ socialLines.push(badges.join('\n \n'));
+ socialLines.push('');
+ socialLines.push('
');
+ return socialLines.join('\n');
+}
+
+function buildCommitPulseSection(state: GeneratorState): string | null {
+ if (!state.showCommitPulse || !state.githubUsername?.trim()) return null;
+
+ const username = state.githubUsername.trim();
+ const badgeUrl = buildBadgeUrl(username, state.commitPulseAccent || '');
+ const dashboardUrl = `${DASHBOARD_BASE}/${username}`;
+ const altText = `CommitPulse Contribution Graph for ${username}`;
+
+ const commitPulseLines = [
+ '## ð GitHub Streak',
+ '',
+ '',
+ '',
+ `[](${dashboardUrl})`,
+ '',
+ '
',
+ ];
- // 4. CommitPulse Badge Section
- if (state.showCommitPulse && state.githubUsername.trim()) {
- const username = state.githubUsername.trim();
- const badgeUrl = buildBadgeUrl(username, state.commitPulseAccent);
- const dashboardUrl = `${DASHBOARD_BASE}/${username}`;
- const altText = `CommitPulse Contribution Graph for ${username}`;
-
- const commitPulseLines = [
- '## ð GitHub Streak',
- '',
- '',
- '',
- `[](${dashboardUrl})`,
- '',
- '
',
- ];
-
- sections.push(commitPulseLines.join('\n'));
+ return commitPulseLines.join('\n');
+}
+
+function buildSpotlightSection(state: GeneratorState): string | null {
+ if (!state.showRepoSpotlight || !state.githubUsername?.trim() || !state.spotlightRepo)
+ return null;
+
+ const username = state.githubUsername.trim();
+ const repo = state.spotlightRepo.trim();
+
+ const params = new URLSearchParams({ user: username, repo });
+ const cleaned = (state.commitPulseAccent || '').replace(/^#/, '');
+ if (/^[0-9a-fA-F]{6}$/.test(cleaned)) {
+ params.set('accent', cleaned);
}
+ const spotlightBadgeUrl = `https://commitpulse.vercel.app/api/spotlight?${params.toString()}`;
+ const repoUrl = `https://github.com/${username}/${repo}`;
+ const altText = `Repository Spotlight: ${repo}`;
+
+ const spotlightLines = [
+ '## ð Repository Spotlight',
+ '',
+ '',
+ '',
+ `[](${repoUrl})`,
+ '',
+ '
',
+ ];
- // 5. Repository Spotlight Section
- if (state.showRepoSpotlight && state.githubUsername.trim() && state.spotlightRepo) {
- const username = state.githubUsername.trim();
- const repo = state.spotlightRepo.trim();
+ return spotlightLines.join('\n');
+}
+
+function buildArticlesSection(state: GeneratorState): string | null {
+ if (!state.showArticles || !state.articlesUsername?.trim()) return null;
- const params = new URLSearchParams({ user: username, repo });
+ const username = state.articlesUsername.trim();
+ const platform = state.articlesPlatform || 'devto';
+ const params = new URLSearchParams({ user: username, platform });
+
+ if (state.commitPulseAccent) {
const cleaned = state.commitPulseAccent.replace(/^#/, '');
if (/^[0-9a-fA-F]{6}$/.test(cleaned)) {
params.set('accent', cleaned);
}
- const spotlightBadgeUrl = `https://commitpulse.vercel.app/api/spotlight?${params.toString()}`;
- const repoUrl = `https://github.com/${username}/${repo}`;
- const altText = `Repository Spotlight: ${repo}`;
-
- const spotlightLines = [
- '## ð Repository Spotlight',
- '',
- '',
- '',
- `[](${repoUrl})`,
- '',
- '
',
- ];
-
- sections.push(spotlightLines.join('\n'));
}
- // 6. Articles Section
- if (state.showArticles && state.articlesUsername?.trim()) {
- const username = state.articlesUsername.trim();
- const platform = state.articlesPlatform || 'devto';
- const params = new URLSearchParams({ user: username, platform });
-
- // Optional: inherit the global accent color if set
- if (state.commitPulseAccent) {
- const cleaned = state.commitPulseAccent.replace(/^#/, '');
- if (/^[0-9a-fA-F]{6}$/.test(cleaned)) {
- params.set('accent', cleaned);
- }
- }
+ const articlesBadgeUrl = `https://commitpulse.vercel.app/api/articles?${params.toString()}`;
+ const blogUrl =
+ platform === 'devto'
+ ? `https://dev.to/${username}`
+ : `https://${username.replace('.hashnode.dev', '')}.hashnode.dev/`;
+
+ const altText = `Latest Articles from ${platform === 'devto' ? 'Dev.to' : 'Hashnode'}`;
+
+ const articlesLines = [
+ '## ð Latest Articles',
+ '',
+ '',
+ '',
+ `[](${blogUrl})`,
+ '',
+ '
',
+ ];
- const articlesBadgeUrl = `https://commitpulse.vercel.app/api/articles?${params.toString()}`;
- const blogUrl =
- platform === 'devto'
- ? `https://dev.to/${username}`
- : `https://${username.replace('.hashnode.dev', '')}.hashnode.dev/`;
-
- const altText = `Latest Articles from ${platform === 'devto' ? 'Dev.to' : 'Hashnode'}`;
-
- const articlesLines = [
- '## ð Latest Articles',
- '',
- '',
- '',
- `[](${blogUrl})`,
- '',
- '
',
- ];
-
- sections.push(articlesLines.join('\n'));
+ return articlesLines.join('\n');
+}
+
+type SectionKey =
+ 'header' | 'hero' | 'tech' | 'socials' | 'commitPulse' | 'spotlight' | 'articles' | 'graphs';
+
+export function generateReadme(state: GeneratorState): string {
+ const builtSections: Record = {
+ header: buildHeaderSection(state),
+ hero: buildHeroSection(state),
+ tech: buildTechSection(state),
+ socials: buildSocialsSection(state),
+ commitPulse: buildCommitPulseSection(state),
+ spotlight: buildSpotlightSection(state),
+ articles: buildArticlesSection(state),
+ graphs: buildGraphsMarkdown(state),
+ };
+
+ const template = state.layoutTemplate || 'classic';
+ let order: SectionKey[];
+
+ if (template === 'minimalist') {
+ order = ['header', 'socials', 'tech', 'hero', 'commitPulse', 'spotlight', 'articles', 'graphs'];
+ } else if (template === 'data-heavy') {
+ order = ['header', 'commitPulse', 'spotlight', 'graphs', 'tech', 'socials', 'hero', 'articles'];
+ } else if (template === 'storyteller') {
+ order = ['header', 'hero', 'articles', 'spotlight', 'tech', 'commitPulse', 'socials', 'graphs'];
+ } else {
+ order = ['header', 'hero', 'tech', 'socials', 'commitPulse', 'spotlight', 'articles'];
}
- // Inject bottom graphs
- if (state.graphPlacement === 'bottom' && graphsMarkdown) {
- sections.push(graphsMarkdown);
+ // Respect graph placement overrides if specified
+ if (state.graphPlacement === 'top') {
+ order = order.filter((k) => k !== 'graphs');
+ const headerIdx = order.indexOf('header');
+ const insertIdx = headerIdx !== -1 ? headerIdx + 1 : 0;
+ order.splice(insertIdx, 0, 'graphs');
+ } else if (state.graphPlacement === 'middle') {
+ order = order.filter((k) => k !== 'graphs');
+ const techIdx = order.indexOf('tech');
+ const insertIdx = techIdx !== -1 ? techIdx + 1 : order.length;
+ order.splice(insertIdx, 0, 'graphs');
+ } else if (
+ template === 'classic' &&
+ (!state.graphPlacement || state.graphPlacement === 'bottom')
+ ) {
+ order = order.filter((k) => k !== 'graphs');
+ order.push('graphs');
+ }
+
+ const sections: string[] = [];
+ for (const key of order) {
+ const content = builtSections[key];
+ if (content) {
+ sections.push(content);
+ }
}
return sections.join('\n\n---\n\n');
From 029cef514a85f2c828a8085d8fc505697c8a7daa Mon Sep 17 00:00:00 2001
From: Rakshak05 <159248180+Rakshak05@users.noreply.github.com>
Date: Mon, 10 Aug 2026 17:12:43 +0530
Subject: [PATCH 2/3] Resolves issue-#7814
---
app/generator/utils/readmeGenerator.ts | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/app/generator/utils/readmeGenerator.ts b/app/generator/utils/readmeGenerator.ts
index 2142630cf..d73142eb6 100644
--- a/app/generator/utils/readmeGenerator.ts
+++ b/app/generator/utils/readmeGenerator.ts
@@ -204,7 +204,7 @@ function buildSocialsSection(state: GeneratorState): string | null {
}
function buildCommitPulseSection(state: GeneratorState): string | null {
- if (!state.showCommitPulse || !state.githubUsername?.trim()) return null;
+ if (!state.showCommitPulse || !state.githubUsername.trim()) return null;
const username = state.githubUsername.trim();
const badgeUrl = buildBadgeUrl(username, state.commitPulseAccent || '');
@@ -225,8 +225,7 @@ function buildCommitPulseSection(state: GeneratorState): string | null {
}
function buildSpotlightSection(state: GeneratorState): string | null {
- if (!state.showRepoSpotlight || !state.githubUsername?.trim() || !state.spotlightRepo)
- return null;
+ if (!state.showRepoSpotlight || !state.githubUsername.trim() || !state.spotlightRepo) return null;
const username = state.githubUsername.trim();
const repo = state.spotlightRepo.trim();
From ef3630ac2f8776f1e32b8e7304e7f1286629c209 Mon Sep 17 00:00:00 2001
From: Rakshak05 <159248180+Rakshak05@users.noreply.github.com>
Date: Mon, 10 Aug 2026 18:39:21 +0530
Subject: [PATCH 3/3] Made a few changes.
---
.../EditorPanel.type-compiler.test.tsx | 3 ++-
utils/dateHelpers.test.ts | 20 +++++++------------
2 files changed, 9 insertions(+), 14 deletions(-)
diff --git a/app/generator/components/EditorPanel.type-compiler.test.tsx b/app/generator/components/EditorPanel.type-compiler.test.tsx
index eed8f56f3..5573a91fa 100644
--- a/app/generator/components/EditorPanel.type-compiler.test.tsx
+++ b/app/generator/components/EditorPanel.type-compiler.test.tsx
@@ -1,11 +1,12 @@
import { describe, expectTypeOf, it } from 'vitest';
import type { EditorPanelProps } from './EditorPanel';
-import type { GeneratorState } from '../types';
+import type { GeneratorState, LayoutTemplate } from '../types';
import type { ImportedData } from '../utils/githubMapper';
describe('EditorPanel Type Compiler Validation', () => {
it('Test 1: validates GeneratorState structure', () => {
expectTypeOf().toEqualTypeOf<{
+ layoutTemplate?: LayoutTemplate;
name: string;
description: string;
selectedTechs: string[];
diff --git a/utils/dateHelpers.test.ts b/utils/dateHelpers.test.ts
index ff4e95114..204ffa19e 100644
--- a/utils/dateHelpers.test.ts
+++ b/utils/dateHelpers.test.ts
@@ -68,14 +68,12 @@ describe('dateHelpers', () => {
});
it('returns zero metrics for an array containing only Invalid Date strings', () => {
- // Removed Z here
- const result = processCommitTimestamps(['2024-13-99T25:99:00', 'hello world']);
+ const result = processCommitTimestamps(['2024-13-99T25:99:00Z', 'hello world']);
expect(result).toEqual({ morning: 0, afternoon: 0, evening: 0, night: 0 });
});
it('counts valid morning commits correctly', () => {
- // Removed Z from both strings
- const result = processCommitTimestamps(['2024-03-10T09:00:00', '2024-03-10T11:30:00']);
+ const result = processCommitTimestamps(['2024-03-10T09:00:00Z', '2024-03-10T11:30:00Z']);
expect(result.morning).toBe(2);
expect(result.afternoon).toBe(0);
expect(result.evening).toBe(0);
@@ -83,30 +81,26 @@ describe('dateHelpers', () => {
});
it('counts valid afternoon commits correctly', () => {
- // Removed Z from both strings
- const result = processCommitTimestamps(['2024-03-10T12:00:00', '2024-03-10T17:59:00']);
+ const result = processCommitTimestamps(['2024-03-10T12:00:00Z', '2024-03-10T17:59:00Z']);
expect(result.morning).toBe(0);
expect(result.afternoon).toBe(2);
});
it('counts valid evening commits correctly', () => {
- // Removed Z from both strings
- const result = processCommitTimestamps(['2024-03-10T18:00:00', '2024-03-10T23:59:00']);
+ const result = processCommitTimestamps(['2024-03-10T18:00:00Z', '2024-03-10T23:59:00Z']);
expect(result.evening).toBe(2);
});
it('counts valid night commits correctly', () => {
- // Removed Z from both strings
- const result = processCommitTimestamps(['2024-03-10T00:00:00', '2024-03-10T05:59:00']);
+ const result = processCommitTimestamps(['2024-03-10T00:00:00Z', '2024-03-10T05:59:00Z']);
expect(result.night).toBe(2);
});
it('ignores invalid dates while counting valid ones', () => {
- // Removed Z from strings
const result = processCommitTimestamps([
- '2024-03-10T09:00:00',
+ '2024-03-10T09:00:00Z',
'invalid-date',
- '2024-03-10T14:00:00',
+ '2024-03-10T14:00:00Z',
]);
expect(result.morning).toBe(1);
expect(result.afternoon).toBe(1);