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(`
\n\n

${description}

\n\n
`); + return `
\n\n

${description}

\n\n
`; } + 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 `${tech.name}`; - } - - 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 [ - '', - ` `, - ` ${tech.name}`, - '', - ].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 `${tech.name}`; + } + + 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 [ + '', + ` `, + ` ${tech.name}`, + '', + ].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 [ - ``, - ' ', - ` `, - ` ${social.name}`, - ' ', - '', - ].join('\n'); - } else { - return [ - ``, - ` ${social.name}`, - '', - ].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 [ + ``, + ' ', + ` `, + ` ${social.name}`, + ' ', + '', + ].join('\n'); + } else { + return [ + ``, + ` ${social.name}`, + '', + ].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', + '', + '
', + '', + `[![${altText}](${badgeUrl})](${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', - '', - '
', - '', - `[![${altText}](${badgeUrl})](${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', + '', + '
', + '', + `[![${altText}](${spotlightBadgeUrl})](${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', - '', - '
', - '', - `[![${altText}](${spotlightBadgeUrl})](${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', + '', + '
', + '', + `[![${altText}](${articlesBadgeUrl})](${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', - '', - '
', - '', - `[![${altText}](${articlesBadgeUrl})](${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);