Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found
Not Found

Submitted

Responsive time tracking dashboard using CSS Grid, Tailwind & Astro

ThaBeanBoy• 230

@ThaBeanBoy

Desktop design screenshot for the Time tracking dashboard coding challenge

This is a solution for...

  • HTML
  • CSS
  • JS
2junior
View challenge

Design comparison


SolutionDesign

Solution retrospective


This is a solution to the Time tracking dashboard challenge on Frontend Mentor.

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

  • Semantic HTML5 markup
  • Flexbox
  • CSS Grid
  • Mobile-first workflow
  • Tailwind - For styles
  • React - JS library
  • Astro - Static Site Generator
  • Vercel - Hosting website

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

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>

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... 😃

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>

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 < & >)

Buttons

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.

It seems like I will have to learn client:directives & other directives

<Buttons client:load />
  • 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.

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.

Community feedback

Please log in to post a comment

Log in with GitHub
Discord logo

Join 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