Skip to content

Commit c4fd486

Browse files
committed
refactor: Update Experience components with new carousel styling and memoization
- Refactored ContentCarousel styles to improve layout and removed unnecessary comments. - Introduced ExperienceCarouselContainer for consistent styling across Experience-related components. - Memoized sorting logic for WORK_EXPERIENCE and OLDER_EXPERIENCE to optimize performance. - Updated EducationItemDisplay, ProjectCard, and ExperienceSection components to utilize the new carousel container. - Cleaned up imports and ensured proper path resolution for components.
1 parent a2d55e3 commit c4fd486

File tree

6 files changed

+188
-76
lines changed

6 files changed

+188
-76
lines changed

src/shared-components/organisms/ContentCarousel/ContentCarousel.styles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Carousel } from '@mantine/carousel';
33

44
export const StyledCarousel = styled(Carousel)`
55
/* Root styles */
6-
padding-bottom: 30px; // Space for indicators
6+
/* padding-bottom: 30px; // Space for indicators */
77
scroll-margin-top: 58px; // Account for sticky headers when scrolling into view
88
99
/* Slide container */

src/shared-components/pages/Experience/Experience.styles.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,4 +428,86 @@ export const MediaContainer = styled.div`
428428
display: block;
429429
width: 100%;
430430
}
431+
`;
432+
433+
// --- Experience Specific Carousel Container ---
434+
export const ExperienceCarouselContainer = styled.div`
435+
/* Copied & adapted from Home.styles.ts for light theme */
436+
.mantine-Carousel-controls {
437+
position: relative;
438+
display: flex;
439+
justify-content: space-between;
440+
/* Align items vertically using baseline */
441+
align-items: baseline;
442+
/* Remove padding-top */
443+
/* padding-top: ${({ theme }) => theme.spacing.md}; */
444+
margin-top: ${({ theme }) => theme.spacing.md}; /* Add margin-top instead for spacing */
445+
top: unset;
446+
left: unset;
447+
right: unset;
448+
transform: unset;
449+
}
450+
451+
.mantine-Carousel-control {
452+
position: relative;
453+
/* Light theme styles */
454+
background: ${({ theme }) => theme.colors.gray[1]};
455+
border: 1px solid ${({ theme }) => theme.colors.gray[3]};
456+
color: ${({ theme }) => theme.colors.dark[6]};
457+
border-radius: 50%;
458+
width: 30px;
459+
height: 30px;
460+
display: flex;
461+
align-items: center;
462+
justify-content: center;
463+
top: unset;
464+
left: unset;
465+
right: unset;
466+
bottom: unset;
467+
transform: unset;
468+
margin: 0;
469+
470+
&:hover {
471+
background-color: ${({ theme }) => theme.colors.gray[2]};
472+
}
473+
474+
&[data-inactive] {
475+
opacity: 0.4 !important;
476+
cursor: default;
477+
}
478+
479+
&[data-carousel-prev] {
480+
order: -1;
481+
margin-right: 16px; /* Adjust spacing */
482+
}
483+
484+
&[data-carousel-next] {
485+
order: 1;
486+
margin-left: 16px; /* Adjust spacing */
487+
}
488+
}
489+
490+
.mantine-Carousel-indicators {
491+
display: flex;
492+
margin: 0;
493+
padding: 0;
494+
align-items: center;
495+
order: 0;
496+
}
497+
498+
.mantine-Carousel-indicator {
499+
/* Use light theme indicator colors */
500+
background-color: ${({ theme }) => theme.colors.gray[3]};
501+
width: 8px;
502+
height: 8px;
503+
transition: width 250ms ease;
504+
border-radius: 4px;
505+
margin: 0 4px;
506+
507+
&[data-active] {
508+
/* Use primary color for active indicator */
509+
background-color: ${({ theme }) => theme.colors[theme.primaryColor]?.[6] || theme.colors.blue[6]};
510+
width: 24px;
511+
}
512+
}
431513
`;

src/shared-components/pages/Experience/Experience.tsx

Lines changed: 45 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import React, { useEffect, useState } from 'react';
3+
import React, { useEffect, useState, useMemo } from 'react';
44
import { Grid, Box, useMantineTheme, Image, Avatar } from '@mantine/core';
55
import { useMediaQuery } from '@mantine/hooks';
66
import {
@@ -125,47 +125,50 @@ export const Experience: React.FC<ExperienceProps> = ({
125125
...INFRASTRUCTURE_SKILL_CATEGORIES
126126
];
127127

128-
// Combine and sort WORK_EXPERIENCE
129-
const devExperiences = [...WORK_EXPERIENCE].sort((a, b) => {
130-
if (a.sortOrder !== undefined && b.sortOrder !== undefined) {
131-
return a.sortOrder - b.sortOrder;
132-
}
133-
const monthToNum: Record<string, number> = {
134-
'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
135-
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12
136-
};
137-
const aDateParts = a.startDate?.split(' ') ?? [];
138-
const bDateParts = b.startDate?.split(' ') ?? [];
139-
const aYear = parseInt(aDateParts[1] || '0', 10);
140-
const bYear = parseInt(bDateParts[1] || '0', 10);
141-
if (aYear !== bYear) {
142-
return bYear - aYear;
143-
}
144-
const aMonth = monthToNum[aDateParts[0]] || 0;
145-
const bMonth = monthToNum[bDateParts[0]] || 0;
146-
return bMonth - aMonth;
147-
});
148-
149-
// Combine and sort OLDER_EXPERIENCE
150-
const salesExperiences = [...OLDER_EXPERIENCE].sort((a, b) => {
151-
if (a.sortOrder !== undefined && b.sortOrder !== undefined) {
152-
return a.sortOrder - b.sortOrder;
153-
}
154-
const monthToNum: Record<string, number> = {
155-
'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
156-
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12
157-
};
158-
const aDateParts = a.startDate?.split(' ') ?? [];
159-
const bDateParts = b.startDate?.split(' ') ?? [];
160-
const aYear = parseInt(aDateParts[1] || '0', 10);
161-
const bYear = parseInt(bDateParts[1] || '0', 10);
162-
if (aYear !== bYear) {
163-
return bYear - aYear;
164-
}
165-
const aMonth = monthToNum[aDateParts[0]] || 0;
166-
const bMonth = monthToNum[bDateParts[0]] || 0;
167-
return bMonth - aMonth;
168-
});
128+
// Memoize the sorted experience arrays
129+
const devExperiences = useMemo(() => {
130+
return [...WORK_EXPERIENCE].sort((a, b) => {
131+
if (a.sortOrder !== undefined && b.sortOrder !== undefined) {
132+
return a.sortOrder - b.sortOrder;
133+
}
134+
const monthToNum: Record<string, number> = {
135+
'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
136+
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12
137+
};
138+
const aDateParts = a.startDate?.split(' ') ?? [];
139+
const bDateParts = b.startDate?.split(' ') ?? [];
140+
const aYear = parseInt(aDateParts[1] || '0', 10);
141+
const bYear = parseInt(bDateParts[1] || '0', 10);
142+
if (aYear !== bYear) {
143+
return bYear - aYear;
144+
}
145+
const aMonth = monthToNum[aDateParts[0]] || 0;
146+
const bMonth = monthToNum[bDateParts[0]] || 0;
147+
return bMonth - aMonth;
148+
});
149+
}, []); // Empty dependency array since WORK_EXPERIENCE is constant
150+
151+
const salesExperiences = useMemo(() => {
152+
return [...OLDER_EXPERIENCE].sort((a, b) => {
153+
if (a.sortOrder !== undefined && b.sortOrder !== undefined) {
154+
return a.sortOrder - b.sortOrder;
155+
}
156+
const monthToNum: Record<string, number> = {
157+
'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
158+
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12
159+
};
160+
const aDateParts = a.startDate?.split(' ') ?? [];
161+
const bDateParts = b.startDate?.split(' ') ?? [];
162+
const aYear = parseInt(aDateParts[1] || '0', 10);
163+
const bYear = parseInt(bDateParts[1] || '0', 10);
164+
if (aYear !== bYear) {
165+
return bYear - aYear;
166+
}
167+
const aMonth = monthToNum[aDateParts[0]] || 0;
168+
const bMonth = monthToNum[bDateParts[0]] || 0;
169+
return bMonth - aMonth;
170+
});
171+
}, []); // Empty dependency array since OLDER_EXPERIENCE is constant
169172

170173
// Sort side projects consistent with the hook's logic
171174
const sortedSideProjects = [...sideProjects].sort((a, b) => {

src/shared-components/pages/Experience/components/EducationSection/components/EducationItemDisplay/EducationItemDisplay.tsx

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ import { useMantineTheme } from '@mantine/core';
3737
import { MetadataLine, Title as EntityTitle } from '@shared-components/molecules/EntityHeader/EntityHeader.styles';
3838
// Import the shared ContentCarousel
3939
import { ContentCarousel } from '@shared-components/organisms/ContentCarousel';
40+
import { MediaItemRenderer } from '../../../ExperienceSection/components/MediaItemRenderer/MediaItemRenderer';
41+
import { TechnologyList } from '@shared-components/pages/Experience/components/ExperienceSection/components/TechnologyList/TechnologyList';
42+
import { ModalImage } from '@shared-components/pages/Experience/components/ExperienceSection/ExperienceSection.hook';
43+
44+
// Import ExperienceCarouselContainer from Experience page styles (Corrected path again)
45+
import { ExperienceCarouselContainer } from '../../../../Experience.styles';
4046

4147
// --- Local Styled Components ---
4248

@@ -131,24 +137,34 @@ export const EducationItemDisplay: React.FC<EducationItemDisplayProps> = ({
131137
)}
132138
{/* Conditionally render ContentCarousel or MediaRow */}
133139
{showCarousel ? (
134-
<ContentCarousel>
135-
{edu.media?.map((mediaItem: MediaItem, mediaIndex: number) => (
136-
<MediaItemDisplay
137-
key={`carousel-media-${edu.school}-${mediaIndex}`}
138-
mediaItem={mediaItem}
139-
schoolName={edu.school}
140-
onImageClick={onImageClick ?? (() => { })}
141-
/>
142-
))}
143-
</ContentCarousel>
140+
<ExperienceCarouselContainer>
141+
<ContentCarousel>
142+
{edu.media?.map((mediaItem: MediaItem, mediaIndex: number) => (
143+
<MediaItemRenderer
144+
key={`carousel-media-${edu.school}-${mediaIndex}`}
145+
mediaItem={mediaItem}
146+
job={edu as any}
147+
index={0}
148+
mediaIndex={mediaIndex}
149+
jobMediaLength={edu.media?.length || 0}
150+
setModalImage={onImageClick as any}
151+
isMobileLayout={isMobile}
152+
/>
153+
))}
154+
</ContentCarousel>
155+
</ExperienceCarouselContainer>
144156
) : hasMedia && (
145157
<MediaRow style={{ marginTop: edu.description ? '1rem' : '0' }}>
146158
{edu.media?.map((mediaItem: MediaItem, mediaIndex: number) => (
147-
<MediaItemDisplay
159+
<MediaItemRenderer
148160
key={`media-${edu.school}-${mediaIndex}`}
149161
mediaItem={mediaItem}
150-
schoolName={edu.school}
151-
onImageClick={onImageClick ?? (() => { })}
162+
job={edu as any}
163+
index={0}
164+
mediaIndex={mediaIndex}
165+
jobMediaLength={edu.media?.length || 0}
166+
setModalImage={onImageClick as any}
167+
isMobileLayout={isMobile}
152168
/>
153169
))}
154170
</MediaRow>

src/shared-components/pages/Experience/components/ExperienceSection/ExperienceSection.logic.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ import { MediaRow } from './styles/Media.styles';
3636
import { ExperienceItem as ExperienceItemType, MediaItem } from './ExperienceSection.types';
3737
import { ModalImage } from './ExperienceSection.hook';
3838

39+
// Import ExperienceCarouselContainer from Experience page styles
40+
import { ExperienceCarouselContainer } from '../../Experience.styles';
41+
3942
// --- Helper Functions ---
4043
const getBulletIcon = (text: string) => {
4144
const lowerText = text.toLowerCase();
@@ -184,16 +187,18 @@ export const renderExperienceItem = (
184187
<> { /* Render mobile layout */}
185188
{/* Only render Carousel if more than one media item */}
186189
{jobMedia.length > 1 ? (
187-
<ContentCarousel scrollIntoViewOnSelect={true}>
188-
{jobMedia.map((item, idx) => (
189-
// Pass the rendered item directly as a child
190-
renderSingleMediaItem(item, idx)
191-
))}
192-
</ContentCarousel>
190+
<ExperienceCarouselContainer> { /* Use Experience specific wrapper */}
191+
<ContentCarousel scrollIntoViewOnSelect={true}>
192+
{jobMedia.map((item, idx) => (
193+
// Pass the rendered item directly as a child
194+
renderSingleMediaItem(item, idx)
195+
))}
196+
</ContentCarousel>
197+
</ExperienceCarouselContainer>
193198
) : jobMedia.length === 1 ? (
194199
// Render single item directly if only one exists
195200
renderSingleMediaItem(jobMedia[0], 0)
196-
) : null /* No media items */}
201+
) : null /* No media items - Moved comment inside */}
197202
</>
198203
) : (
199204
// Render desktop layout: All items in a single MediaRow

src/shared-components/pages/Experience/components/SideProjectsSection/components/ProjectCard/ProjectCard.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ProjectLogo } from '@shared-components/atoms/ProjectLogo';
66
import { MarkdownRenderer } from '@shared-components/molecules/MarkdownRenderer';
77
import { ContentCarousel } from '@shared-components/organisms/ContentCarousel';
88
import { MediaRenderer } from '../MediaRenderer';
9-
import { TechnologyList } from '../../../ExperienceSection/components/TechnologyList';
9+
import { TechnologyList } from '@shared-components/pages/Experience/components/ExperienceSection/components/TechnologyList/TechnologyList';
1010
import { EntityHeader } from '@shared-components/molecules/EntityHeader';
1111
import * as S from './ProjectCard.styles';
1212
import { Title as EntityTitle, MetadataLine } from '@shared-components/molecules/EntityHeader/EntityHeader.styles';
@@ -20,6 +20,10 @@ import {
2020
ProjectLinks,
2121
CategoryPill
2222
} from './ProjectCard.styles';
23+
// Import ExperienceCarouselContainer from Experience page styles (Corrected path)
24+
import { ExperienceCarouselContainer } from '../../../../Experience.styles';
25+
import { Box } from '@mantine/core';
26+
import { MediaRow } from '../../../ExperienceSection/styles/Media.styles';
2327

2428
// Helper function to format project dates
2529
const formatProjectDate = (startDateStr?: string, endDateStr?: string): string | null => {
@@ -189,11 +193,13 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
189193
if (isMobileLayout) {
190194
if (projectMedia.length > 1) {
191195
return (
192-
<ContentCarousel scrollIntoViewOnSelect={true}>
193-
{projectMedia.map((item, idx) => (
194-
renderSingleMediaItem(item, idx)
195-
))}
196-
</ContentCarousel>
196+
<ExperienceCarouselContainer style={{ marginBottom: '1rem' }}>
197+
<ContentCarousel scrollIntoViewOnSelect={true}>
198+
{projectMedia.map((item, idx) => (
199+
renderSingleMediaItem(item, idx)
200+
))}
201+
</ContentCarousel>
202+
</ExperienceCarouselContainer>
197203
);
198204
} else if (projectMedia.length === 1) {
199205
return renderSingleMediaItem(projectMedia[0] as LocalMediaItem, 0);
@@ -203,13 +209,13 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
203209
}
204210

205211
return (
206-
<MediaRenderer
207-
media={projectMedia as ParentMediaItem[]}
208-
project={project}
209-
onImageClick={onImageClick as (image: ParentMediaItem) => void}
210-
isHalfWidthContext={isHalfWidthContext}
211-
isMobileLayout={isMobileLayout}
212-
/>
212+
<MediaRow>
213+
{projectMedia.map((item, idx) => (
214+
<Box key={`media-${project.title}-${idx}`} pt="sm">
215+
{renderSingleMediaItem(item, idx)}
216+
</Box>
217+
))}
218+
</MediaRow>
213219
);
214220
};
215221

0 commit comments

Comments
 (0)