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
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
COPY custom_nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
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.
Install them by running ``npm install``
Install them by running ``npm install`` or ``docker-compose up dev npm install``
##### Development
@ -18,4 +18,4 @@ Install them by running ``npm install``
##### 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",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@mui/material": "^5.15.0",
"@mui/material": "^5.15.10",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.1",
"react-scripts": "5.0.1",
"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 React from 'react';
import {BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// Import Components
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 FootPadding from './components/FootPadding';
// Import Pages
import LandingPage from './pages/LandingPage';
import QuotesPage from './pages/QuotesPage';
import LifeTipsPage from './pages/LifeTipsPage';
function App() {
return (
<Router>
<div className="App">
<Navbar />
<main>
<WelcomeText />
<AboutMe />
<ProjectsText />
<ProjectList />
<CompetenciesSkills />
<OnlinePresence />
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/quotes/" element={<QuotesPage />} />
<Route path="/lifetips/" element={<LifeTipsPage />} />
</Routes>
</main>
<Footer />
<FootPadding />
</div>
</Router>
);
}

View File

@ -3,7 +3,7 @@ import { Typography } from '@mui/material';
function FootPadding() {
return (
<footer style={{ textAlign: 'center', marginTop: '20px'}}>
<footer style={{ textAlign: 'center', marginTop: '20px', paddingTop: '1000px' }}>
<Typography variant="body2">
The padding was on purpose :)
<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() {
return (
<footer style={{ mainDivStyle, paddingBottom: '1000px' }}>
<Typography variant="body2">© 2023 by Me. All (some?) rights reserved, I think?</Typography>
<footer style={{ mainDivStyle, paddingTop: '50px'}}>
<Typography variant="body2">© 2024 by Me. All (some?) rights reserved, I think?</Typography>
<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>
</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 = {
name: 'website_logo_192.png',
name: '/website_logo_192.png',
alt: 'Carlos Sousa Logo'
}
@ -14,23 +14,19 @@ function Navbar() {
<Toolbar>
<Box display="flex" justifyContent="space-between" width="100%">
<Typography variant="h6" color="inherit" noWrap>
<MuiLink href="/" color="inherit" variant="button" sx={{ margin: 1 }}>
<img src={ bannerImage.name } width="30px" alt={ bannerImage.alt }/> Carlos Sousa
</MuiLink>
</Typography>
<Box>
<MuiLink href="#aboutme" color="inherit" variant="button" sx={{ margin: 1 }}>
About Me
<MuiLink href="/" color="inherit" variant="button" sx={{ margin: 1 }}>
Landing Page
</MuiLink>
<MuiLink href="#projects" color="inherit" variant="button" sx={{ margin: 1 }}>
Projects
<MuiLink href="/quotes" color="inherit" variant="button" sx={{ margin: 1 }}>
Arts Quotes
</MuiLink>
<MuiLink href="#oss" color="inherit" variant="button" sx={{ margin: 1 }}>
OSS
</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 href="/lifetips" color="inherit" variant="button" sx={{ margin: 1 }}>
Life Tips
</MuiLink>
</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;