Skip to content

Bootstrapping a simple Vite app

Last updated:

flash-cards

Introduction

Flash cards are a great learning tool for all sorts of things. They can be used to learn vocabulary, practice math, or memorize historic event or science facts. However, creating flashcards by hand can be time-consuming and prone to errors.

In this tutorial, we will create a simple Vite app that generates flashcards for Math operations for the numbers 1-20 that can be printed (but it will be easy to extend for any other subject given we have the questions/answers data). This app will allow you to create flashcards quickly and easily, without having to spend hours writing them out by hand.

The source code is available on https://github.com/nephridium/learning-cards.

A live demo of the app in action can be found at https://nephridium.github.io/learning-cards.

Bootstrapping a Vite App

To get started, we need to bootstrap a new Vite project which we will call “learning-cards”. Vite is a build tool that provides a fast development server and efficient production builds. It also supports modern JavaScript features like ES modules and TypeScript and has a host of plugins to work with other frameworks.

Terminal window
npx create-vite@latest learning-cards --template react

Once the setup is done we can fire up the dev server using

Terminal window
npm run dev

which will start a web server that hot-reloads on any changes.

We will create all our app files under the src folder.

Deployment

For printing our cards we can just run this project locally, there is no need for build and deployment. But just in case we do find a reason for deployment we run:

Terminal window
npm run build

This will minify/optimize our project and place the deployable files into the /dist folder. To make sure the build process doesn’t cause any unexpected changes it is a good idea to check the built project buy running the preview server locally:

Terminal window
npm run preview

If everything looks fine we can push the /dist folder to our web server. Many web hosts offer more streamlined deployments that are simply triggered by commits to the deployment branch of the repo.

Defining the source data

A card will have two fields: a value field for what text to show and an optional image field that can be used for illustration or just to make the cards look nicer.

{
image?: string, // link to optional image
value: string // string value to show on card
}

Cards are organized in Decks which will contain a generator function returning all the cards needed. A “question” deck will return all questions and an “answer” deck all answers.

{
id: string, // unique identifier for the deck
title: string, // title of the deck
getCards: Function // returns array of cards
}

Deck generator functions

Adding the question and answer decks for additions is pretty straight forward, we create an addition for every number combination and their solution.

Note, once we print the pages the solutions are reversed horizontally since the page is “flipped”. Therefore we need to apply reverseRows() to the answers card array.

Decks.js
const Decks = (id) => [
{
id: 'a',
title: 'Addition',
getCards: () => {
let cards = []
for (let i = 0; i < 20; i++) {
for (let j = 0; j < 20; j++) {
const n = i + 1
const m = j + 1
const card = {
image: `images/img_${m % 10}.png`,
value:`${n} + ${m}`
}
cards.push(card)
}
}
return cards
}
},
{
id: 'aa',
title: 'Addition Solutions',
getCards: () => {
let cards = []
for (let i = 0; i < 20; i++) {
for (let j = 0; j < 20; j++) {
const n = i + 1
const m = j + 1
const card = {
image: `images/img_${m % 10}.png`,
value: n + m
}
cards.push(card)
}
}
return reverseRows(cards)
}
}
].find(deck => deck.id === id)
// This function is needed for the solutions pages to reverse each row so the print-outs on the obverse sides of each page match up correctly
function reverseRows (arr) {
const chunkSize = 4
let result = []
for (let i = 0; i < arr.length; i += chunkSize) {
result.push(...arr.slice(i, i + chunkSize).reverse())
}
return result
}
export default Decks

The data for the other math operations can be found in the source code.

Using the getCards() function for each deck gives us the flexibility to import any kind of data source and apply the appropriate transformations. Alternatively we could also prepare the data beforehand and then simply load it from a JSON file. E.g. the flashcard decks for practicing Japanese phrases could look like this:

jp.json
{
"questions": [
{
"question": "こんにちは",
"answer": "Hello"
},
{
"question": "ありがとうございます",
"answer": "Thank you"
},
{
"question": "ください",
"answer": "Please"
},
{
"question": "すみません",
"answer": "Excuse me"
}
]
}

with fairly simple retrieval functions for the data that we load into an items var at the top of the file:

{
id: 'questions',
getCards: () => items.map(x => ({value: x.question}))
},
{
id: 'answers',
getCards: () => reverseRows(items.map(x => ({value: x.answer})))
}

Add the app logic

We set main.js to load the app.

main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

In App.jsx we define a collection of GridPages that each hold a Page that contains a 4 x 4 grid of <div>s representing each flashcard. deckId references which deck will be loaded.

App.jsx
import Decks from './Decks.jsx'
const deckId = 'a' // addition (a & aa), subtraction (s & ss) , multiplication (m & mm), division (d & dd)
const Page = ({ cards }) => (
<div className="page">
{cards.map((item, index) => (
<div
key={index}
className="card"
>
<span className="card-image-container" >
<img className="card-image" src={item.image} />
</span>
<span className="card-text">{item.value}</span>
</div>
))}
</div>
)
const GridPages = () => {
const chunkSize = 16 // cards per page
const pages = []
const cards = Decks(deckId).getCards()
for (let i = 0; i < cards.length; i += chunkSize) {
pages.push(cards.slice(i, i + chunkSize))
}
return (
<div className="container">
{pages.map((cards, index) => (<Page key={index} cards={cards} />))}
</div>
)
}
export default GridPages

Add the CSS

After verifying that the data is showing up we can do the CSS part to make sure the cards are displayed nicely but also printed correctly as a 4x4 grid on an A4 page, resulting in the A8 sized flashcards we want.

We will use flex to fill the pages with rows by defining the card width to be a forth of the page width. Instead of margin we will use box-shadow for the cutting indicators between the cards so they don’t use additional space.

GridPages.css
.body { background-color: black; }
.container {
display: flex;
flex-direction: column;
align-items: center;
background: grey;
}
.page {
width: 297mm;
height: 210mm;
display: flex;
flex-wrap: wrap;
flex-direction: row;
justify-content: flex-start;
align-content: flex-start;
background: white;
padding-bottom: 0.1mm;
}
.card {
width: 74.25mm;
height: 52.4mm;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
position: relative;
box-shadow: 0px 0px 1px 0px rgba(0,0,0,0.5);
}
.card-image {
width: 64px;
height: 64px;
padding-top: 8px;
padding-right: 8px;
}
.card-text {
font-size: 46px;
color: grey;
}
@page {
margin: 0;
}

..and import the CSS to our App.jsx:

App.jsx
import './GridPages.css'
import Decks from './Decks.jsx'
// ...

Now we can simply print what we have. For a good haptic feel of the cards the paper weight should be at least 160 g/m² (make sure the printer supports it). First print all the questions, then turn the stack around and reverse the order of the pages so the first answer page will be printed on the first question page and so on. Also make sure to orient the pages correctly with the top row pointing towards the intake.

After printing we can use a paper cutter to cut the flashcards setting the measurements to A8 size (74 x 52 mm).

Potential improvements

  • Add a little button/drop-down (that will not be printed) showing which data sets are available.
  • Loading the data based on a GET input param instead of setting it with a variable.
  • Add the ability to interactively flip the cards so it works “online” too ;)