Frontend Quiz App

A multi-page app that quizzes the user on frontend-related questions, with a progress bar and a score counter. A challenge by Frontend Mentor.

vue
tailwindcss
astro
sass

Previews

Preview image #1Preview image of project Frontend Quiz App
Preview image #2Preview image of project Frontend Quiz App
Preview image #3Preview image of project Frontend Quiz App
Preview image #4Preview image of project Frontend Quiz App
Preview image #5Preview image of project Frontend Quiz App
Preview image #6Preview image of project Frontend Quiz App

The challenge

Users should be able to:

Built with

Logs

Day 1

There’s a bonus challenge this time, which is the dark mode - light mode switcher. This time I’d like to try making a multi-page application, instead of reusing the same page and swapping out elements depending on state. I also want to practice this kinda of app with Vue. The first roadblock that has been struggling me is that the “theme state” does not persist through pages. I’ve tried Vue’s reactivity stores, and Astro’s nanostores, none of which worked, or I’m just not using them correctly.

Taking a break, and learning that there’s a package called @nanostores/persistent that can persist the store’s state to local storage. Tried implementing and it just doesn’t seem to work at all and I’m losing my mind. What is wrong with me? All I want is a server-rendered page, but the HTML tag has the dark class when the theme is set to dark, that’s it. It sometimes works but there’s a flash of the wrong theme before it gets the right one.

A long time has passed again, and it seems like @nanostores/persistent really does persist through pages, but there’s some problem with the way I’m using it that it causes not just a flash of the wrong theme, but a hydration mismatch between server and client also. Turns out, the solution is just using an inline-script straight in Astro, that forces the theme to be set correctly before the DOM is even started to be rendered. Below is a snippet of code that should be put in a <head> component to make it work. That’s it. That’s really it. Of course, if the user is malicious and tries changing the theme in local storage, make sure to check that before doing anything. I’m thanking everything that it’s over!!

<!-- Courtesy of Kevin Zuniga Cuellar -->
<script is:inline>
  const theme = (() => {
    if (localStorage.getItem("theme")) return localStorage.getItem("theme");
    return window.matchMedia("(prefers-color-scheme: dark)").matches
      ? "dark"
      : "light";
  })();

  document.documentElement.classList.toggle("dark", theme === "dark");
  window.localStorage.setItem("theme", theme);
</script>

Next roadblock is the fact that even though nanostores does persist some data through pages, there’s a hydration mismatch on any component that uses nanostores. Why is there a problem with literally every single thing I want to use? I tried Vue’s pinia, but that does not persist through page changes. The issue with @nanostores/persistance is that hydration mismatch is technically a bug, but it’s not on the library. It’s the way the server’s data does not match up with the client’s localStorage, so there is always a mismatch. I’m checking more to see if Pinia can be used, or is there a way to let nanostores hydrate correctly,… tomorrow.

Day 2

Apparently, you can just watch the pinia state, and make it so it auto saves to local storage whenever pinia is “disabled” to preserve the state. Sadly, this doesn’t work, as Astro is a lot different from something tailored for Vue apps like Nuxt, and when Astro tries to prerender, it hits window and crashes instead.

The idea seems a bit inefficient. But when using @nanostores/persistent, we need to delay the localStorage check until the client-side is ready. We need to refrain from reading the store variable until it is mounted.

Vue:

// SCript-setup Vue
const store = useStore($store);
const ready = ref(false);
onMounted(() => (ready = true));

// In template, only access store if ready is true

React:

const [ready, setReady] = useState(false);
const store = useStore($store);

useEffect(() => {
  setReady(true);
}, []);

// In template, only access store if ready is true

After this, I worked on making the page layout for a question, given a list of questions. If I can do this for one, the project’s done. The last part of using quiz screens and handling scores wasn’t too difficult, and was very smooth sailing once props are properly (no pun intended) passed down.

What I learned

I’d like to use multi page and routing in Astro more.

Useful resources

Author