Skip to content

Commit 4c6ba21

Browse files
committed
Merge branch 'main' of github.com:conorheffron/ironoc
2 parents 0d6d56c + ae22ed2 commit 4c6ba21

18 files changed

Lines changed: 452 additions & 43 deletions

File tree

.github/workflows/auto-assign.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
pull-requests: write
1313
steps:
1414
- name: 'Auto-assign issue'
15-
uses: pozil/auto-assign-issue@v1
15+
uses: pozil/auto-assign-issue@v4
1616
with:
1717
repo-token: ${{ secrets.GITHUB_TOKEN }}
1818
assignees: conorheffron

frontend/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,23 @@ frontend
7474
└── setupTests.js
7575
```
7676

77+
## Camera Roll Background Config
78+
79+
Home and About background image rotation is driven by `public/camera-roll.yml`.
80+
81+
Example:
82+
83+
```yml
84+
home:
85+
- teal-bg
86+
- navy-bg
87+
about:
88+
- navy-bg
89+
- red-bg
90+
```
91+
92+
Supported keys are `teal-bg`, `navy-bg`, and `red-bg`.
93+
7794
#### Quick start
7895
```shell
7996
cd frontend

frontend/package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/public/camera-roll.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Camera roll keys for rotating Home/About backgrounds
2+
home:
3+
- teal-bg
4+
- navy-bg
5+
- red-bg
6+
about:
7+
- navy-bg
8+
- red-bg
9+
- teal-bg

frontend/src/App.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@
2727
overflow: hidden;
2828
}
2929

30+
.App-header-camera-roll {
31+
background-position: center;
32+
background-size: cover;
33+
background-repeat: no-repeat;
34+
animation: subtle-camera-roll-pan 24s ease-in-out infinite alternate;
35+
box-shadow: inset 0 0 0 1000px rgba(12, 31, 66, 0.15);
36+
}
37+
38+
.App-header-camera-roll--home {
39+
background-color: #1b3f83;
40+
}
41+
42+
.App-header-camera-roll--about {
43+
background-color: #16356c;
44+
}
45+
3046
.App-link {
3147
color: #61dafb;
3248
}
@@ -40,6 +56,17 @@
4056
}
4157
}
4258

59+
@keyframes subtle-camera-roll-pan {
60+
from {
61+
background-position: 48% 50%;
62+
background-size: 104%;
63+
}
64+
to {
65+
background-position: 52% 46%;
66+
background-size: 110%;
67+
}
68+
}
69+
4370
.App-header, .body, .table-headers, .table, .mb-3 {
4471
font-family: "Open Sans";
4572
font-size: 1.4em;

frontend/src/App.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,18 @@ import RepoIssues from './components/RepoIssues';
1212
import ControlledCarousel from './components/ControlledCarousel';
1313

1414
class App extends Component {
15+
constructor(props) {
16+
super(props);
17+
// Create Apollo Client once for '/donate' route
18+
this.donateClient = new ApolloClient({
19+
link: new HttpLink({ uri: '/graphql' }),
20+
cache: new InMemoryCache(),
21+
});
22+
}
23+
1524
render() {
1625
// Default props
1726
const {
18-
forceRefresh = true,
1927
routes = [
2028
{ path: '/', exact: true, component: Home },
2129
{ path: '/about', exact: true, component: About },
@@ -29,14 +37,8 @@ class App extends Component {
2937
]
3038
} = this.props;
3139

32-
// Create Apollo Client for '/donate' route
33-
const donateClient = new ApolloClient({
34-
link: new HttpLink({ uri: '/graphql' }),
35-
cache: new InMemoryCache(),
36-
});
37-
3840
return (
39-
<Router forceRefresh={forceRefresh}>
41+
<Router>
4042
<Routes>
4143
{routes.map((route, index) => {
4244
// Wrap the Donate component with ApolloProvider
@@ -47,7 +49,7 @@ class App extends Component {
4749
path={route.path}
4850
{...(route.exact ? { exact: true } : {})}
4951
element={
50-
<ApolloProvider client={donateClient}>
52+
<ApolloProvider client={this.donateClient}>
5153
<Donate />
5254
</ApolloProvider>
5355
}

frontend/src/components/About.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,48 @@ import React, { Component } from 'react';
22
import { Container } from 'reactstrap';
33
import '.././App.css';
44
import AppNavbar from '.././AppNavbar';
5+
import { loadCameraRollImages } from '../utils/cameraRollConfig';
56

67
class About extends Component {
8+
constructor(props) {
9+
super(props);
10+
this.state = {
11+
backgroundImages: [],
12+
backgroundImageIndex: 0
13+
};
14+
15+
this.rotateCameraRollImage = this.rotateCameraRollImage.bind(this);
16+
this.rotationIntervalId = null;
17+
this.isMountedView = false;
18+
}
19+
20+
async componentDidMount() {
21+
this.isMountedView = true;
22+
const backgroundImages = await loadCameraRollImages('about');
23+
24+
if (this.isMountedView) {
25+
this.setState({ backgroundImages, backgroundImageIndex: 0 });
26+
27+
if (backgroundImages.length > 1) {
28+
this.rotationIntervalId = setInterval(this.rotateCameraRollImage, 10000);
29+
}
30+
}
31+
}
32+
33+
componentWillUnmount() {
34+
this.isMountedView = false;
35+
36+
if (this.rotationIntervalId) {
37+
clearInterval(this.rotationIntervalId);
38+
}
39+
}
40+
41+
rotateCameraRollImage() {
42+
this.setState((prevState) => ({
43+
backgroundImageIndex: (prevState.backgroundImageIndex + 1) % prevState.backgroundImages.length
44+
}));
45+
}
46+
747
render() {
848
const {
949
link = "https://www.linkedin.com/in/conorheffron",
@@ -15,12 +55,21 @@ class About extends Component {
1555
stravaImgSrc = 'https://badges.strava.com/logo-strava.png',
1656
stravaImgAlt = 'Strava'
1757
} = this.props;
58+
const { backgroundImages, backgroundImageIndex } = this.state;
59+
const activeBackgroundImage = backgroundImages[backgroundImageIndex];
60+
const headerStyle = activeBackgroundImage
61+
? { backgroundImage: `linear-gradient(rgba(8, 36, 74, 0.6), rgba(8, 36, 74, 0.6)), url(${activeBackgroundImage})` }
62+
: undefined;
1863

1964
return (
2065
<div className="App">
2166
<AppNavbar />
2267
<Container>
23-
<header className="App-header">
68+
<header
69+
data-testid="about-header"
70+
className="App-header App-header-camera-roll App-header-camera-roll--about"
71+
style={headerStyle}
72+
>
2473
<br /><br />
2574
<a href={link} target="_blank" rel="noreferrer">
2675
<img src={imgSrc} width={imgWidth} height={imgHeight} border="0" alt={imgAlt}></img>

frontend/src/components/Home.js

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
22
import { Container } from 'reactstrap';
33
import '.././App.css';
44
import AppNavbar from '.././AppNavbar';
5+
import { loadCameraRollImages } from '../utils/cameraRollConfig';
56

67
class Home extends Component {
78
constructor(props) {
@@ -17,18 +18,59 @@ class Home extends Component {
1718
className,
1819
headerClassName,
1920
introId,
20-
welcomeMessage
21+
welcomeMessage,
22+
backgroundImages: [],
23+
backgroundImageIndex: 0
2124
};
25+
26+
this.rotateCameraRollImage = this.rotateCameraRollImage.bind(this);
27+
this.rotationIntervalId = null;
28+
this.isMountedView = false;
29+
}
30+
31+
async componentDidMount() {
32+
this.isMountedView = true;
33+
const backgroundImages = await loadCameraRollImages('home');
34+
35+
if (this.isMountedView) {
36+
this.setState({ backgroundImages, backgroundImageIndex: 0 });
37+
38+
if (backgroundImages.length > 1) {
39+
this.rotationIntervalId = setInterval(this.rotateCameraRollImage, 10000);
40+
}
41+
}
42+
}
43+
44+
componentWillUnmount() {
45+
this.isMountedView = false;
46+
47+
if (this.rotationIntervalId) {
48+
clearInterval(this.rotationIntervalId);
49+
}
50+
}
51+
52+
rotateCameraRollImage() {
53+
this.setState((prevState) => ({
54+
backgroundImageIndex: (prevState.backgroundImageIndex + 1) % prevState.backgroundImages.length
55+
}));
2256
}
2357

2458
render() {
25-
const { className, headerClassName, introId, welcomeMessage } = this.state;
59+
const { className, headerClassName, introId, welcomeMessage, backgroundImages, backgroundImageIndex } = this.state;
60+
const activeBackgroundImage = backgroundImages[backgroundImageIndex];
61+
const headerStyle = activeBackgroundImage
62+
? { backgroundImage: `linear-gradient(rgba(10, 34, 80, 0.55), rgba(10, 34, 80, 0.55)), url(${activeBackgroundImage})` }
63+
: undefined;
2664

2765
return (
2866
<div className={className}>
2967
<AppNavbar />
3068
<Container>
31-
<header className={headerClassName}>
69+
<header
70+
data-testid="home-header"
71+
className={`${headerClassName} App-header-camera-roll App-header-camera-roll--home`}
72+
style={headerStyle}
73+
>
3274
<br /><br />
3375
<p id={introId}>
3476
{welcomeMessage}<br /><br />
Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
1-
import { screen, render } from '@testing-library/react';
1+
import { screen, render, waitFor, act } from '@testing-library/react';
22
import About from '../About';
33

44
describe('About Component', () => {
5-
test('renders with default props', () => {
6-
render(<About />);
5+
beforeEach(() => {
6+
jest.spyOn(global, 'fetch').mockResolvedValue({
7+
ok: true,
8+
text: async () => `about:\n - navy-bg\n - red-bg`,
9+
});
10+
});
11+
12+
afterEach(() => {
13+
jest.restoreAllMocks();
14+
});
15+
16+
test('renders with default props', async () => {
17+
await act(async () => {
18+
render(<About />);
19+
});
720

821
expect(screen.getByAltText("View Conor Heffron's profile on LinkedIn")).toBeInTheDocument();
922
expect(screen.getByAltText('Strava')).toBeInTheDocument();
1023
});
1124

12-
test('renders with provided props', () => {
25+
test('renders with provided props', async () => {
1326
const props = {
1427
link: 'https://example.com',
1528
imgSrc: 'https://example.com/image.png',
@@ -18,8 +31,20 @@ describe('About Component', () => {
1831
stravaImgSrc: 'https://example.com/strava-image.png',
1932
stravaImgAlt: 'Example Strava',
2033
};
21-
render(<About {...props} />);
34+
await act(async () => {
35+
render(<About {...props} />);
36+
});
2237
expect(screen.getByAltText('Example Image')).toBeInTheDocument();
2338
expect(screen.getByAltText('Example Strava')).toBeInTheDocument();
2439
});
40+
41+
test('requests camera roll config on mount', async () => {
42+
await act(async () => {
43+
render(<About />);
44+
});
45+
46+
await waitFor(() => {
47+
expect(global.fetch).toHaveBeenCalledWith('/camera-roll.yml', { cache: 'no-store' });
48+
});
49+
});
2550
});
Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
1-
import { render, screen, act } from '@testing-library/react';
1+
import { render, screen, act, waitFor } from '@testing-library/react';
22
import Home from '../Home';
33

44
describe('Home', () => {
5+
beforeEach(() => {
6+
jest.spyOn(global, 'fetch').mockResolvedValue({
7+
ok: true,
8+
text: async () => `home:\n - red-bg\n - teal-bg`,
9+
});
10+
});
11+
12+
afterEach(() => {
13+
jest.restoreAllMocks();
14+
});
15+
516
test('renders Home component', async () => {
617
await act(async () => {
718
render(<Home />);
819
});
920
const element = screen.getByText(/Home/i);
1021
expect(element).toBeInTheDocument();
1122
});
23+
24+
test('requests camera roll config on mount', async () => {
25+
render(<Home />);
26+
27+
await waitFor(() => {
28+
expect(global.fetch).toHaveBeenCalledWith('/camera-roll.yml', { cache: 'no-store' });
29+
});
30+
});
1231
});

0 commit comments

Comments
 (0)