Bootstrapping a simple Vite app
Last updated:
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.
npx create-vite@latest learning-cards --template react
Once the setup is done we can fire up the dev server using
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:
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:
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 Deck
s 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.
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 correctlyfunction 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:
{ "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.
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.
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.
.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
:
import './GridPages.css'import Decks from './Decks.jsx'
// ...
Print and cut
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 ;)