Skip to content

Commit b6c782a

Browse files
Practice app examples (#2)
* Add coffee cart checkout example Co-authored-by: me <me@kentcdodds.com> * Replace coffee cart with muffin checkout Co-authored-by: me <me@kentcdodds.com> * Add muffin shop dashboard example Co-authored-by: me <me@kentcdodds.com> * Add expansion ideas to example docs Co-authored-by: me <me@kentcdodds.com> * Add examples overview README Co-authored-by: me <me@kentcdodds.com> * Move sample output to muffin checkout README Co-authored-by: me <me@kentcdodds.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 918151c commit b6c782a

File tree

15 files changed

+1389
-0
lines changed

15 files changed

+1389
-0
lines changed

examples/README.mdx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Examples
2+
3+
👨‍💼 These examples are here to help you *practice* what you have already learned
4+
in the workshop. They are small, practical projects that keep the focus on the
5+
foundational skills: values, variables, functions, control flow, loops, and
6+
basic types.
7+
8+
📜 There are **no solutions** and **no tests** in this section. That is
9+
intentional. This is about exploring, practicing, and building confidence by
10+
applying workshop concepts to something that feels real.
11+
12+
🤔 You may see code that looks unfamiliar (React, Vite, styling, etc.). Feel
13+
free to explore it, but you do not need to master it right now. Focus on the
14+
areas with 🐨 instructions. Those are the parts that line up with what you've
15+
learned so far.
16+
17+
🧠 **Spaced repetition** helps you remember what you learn by revisiting it over
18+
time. Practical examples like these give you lots of reps and quick wins, which
19+
makes the concepts stick and builds momentum.
20+
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Muffin Shop Checkout
2+
// Build a simple receipt for a muffin shop order.
3+
4+
const shopName = 'Morning Muffins'
5+
const customerName = 'Avery'
6+
const isMember = true
7+
const muffinCount = 3
8+
const coffeeCount = 2
9+
const muffinPrice = 3.25
10+
const coffeePrice = 4
11+
const tipPercent = 0.15
12+
const taxRate = 0.0825
13+
const pickupMethod = 'counter'
14+
15+
function buildDivider(char: string, length: number) {
16+
// 🐨 Build and return a divider string by repeating char
17+
// 💰 Start with let line = '' and add char in a for loop
18+
let line = ''
19+
return line
20+
}
21+
22+
function formatMoney(amount: number) {
23+
// 🐨 Return a string like `$4.5` using a template literal
24+
return `$${amount}`
25+
}
26+
27+
function calculateDiscount(subtotal: number, member: boolean) {
28+
// 🐨 If the customer is a member and subtotal is at least 15,
29+
// return 10% of subtotal. Otherwise return 0.
30+
return 0
31+
}
32+
33+
function calculateTax(subtotal: number, rate: number) {
34+
// 🐨 Return subtotal multiplied by rate
35+
return 0
36+
}
37+
38+
function calculateTip(totalBeforeTip: number, percent: number) {
39+
// 🐨 Return totalBeforeTip multiplied by percent
40+
return 0
41+
}
42+
43+
function calculatePickupFee(method: string) {
44+
// 🐨 Use a switch statement to return:
45+
// 'counter' -> 0
46+
// 'curbside' -> 3
47+
// 'delivery' -> 7
48+
return 0
49+
}
50+
51+
let subtotal = 0
52+
// 🐨 Set subtotal using item counts and prices
53+
// 💰 (muffinCount * muffinPrice) + (coffeeCount * coffeePrice)
54+
55+
const discount = calculateDiscount(subtotal, isMember)
56+
const taxableAmount = subtotal - discount
57+
const tax = calculateTax(taxableAmount, taxRate)
58+
const tip = calculateTip(taxableAmount + tax, tipPercent)
59+
const pickupFee = calculatePickupFee(pickupMethod)
60+
61+
let pickupLabel = ''
62+
// 🐨 If pickupFee is 0, set pickupLabel to 'FREE'
63+
// otherwise set it to formatMoney(pickupFee)
64+
65+
let memberMessage = ''
66+
// 🐨 If isMember is true, set memberMessage to 'Member discount applied'
67+
// otherwise set it to 'Join the club for 10% off'
68+
69+
let total = 0
70+
// 🐨 Set total using subtotal, discount, tax, tip, and pickupFee
71+
72+
const header = buildDivider('=', 32)
73+
const divider = buildDivider('-', 32)
74+
75+
const receipt =
76+
`${header}\n` +
77+
`${shopName}\n` +
78+
`Order for ${customerName}\n` +
79+
`${divider}\n` +
80+
`Muffins: ${muffinCount} x ${formatMoney(muffinPrice)}\n` +
81+
`Coffee: ${coffeeCount} x ${formatMoney(coffeePrice)}\n` +
82+
`${divider}\n` +
83+
`Subtotal: ${formatMoney(subtotal)}\n` +
84+
`Discount: -${formatMoney(discount)}\n` +
85+
`Tax: ${formatMoney(tax)}\n` +
86+
`Tip: ${formatMoney(tip)}\n` +
87+
`Pickup fee: ${pickupLabel}\n` +
88+
`${divider}\n` +
89+
`Total: ${formatMoney(total)}\n` +
90+
`${memberMessage}\n` +
91+
`${header}`
92+
93+
console.log(receipt)
94+
95+
export { receipt }
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "examples_muffin-shop-checkout",
3+
"type": "module",
4+
"scripts": {
5+
"start": "npx @kentcdodds/log-module ./index.ts"
6+
}
7+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Muffin Shop Checkout
2+
3+
👨‍💼 The muffin shop needs a simple receipt that totals an order and prints a
4+
clean summary for customers.
5+
6+
📜 This example sticks to the skills from the workshop: numbers, strings,
7+
booleans, variables, functions, if/else, switch statements, and loops.
8+
9+
🐨 Open <InlineFile file="index.ts" /> and complete the following tasks:
10+
11+
1. Build the divider string with a loop.
12+
2. Calculate `subtotal`.
13+
3. Implement `calculateDiscount`.
14+
4. Implement `calculateTax`.
15+
5. Implement `calculateTip`.
16+
6. Implement `calculatePickupFee` with a switch statement.
17+
7. Set `pickupLabel` with an if/else.
18+
8. Set `memberMessage` with an if/else.
19+
9. Calculate `total`.
20+
10. Make sure the receipt prints with `console.log`.
21+
22+
💰 Flip `isMember` to see the discount and message change.
23+
💰 Try `pickupMethod` as `curbside` or `delivery` to test the switch logic.
24+
25+
## Run the example
26+
27+
```sh nonumber
28+
npm run start --workspace examples/muffin-shop-checkout
29+
```
30+
31+
## What the output can look like
32+
33+
Here is a sample console receipt output:
34+
35+
```text
36+
================================
37+
Morning Muffins
38+
Order for Avery
39+
--------------------------------
40+
Muffins: 3 x $3.25
41+
Coffee: 2 x $4
42+
--------------------------------
43+
Subtotal: $17.75
44+
Discount: -$1.78
45+
Tax: $1.31
46+
Tip: $2.86
47+
Pickup fee: FREE
48+
--------------------------------
49+
Total: $20.14
50+
Member discount applied
51+
================================
52+
```
53+
54+
## Try next
55+
56+
- 🐨 Add a `dailySpecialCount` and `dailySpecialPrice`, then include them in the
57+
`subtotal`.
58+
- 🐨 Create a `hasCoupon` boolean and apply an extra discount with an `if` check.
59+
- 🐨 Use a loop to build a `summaryLine` like `Items: 5 total`.
60+
- 🐨 Add a `rushOrder` flag that increases the `tipPercent` when true.
61+
- 💰 Try a new `pickupMethod` value and update the switch to handle it.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Muffin Shop Dashboard</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "examples_muffin-shop-dashboard",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite",
7+
"build": "vite build",
8+
"preview": "vite preview"
9+
},
10+
"dependencies": {
11+
"react": "^19.2.3",
12+
"react-dom": "^19.2.3"
13+
},
14+
"devDependencies": {
15+
"@types/react": "^19.2.8",
16+
"@types/react-dom": "^19.2.3",
17+
"@vitejs/plugin-react": "^5.1.2",
18+
"typescript": "^5.9.3",
19+
"vite": "^7.3.1"
20+
}
21+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Muffin Shop Dashboard
2+
3+
👨‍💼 The muffin shop wants a simple dashboard for a single order so the team can
4+
double-check totals before handing it off.
5+
6+
📜 This example uses React + Vite, but everything React-related is already done
7+
for you. You only need the workshop skills: numbers, strings, booleans,
8+
variables, functions, if/else, switch statements, loops, and `never`.
9+
10+
🐨 Open <InlineFile file="src/muffin-utils.ts" /> and complete the following tasks:
11+
12+
1. Format money with a template literal.
13+
2. Add the `(GF)` label for gluten-free items.
14+
3. Sum the subtotal with a loop.
15+
4. Apply the member discount.
16+
5. Calculate tax and tip.
17+
6. Calculate the final total.
18+
7. Return the pickup fee with a switch statement.
19+
8. Return the pickup label with a switch statement.
20+
9. Format the special note when it's empty.
21+
10. Log the ready message.
22+
11. Implement `assertNever`.
23+
24+
💰 Toggle `memberStatus` or `pickupMethod` in <InlineFile file="src/app.tsx" /> to
25+
see different outputs.
26+
27+
## Run the example
28+
29+
```sh nonumber
30+
npm run dev --workspace examples/muffin-shop-dashboard
31+
```
32+
33+
## Try next
34+
35+
- 🐨 Add another `OrderItem` and watch the totals change.
36+
- 🐨 Add a `rushOrder` boolean and increase the tip when it is true.
37+
- 🐨 Add a `itemCount` helper that uses a loop to count total quantity.
38+
- 🐨 Add a `happyHour` boolean and reduce the muffin price when true.
39+
- 🐨 Add a `memberMessage` string in `app.tsx` using an `if` check.
40+
- 💰 Change `specialNote` to a real note and confirm the fallback logic.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
.app {
2+
max-width: 960px;
3+
margin: 0 auto;
4+
padding: 32px 24px 48px;
5+
}
6+
7+
.app-header {
8+
display: flex;
9+
justify-content: space-between;
10+
align-items: center;
11+
gap: 16px;
12+
margin-bottom: 24px;
13+
}
14+
15+
.app-header h1 {
16+
margin: 4px 0 0;
17+
font-size: 32px;
18+
}
19+
20+
.eyebrow {
21+
margin: 0;
22+
font-size: 12px;
23+
letter-spacing: 0.2em;
24+
text-transform: uppercase;
25+
color: #8a6f5a;
26+
}
27+
28+
.badge {
29+
background: #fff;
30+
border-radius: 999px;
31+
padding: 8px 16px;
32+
font-size: 12px;
33+
text-transform: uppercase;
34+
letter-spacing: 0.12em;
35+
color: #5b4a3f;
36+
box-shadow: 0 8px 24px rgba(59, 45, 34, 0.08);
37+
}
38+
39+
.layout {
40+
display: grid;
41+
gap: 24px;
42+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
43+
}
44+
45+
.card {
46+
background: #fff;
47+
border-radius: 16px;
48+
padding: 24px;
49+
box-shadow: 0 16px 40px rgba(59, 45, 34, 0.08);
50+
}
51+
52+
.card h2 {
53+
margin-top: 0;
54+
margin-bottom: 16px;
55+
}
56+
57+
.line-items {
58+
list-style: none;
59+
margin: 0;
60+
padding: 0;
61+
display: grid;
62+
gap: 12px;
63+
}
64+
65+
.line-item {
66+
display: flex;
67+
justify-content: space-between;
68+
gap: 16px;
69+
font-weight: 600;
70+
}
71+
72+
.note {
73+
margin-top: 16px;
74+
padding-top: 12px;
75+
border-top: 1px solid #efe5db;
76+
display: flex;
77+
justify-content: space-between;
78+
gap: 12px;
79+
font-size: 14px;
80+
color: #6f655b;
81+
}
82+
83+
.summary-row {
84+
display: flex;
85+
justify-content: space-between;
86+
gap: 16px;
87+
margin-top: 8px;
88+
}
89+
90+
.summary-row.total {
91+
margin-top: 16px;
92+
padding-top: 12px;
93+
border-top: 1px solid #efe5db;
94+
font-size: 18px;
95+
font-weight: 700;
96+
}
97+
98+
.pill {
99+
margin-top: 16px;
100+
display: inline-block;
101+
padding: 6px 12px;
102+
border-radius: 999px;
103+
background: #f1e4d7;
104+
font-size: 12px;
105+
text-transform: uppercase;
106+
letter-spacing: 0.08em;
107+
color: #6a4d3b;
108+
}
109+
110+
.helper {
111+
margin-top: 12px;
112+
font-size: 14px;
113+
color: #6f655b;
114+
}

0 commit comments

Comments
 (0)