mirror of
https://github.com/zebrajr/website.git
synced 2025-12-06 00:19:54 +01:00
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:
parent
2a20f1846c
commit
dcaa9eaf2a
|
|
@ -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;"]
|
||||||
|
|
|
||||||
|
|
@ -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
18
custom_nginx.conf
Normal 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;
|
||||||
|
#}
|
||||||
|
}
|
||||||
5176
frontend/package-lock.json
generated
5176
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
17
frontend/public/lifetips/quotes.json
Normal file
17
frontend/public/lifetips/quotes.json
Normal 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" : ""
|
||||||
|
}
|
||||||
|
]
|
||||||
27
frontend/public/lifetips/tips.json
Normal file
27
frontend/public/lifetips/tips.json
Normal 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" : ""
|
||||||
|
}
|
||||||
|
]
|
||||||
13
frontend/public/quotes/movies.json
Normal file
13
frontend/public/quotes/movies.json
Normal 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."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 :)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
66
frontend/src/components/LifeTips.js
Normal file
66
frontend/src/components/LifeTips.js
Normal 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;
|
||||||
21
frontend/src/components/LifeTipsHeader.js
Normal file
21
frontend/src/components/LifeTipsHeader.js
Normal 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;
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
25
frontend/src/pages/LandingPage.js
Normal file
25
frontend/src/pages/LandingPage.js
Normal 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;
|
||||||
20
frontend/src/pages/LifeTipsPage.js
Normal file
20
frontend/src/pages/LifeTipsPage.js
Normal 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;
|
||||||
88
frontend/src/pages/QuotesPage.js
Normal file
88
frontend/src/pages/QuotesPage.js
Normal 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>
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<h3>
|
||||||
|
<a href={quote.reference} target='_blank'>
|
||||||
|
{quote.source}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QuotesPage;
|
||||||
Loading…
Reference in New Issue
Block a user