Add tips and quotes pages (#6)

* add basic routing, multi-page support

* update package versions

* add PoC for Quotes handling

* update quotes ux

* add PoC for life tips

* add custom nginx config file

* update docker build

* update first run process

* add life tips and quotes components and pages

---------

Co-authored-by: Carlos Sousa <me@carlossousa.tech>
This commit is contained in:
Carlos Sousa 2024-03-22 17:06:05 +01:00 committed by GitHub
parent 2a20f1846c
commit dcaa9eaf2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 2827 additions and 2712 deletions

View File

@ -9,5 +9,6 @@ RUN npm run build
# Stage 2: Serve the application with Nginx # Stage 2: Serve the application with Nginx
FROM nginx:alpine FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html COPY --from=build /app/build /usr/share/nginx/html
COPY custom_nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80 EXPOSE 80
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]

View File

@ -10,7 +10,7 @@ Live version can be found at [carlossousa.tech](https://carlossousa.tech)
If it is the first time you are running the project, the npm modules will be missing. If it is the first time you are running the project, the npm modules will be missing.
Install them by running ``npm install`` Install them by running ``npm install`` or ``docker-compose up dev npm install``
##### Development ##### Development
@ -18,4 +18,4 @@ Install them by running ``npm install``
##### Production ##### Production
``docker-compose up --build prod`` ``docker-compose up --build website``

18
custom_nginx.conf Normal file
View File

@ -0,0 +1,18 @@
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri /index.html;
}
#error_page 404 /404.html;
#error_page 500 502 503 504 /50x.html;
#location = /50x.html {
# root /usr/share/nginx/html;
#}
}

File diff suppressed because it is too large Load Diff

View File

@ -3,14 +3,15 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@mui/material": "^5.15.0", "@mui/material": "^5.15.10",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.22.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },

View File

@ -0,0 +1,17 @@
[
{
"tip" : "In three words I can sum up everything I've learned about life: it goes on.",
"source" : "Robert Frost",
"description" : ""
},
{
"tip" : "Let me be clear. You are entitled to nothing",
"source" : "Frank Underwood",
"description" : ""
},
{
"tip" : "Don't find fault, find a remedy; anybody can complain.",
"source" : "Henry Ford",
"description" : ""
}
]

View File

@ -0,0 +1,27 @@
[
{
"tip" : "Don't be interesting. Be interested.",
"source" : "",
"description" : ""
},
{
"tip" : "Learn to say 'No'",
"source" : "",
"description" : ""
},
{
"tip" : "Worrying about things is like sitting in a rocking chair. It gives you something to do for a while but it won't get you any where.",
"source" : "",
"description" : ""
},
{
"tip" : "The single raindrop never feels responsible for the flood.",
"source" : "",
"description" : ""
},
{
"tip" : "Society progresses when old men plant trees whose shade they know they'll never sit under.",
"source" : "",
"description" : ""
}
]

View File

@ -0,0 +1,13 @@
[
{
"source" : "Her (2013)",
"reference": "https://www.imdb.com/title/tt1798709/",
"quotes" : [
"But the heart's not like a box that gets filled up. It expands in size the more you love.",
"You helped make me who I am. There will be a piece of you in me always.",
"That's the difficult part. Growing without growing apart. Or changing without it scaring the other person.",
"Sometimes I think I have felt everything I'm ever gonna feel. And from here on out, I'm not gonna feel anything new. Just lesser versions of what I've already felt.",
"The past is just a story we tell ourselves."
]
}
]

View File

@ -1,31 +1,33 @@
import './App.css'; import './App.css';
import React from 'react'; import React from 'react';
import {BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// Import Components
import Navbar from './components/Navbar'; import Navbar from './components/Navbar';
import WelcomeText from './components/Welcome';
import AboutMe from './components/AboutMe';
import ProjectList from './components/ProjectList';
import ProjectsText from './components/ProjectsText';
import OnlinePresence from './components/OnlinePresence';
import CompetenciesSkills from './components/Skills';
import Footer from './components/Footer'; import Footer from './components/Footer';
import FootPadding from './components/FootPadding'; import FootPadding from './components/FootPadding';
// Import Pages
import LandingPage from './pages/LandingPage';
import QuotesPage from './pages/QuotesPage';
import LifeTipsPage from './pages/LifeTipsPage';
function App() { function App() {
return ( return (
<Router>
<div className="App"> <div className="App">
<Navbar /> <Navbar />
<main> <main>
<WelcomeText /> <Routes>
<AboutMe /> <Route path="/" element={<LandingPage />} />
<ProjectsText /> <Route path="/quotes/" element={<QuotesPage />} />
<ProjectList /> <Route path="/lifetips/" element={<LifeTipsPage />} />
<CompetenciesSkills /> </Routes>
<OnlinePresence />
</main> </main>
<Footer /> <Footer />
<FootPadding /> <FootPadding />
</div> </div>
</Router>
); );
} }

View File

@ -3,7 +3,7 @@ import { Typography } from '@mui/material';
function FootPadding() { function FootPadding() {
return ( return (
<footer style={{ textAlign: 'center', marginTop: '20px'}}> <footer style={{ textAlign: 'center', marginTop: '20px', paddingTop: '1000px' }}>
<Typography variant="body2"> <Typography variant="body2">
The padding was on purpose :) The padding was on purpose :)
<br />It's <b>very</b> nice to be able to scroll past the bottom of the page :) <br />It's <b>very</b> nice to be able to scroll past the bottom of the page :)

View File

@ -4,8 +4,8 @@ import { mainDivStyle } from '../style';
function Footer() { function Footer() {
return ( return (
<footer style={{ mainDivStyle, paddingBottom: '1000px' }}> <footer style={{ mainDivStyle, paddingTop: '50px'}}>
<Typography variant="body2">© 2023 by Me. All (some?) rights reserved, I think?</Typography> <Typography variant="body2">© 2024 by Me. All (some?) rights reserved, I think?</Typography>
<Typography variant="body2"> <Typography variant="body2">
If you don't know me, you can try reaching out via Discord (zebra.jr) or <a href="mailto:contact@carlossousa.tech">contact@carlossousa.tech</a> If you don't know me, you can try reaching out via Discord (zebra.jr) or <a href="mailto:contact@carlossousa.tech">contact@carlossousa.tech</a>
</Typography> </Typography>

View File

@ -0,0 +1,66 @@
import React, { useEffect, useState } from 'react';
const lifeTipContainerStyle = {
display: 'flex',
flexWrap: 'wrap',
gap: '10px',
alignItems: 'center',
justifyContent: 'center'
}
const lifeTipSpanStyle = {
flex: '1 0 19%',
textAlign: 'center',
padding: '25px 50px 25px 50px',
borderRadius: '25px',
background: '#FFBD33',
maxWidth: '450px'
}
const backgroundDivColors = [
"#FFBD33",
"#3375FF",
"#33FFBD"
]
const selectRandomBackgroundColor = () => {
let backgroundColorSelection = Math.floor(Math.random() * backgroundDivColors.length);
let selectedBackgroundColor = backgroundDivColors[backgroundColorSelection];
let combinedSytle = {
...lifeTipSpanStyle,
backgroundColor: selectedBackgroundColor
};
return combinedSytle;
}
const useFetchData = (url) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData);
}, [url]);
return data;
};
function LifeTipsComponent({ filePath }) {
const lifeTipsData = useFetchData(filePath);
return (
<div style={lifeTipContainerStyle}>
{lifeTipsData && lifeTipsData.map((lifeTipElement, index) => (
// <span className='quotes lifetips' style={ lifeTipSpanStyle }>
<span className='quotes lifetips' style={ selectRandomBackgroundColor() }>
{lifeTipElement.tip}
{lifeTipElement.source && ` - ${lifeTipElement.source}`}
</span>
))}
</div>
);
}
export default LifeTipsComponent;

View File

@ -0,0 +1,21 @@
import { Button, ButtonGroup } from '@mui/material';
function TipsTypeSelector({ onComponentLoaded }) {
const loadContent = (filePath) => {
onComponentLoaded(filePath); // Use the callback to pass the file path
};
return (
<div className="lifeTipsTypeSelector" style={{ paddingTop: '10px', paddingBottom: '10px' }}>
<div>
<ButtonGroup variant='contained'>
<Button onClick={() => loadContent('/lifetips/quotes.json')}>Quotes</Button>
<Button onClick={() => loadContent('/lifetips/tips.json')}>Tips</Button>
</ButtonGroup>
</div>
</div>
);
}
export default TipsTypeSelector;

View File

@ -3,7 +3,7 @@ import { AppBar, Toolbar, Typography, Link as MuiLink, Box } from '@mui/material
const bannerImage = { const bannerImage = {
name: 'website_logo_192.png', name: '/website_logo_192.png',
alt: 'Carlos Sousa Logo' alt: 'Carlos Sousa Logo'
} }
@ -14,23 +14,19 @@ function Navbar() {
<Toolbar> <Toolbar>
<Box display="flex" justifyContent="space-between" width="100%"> <Box display="flex" justifyContent="space-between" width="100%">
<Typography variant="h6" color="inherit" noWrap> <Typography variant="h6" color="inherit" noWrap>
<img src={ bannerImage.name } width="30px" alt={ bannerImage.alt }/> Carlos Sousa <MuiLink href="/" color="inherit" variant="button" sx={{ margin: 1 }}>
<img src={ bannerImage.name } width="30px" alt={ bannerImage.alt }/> Carlos Sousa
</MuiLink>
</Typography> </Typography>
<Box> <Box>
<MuiLink href="#aboutme" color="inherit" variant="button" sx={{ margin: 1 }}> <MuiLink href="/" color="inherit" variant="button" sx={{ margin: 1 }}>
About Me Landing Page
</MuiLink> </MuiLink>
<MuiLink href="#projects" color="inherit" variant="button" sx={{ margin: 1 }}> <MuiLink href="/quotes" color="inherit" variant="button" sx={{ margin: 1 }}>
Projects Arts Quotes
</MuiLink> </MuiLink>
<MuiLink href="#oss" color="inherit" variant="button" sx={{ margin: 1 }}> <MuiLink href="/lifetips" color="inherit" variant="button" sx={{ margin: 1 }}>
OSS Life Tips
</MuiLink>
<MuiLink href="#skills" color="inherit" variant="button" sx={{ margin: 1 }}>
Skills
</MuiLink>
<MuiLink href="#onlinePresence" color="inherit" variant="button" sx={{ margin: 1 }}>
Online Presence
</MuiLink> </MuiLink>
</Box> </Box>
</Box> </Box>

View File

@ -0,0 +1,25 @@
import React from 'react';
import WelcomeText from '../components/Welcome';
import AboutMe from '../components/AboutMe';
import ProjectList from '../components/ProjectList';
import ProjectsText from '../components/ProjectsText';
import OnlinePresence from '../components/OnlinePresence';
import CompetenciesSkills from '../components/Skills';
function LandingPage() {
return(
<div>
<WelcomeText />
<AboutMe />
<ProjectsText />
<ProjectList />
<CompetenciesSkills />
<OnlinePresence />
</div>
);
}
export default LandingPage;

View File

@ -0,0 +1,20 @@
import React, { useState } from 'react';
import TipsTypeSelector from '../components/LifeTipsHeader';
import LifeTipsComponent from '../components/LifeTips'
//const defaultComponentLoading = 'LifeTipsQuotes';
function LifeTipsPage() {
const [filePath, setFilePath] = useState(null);
return (
<div>
<TipsTypeSelector onComponentLoaded={setFilePath} />
{filePath && <LifeTipsComponent filePath={filePath} />}
</div>
);
}
export default LifeTipsPage;

View File

@ -0,0 +1,88 @@
import React, { useEffect, useState, useCallback } from 'react';
const quoteDivContainerStyle = {
display: 'block',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
marginTop: '50px',
marginBottom: '20px',
width: '100%',
paddingLeft: '50px',
paddingRight: '50px',
boxSizing: 'border-box',
wordWrap: 'break-word'
}
const pStyles = [
{
fontSize: '16px',
color: 'navy',
display: 'inline'
},
{
fontSize: '16px',
color: 'crimson',
display: 'inline'
},
{
fontSize: '16px',
color: 'gold',
display: 'inline'
},
{
fontSize: '16px',
color: 'dodgerBlue',
display: 'inline'
},
{
fontSize: '16px',
color: 'olive',
display: 'inline'
}
]
const useFetchData = (url) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData);
}, [url]);
return data;
};
const selectRandomPStyle = () => {
return Math.floor(Math.random() * pStyles.length);
}
function QuotesPage() {
const quotesData = useFetchData('/quotes/movies.json');
return (
<div>
{quotesData && quotesData.map((quote, index) => (
<div className='quotes movies' style={quoteDivContainerStyle}>
{quote && quote.quotes.map((individualQuote, index) => (
<p
key={index}
style={pStyles[selectRandomPStyle(2)]}>
{individualQuote} <b>|</b>&nbsp;
</p>
))}
<h3>
<a href={quote.reference} target='_blank'>
{quote.source}
</a>
</h3>
</div>
))}
</div>
);
}
export default QuotesPage;