Responsive time tracking dashboard using CSS Grid, Tailwind & Astro
Design comparison
Solution retrospective
Frontend Mentor - Time tracking dashboard solution
This is a solution to the Time tracking dashboard challenge on Frontend Mentor.
Table of contents
- Frontend Mentor - Time tracking dashboard solution
- Table of contents
- Overview
- The challenge
- Screenshot
- Links
- My process
- Built with
- What I learned
- Learning Astro
- Layout & Pages
- Installing astro packages
- Using Js Frameworks & Libraries
- Sharing state between islands
- Making the store
- Using values from the store
- Continued development
- Useful resources
- Perfomance
- Responsiveness
- Lighthouse results
- Author
Overview
The challenge
Users should be able to:
- View the optimal layout for the site depending on their device's screen size
- See hover states for all interactive elements on the page
- Switch between viewing Daily, Weekly, and Monthly stats
Screenshot
Links
My process
Built with
- Semantic HTML5 markup
- Flexbox
- CSS Grid
- Mobile-first workflow
- Tailwind - For styles
- React - JS library
- Astro - Static Site Generator
- Vercel - Hosting website
What I learned
Learning Astro
Astro is used to build static sites that support multiple javascript frameworks & libraries while shipping little javascript. From my understanding, Astro islands are similar to react components, but this time, islands are converted to static HTML. you can find more information on Astro's islands architecture here
Unlike other Frameworks & libraries I've worked with in the past, Astro generates an MPA (Multi-Page Application) instead of an SPA (Single-Page Application). More details on this can be found here.
You can learn more here
Layout & Pages
The concept of a layout component & different pages in the pages directory isn't a new thing to me. The only thing I had to learn when making a layout component is that you need to insert a <slot />
in the layout, this is where the page would put itself in.
My layout component & my homepage
The code below is from the astro docs
--- import BaseHead from '../components/BaseHead.astro'; import Footer from '../components/Footer.astro'; const { title } = Astro.props; --- <html lang='en'> <head> <meta charset='utf-8' /> <meta name='viewport' content='width=device-width, initial-scale=1' /> <BaseHead title={title} /> </head> <body> <nav> <a href='#'>Home</a> <a href='#'>Posts</a> <a href='#'>Contact</a> </nav> <h1>{title}</h1> <article> <slot /> </article> <Footer /> </body> </html>
--- import MySiteLayout from '../layouts/MySiteLayout.astro'; --- <MySiteLayout title='Home Page'> <p>My page content, wrapped in a layout!</p> </MySiteLayout>
Installing astro packages
One of the most enjoyable things about Astro is working with the CLI. This is a simple project, but when installing packages from Astro, I didn't have to touch any config files. This seems small, but it automatically added tailwind, image & react to the astro config.
For example, when I wanted to use Tailwind, I simply ran the following command in the terminal, npx astro add tailwind
. I didn't have to manually install tailwind, configure astro etc... 😃
Using Js Frameworks & Libraries
In order to use react, I ran the following command:
npx astro add react
I placed my react components inside the components directory & used the .tsx
file extension.
Importing and using the components was easy, this is how it looks like:
--- import Buttons from '../components/Buttons'; import DashboardCard from '../components/DashboardCard'; --- <Layout title='Time Tracking Dashboard'> <main> <Buttons client:load /> <DashboardCard title={title} illustration={illustration} tframes={timeframes} bgColor={bgColor} client:idle /> </main> </Layout>
Sharing state between islands
Making the store
This was the trickiest part for me, but before I explain 'sharing state between islands', we first need to understand why we had to share state between them in the first place.
Making a component, which has its own state is easy, but when the user clicks Daily
, Weekly
or Monthly
, the content of each Dashboard has to update and display the correct information. In order to achieve this, I made use of Nano Store.
Using Nano Store reminded me of Redux and React's useContext hook.
The store can be found here, let's go over it line by line:
import { atom } from 'nanostores'; type timeFrameTypes = 'Daily' | 'Weekly' | 'Monthly'; export const timeFrames: timeFrameTypes[] = ['Daily', 'Weekly', 'Monthly']; export const activeTimeFrame = atom<timeFrameTypes>('Weekly');
import { atom } from 'nanostores';
- I import atom
from nanostores. The reason I used atom is because we are going to store a string, nothing complicated.
type timeFrameTypes = 'Daily' | 'Weekly' | 'Monthly';
- This is typescript, and I wanted to leverage it by restricting the type of value the string would contain, Similar to using an enum. because of this, we know that there can only be 3 different values, & when we set the value, we are only restricted to those 3 values. This is why I love Typescript.
export const timeFrames: timeFrameTypes[] = ['Daily', 'Weekly', 'Monthly'];
- I wanted to store the 3 different values in an array, this is important for buttons component. I used this array to display the 3 different time frames & assign event listeners to them.
export const activeTimeFrame = atom<timeFrameTypes>('Weekly');
- Export the store. We also assign the type that the atom store holds by using Generics, (the angle brackets < & >)
Using values from the store
import { useStore } from '@nanostores/react';
import { timeFrames, activeTimeFrame } from '../timeFramesStore';
export default function Buttons() {
return (
<div
id='timeframes'
className='flex w-full items-start justify-between gap-4 p-8 brk:flex-col'
>
{timeFrames.map((timeFrame, key) => {
const $active = useStore(activeTimeFrame);
const isActive = timeFrame === $active;
const handleClick = () => activeTimeFrame.set(timeFrame);
return (
<button
className={`${isActive && 'text-white'}`}
key={key}
onClick={handleClick}
>
{timeFrame}
</button>
);
})}
</div>
);
}
import { timeFrames, activeTimeFrame } from '../timeFramesStore';
- activeTimeFrame stores the current value/state. in order to get its value, we place the store inside the useStore method. I remember while reading the docs, it said that you have to prefix the variable name with $
. This is how we end up with const $active = useStore(activeTimeFrame);
.
in order to update the state, we use the .set(/* new state */)
method.
Continued development
It seems like I will have to learn client:directives & other directives
<Buttons client:load />
Useful resources
- Astro Docs - This helped me for XYZ reason. I really liked this pattern and will use it going forward.
- Tailwind Docs - This is an amazing article which helped me finally understand XYZ. I'd recommend it to anyone still learning this concept.
Perfomance
Responsiveness
I initially wanted to use flexbox, but eventually realised that css grid would fit this well. I wanted it to look good on desktop, tablet & mobile devices.
Lighthouse results
Author
- GitHub - @ThaBeanBoy
- Frontend Mentor - @ThaBeanBoy
- LinkedIn - Tineyi G Chipoyera
Community feedback
Please log in to post a comment
Log in with GitHubJoin our Discord community
Join thousands of Frontend Mentor community members taking the challenges, sharing resources, helping each other, and chatting about all things front-end!
Join our Discord