Hey there, lovely people! ๐
It's summer in the United States, which means it's the season for weekend getaways, trips to the beach, or maybe just a day off lounging at home pretending you're in Bali. Whether you're a hardcore planner or a last-minute packer, we've all been through the struggle of forgetting something essential on a trip, haven't we?
To save us all from the โOh no, I forgot my _____!โ moment, I built a packing list app using React and TailwindCSS. You can check it out here and see a live demo here. This post will walk you through how I built it.
Getting Started with the Project
I used Vite with the React template to scaffold the project. This was as simple as running the following command in my terminal:
npm create vite@latest my-vue-app --template react
I then used TailwindCSS for the styling because, well, who doesn't love utility-first CSS frameworks, am I right?
Configuring TailwindCSS
I customized the default Tailwind configuration (tailwind.config.js
) to add some extra colors and fonts that I wanted to use throughout the app. These colors and fonts help give the app a little nautical โ๏ธ personality, and who doesn't love that? Here's a peek at my Tailwind config:
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
whip: '#fdf0d5',
maroon: '#780000',
lava: '#c1121f',
navy: '#003049',
cerulean: '#669bbc',
},
fontFamily: {
body: ['"Karla"', 'sans-serif'],
heading: ['"Caprasimo"', 'cursive'],
},
},
},
plugins: [],
};
Pro Tip: Adding custom properties to the
extend
field in the theme section lets you extend Tailwind's default config rather than completely replacing it.
Styling with TailwindCSS
Next, let's dive into how I styled the app. The majority of the styling is defined in index.css
and App.css
. I used the @apply
directive from Tailwind to apply multiple utility classes to my own custom class, and used @layer
to ensure that these styles are included in the right place in the final CSS file.
The index.css
file:
@import url('https://fonts.googleapis.com/css2?family=Caprasimo&family=Karla:wght@500;700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
.list ul {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
@layer components {
.pill {
@apply bg-whip text-navy font-body px-8 py-3 text-lg font-bold border-none rounded-full cursor-pointer;
}
.pill-sm {
@apply px-6 py-2 mx-2 text-sm font-bold uppercase;
}
}
The App.css
file simply defines the grid for the app:
.app {
grid-template-rows: auto auto 1fr auto;
}
Fun Fact: Did you know that with TailwindCSS, you can use the
@apply
directive to dry up your styles by reusing utility patterns?
Creating the Components
Our app is divided into six main components: App
, Header
, Form
, PackingList
, Item
, and Stats
.
Here's a quick rundown:
-
App
: This is the root component of our application. It's where we manage all our state and handle all the logic for our app. -
Header
: Just a simple header to give our app a title. -
Form
: The input form where users can add items to their packing list. -
PackingList
: The heart of our app where we display all the items on the packing list. -
Item
: The individual items in the PackingList. Each item has its own checkbox and remove button for user interactivity. -
Stats
: A component to show some fun statistics about our packing progress.
Now, let's take a closer look at each component.
The App Component
import { useState } from 'react';
import Header from './components/Header';
import Form from './components/Form';
import PackingList from './components/PackingList';
import Stats from './components/Stats';
import './App.css';
function App() {
const [items, setItems] = useState([]);
const [description, setDescription] = useState('');
const [quantity, setQuantity] = useState(1);
function handleQuantityChange(e) {
setQuantity(e.target.value);
}
function handleDescriptionChange(e) {
setDescription(e.target.value);
}
function handleFormSubmit(e) {
e.preventDefault();
const newItem = {
id: items.length + 1,
inputOrder: items.length + 1,
quantity: quantity,
description: description,
packed: false,
};
setItems([...items, newItem]);
setQuantity(1);
setDescription('');
}
// Handle packed state of each item
function handlePackedChange(id) {
// Create a new array with the same items, but with the packed state of the selected item toggled
const newItems = items.map((item) => (item.id === id ? { ...item, packed: !item.packed } : item));
setItems(newItems);
}
function handleRemoveItem(id) {
const updatedItems = items.filter((item) => item.id !== id);
setItems(updatedItems);
}
function calculatePackedItems() {
return items.filter((item) => item.packed).length;
}
function calculatePercentagePacked() {
return Math.round((calculatePackedItems() / items.length) * 100);
}
function handleSort(e) {
const sortBy = e.target.value;
setItems((currentItems) => {
const sortedItems = [...currentItems];
if (sortBy === 'input') {
sortedItems.sort((a, b) => a.inputOrder - b.inputOrder);
return sortedItems;
} else if (sortBy === 'description') {
sortedItems.sort((a, b) => {
if (a.description.toLowerCase() < b.description.toLowerCase()) {
return -1;
} else if (a.description.toLowerCase() > b.description.toLowerCase()) {
return 1;
} else {
return 0;
}
});
return sortedItems;
} else if (sortBy === 'packed') {
sortedItems.sort((a, b) => {
if (a.packed < b.packed) {
return -1;
} else if (a.packed > b.packed) {
return 1;
} else {
return 0;
}
});
return sortedItems;
}
});
}
function handleClearList() {
setItems([]);
}
const numOfItems = items.length;
const numOfPacked = calculatePackedItems();
const percentPacked = numOfItems > 0 ? calculatePercentagePacked() : 0;
return (
<div className='app font-body text-navy grid w-full h-screen'>
<Header />
<Form
onFormSubmit={handleFormSubmit}
quantity={quantity}
description={description}
onDescriptionChange={handleDescriptionChange}
onQuantityChange={handleQuantityChange}
/>
<PackingList
items={items}
onPackedChange={handlePackedChange}
onRemoveItem={handleRemoveItem}
onClearList={handleClearList}
onSort={handleSort}
/>
<Stats numOfItems={numOfItems} numOfPacked={numOfPacked} percentPacked={percentPacked} />
</div>
);
}
export default App;
This is the main component of our application. We use React's useState
hook to keep track of the list items and the current item quantity and description. There are several helper functions for managing the packing list:
handleQuantityChange(e)
: This function updates the quantity value in the state whenever the user changes the quantity input field.handleDescriptionChange(e)
: This function updates the description value in the state whenever the user changes the description input field.handleFormSubmit(e)
: When the user submits the form, this function prevents the page from reloading, creates a new item object, adds this object to the current items list, and resets the quantity and description fields for further input.handlePackedChange(id)
: This function toggles the packed status of a specific item when the user checks or unchecks the associated checkbox.handleRemoveItem(id)
: If a user clicks the remove button on a specific item, this function removes that item from the items list.calculatePackedItems()
: This function calculates the number of packed items in the list.calculatePercentagePacked()
: This function calculates the percentage of items that are packed.handleSort(e)
: This function sorts the list of items based on the user's preference: by input order, by description, or by packed status.handleClearList()
: This function clears all items from the list when the user clicks the "Clear List" button.
The Header Component
Header.jsx
is the simplest of our components. It only returns a styled h1
tag, and there are no props or state involved. Easy peasy! ๐
export default function Header() {
return (
<h1 className='font-heading bg-cerulean py-6 text-6xl tracking-tight text-center uppercase'>
โ๏ธ AFK Packing List โจ๏ธ
</h1>
);
}
The Form Component
The Form component handles user inputs for new items and their quantities. It takes the current quantity and description from the App component via props and uses these to pre-fill the form fields.
export default function Form({ onFormSubmit, quantity, description, onQuantityChange, onDescriptionChange }) {
return (
<form className='bg-lava py-7 flex items-center justify-center gap-3' onSubmit={onFormSubmit}>
<h3 className='mr-4 text-2xl'>what do you need for your trip?</h3>
<select value={quantity} onChange={onQuantityChange} id='quantity' className='pill focus:outline-lava'>
{[...Array(20)].map((_, i) => (
<option key={i} value={i + 1}>
{i + 1}
</option>
))}
</select>
<input
value={description}
onChange={onDescriptionChange}
id='description'
className='pill focus:outline-lava'
type='text'
placeholder='Item...'
/>
<button className='pill bg-cerulean focus:outline-navy uppercase' type='submit'>
Add
</button>
</form>
);
}
This component also uses the onFormSubmit
, onQuantityChange
, and onDescriptionChange
props to handle form submissions and updates to the quantity and description fields.
The select
input for quantity is a neat little feature! ๐ฉ The quantity input leverages JavaScript's array and map methods to create a drop-down menu with 20 options, allowing the user to easily select the quantity of an item.
[...Array(20)].map((_, i) => (
<option key={i} value={i + 1}>
{i + 1}
</option>
))
- First, we create an array with 20 empty items by using the Array constructor and the ES6 array spread operator.
- Then, we call the map() method to iterate over the array and create a new array with the number of options we need.
- The map() method calls the callback function on each array item.
- The value of the first argument is always the current array item.
- The value of the second argument is always the index of the current item.
- We use the index to generate a unique key for each option. Each Item also has a checkbox and a remove button. The checkbox allows the user to toggle the packed status of an item, and the remove button allows the user to remove an item from the list.
The Packing List Component
This is where the magic happens! โจ The PackingList component takes the array of items and maps over them, creating a new Item component for each.
import Item from './Item';
export default function PackingList({ items, onPackedChange, onRemoveItem, onClearList, onSort }) {
return (
<section className='bg-navy text-whip list flex flex-col items-center justify-between gap-8 py-10'>
<ul className='grid content-start justify-center w-4/5 gap-3 overflow-scroll list-none'>
{items.map((item) => {
return (
<Item
key={item.id}
id={item.id}
description={item.description}
quantity={item.quantity}
packed={item.packed}
onPackedChange={onPackedChange}
onRemoveItem={onRemoveItem}
/>
);
})}
</ul>
<div>
<select onChange={onSort} className='pill pill-sm'>
<option value='input'>Sort by input order</option>
<option value='description'>Sort by description</option>
<option value='packed'>Sort by packed status</option>
</select>
<button onClick={onClearList} className='pill pill-sm'>
Clear List
</button>
</div>
</section>
);
}
Additionally, this component includes controls for sorting the list and clearing all items, which makes our packing list not only functional, but also user-friendly!
The Item Component
Now, let's take a look at the Item component. Each Item represents an individual item on the packing list and is responsible for rendering the item's quantity, description, and packed status.
export default function Item({ id, description, quantity, packed, onPackedChange, onRemoveItem }) {
return (
<>
<li className='flex items-center gap-3'>
<input
className='accent-cerulean w-5 h-5'
type='checkbox'
checked={packed}
onChange={() => onPackedChange(id)}
/>
<span style={packed ? { textDecoration: 'line-through' } : {}}>
{quantity} {description}
</span>
<button
className='bg-none p-2 text-lg translate-y-0.5 border-none cursor-pointer'
onClick={() => onRemoveItem(id)}
>
โ
</button>
</li>
</>
);
}
The Stats Component
Last but not least, the Stats component. This component displays some statistics about our packing list, like the total number of items, the number of items already packed, and the percentage of items packed.
export default function Stats({ numOfItems = 0, numOfPacked = 0, percentPacked = 0 }) {
return (
<footer className='bg-maroon text-whip py-8 text-lg font-bold text-center'>
<p>
You have {numOfItems} items on your list and you've already packed {numOfPacked} ({percentPacked}%)
</p>
</footer>
);
}
And there you have it โ a summer-ready packing list app built with React and TailwindCSS! I hope you've enjoyed this little peek into my process, and I hope it's inspired you to create something of your own.
I hope this walkthrough helps you understand how I built the AFK Packing List. If you're interested in building your own version or contributing to mine, feel free to clone the repository and give it a go! ๐
Until next time, happy coding and happy travels! ๐
Takeaway: Building this packing list app not only helps keep your travels stress-free, but also gives you a practical and fun way to put your React and TailwindCSS knowledge into practice. What other projects have you built to solve everyday problems? Share in the comments below!
Top comments (0)