feat: SEO (react-helmet-async), ProjectCard, lazy images, build fixes
This commit is contained in:
Generated
+31
@@ -22,6 +22,8 @@
|
|||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-bootstrap": "^1.2.2",
|
"react-bootstrap": "^1.2.2",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
|
"react-helmet-async": "^1.3.0",
|
||||||
|
"react-is": "^19.2.7",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
"styled-components": "^5.1.1"
|
"styled-components": "^5.1.1"
|
||||||
@@ -44851,6 +44853,35 @@
|
|||||||
"integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==",
|
"integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-fast-compare": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/react-helmet-async": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"invariant": "^2.2.4",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-fast-compare": "^3.2.0",
|
||||||
|
"shallowequal": "^1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.6.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-is": {
|
||||||
|
"version": "19.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz",
|
||||||
|
"integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react-is-18": {
|
"node_modules/react-is-18": {
|
||||||
"name": "react-is",
|
"name": "react-is",
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-bootstrap": "^1.2.2",
|
"react-bootstrap": "^1.2.2",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
|
"react-helmet-async": "^1.3.0",
|
||||||
|
"react-is": "^19.2.7",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
"styled-components": "^5.1.1"
|
"styled-components": "^5.1.1"
|
||||||
|
|||||||
+6
-5
@@ -1,12 +1,13 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
|
import { Helmet } from 'react-helmet-async';
|
||||||
|
|
||||||
export const NoMatch = () => {
|
export const NoMatch = () => {
|
||||||
useEffect(() => {
|
|
||||||
document.title = '404 Error';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="notfound-page">
|
<main className="notfound-page">
|
||||||
|
<Helmet>
|
||||||
|
<title>404 Error</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1>404 ERROR</h1>
|
<h1>404 ERROR</h1>
|
||||||
<p>Hey, you shouldn't be here!</p>
|
<p>Hey, you shouldn't be here!</p>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||||
|
import { HelmetProvider } from 'react-helmet-async';
|
||||||
import { Home } from './Home';
|
import { Home } from './Home';
|
||||||
import { Resume } from './Resume';
|
import { Resume } from './Resume';
|
||||||
import { Projects } from './Projects';
|
import { Projects } from './Projects';
|
||||||
@@ -11,6 +12,7 @@ import { Jumbotron } from './components/Jumbotron';
|
|||||||
import Footer from './components/Footer';
|
import Footer from './components/Footer';
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
|
<HelmetProvider>
|
||||||
<Router>
|
<Router>
|
||||||
<NavigationBar />
|
<NavigationBar />
|
||||||
<Jumbotron />
|
<Jumbotron />
|
||||||
@@ -25,6 +27,7 @@ const App = () => (
|
|||||||
</Layout>
|
</Layout>
|
||||||
<Footer />
|
<Footer />
|
||||||
</Router>
|
</Router>
|
||||||
|
</HelmetProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
+7
-5
@@ -1,12 +1,14 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
|
import { Helmet } from 'react-helmet-async';
|
||||||
|
|
||||||
export const Contact = () => {
|
export const Contact = () => {
|
||||||
useEffect(() => {
|
|
||||||
document.title = 'Contact Gregory Campbell';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="contact-page">
|
<main className="contact-page">
|
||||||
|
<Helmet>
|
||||||
|
<title>Contact Gregory Campbell</title>
|
||||||
|
<meta name="description" content="Contact details for Gregory Campbell." />
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1>How to Reach Me</h1>
|
<h1>How to Reach Me</h1>
|
||||||
<p>Give me a shout!</p>
|
<p>Give me a shout!</p>
|
||||||
|
|||||||
+8
-6
@@ -1,13 +1,15 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
|
import { Helmet } from 'react-helmet-async';
|
||||||
import Me from './assets/Me.jpeg';
|
import Me from './assets/Me.jpeg';
|
||||||
|
|
||||||
export const Home = () => {
|
export const Home = () => {
|
||||||
useEffect(() => {
|
|
||||||
document.title = 'The Works of Gregory Campbell';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="home-page">
|
<main className="home-page">
|
||||||
|
<Helmet>
|
||||||
|
<title>The Works of Gregory Campbell</title>
|
||||||
|
<meta name="description" content="Personal website and portfolio of Gregory Campbell." />
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Welcome to the personal website of Gregory Campbell</h2>
|
<h2>Welcome to the personal website of Gregory Campbell</h2>
|
||||||
<p>I hope you enjoy everything it has to offer.</p>
|
<p>I hope you enjoy everything it has to offer.</p>
|
||||||
@@ -23,7 +25,7 @@ export const Home = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<img src={Me} alt="Portrait of Gregory Campbell" className="homeimage" />
|
<img src={Me} alt="Portrait of Gregory Campbell" className="homeimage" loading="lazy" decoding="async" />
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
+76
-95
@@ -1,27 +1,82 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
|
import { Helmet } from 'react-helmet-async';
|
||||||
|
import ProjectCard from './components/ProjectCard';
|
||||||
import ShowingOff from './assets/ShowingOff.jpg';
|
import ShowingOff from './assets/ShowingOff.jpg';
|
||||||
import MJB from './assets/MJB.jpg';
|
|
||||||
import GC from './assets/GunControl.png';
|
|
||||||
import Palish from './assets/Palish.png';
|
|
||||||
import SRS from './assets/SRS.png';
|
|
||||||
import WH from './assets/W&H.png';
|
|
||||||
import StateMachine from './assets/StateMachine.png';
|
import StateMachine from './assets/StateMachine.png';
|
||||||
import Skin2 from './assets/skin2.png';
|
import Skin2 from './assets/skin2.png';
|
||||||
import HSV from './assets/HSV.png';
|
|
||||||
import EulerCycle from './assets/EulerCycle.png';
|
|
||||||
import EulerExample from './assets/EulerExample.png';
|
import EulerExample from './assets/EulerExample.png';
|
||||||
import Hangman from './assets/Hangman.png';
|
import Hangman from './assets/Hangman.png';
|
||||||
import KngihtsTour from './assets/KnightsTour.png';
|
import KngihtsTour from './assets/KnightsTour.png';
|
||||||
import TextAnalyzer from './assets/TextAnalyzer.png';
|
import TextAnalyzer from './assets/TextAnalyzer.png';
|
||||||
import AlgorithmComparison from './assets/AlgorithmComparison.png';
|
import AlgorithmComparison from './assets/AlgorithmComparison.png';
|
||||||
|
|
||||||
|
const projects = [
|
||||||
|
{
|
||||||
|
title: 'Python State Machine',
|
||||||
|
link: 'https://github.com/gjcampbell777/python-state-machine',
|
||||||
|
description: 'A Python state machine used to emulate a locked/unlocked device.',
|
||||||
|
img: StateMachine,
|
||||||
|
imgAlt: 'Python state machine demo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Python Skin Detection Algorithms',
|
||||||
|
link: 'https://github.com/gjcampbell777/python-photo-skin-detection',
|
||||||
|
description: 'Replicated multiple skin detection algorithms in Python and modernized them for Python 3.',
|
||||||
|
img: Skin2,
|
||||||
|
imgAlt: 'Skin detection before/after',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Euler Checking Algorithm',
|
||||||
|
link: 'https://github.com/gjcampbell777/euler-checking-algorithm',
|
||||||
|
description: 'C program to check if graphs are Eulerian.',
|
||||||
|
img: EulerExample,
|
||||||
|
imgAlt: 'Euler example'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Fortran Hangman',
|
||||||
|
link: 'https://github.com/gjcampbell777/fortran-hangman',
|
||||||
|
description: 'Updated a Fortran 77 hangman game to Fortran 95 and improved the code.',
|
||||||
|
img: Hangman,
|
||||||
|
imgAlt: 'Hangman game'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Ada Knights Tour",
|
||||||
|
link: 'https://github.com/gjcampbell777/ada-knights-tour',
|
||||||
|
description: "Solves the knight's tour problem in Ada.",
|
||||||
|
img: KngihtsTour,
|
||||||
|
imgAlt: 'Knights tour output'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'COBOL Text Analyzer',
|
||||||
|
link: 'https://github.com/gjcampbell777/cobol-text-analyzer',
|
||||||
|
description: 'Modernized COBOL text analyzer to report text statistics.',
|
||||||
|
img: TextAnalyzer,
|
||||||
|
imgAlt: 'Text analyzer output'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Legacy Algorithm Comparison',
|
||||||
|
link: 'https://github.com/gjcampbell777/legacy-algorithm-comparison',
|
||||||
|
description: 'Compare KMP implementations across C, Fortran, and Ada.',
|
||||||
|
img: AlgorithmComparison,
|
||||||
|
imgAlt: 'Algorithm comparison charts'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'My Many Games',
|
||||||
|
link: 'https://gregorysoup.itch.io',
|
||||||
|
description: 'A selection of games made with Unity and Godot.',
|
||||||
|
img: ShowingOff,
|
||||||
|
imgAlt: 'Game showcase'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
export const Projects = () => {
|
export const Projects = () => {
|
||||||
useEffect(() => {
|
|
||||||
document.title = 'The Projects of Gregory Campbell';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="projects-page">
|
<main className="projects-page">
|
||||||
|
<Helmet>
|
||||||
|
<title>The Projects of Gregory Campbell</title>
|
||||||
|
<meta name="description" content="Collection of projects by Gregory Campbell." />
|
||||||
|
</Helmet>
|
||||||
<header>
|
<header>
|
||||||
<h1>My Projects</h1>
|
<h1>My Projects</h1>
|
||||||
<aside>Look at what I've created!</aside>
|
<aside>Look at what I've created!</aside>
|
||||||
@@ -35,90 +90,16 @@ export const Projects = () => {
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<article className="project">
|
{projects.map((p) => (
|
||||||
<h2><a href="https://github.com/gjcampbell777/python-state-machine" target="_blank" rel="noreferrer noopener">Python State Machine</a></h2>
|
<ProjectCard
|
||||||
<p>
|
key={p.title}
|
||||||
Recently while going through Godot documentation I came across a tutorial for building your own state machine with Python.
|
title={p.title}
|
||||||
I continued working on that state machine to emulate a locked/unlocked device.
|
description={p.description}
|
||||||
<a href="https://github.com/gjcampbell777/python-state-machine" target="_blank" rel="noreferrer noopener">View the project</a>.
|
imgSrc={p.img}
|
||||||
</p>
|
imgAlt={p.imgAlt}
|
||||||
<img src={StateMachine} alt="Python state machine demo" />
|
link={p.link}
|
||||||
</article>
|
/>
|
||||||
|
))}
|
||||||
<article className="project">
|
|
||||||
<h2><a href="https://github.com/gjcampbell777/python-photo-skin-detection" target="_blank" rel="noreferrer noopener">Python Skin Detection Algorithms</a></h2>
|
|
||||||
<p>
|
|
||||||
In an image processing course I replicated multiple skin detection algorithms in Python.
|
|
||||||
I cleaned up the original assignment and made it compatible with Python 3.
|
|
||||||
<a href="https://github.com/gjcampbell777/python-photo-skin-detection" target="_blank" rel="noreferrer noopener">View the project</a>.
|
|
||||||
</p>
|
|
||||||
<img src={Skin2} alt="Skin detection before" />
|
|
||||||
<p>Before the algorithm.</p>
|
|
||||||
<img src={HSV} alt="Skin detection after" />
|
|
||||||
<p>After the HSV algorithm.</p>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article className="project">
|
|
||||||
<h2><a href="https://github.com/gjcampbell777/euler-checking-algorithm" target="_blank" rel="noreferrer noopener">Euler Checking Algorithm</a></h2>
|
|
||||||
<p>
|
|
||||||
This C program checks whether a graph is Eulerian by reading a text file and analyzing graph structure.
|
|
||||||
It repeats the process until end-of-file and reports whether the graph is Eulerian.
|
|
||||||
<a href="https://github.com/gjcampbell777/euler-checking-algorithm" target="_blank" rel="noreferrer noopener">View the project</a>.
|
|
||||||
</p>
|
|
||||||
<img src={EulerExample} alt="Euler graph input example" />
|
|
||||||
<p>Example of a graph from euler-graphs.txt.</p>
|
|
||||||
<img src={EulerCycle} alt="Euler cycle output" />
|
|
||||||
<p>Output from running the algorithm.</p>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article className="project">
|
|
||||||
<h2><a href="https://github.com/gjcampbell777/fortran-hangman" target="_blank" rel="noreferrer noopener">Fortran Hangman</a></h2>
|
|
||||||
<p>
|
|
||||||
A legacy programming course project to update a Fortran 77 hangman game to Fortran 95 and improve the code.
|
|
||||||
Use the executable or Makefile to run the game.
|
|
||||||
</p>
|
|
||||||
<img src={Hangman} alt="Fortran hangman game" />
|
|
||||||
<p>A very quick hangman game.</p>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article className="project">
|
|
||||||
<h2><a href="https://github.com/gjcampbell777/ada-knights-tour" target="_blank" rel="noreferrer noopener">Ada Knights Tour</a></h2>
|
|
||||||
<p>
|
|
||||||
A program that solves the knight's tour problem in Ada.
|
|
||||||
It generates a solution for small boards and shows the move sequence.
|
|
||||||
</p>
|
|
||||||
<img src={KngihtsTour} alt="Knights tour output" />
|
|
||||||
<p>Knights tour output for a 5x5 chessboard starting at position (1,1).</p>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article className="project">
|
|
||||||
<h2><a href="https://github.com/gjcampbell777/cobol-text-analyzer" target="_blank" rel="noreferrer noopener">COBOL Text Analyzer</a></h2>
|
|
||||||
<p>
|
|
||||||
A modernized COBOL text analyzer updated from a legacy script.
|
|
||||||
It analyzes fixed data and reports text statistics.
|
|
||||||
</p>
|
|
||||||
<img src={TextAnalyzer} alt="COBOL text analyzer output" />
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article className="project">
|
|
||||||
<h2><a href="https://github.com/gjcampbell777/legacy-algorithm-comparison" target="_blank" rel="noreferrer noopener">Legacy Code Algorithm Comparison</a></h2>
|
|
||||||
<p>
|
|
||||||
A comparison of C, Fortran, and Ada implementations of the KMP search algorithm.
|
|
||||||
The project generates executables for each language to compare performance.
|
|
||||||
</p>
|
|
||||||
<img src={AlgorithmComparison} alt="Algorithm comparison charts" />
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article className="project">
|
|
||||||
<h2><a href="https://gregorysoup.itch.io" target="_blank" rel="noreferrer noopener">My Many Games</a></h2>
|
|
||||||
<p>
|
|
||||||
I have built a collection of games using Unity and Godot, including endless runners, turn-based combat puzzles, and game jam projects.
|
|
||||||
</p>
|
|
||||||
<img src={ShowingOff} alt="Presenting a game at a Toronto indie game dev meetup" />
|
|
||||||
<img src={MJB} alt="Game project showcase" />
|
|
||||||
<img src={WH} alt="Game project showcase" />
|
|
||||||
<img src={SRS} alt="Game project showcase" />
|
|
||||||
</article>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+8
-6
@@ -1,14 +1,16 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
|
import { Helmet } from 'react-helmet-async';
|
||||||
import pdf from './assets/Gregory Campbell Resume.pdf';
|
import pdf from './assets/Gregory Campbell Resume.pdf';
|
||||||
import pic from './assets/Resume.png';
|
import pic from './assets/Resume.png';
|
||||||
|
|
||||||
export const Resume = () => {
|
export const Resume = () => {
|
||||||
useEffect(() => {
|
|
||||||
document.title = 'The Resume of Gregory Campbell';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="resume-page">
|
<main className="resume-page">
|
||||||
|
<Helmet>
|
||||||
|
<title>The Resume of Gregory Campbell</title>
|
||||||
|
<meta name="description" content="Resume and professional experience of Gregory Campbell." />
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1>My Resume</h1>
|
<h1>My Resume</h1>
|
||||||
<p>Check out everything I have accomplished in my career.</p>
|
<p>Check out everything I have accomplished in my career.</p>
|
||||||
@@ -24,7 +26,7 @@ export const Resume = () => {
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Written Resume</h2>
|
<h2>Written Resume</h2>
|
||||||
<img src={pic} alt="Resume preview" />
|
<img src={pic} alt="Resume preview" loading="lazy" decoding="async" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const ProjectCard = ({ title, description, imgSrc, imgAlt, link, children }) => {
|
||||||
|
return (
|
||||||
|
<article className="project-card">
|
||||||
|
<h3>
|
||||||
|
{link ? (
|
||||||
|
<a href={link} target="_blank" rel="noreferrer noopener">{title}</a>
|
||||||
|
) : (
|
||||||
|
title
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<p>{description}</p>
|
||||||
|
{imgSrc && (
|
||||||
|
<img src={imgSrc} alt={imgAlt || title} loading="lazy" decoding="async" />
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectCard;
|
||||||
Reference in New Issue
Block a user