Simple JSON data based shopping site built with React. Used for conducting lessons in primary schools to learn about budgetting, sums and percentages.
To quickly start up the project:
- Clone the repository
- cd into the /shopping-app directory
- Run
npm installto install dependencies - Run
npm startto start the development server - Under /shopping-app/public, change sample-img folder name to 'img' and sample-stores folder name to 'stores'
- Open
http://localhost:5173in your browser to view the app
This section explains how the pricing engine works (priceStore), the order of operations (discounts, shipping, GST), and how to add your own store JSON so the app can render products, discounts, alerts, and classes.
The app prices each store independently, then the checkout page sums up store totals into a grand total. For each store, we compute:
itemsSubtotal— sum of price × qty for all cart linesitemsDiscount— total discount applied on items (all item-level rules)itemsNet— itemsSubtotal - itemsDiscountshippingBase— the store’s base shipping feeshippingDiscount— discount on shipping (e.g., free shipping threshold)shippingNet— shippingBase - shippingDiscount (min 0)gst— GST (9%) on (itemsNet + shippingNet)storeTotal— itemsNet + shippingNet + gst
All money values are rounded to 2 decimals (banker’s rounding not required; simple Math.round(n*100)/100).
-
nthItemPercent: Apply % off to every N-th unit across the whole cart (store-scoped), counting per unit ordered (not per SKU).{ type: "nthItemPercent"; nth: number; percentOff: number } -
overallPercent: Apply a flat percent off after the nthItemPercent rule on the items net (not shipping).{ type: "overallPercent"; percentOff: number } -
shippingThreshold: If itemsNet >= threshold, discount shipping by shippingPercentOff.{ type: "shippingThreshold"; threshold: number; shippingPercentOff: number }
The order of application is:
- Sum items → itemsSubtotal
- Apply nthItemPercent → increase itemsDiscount
- Apply overallPercent on itemsNet → increase itemsDiscount
- Evaluate shippingThreshold against current itemsNet
- Compute shippingNet, then gst = 9% × (itemsNet + shippingNet)
- storeTotal = itemsNet + shippingNet + gst
/public/stores/index.json drives the gallery tabs and (optionally) your class list and site title:
Example:
{
"title": "School Cart",
"alerts": [
{
"message": "Welcome to School Cart! Check out our latest discounts.",
"severity": "info"
}
],
"discountCodes": [
{
"code": "WELCOME10",
"kind": "percent",
"amount": 10,
"description": "10% off the whole order"
},
{
"code": "MINUS5",
"kind": "absolute",
"amount": 5,
"description": "S$5 off the grand total"
}
],
"classes": ["Class 1", "Class 2", "Class 3"],
"stores": [
{ "id": "store-1", "name": "Store 1" },
{ "id": "store-2", "name": "Store 2" }
],
"discountCap": {
"percentMax": 100,
"absoluteMax": 250
}
}titleappears in the AppBar.classespopulates the class dropdown in the checkout dialog.stores[]controls the tabs and landing gallery cards.alerts[](optional) array of alert messages to show at the top of the gallery pageseverity: "info" | "warning" | "error" | "success"
discountCodes[](optional) array of discount codes that users can apply at checkoutcode: the code string users enterkind: "percent" | "absolute"amount: number representing percent or absolute amountdescription: short description of the discount code
discountCap(optional) object to cap total discount amounts from discount codespercentMax: maximum total percent discount from all percent-type discount codesabsoluteMax: maximum total absolute discount from all absolute-type discount codes
Each store JSON is located at /public/stores/{storeId}.json and defines products, discounts, and alerts for that store.
Example:
{
"id": "rodalink",
"name": "Rodalink",
"showDiscountBreakdown": true,
"alerts": [
{
"message": "18% storewide discount applied at checkout.",
"severity": "success"
},
{
"message": "Free shipping above S$100. Base shipping S$35.",
"severity": "info"
}
],
"shipping": { "baseFee": 35 },
"constraints": { "maxQtyPerItem": 3 },
"discounts": [
{ "type": "overallPercent", "percentOff": 18 },
{ "type": "shippingThreshold", "threshold": 100, "shippingPercentOff": 100 }
],
"products": [
{
"sku": "basket",
"name": "Basket",
"price": 52,
"img": "/img/rodalink/basket.png"
},
{
"sku": "bell",
"name": "Bell",
"price": 15,
"img": "/img/rodalink/bell.png"
},
{
"sku": "frame",
"name": "Frame",
"price": 2199,
"img": "/img/rodalink/frame.png"
}
]
}Each store JSON supports:
id: store identifiername: store display nameshowDiscountBreakdown: boolean to control whether per-item discount breakdowns are shown (Used for educational purposes to demonstrate discount calculations)alerts[]: array of alert messages to show at the top of the store pageseverity: "info" | "warning" | "error" | "success"
shipping: object withbaseFeenumberconstraints: object withmaxQtyPerItemnumberdiscounts[]: array of DiscountRule objects as described aboveproducts[]: array of Product objects withsku,name,priceandimgpath
This application also supports mock gift cards for payment testing. These are located at /public/stores/cards.json.
Example:
{
"cards": [
{ "number": "777700000001", "balance": 100.0 },
{ "number": "777700000002", "balance": 50.0 }
]
}Each card object supports:
number: card number stringbalance: number representing available balance on the card