Skip to content

Commit 47fc31e

Browse files
committed
Refactor project structure and add new components
- Introduce PageHeading component for consistent page titles - Migrate Welcome page to use new PageHeading and improve layout - Create dedicated Projects page with project showcase - Update navigation to include Projects route - Enhance responsive design and styling for new components - Remove deprecated page-title component
1 parent 18573e1 commit 47fc31e

File tree

19 files changed

+554
-246
lines changed

19 files changed

+554
-246
lines changed
Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
1-
import React, { useState, useEffect } from 'react';
21
import PropTypes from 'prop-types';
2+
import React from 'react';
3+
34
import ResponsiveImage from '../ResponsiveImage';
45
import './ImageWithContent.scss';
56

67
/**
7-
* A component that displays an image alongside content
8-
* Responsive layout switches from 2 columns to 1 column on smaller screens
8+
* A component that displays an image alongside content (text, buttons, etc.)
9+
* Features:
10+
* - Configurable image position (left or right)
11+
* - Customizable image width
12+
* - Responsive layout that switches from side-by-side to stacked on smaller screens
13+
* - Support for different image sizes and formats via ResponsiveImage
14+
* - Optional lazy loading for performance optimization
915
*
1016
* @param {Object} props - The component props
1117
* @param {Object} props.sources - Image sources with different sizes/formats
12-
* @param {string} props.imageAlt - Alt text for the image
18+
* @param {string} props.imageAlt - Alt text for the image (important for accessibility)
1319
* @param {React.ReactNode} props.children - Content to display next to the image
14-
* @param {string} [props.className] - Additional CSS classes
20+
* @param {string} [props.className] - Additional CSS classes for customization
1521
* @param {string} [props.imagePosition='left'] - Position of image ('left' or 'right')
1622
* @param {number} [props.imageWidth=300] - Width of image column in pixels
23+
* @param {number} [props.mobileMaxWidth=null] - Maximum width of image on mobile screens in pixels
1724
* @param {boolean} [props.lazy=true] - Whether to use lazy loading for image
1825
* @returns {JSX.Element} A responsive layout with image and content
1926
*/
@@ -24,31 +31,26 @@ const ImageWithContent = ({
2431
className = '',
2532
imagePosition = 'left',
2633
imageWidth = 300,
34+
mobileMaxWidth = null,
2735
lazy = true
2836
}) => {
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-
37+
// Define inline CSS variables to configure the component's layout
38+
// These variables are used in the SCSS file to control image dimensions
4039
const containerStyle = {
4140
'--image-width': `${imageWidth}px`,
41+
'--mobile-max-width': mobileMaxWidth ? `${mobileMaxWidth}px` : 'none',
4242
};
4343

4444
return (
4545
<div
4646
data-testid="image-with-content"
47+
// Apply base class, conditional modifier for right alignment, and any custom classes
4748
className={`image-with-content ${
4849
imagePosition === 'right' ? 'image-with-content--image-right' : ''
4950
} ${className}`}
5051
style={containerStyle}
5152
>
53+
{/* Image container - maintains image dimensions and spacing */}
5254
<div className="image-with-content__image-container" data-testid="image-container">
5355
<ResponsiveImage
5456
sources={sources}
@@ -58,28 +60,32 @@ const ImageWithContent = ({
5860
/>
5961
</div>
6062

63+
{/* Content container - holds children passed to component */}
6164
<div className="image-with-content__content" data-testid="content-container">
6265
{children}
6366
</div>
6467
</div>
6568
);
6669
};
6770

71+
// PropTypes definition for component documentation and type checking
6872
ImageWithContent.propTypes = {
73+
// Image sources object with required small size and optional larger sizes
6974
sources: PropTypes.shape({
70-
small: PropTypes.string.isRequired,
71-
medium: PropTypes.string,
72-
large: PropTypes.string,
73-
smallWebp: PropTypes.string,
75+
small: PropTypes.string.isRequired, // Base/small image (required)
76+
medium: PropTypes.string, // Medium size image for larger screens
77+
large: PropTypes.string, // Large size image for desktop screens
78+
smallWebp: PropTypes.string, // WebP format options for better performance
7479
mediumWebp: PropTypes.string,
7580
largeWebp: PropTypes.string
7681
}).isRequired,
77-
imageAlt: PropTypes.string.isRequired,
78-
children: PropTypes.node.isRequired,
79-
className: PropTypes.string,
80-
imagePosition: PropTypes.oneOf(['left', 'right']),
81-
imageWidth: PropTypes.number,
82-
lazy: PropTypes.bool
82+
imageAlt: PropTypes.string.isRequired, // Alt text is required for accessibility
83+
children: PropTypes.node.isRequired, // Content to display is required
84+
className: PropTypes.string, // Optional additional classes
85+
imagePosition: PropTypes.oneOf(['left', 'right']), // Restrict to only valid options
86+
imageWidth: PropTypes.number, // Width in pixels
87+
mobileMaxWidth: PropTypes.number, // Mobile max width in pixels
88+
lazy: PropTypes.bool // Toggle for lazy loading
8389
};
8490

8591
export default ImageWithContent;
Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,110 @@
1-
@use '../../styles/variables' as *;
1+
@use "../../styles/variables" as vars;
2+
3+
// Component dimensions
4+
// Default width for the image column that can be overridden via CSS custom properties
5+
$image-default-width: 300px;
26

37
.image-with-content {
48
display: grid;
5-
grid-template-columns: var(--image-width, 300px) 1fr;
6-
gap: $spacing-l;
7-
margin-bottom: $spacing-l;
8-
9+
// Two-column layout with fixed width for image and flexible width for content
10+
// Uses CSS custom property (--image-width) with SCSS variable fallback for configuration flexibility
11+
grid-template-columns: var(--image-width, $image-default-width) 1fr;
12+
gap: vars.$spacing-l;
13+
margin-bottom: vars.$spacing-l;
14+
15+
// Modifier class that reverses the layout to show image on the right side
16+
// This is currently unused and not tested. (TODO test)
917
&--image-right {
10-
grid-template-columns: 1fr var(--image-width, 300px);
11-
18+
// Reversed column order - content first, then image
19+
grid-template-columns: 1fr var(--image-width, $image-default-width);
20+
21+
// Position the image in the second column
1222
.image-with-content__image-container {
1323
grid-column: 2;
1424
grid-row: 1;
1525
}
16-
26+
27+
// Position the content in the first column
1728
.image-with-content__content {
1829
grid-column: 1;
1930
grid-row: 1;
2031
}
2132
}
22-
33+
34+
// Container for the image with size constraints
2335
&__image-container {
2436
width: 100%;
25-
max-width: var(--image-width, 300px);
26-
padding: $spacing-s;
37+
max-width: var(--image-width, $image-default-width);
38+
padding: vars.$spacing-s;
2739
margin: 0 auto;
2840
}
29-
41+
42+
// Container for the content with vertical arrangement
3043
&__content {
3144
display: flex;
3245
flex-direction: column;
33-
gap: $spacing-m;
46+
gap: vars.$spacing-m;
3447
}
35-
36-
@media (max-width: $breakpoint-m) {
48+
49+
// Medium screen responsiveness (tablets, small laptops)
50+
// Changes from two-column to one-column layout
51+
@include vars.below("medium") {
52+
// Switch to single column layout
3753
grid-template-columns: 1fr;
38-
gap: $spacing-m;
39-
54+
gap: vars.$spacing-m;
55+
56+
// Special handling for right-image variant on medium screens
4057
&--image-right {
4158
grid-template-columns: 1fr;
42-
59+
60+
// Reset the image container position for single column layout
4361
.image-with-content__image-container {
4462
grid-column: 1;
4563
grid-row: 1;
64+
// Use the mobile max width if specified, otherwise remove constraints
65+
max-width: var(--mobile-max-width, none);
66+
margin: 0 auto;
67+
}
68+
69+
// Ensure responsive image scaling
70+
.responsive-image__img {
71+
max-width: 100%;
72+
height: auto;
4673
}
47-
74+
75+
// Position content below the image in the vertical layout
4876
.image-with-content__content {
4977
grid-column: 1;
5078
grid-row: 2;
5179
}
5280
}
53-
81+
82+
// Standard image container styles for medium screens
83+
// Reduces width to 80% to prevent images from being too large
5484
&__image-container {
5585
max-width: 80%;
5686
grid-row: 1;
5787
}
58-
88+
89+
// Position content below the image in the vertical layout
5990
&__content {
6091
grid-row: 2;
6192
}
6293
}
63-
}
94+
95+
// Small screen responsiveness (mobile phones)
96+
@include vars.below("small") {
97+
// Adjust image container for small screens
98+
.image-with-content__image-container {
99+
// Use the mobile max width if specified, otherwise remove constraints
100+
max-width: var(--mobile-max-width, none);
101+
margin: 0 auto;
102+
}
103+
104+
// Ensure proper image scaling for small screens
105+
.responsive-image__img {
106+
max-width: 100%;
107+
height: auto;
108+
}
109+
}
110+
}
Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import PropTypes from 'prop-types';
22
import React from 'react';
3+
import { Link } from 'react-router-dom';
34
import '../styles/components/link-with-description.scss';
45

56
/**
@@ -8,17 +9,24 @@ import '../styles/components/link-with-description.scss';
89
* @param {string} props.url - The URL that the link points to
910
* @param {string} props.linkText - The text content of the link
1011
* @param {string} props.description - A description of the link that appears below it
12+
* @param {boolean} props.isInternal - Whether the link is an internal route (uses React Router Link)
1113
* @returns {JSX.Element} A div containing a link and description
1214
*/
13-
const LinkWithDescription = ({ url, linkText, description }) => (
14-
<div className="link-with-description">
15-
<a
16-
href={url}
17-
className="link-with-description__link"
18-
target="_blank"
19-
rel="noopener noreferrer">
20-
{linkText}
21-
</a>
15+
const LinkWithDescription = ({ url, linkText, description, isInternal = false, className = '' }) => (
16+
<div className={`link-with-description ${className}`}>
17+
{isInternal ? (
18+
<Link to={url} className="link-with-description__link">
19+
{linkText}
20+
</Link>
21+
) : (
22+
<a
23+
href={url}
24+
className="link-with-description__link"
25+
target="_blank"
26+
rel="noopener noreferrer">
27+
{linkText}
28+
</a>
29+
)}
2230
<div className="link-with-description__description">
2331
<span>{description}</span>
2432
</div>
@@ -28,7 +36,9 @@ const LinkWithDescription = ({ url, linkText, description }) => (
2836
LinkWithDescription.propTypes = {
2937
url: PropTypes.string.isRequired,
3038
linkText: PropTypes.string.isRequired,
31-
description: PropTypes.string.isRequired
39+
description: PropTypes.string.isRequired,
40+
isInternal: PropTypes.bool,
41+
className: PropTypes.string
3242
};
3343

3444
export default LinkWithDescription;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import PropTypes from "prop-types";
2+
import React from "react";
3+
import "./PageHeading.scss";
4+
5+
/**
6+
* PageHeading component for displaying page titles and subtitles
7+
* Can be used across different pages with custom content
8+
*/
9+
function PageHeading({ title, subtitle, className }) {
10+
return (
11+
<div className={`page-heading__container ${className || ""}`}>
12+
<h1 className="page-heading__title">{title}</h1>
13+
{subtitle && <p className="page-heading__subtitle">{subtitle}</p>}
14+
</div>
15+
);
16+
}
17+
18+
PageHeading.propTypes = {
19+
/**
20+
* Main title text for the page
21+
*/
22+
title: PropTypes.string.isRequired,
23+
24+
/**
25+
* Optional subtitle text
26+
*/
27+
subtitle: PropTypes.string,
28+
29+
/**
30+
* Optional additional CSS classes
31+
*/
32+
className: PropTypes.string,
33+
};
34+
35+
PageHeading.defaultProps = {
36+
subtitle: "",
37+
className: "",
38+
};
39+
40+
export default PageHeading;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
@use '../../styles/variables' as vars;
2+
@use '../../styles/base/_typography.scss' as typo;
3+
4+
.page-heading {
5+
// Main wrapper for the page heading component
6+
7+
&__container {
8+
margin: 0 auto;
9+
padding: vars.$spacing-m;
10+
11+
@include vars.below("medium") {
12+
padding: vars.$spacing-s;
13+
}
14+
}
15+
16+
&__title {
17+
@extend h1;
18+
margin-top: var(--title-margin-top, 0);
19+
margin-bottom: var(--title-margin-bottom, vars.$spacing-s);
20+
}
21+
22+
&__subtitle {
23+
@extend .subtitle;
24+
margin-top: var(--subtitle-margin-top, 0);
25+
margin-bottom: var(--subtitle-margin-bottom, vars.$spacing-m);
26+
}
27+
}

0 commit comments

Comments
 (0)