Skip to content

Commit 6ea34f3

Browse files
committed
Add ResponsiveImage and ImageWithTexts components
- Create responsive image component with WebP and multiple size support - Implement ImageWithTexts layout component with flexible positioning - Add comprehensive README documentation for both components - Include SCSS styling with responsive design and hover effects
1 parent 9e315cc commit 6ea34f3

File tree

8 files changed

+479
-0
lines changed

8 files changed

+479
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React, { useState, useEffect } from 'react';
2+
import PropTypes from 'prop-types';
3+
import ResponsiveImage from '../ResponsiveImage';
4+
import './ImageWithTexts.scss';
5+
6+
/**
7+
* A component that displays an image alongside text content
8+
* Responsive layout switches from 2 columns to 1 column on smaller screens
9+
*
10+
* @param {Object} props - The component props
11+
* @param {Object} props.sources - Image sources with different sizes/formats
12+
* @param {string} props.imageAlt - Alt text for the image
13+
* @param {React.ReactNode} props.children - Content to display next to the image
14+
* @param {string} [props.className] - Additional CSS classes
15+
* @param {string} [props.imagePosition='left'] - Position of image ('left' or 'right')
16+
* @param {number} [props.imageWidth=300] - Width of image column in pixels
17+
* @param {boolean} [props.lazy=true] - Whether to use lazy loading for image
18+
* @returns {JSX.Element} A responsive layout with image and text
19+
*/
20+
const ImageWithTexts = ({
21+
sources,
22+
imageAlt,
23+
children,
24+
className = '',
25+
imagePosition = 'left',
26+
imageWidth = 300,
27+
lazy = true
28+
}) => {
29+
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
30+
31+
useEffect(() => {
32+
const handleResize = () => {
33+
setIsMobile(window.innerWidth <= 768);
34+
};
35+
36+
window.addEventListener('resize', handleResize);
37+
return () => window.removeEventListener('resize', handleResize);
38+
}, []);
39+
40+
const containerStyle = {
41+
'--image-width': `${imageWidth}px`,
42+
};
43+
44+
return (
45+
<div
46+
className={`image-with-texts ${
47+
imagePosition === 'right' ? 'image-with-texts--image-right' : ''
48+
} ${className}`}
49+
style={containerStyle}
50+
>
51+
<div className="image-with-texts__image-container">
52+
<ResponsiveImage
53+
sources={sources}
54+
alt={imageAlt}
55+
lazy={lazy}
56+
className="image-with-texts__image"
57+
/>
58+
</div>
59+
60+
<div className="image-with-texts__content">
61+
{children}
62+
</div>
63+
</div>
64+
);
65+
};
66+
67+
ImageWithTexts.propTypes = {
68+
sources: PropTypes.shape({
69+
small: PropTypes.string.isRequired,
70+
medium: PropTypes.string,
71+
large: PropTypes.string,
72+
smallWebp: PropTypes.string,
73+
mediumWebp: PropTypes.string,
74+
largeWebp: PropTypes.string
75+
}).isRequired,
76+
imageAlt: PropTypes.string.isRequired,
77+
children: PropTypes.node.isRequired,
78+
className: PropTypes.string,
79+
imagePosition: PropTypes.oneOf(['left', 'right']),
80+
imageWidth: PropTypes.number,
81+
lazy: PropTypes.bool
82+
};
83+
84+
export default ImageWithTexts;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
@use '../../styles/variables' as *;
2+
3+
.image-with-texts {
4+
display: grid;
5+
grid-template-columns: var(--image-width, 300px) 1fr;
6+
gap: $spacing-l;
7+
margin-bottom: $spacing-l;
8+
9+
&--image-right {
10+
grid-template-columns: 1fr var(--image-width, 300px);
11+
12+
.image-with-texts__image-container {
13+
grid-column: 2;
14+
grid-row: 1;
15+
}
16+
17+
.image-with-texts__content {
18+
grid-column: 1;
19+
grid-row: 1;
20+
}
21+
}
22+
23+
&__image-container {
24+
width: 100%;
25+
max-width: var(--image-width, 300px);
26+
padding: $spacing-s;
27+
margin: 0 auto;
28+
}
29+
30+
&__content {
31+
display: flex;
32+
flex-direction: column;
33+
gap: $spacing-m;
34+
}
35+
36+
@media (max-width: $breakpoint-m) {
37+
grid-template-columns: 1fr;
38+
gap: $spacing-m;
39+
40+
&--image-right {
41+
grid-template-columns: 1fr;
42+
43+
.image-with-texts__image-container {
44+
grid-column: 1;
45+
grid-row: 1;
46+
}
47+
48+
.image-with-texts__content {
49+
grid-column: 1;
50+
grid-row: 2;
51+
}
52+
}
53+
54+
&__image-container {
55+
max-width: 80%;
56+
grid-row: 1;
57+
}
58+
59+
&__content {
60+
grid-row: 2;
61+
}
62+
}
63+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# ImageWithTexts Component
2+
3+
A responsive layout component that displays an image alongside text content.
4+
The layout automatically adjusts from two columns to a single column on smaller screens.
5+
6+
## Features
7+
8+
- Responsive two-column layout that collapses to single column on small screens
9+
- Configurable image position (left or right)
10+
- Customizable image width
11+
- Uses ResponsiveImage component for optimal image loading
12+
- Supports any React components as content (TODO If this is is the case (any components as content): should we rename the component to ImageWithComponents (something like that)?)
13+
14+
## Usage
15+
16+
```jsx
17+
import ImageWithTexts from '../components/ImageWithTexts';
18+
19+
function MyComponent() {
20+
const imageSources = {
21+
small: '/images/person-small.jpg',
22+
medium: '/images/person-medium.jpg',
23+
smallWebp: '/images/person-small.webp',
24+
mediumWebp: '/images/person-medium.webp'
25+
};
26+
27+
return (
28+
<ImageWithTexts
29+
imageSources={imageSources}
30+
imageAlt="Person smiling"
31+
imagePosition="left"
32+
imageWidth={300}
33+
>
34+
<h2>About Me</h2>
35+
<p>This is some text content that will appear next to the image.</p>
36+
<p>The layout will automatically adjust on smaller screens.</p>
37+
</ImageWithTexts>
38+
);
39+
}
40+
```
41+
42+
## Props
43+
44+
| Prop | Type | Required | Default | Description |
45+
| ------------- | ------- | -------- | ------- | ------------------------------------------------------- |
46+
| imageSources | object | Yes | - | Object containing image paths (same as ResponsiveImage) |
47+
| imageAlt | string | Yes | - | Alternative text for the image |
48+
| children | node | Yes | - | Content to display next to the image |
49+
| className | string | No | '' | Additional CSS class names |
50+
| imagePosition | string | No | 'left' | Position of image ('left' or 'right') |
51+
| imageWidth | number | No | 300 | Width of image column in pixels |
52+
| lazyLoad | boolean | No | true | Whether to use lazy loading for image |
53+
54+
## Responsive Behavior
55+
56+
- Desktop: Two-column layout with image on left or right (configurable)
57+
- Mobile (≤ 768px): Single-column layout with image always on top
58+
59+
## Styling
60+
61+
The component uses ImageWithTexts.scss for styling. You can customize the appearance by:
62+
63+
- Modifying ImageWithTexts.scss directly
64+
- Passing a custom className prop
65+
- Overriding the default styles in your own stylesheet
66+
- Using the imageWidth prop to control image column width
67+
68+
## Dependencies
69+
70+
- React
71+
- PropTypes
72+
- ResponsiveImage component
73+
74+
## Example with Custom Styling
75+
76+
```jsx
77+
<ImageWithTexts
78+
imageSources={profileImages}
79+
imageAlt="Profile picture"
80+
imagePosition="right"
81+
imageWidth={350}
82+
className="about-me-section"
83+
>
84+
<h2 className="about-heading">About Me</h2>
85+
<div className="about-content">
86+
<p>Detailed information about me and my work...</p>
87+
<LinkCard
88+
href="https://example.com/cv"
89+
text="View My Resume"
90+
description="Download my full CV in PDF format"
91+
/>
92+
</div>
93+
</ImageWithTexts>
94+
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import ImageWithTexts from './ImageWithTexts';
2+
3+
export default ImageWithTexts;
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# ResponsiveImage Component
2+
3+
A flexible, reusable component for displaying responsive images with support for multiple sizes and formats.
4+
5+
## Features
6+
7+
- Supports multiple image sizes for different viewport widths
8+
- WebP format support with automatic fallbacks
9+
- Lazy loading for improved performance
10+
- Simple API for common image handling needs
11+
- Consistent styling with customization options
12+
13+
## Usage
14+
15+
```jsx
16+
import ResponsiveImage from '../components/ResponsiveImage';
17+
18+
function MyComponent() {
19+
const imageSourcesFull = {
20+
// Standard formats
21+
small: '/images/photo-small.jpg',
22+
medium: '/images/photo-medium.jpg',
23+
large: '/images/photo-large.jpg',
24+
25+
// WebP formats (better performance)
26+
smallWebp: '/images/photo-small.webp',
27+
mediumWebp: '/images/photo-medium.webp',
28+
largeWebp: '/images/photo-large.webp'
29+
};
30+
31+
// Minimal required example
32+
const imageSourcesMinimal = {
33+
small: '/images/photo-small.jpg'
34+
};
35+
36+
return (
37+
<ResponsiveImage
38+
sources={imageSourcesFull}
39+
alt="Descriptive text for accessibility"
40+
lazy={false} // Load eagerly for above-the-fold content
41+
/>
42+
);
43+
}
44+
```
45+
46+
## API
47+
48+
### Props
49+
50+
| Prop | Type | Required | Default | Description |
51+
| -------------------- | ------- | -------- | ------- | ------------------------------------------------------------- |
52+
| `sources` | object | Yes | - | Object containing image paths for different sizes and formats |
53+
| `sources.small` | string | Yes | - | Path to small image (required, fallback) |
54+
| `sources.medium` | string | No | - | Path to medium-sized image |
55+
| `sources.large` | string | No | - | Path to large-sized image |
56+
| `sources.smallWebp` | string | No | - | Path to small WebP format image |
57+
| `sources.mediumWebp` | string | No | - | Path to medium WebP format image |
58+
| `sources.largeWebp` | string | No | - | Path to large WebP format image |
59+
| `alt` | string | Yes | - | Alternative text for the image |
60+
| `className` | string | No | '' | Additional CSS class names |
61+
| `lazy` | boolean | No | true | Whether to use lazy loading for image |
62+
63+
## Responsive Behavior
64+
65+
The component automatically selects the appropriate image source based on viewport width:
66+
67+
- Large images (and WebP): viewport width ≥ 1024px
68+
- Medium images (and WebP): viewport width ≥ 768px
69+
- Small images (and WebP): viewport width < 768px
70+
71+
## Styling
72+
73+
The component uses ResponsiveImage.scss file for styling. You can customize the appearance by:
74+
75+
- Modifying ResponsiveImage.scss directly
76+
- Passing a custom className prop
77+
- Overriding the default styles in your own stylesheet
78+
79+
## Dependencies
80+
81+
- React
82+
- PropTypes
83+
84+
## Accessibility
85+
86+
The component follows accessibility best practices:
87+
88+
- Required alt text for screen readers
89+
- Maintains aspect ratio to prevent layout shifts
90+
- Supports lazy loading for performance
91+
92+
## Example with WebP and Fallbacks
93+
94+
```jsx
95+
// Full example with all options
96+
const imageSourcesFull = {
97+
// Standard formats
98+
small: '/images/photo-small.jpg',
99+
medium: '/images/photo-medium.jpg',
100+
large: '/images/photo-large.jpg',
101+
102+
// WebP formats (better performance)
103+
smallWebp: '/images/photo-small.webp',
104+
mediumWebp: '/images/photo-medium.webp',
105+
largeWebp: '/images/photo-large.webp'
106+
};
107+
108+
// Minimal required example
109+
const imageSourcesMinimal = {
110+
small: '/images/photo-small.jpg'
111+
};
112+
113+
<ResponsiveImage
114+
sources={imageSourcesFull}
115+
alt="Descriptive text for accessibility"
116+
lazy={false} // Load eagerly for above-the-fold content
117+
/>
118+
```

0 commit comments

Comments
 (0)