Hi dev.to! So, I built my first portfolio and thought about documenting the process, but before jumping in, a disclaimer:
- I believe in choosing the right tool for the job, using React for a portfolio might seem like an overkill but I decided on it mostly because I want to get better at it.
- For that same reason I chose AWS to deploy it instead of Github or Netlifly. AWS is a beast and I want to learn as much as I can.
Phew! Okay, so let's get to it. Oh, here's the portfolio https://georginagrey.com
The interesting bits
When coding the app I learned a few new tricks that I believe are worth sharing.
React's Context API
My portfolio is multi-language, to achieve that I used React's Context, the point is to have a sort of "global" state that can be accessed by other components that could be deeply nested, thus avoiding passing props many levels down the chain. This is how it helped me implement the language switcher:
Provider
On LanguageContext.js is where the text translations live and the Context is created and exported.
//LanguageContext.js
export const languages = {
en: {...
},
es: {...
}
}
export const LanguageContext = React.createContext({
langText: languages.en,
toggleLanguage: () => { }
});
The App component is the most outer component, where the toggleLanguage function is actually implemented. LanguageContext.Provider component wraps every other children that needs to consume the "global" state.
Watch out when sharing functions that access state, such functions need be explicitly binded to state, by either using the super(props) keyword or the bind(this) method, otherwise components nested deep down executing this function will throw an error.
// App.js
...
import { LanguageContext, languages } from './LanguageContext';
...
constructor(props) {
super(props);
this.state = {
language: 'en',
langText: languages.en,
toggleLanguage: this.toggleLanguage
}
}
toggleLanguage = () => {...
}
render() {
return (
<div id="app" className={app}>
<LanguageContext.Provider value={this.state}>
<Menu />
<Main />
<Footer />
</LanguageContext.Provider>
</div>
)
}
Consumer
The LanguagePicker component is nested about 3 levels deep, thanks to the LanguageContext.Consumer component, this is how state can be accessed.
// LanguagePicker.js
const LanguagePicker = () => (
<LanguageContext.Consumer>
{({ toggleLanguage, language }) => (
<div className={main} onClick={() => toggleLanguage()}>
...
<span>{language}</span>
</div>
)}
</LanguageContext.Consumer>
)
This could have been achieved with Redux too, but I didn't need it for anything else. The Context API shouldn't be used lightly though, so keep that in mind.
Intersection Observer API
It's very useful if a behavior needs to be triggered when some element is visible inside the viewport. I used it to trigger some animations, but the most meaningful use case has to do with improving the site's load time, first contentful paint and lower bandwidth usage.
The <img>
tag renders right away whatever's in its source, even if the component hasn't mounted yet, so the user will download images that might never even get to see. A slow down in the first contentful paint is also expected.
The trick here is to use a placeholder, taking the original image and scaling it down to a ~10x10 pixel ratio. Is only when the IntersectionObserver kicks in, that we fetch the original image. Here's a snippet of the implementation:
// Proyects.js
componentDidMount() {
this.observe();
}
observe() {
var options = {
threshold: [0.1]
}
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
const image = entry.target;
const src = image.dataset.src;
this.fetchImage(src).then(() => {
image.src = src;
});
}
});
}, options);
const images = document.querySelectorAll('img');
images.forEach(i => observer.observe(i));
}
Pro tip: instead of scaling down the images myself I used Cloudinary, you can transform images on the fly when the c_scale is provided within the url:
https://res.cloudinary.com/georginagrey/image/upload/c_scale,h_12,w_12/v1532709273/portfolio/portfolio.jpg
, if you take that bit off, you get the original image.
Heads up: The IntersectionObserver it's is not entirely supported across all browsers, so you might want to use a pollyfill or a fallback.
The UI
This is my weakest spot, it wasn't until recently that I kinda got my head around CSS3, or that's what I thought until I started to fall in every "gotcha" possible when styling components using just plain CSS. I had to re-write the whole thing a couple of times, until I decided to use emotion, even though css-in-js causes some outrage, I decided to give it a go and I loved it, I no longer have to worry about overriding rules while working on different components.
The layout is quite simple, I went with a mobile-first approach, and got away with using flexbox only.
The Stack
In a nutshell, this is a React static website hosted on a S3 bucket served by CloudFront and Route53.
- Create-react-app
- Emotion (css-in-js)
- Firebase (for the contact form)
- AWS S3 bucket (static files hosting)
- AWS Cloudfront (CDN, SSL Certificate, text compression)
- AWS Route53 (DNS routing)
How did I end up with that?!
After writing the main React components and styling most of them, I stumbled with Google's Lighthouse auditing tool, I downloaded the Chrome extension and generated a report (locally) and within seconds I got the results and a list of opportunities for optimization, for example, by enabling "text compression" in the server, the app should load about 3 seconds faster in my case.
I didn't know what that meant, so after googling for a bit I came across Cloudfront, to top it off you can request a SSL certificate for free.
Setting everything up is not as difficult as it may sound, here is a very handy guide. What will you get? Hosting, increased performance, faster delivery and secure HTTPs.
Is it free?
S3 and CloudFront are not per se free, is pay-as-you-go service, so for a low traffic website we would be talking about paying cents per month if anything at all, after the 1 year free tier expires.
Route53 is the DNS provider, there's a fixed price of $0.51/month per hosted zone, so we're talking only about $6/year. In this I case I already had a domain registered in Godaddy, to make it work I just grabbed the DNS names Route53 provided me with and saved them inside the Manage Name Servers form in Godaddy.
Caching and invalidating CloudFront
As is expected, every time a request comes into CloudFront it will serve whatever is cached instead of going every time to your S3 bucket looking for the files, how long the content stays cached, depends on the default TTL timeframe configured, read more about it here.
Since I'm still working on the site, I set the default TTL to
3600 seconds (1 hour), I also added a header cache-control:max-age=0
, to the meta-data of the origin S3 bucket. But soon I'll be reverting that and use Invalidation instead, it force flushes the cache without needing to wait for it to expire. Doing it this way is actually cheaper too.
Edit:
I got my monthly statement! So, here's an example of AWS Princing with this setup:
CloudFront served +2300 requests for America/Europe tier. Plus DNS routing and storage for $0.62 total. It won't get more expensive than that since a surge in traffic is not expected.
That's it! ... I think 🤔
This isn't my first time dealing with AWS, but it's my first coding the front-end of a website, so any comments are greatly appreciated.
Thank you for stopping by 👋
Top comments (41)
Hey Georgina, your portfolio looks beautiful. I love the layout.
In the "Let's talk" section, you can try increasing the height of your input fields and the font-size of the text since there are no labels. A good rule is always to add labels though. It's great nevertheless.👍
You're right, small details I tend to miss sometimes. Thanks for bringing it up!
You are welcome
Hola! Qué bueno encontrar una tica por aquí! Me pasas los recursos que le mencionas a Chad de trabajo remoto (el email es mi usuario aquí + "cr" al final, at gmail)? Pequeño typo: "trabajando en bases de daños". Pura vida!
Hola! Claro voy a buscarlos. Jaja a pesar de haber leído eso mil veces no sé cómo se me escapó, gracias. Al rato fue un mensaje del subconsciente 😂 saludos!
😆
On an unrelated note...I’d love to learn how you are monetizing your coding/development and living overseas. I’ve been considering it for some time, and I’m always interested in tapping someone’s brain on the subject. Any input is appreciated!
Hi Chad. I'm actually from Costa Rica. But I do remote work nevertheless, so it doesn't matter where I do it from.
There's many developers that are 'digital nomads'. I think a have a few resources I could share, in case you are not deep into that subject :)
Hey!
Congratulations, nice to see people from my same country doing nice things!, it looks like you are going the right way and using the right tools (although I would have chosen Netlify).
Now that you are diving into front end development I would recommend you to take a look at css grid (cssgridgarden.com/#es).
Any react question you have, you are more than welcome to ping me at GitHub or Twitter.
Pura vida!
Thank you! That looks cool, I will start gardening soon enough 😁🌱
Saludos!
Wonderful website however on lower resolution than 1080p the text on the second menu item bugs out on my second monitor.
Thank you! I'll check it out, thanks for the heads up.
Hey congratulation on your first portofolio. It looks beautifull.
Hi Fajar! Thanks for checking it out :)
Very cool. Thanks for sharing your knowledge and experience, especially the AWA bits!
Thank you! Glad you find it useful.
Good work! 👏
One thing you may want to add is a IntersectionObserver polyfill if you plan to support non Chrome/Firefox browsers 😄
I knew I was forgetting something! Thanks for the reminder.
Awesome job.
Thank you for checking it out! :)
Looks nice 👍
However I'm on mobile(Chrome on an iPhone) and clicking on "experience" completely breaks the site to the point of having to refresh it.
Oh! That ain't good. I'll see if I can reproduce it, thanks a lot!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.