Skip to content

feat(xp): transform experiences page into github-style profile#33

Open
caiokf wants to merge 1 commit intomainfrom
feature/github-profile-experiences
Open

feat(xp): transform experiences page into github-style profile#33
caiokf wants to merge 1 commit intomainfrom
feature/github-profile-experiences

Conversation

@caiokf
Copy link
Owner

@caiokf caiokf commented Dec 14, 2025

github
  • Redesigned experiences page with a GitHub profile-inspired layout
  • Added left sidebar with avatar, bio, stats, and top technologies
  • Implemented main area with README-like intro and contribution heatmap
  • Created pinned "repositories" section for featured experiences
  • Added searchable list of experience repositories

Summary by CodeRabbit

Release Notes

  • New Features

    • Redesigned Experience page with GitHub-style profile layout
    • Added profile sidebar with avatar, stats, and information
    • Added contributions graph visualization
    • Added pinned repositories section
    • Added search filter for repositories
    • Added README-style introduction section
  • Style

    • Updated visual design with enhanced responsiveness and dark theme support

✏️ Tip: You can customize this high-level summary in your review settings.

Implement a GitHub profile inspired redesign for the experiences page to present work history as a developer profile. The page now includes a left sidebar with avatar, bio, stats and top technologies, and a main area with README-like intro, a contribution heatmap, pinned “repositories” (featured experiences), and a searchable list of experience repos. This change was requested after exploring multiple visual options (timeline, git-log, editor, terminal) and the user selected the GitHub profile style to be implemented.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 14, 2025

Walkthrough

ExperiencePage.vue was restructured from an experience-timeline layout to a GitHub-profile-style interface. The redesign introduces a left sidebar with profile info, a contributions graph, pinned repositories section, and a searchable repositories list with new computed properties and utility helpers.

Changes

Cohort / File(s) Change Summary
Profile Page Restructuring
src/pages/ExperiencePage.vue
Replaced sidebar-timeline layout with GitHub-profile-style design. Added computed properties for profile stats (totalProjects, yearsOfExperience, totalContributions, etc.). Introduced contributions graph, pinned experiences section, and searchable repositories list. Added utility functions for formatting (formatRepoName, formatDateRange, formatRelativeDate, calculateMonths, truncateHighlight, getTechColor) and mini graph generation. Removed logo-based patterns and refactored styling for profile-centric UI with dark-theme support.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • The completeness and correctness of new computed properties (totalProjects, totalCompanies, yearsOfExperience, pinnedExperiences, topTechnologies, contributionData) and their data source assumptions
  • Logic of the contribution graph generation and color-level mapping
  • Search/filter implementation (searchQuery and filteredExperiences) for edge cases
  • Activity mini-graph generator function (generateMiniGraph) accuracy and performance
  • Responsive layout handling and theme color consistency across new sections
  • Data binding correctness between removed old patterns (RECENT_YEARS, recentTechnologies, Timeline/experience-card) and new structures

Poem

🐰 A profile page blooms with GitHub flair,
With contributions tracked and pinned repos to share!
Graphs and sidebars dance in the light,
Old timelines fade as the new design takes flight. ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main transformation—converting the experiences page to a GitHub-style profile layout, which aligns with the core changes outlined in the PR objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/github-profile-experiences

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/pages/ExperiencePage.vue (2)

292-293: Comment doesn't match implementation.

The comment says "most recent 6" but the code takes the first 6 items from experiencesConfig. This is only accurate if experiencesConfig is pre-sorted by date (most recent first). If the data ordering changes, the pinned section won't show the most recent experiences.

Consider either updating the comment to reflect the actual behavior, or explicitly sorting:

- // Pinned experiences (most recent 6)
- const pinnedExperiences = computed(() => experiencesConfig.slice(0, 6));
+ // Pinned experiences (first 6 from config - assumes pre-sorted by date)
+ const pinnedExperiences = computed(() => experiencesConfig.slice(0, 6));

384-460: Consider extracting utility functions.

The formatting functions (formatRepoName, formatDateRange, formatRelativeDate, calculateMonths, truncateHighlight) and getTechColor color mapping are general-purpose utilities that could be reused elsewhere. Consider extracting them to a shared utilities file:

// src/utils/formatters.ts
export function formatRepoName(company: string): string { ... }
export function formatDateRange(exp: Experience): string { ... }
// etc.

This would reduce the component's size and improve testability.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9570d4e and b3975cf.

📒 Files selected for processing (1)
  • src/pages/ExperiencePage.vue (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.vue

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.vue: Indent content inside <script> and <style> tags in Vue files (configured via vueIndentScriptAndStyle: true in .prettierrc)
Use Vue 3 Composition API with <script setup> syntax for Vue components
D3 integration should use imperative DOM manipulation within Vue lifecycle hooks, kept separate from Vue reactivity

Files:

  • src/pages/ExperiencePage.vue
**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Use constants: IDEAL_BLIP_WIDTH = 22px, NEW_GROUP_BLIP_WIDTH = 88px, EXISTING_GROUP_BLIP_WIDTH = 124px

Files:

  • src/pages/ExperiencePage.vue
🔇 Additional comments (1)
src/pages/ExperiencePage.vue (1)

262-269: Good use of Vue 3 Composition API.

The component correctly uses <script setup> syntax with proper imports and reactive primitives (ref, computed). The code organization separates stats calculations, filtered data, and utility functions cleanly.

Comment on lines +109 to +117
<div class="graph-grid">
<div
v-for="(month, index) in contributionData"
:key="index"
class="contribution-cell"
:class="getCellClass(month.level)"
:title="`${month.label}: ${month.projects} projects`"
></div>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Contribution grid layout mismatch.

The grid is configured with grid-template-columns: repeat(132, 10px) and grid-template-rows: repeat(7, 10px) with grid-auto-flow: column, but contributionData generates 132 items (11 years × 12 months). With 7 rows and column auto-flow, you'd need 132 columns only if you had 132×7=924 items.

The current setup will place 132 items into 7 rows × ~19 columns, leaving most of the 132-column grid empty and creating a misaligned display.

Consider either:

  1. Generate data per-week (52 weeks × 11 years × 7 days ≈ cells) to match GitHub's actual contribution graph, or
  2. Adjust the grid to match your month-based data structure (e.g., 12 columns × 11 rows).
🤖 Prompt for AI Agents
In src/pages/ExperiencePage.vue around lines 109 to 117, the CSS grid is sized
for 132 columns × 7 rows but contributionData produces 132 month items (11 years
× 12 months), so cells are placed incorrectly; fix by matching the grid to the
month-based data: change the grid layout to 12 columns × 11 rows (e.g.,
grid-template-columns: repeat(12, 10px); grid-template-rows: repeat(11, 10px);
and remove/adjust grid-auto-flow) or alternatively change the data generation to
a week/day granularity to match the original 132×7 layout — pick one approach
and update the grid CSS and/or data generation accordingly so the number of grid
cells equals the number of rendered items.

Comment on lines +304 to +315
const filteredExperiences = computed(() => {
if (!searchQuery.value) {
return visibleExperiences.value;
}
const query = searchQuery.value.toLowerCase();
return visibleExperiences.value.filter(
(exp) =>
exp.company.toLowerCase().includes(query) ||
exp.position.toLowerCase().includes(query) ||
exp.technologies.toLowerCase().includes(query)
);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Search only filters visible experiences, not all.

filteredExperiences filters visibleExperiences, which is limited to INITIAL_VISIBLE_COUNT (6) when showAll is false. Users searching for an experience beyond the first 6 will get no results until they click "Show more."

Consider searching all experiences and then applying visibility limit, or auto-expand when a search query is entered:

  const filteredExperiences = computed(() => {
    if (!searchQuery.value) {
      return visibleExperiences.value;
    }
    const query = searchQuery.value.toLowerCase();
-   return visibleExperiences.value.filter(
+   return experiencesConfig.filter(
      (exp) =>
        exp.company.toLowerCase().includes(query) ||
        exp.position.toLowerCase().includes(query) ||
        exp.technologies.toLowerCase().includes(query)
    );
  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const filteredExperiences = computed(() => {
if (!searchQuery.value) {
return visibleExperiences.value;
}
const query = searchQuery.value.toLowerCase();
return visibleExperiences.value.filter(
(exp) =>
exp.company.toLowerCase().includes(query) ||
exp.position.toLowerCase().includes(query) ||
exp.technologies.toLowerCase().includes(query)
);
});
const filteredExperiences = computed(() => {
if (!searchQuery.value) {
return visibleExperiences.value;
}
const query = searchQuery.value.toLowerCase();
return experiencesConfig.filter(
(exp) =>
exp.company.toLowerCase().includes(query) ||
exp.position.toLowerCase().includes(query) ||
exp.technologies.toLowerCase().includes(query)
);
});
🤖 Prompt for AI Agents
In src/pages/ExperiencePage.vue around lines 304-315, filteredExperiences
currently searches only visibleExperiences (limited by INITIAL_VISIBLE_COUNT),
causing misses for items beyond the first page; change the logic to perform the
search over the full experiences list (e.g., allExperiences or
experiences.value) and then, if showAll is false and there is no search query,
apply the visibility limit, or alternatively always search all experiences and
when a non-empty searchQuery is present either return the full matching set
(ignoring the INITIAL_VISIBLE_COUNT) or programmatically set showAll=true;
implement one of these behaviors consistently and ensure the computed still
respects case-insensitive matching and uses the same fields (company, position,
technologies).

Comment on lines +462 to 477
function generateMiniGraph(exp: Experience): string {
const points: string[] = [];
const width = 155;
const height = 30;

// Generate a simple activity pattern
for (let i = 0; i <= 10; i++) {
const x = (i / 10) * width;
const seed = exp.slug.charCodeAt(i % exp.slug.length) || 50;
const variation = Math.sin(i + seed) * 8;
const y = height / 2 + variation;
points.push(`${i === 0 ? "M" : "L"}${x},${y}`);
}

return points.join(" ");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential error if exp.slug is empty.

Line 470 uses exp.slug.charCodeAt(i % exp.slug.length). If slug is an empty string, exp.slug.length is 0, causing division by zero (i % 0 returns NaN), and charCodeAt(NaN) returns NaN, not triggering the || 50 fallback.

Add a guard for empty slugs:

  function generateMiniGraph(exp: Experience): string {
    const points: string[] = [];
    const width = 155;
    const height = 30;
+   const slugSeed = exp.slug || "default";

    // Generate a simple activity pattern
    for (let i = 0; i <= 10; i++) {
      const x = (i / 10) * width;
-     const seed = exp.slug.charCodeAt(i % exp.slug.length) || 50;
+     const seed = slugSeed.charCodeAt(i % slugSeed.length) || 50;
      const variation = Math.sin(i + seed) * 8;
      const y = height / 2 + variation;
      points.push(`${i === 0 ? "M" : "L"}${x},${y}`);
    }

    return points.join(" ");
  }
🤖 Prompt for AI Agents
In src/pages/ExperiencePage.vue around lines 462 to 477, the generateMiniGraph
function can divide by zero when exp.slug is an empty string because i %
exp.slug.length becomes NaN; change the seed calculation to guard against an
empty slug by precomputing const len = exp.slug.length || 0 and then using const
seed = len > 0 ? exp.slug.charCodeAt(i % len) : 50 (or similar) so the index
operation never uses 0 and a numeric fallback (50) is always used; ensure seed
is coerced to a number before using it in Math.sin.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant