Skip to content

🧹 [Code Health] Refactor cardRenderer.tsx to extract options and elements#127

Merged
is0692vs merged 4 commits intomainfrom
jules-5519682949436094347-5396e10d
Apr 12, 2026
Merged

🧹 [Code Health] Refactor cardRenderer.tsx to extract options and elements#127
is0692vs merged 4 commits intomainfrom
jules-5519682949436094347-5396e10d

Conversation

@is0692vs
Copy link
Copy Markdown
Contributor

@is0692vs is0692vs commented Apr 3, 2026

🎯 What: Extracted code from src/lib/cardRenderer.tsx into two new files: src/lib/cardOptions.ts (for types and parsing URL query parameters) and src/lib/cardElements.tsx (for pure React UI components).
💡 Why: src/lib/cardRenderer.tsx was a monolithic 680+ line file. By separating concerns, it is now much easier to navigate, test, and maintain. Options handling, UI logic, and Image generation (via Satori / Vercel OG) are clearly separated.
Verification: Verified by running the entire Vitest suite (npm run test -- --run) which showed all tests passing (including specifically cardRenderer.test.ts and route.test.ts), and also executed npx tsc --noEmit and npm run lint with no new errors.
Result: A more robust and readable codebase following standard single-responsibility principles without any breaking changes to consumers (thanks to export * from "./cardOptions" in the main cardRenderer.tsx file).


PR created automatically by Jules for task 5519682949436094347 started by @is0692vs

Co-authored-by: is0692vs <135803462+is0692vs@users.noreply.github.com>
@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
github-user-summary Ready Ready Preview, Comment Apr 12, 2026 4:32pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

Warning

Rate limit exceeded

@is0692vs has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 17 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 10 minutes and 17 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 80b7ac54-9f57-4706-abef-8a30312419d9

📥 Commits

Reviewing files that changed from the base of the PR and between 586a2d5 and a559b22.

📒 Files selected for processing (4)
  • src/lib/__tests__/cardRenderer.test.ts
  • src/lib/cardElements.tsx
  • src/lib/cardOptions.ts
  • src/lib/cardRenderer.tsx
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jules-5519682949436094347-5396e10d

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Refactor cardRenderer.tsx into modular options and elements files

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Extracted options parsing logic into dedicated cardOptions.ts file
• Separated React UI components into new cardElements.tsx file
• Reduced cardRenderer.tsx from 680+ lines to focused rendering logic
• Maintained backward compatibility via re-exports in main file
Diagram
flowchart LR
  CR["cardRenderer.tsx<br/>680+ lines"]
  CO["cardOptions.ts<br/>types & parsing"]
  CE["cardElements.tsx<br/>React components"]
  CR -- "extract types<br/>& parseCardQueryParams" --> CO
  CR -- "extract UI logic<br/>& themes" --> CE
  CR -- "re-export from<br/>cardOptions" --> CO
  CR -- "import from<br/>cardElements" --> CE
Loading

Grey Divider

File Changes

1. src/lib/cardOptions.ts Refactoring +169/-0

Card options types and query parameter parsing

• New file containing all card configuration types (CardFormat, CardTheme, CardBlockType,
 CardLayoutSlot, CardRenderOptions)
• Extracted parsing functions: parseCardQueryParams(), resolveBlockLayout(), and helper
 functions (toList(), parseWidth(), parseBlocks(), parseLayout())
• Exported constants: DEFAULT_BLOCKS, VALID_BLOCKS, VALID_LAYOUT_SLOTS

src/lib/cardOptions.ts


2. src/lib/cardElements.tsx Refactoring +394/-0

React components and theme palette definitions

• New file containing all React UI component logic and theme definitions
• Exported theme palettes for light and dark modes
• Extracted functions: estimateHeight(), levelColor(), createBlock(), blockContainer(),
 cardTree(), errorTree()
• Imports types from cardOptions.ts and data from cardDataFetcher

src/lib/cardElements.tsx


3. src/lib/cardRenderer.tsx Refactoring +4/-555

Simplified to core rendering orchestration logic

• Removed 500+ lines of extracted code (types, parsing, UI components, themes)
• Added imports from new cardOptions.ts and cardElements.tsx modules
• Added export * from "./cardOptions" for backward compatibility
• Retained core rendering logic: font handling and SVG/PNG generation via Satori

src/lib/cardRenderer.tsx


View more (1)
4. src/lib/__tests__/cardRenderer.test.ts Refactoring +1/-1

Updated imports to reflect new module structure

• Updated import statement to source parseCardQueryParams and resolveBlockLayout from
 cardOptions instead of cardRenderer
• Test logic and assertions remain unchanged

src/lib/tests/cardRenderer.test.ts


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Apr 3, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the card rendering logic by decomposing the monolithic cardRenderer.tsx into specialized modules: cardElements.tsx for UI components and cardOptions.ts for configuration parsing and layout logic. The feedback focuses on further improving code maintainability and performance. Specifically, it is suggested to refactor the large createBlock function into smaller, dedicated components using a component map and to utilize a Set for more efficient uniqueness checks when parsing requested blocks.

Comment on lines +74 to +256
export function createBlock(
block: CardBlockType,
data: CardData,
theme: ThemePalette,
hide: Set<string>,
): ReactElement {
if (block === "bio") {
return (
<div
style={{
display: "flex",
flexDirection: "row",
gap: 14,
alignItems: "center",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={data.profile.avatarUrl}
width={58}
height={58}
style={{ borderRadius: 999, border: `2px solid ${theme.border}` }}
alt="avatar"
/>
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
<div style={{ color: theme.text, fontSize: 22, fontWeight: 700 }}>
{data.profile.name}
</div>
<div style={{ color: theme.subtext, fontSize: 14 }}>
@{data.profile.login}
</div>
{data.profile.bio ? (
<div style={{ color: theme.subtext, fontSize: 13, maxWidth: 470 }}>
{data.profile.bio.length > 110
? `${data.profile.bio.slice(0, 110)}...`
: data.profile.bio}
</div>
) : null}
</div>
</div>
);
}

if (block === "stats") {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div style={{ color: theme.text, fontSize: 16, fontWeight: 700 }}>
Stats
</div>
<div
style={{
display: "flex",
flexDirection: "row",
gap: 14,
color: theme.subtext,
fontSize: 14,
}}
>
<div>Followers: {data.profile.followers.toLocaleString()}</div>
<div>Following: {data.profile.following.toLocaleString()}</div>
<div>Repos: {data.profile.publicRepos.toLocaleString()}</div>
{!hide.has("stars") ? (
<div>Stars: {data.totalStars.toLocaleString()}</div>
) : null}
</div>
</div>
);
}

if (block === "langs") {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 7 }}>
<div style={{ color: theme.text, fontSize: 16, fontWeight: 700 }}>
Top Languages
</div>
{data.languages.slice(0, 4).map((lang) => (
<div
key={lang.name}
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
fontSize: 13,
color: theme.subtext,
}}
>
<span>{lang.name}</span>
<span>{lang.percentage.toFixed(1)}%</span>
</div>
))}
</div>
);
}

if (block === "repos") {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 7 }}>
<div style={{ color: theme.text, fontSize: 16, fontWeight: 700 }}>
Top Repositories
</div>
{data.repos.slice(0, 3).map((repo) => (
<div
key={repo.name}
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
fontSize: 13,
color: theme.subtext,
}}
>
<span>{repo.name}</span>
<span>
{!hide.has("stars") ? `★${repo.stars}` : ""}
{!hide.has("stars") && !hide.has("forks") ? " / " : ""}
{!hide.has("forks") ? `⑂${repo.forks}` : ""}
</span>
</div>
))}
</div>
);
}

if (block === "streak") {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div style={{ color: theme.text, fontSize: 16, fontWeight: 700 }}>
Streak
</div>
<div
style={{
display: "flex",
flexDirection: "row",
gap: 16,
color: theme.subtext,
fontSize: 13,
}}
>
<span>Current: {data.streak.current} days</span>
<span>Longest: {data.streak.longest} days</span>
</div>
</div>
);
}

const days = data.heatmap.days.slice(-42);
const columns: Array<Array<{ date: string; count: number }>> = [];
for (let i = 0; i < days.length; i += 7) {
columns.push(days.slice(i, i + 7));
}

return (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div style={{ color: theme.text, fontSize: 16, fontWeight: 700 }}>
Heatmap
</div>
<div style={{ display: "flex", flexDirection: "row", gap: 4 }}>
{columns.map((column, colIndex) => (
<div
key={`col-${colIndex}`}
style={{ display: "flex", flexDirection: "column", gap: 4 }}
>
{column.map((day) => (
<div
key={day.date}
style={{
width: 10,
height: 10,
borderRadius: 2,
backgroundColor: levelColor(
day.count,
data.heatmap.maxCount,
theme,
),
}}
/>
))}
</div>
))}
</div>
</div>
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The createBlock function is quite large (over 180 lines) and handles the rendering logic for all block types. This reduces readability and makes it harder to maintain.

A good refactoring would be to break this down into smaller, dedicated components for each block type (e.g., BioBlock, StatsBlock). You could then use a component map to render the correct block dynamically.

Additionally, the logic for the heatmap block is implicit. It's the fall-through case if no other if condition matches. It would be clearer to handle all block types explicitly, for example with a switch statement or a component map.

Here's an example of how you could use a component map:

type BlockProps = { data: CardData; theme: ThemePalette; hide: Set };

const BioBlock = ({ data, theme }: BlockProps) => { /* ... / };
const StatsBlock = ({ data, theme, hide }: BlockProps) => { /
... */ };

const blockComponents: Record<CardBlockType, React.FC> = {
bio: BioBlock,
stats: StatsBlock,
langs: LangsBlock,
repos: ReposBlock,
streak: StreakBlock,
heatmap: HeatmapBlock,
};

export function createBlock(block: CardBlockType, data: CardData, theme: ThemePalette, hide: Set): ReactElement {
const BlockComponent = blockComponents[block];
return ;
}

This approach makes the code more modular, easier to read, and easier to test.

Comment on lines +54 to +71
function parseBlocks(raw: string | null): CardBlockType[] {
const requested = toList(raw);
if (requested.length === 0) {
return DEFAULT_BLOCKS;
}

const unique: CardBlockType[] = [];
for (const block of requested) {
if (!VALID_BLOCKS.includes(block as CardBlockType)) {
continue;
}
if (!unique.includes(block as CardBlockType)) {
unique.push(block as CardBlockType);
}
}

return unique.length > 0 ? unique : DEFAULT_BLOCKS;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation for creating a unique list of blocks uses unique.includes() inside a loop. This has a time complexity of O(n^2) and can be inefficient. Using a Set is a more performant and idiomatic way to handle uniqueness in modern JavaScript.

function parseBlocks(raw: string | null): CardBlockType[] {
  const requested = toList(raw);
  if (requested.length === 0) {
    return DEFAULT_BLOCKS;
  }

  const validBlocks = [...new Set(requested)].filter((block): block is CardBlockType =>
    VALID_BLOCKS.includes(block as CardBlockType),
  );

  return validBlocks.length > 0 ? validBlocks : DEFAULT_BLOCKS;
}

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 12, 2026

Deployment failed with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/hirokis-projects-afd618c7?upgradeToPro=build-rate-limit

@is0692vs is0692vs merged commit 3ef94d4 into main Apr 12, 2026
4 of 6 checks passed
@is0692vs is0692vs deleted the jules-5519682949436094347-5396e10d branch April 12, 2026 16:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant