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
|
||||
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;"]
|
||||
|
|
|
|||
|
|
@ -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
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;
|
||||
#}
|
||||
}
|
||||
5164
frontend/package-lock.json
generated
5164
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
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 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 :)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
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 = {
|
||||
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>
|
||||
|
|
|
|||
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