afto-ui
v1.2.6
Published
Shared UI components for AFTO e-commerce storefronts
Maintainers
Readme
afto-ui
Shared React UI component library for AFTO e-commerce storefronts.
Published on npm as afto-ui. Drop it into any AFTO-powered React app and get production-ready, fully-themed components with zero setup.
Table of Contents
- What is this library?
- How it works (architecture)
- Installation & peer dependencies
- Available components
- How to use components in your app
- Creating a new component
- Building the library
- Publishing to npm
- Project file structure explained
- Theming / design tokens
- Triggering DealsPopup manually
- Common mistakes for beginners
1. What is this library?
afto-ui is an npm component library — a collection of reusable React components that you install once and use across multiple projects.
Think of it like a LEGO kit:
- This repo contains the moulds (component source code).
- When you
npm run build, it creates the plastic pieces (compiled JS in/dist). - Other apps install the package from npm and just use the pieces.
Current components (v1.1.0):
| Component | Purpose |
|---|---|
| DealsPopup | Smart popup showing coupons, loyalty rewards, and points balance |
| StoreSelectionModal | Modal for picking a store location before shopping |
2. How it works (architecture)
afto-ui/
│
├── src/ ← Source code (what you edit)
│ ├── index.js ← Entry point — re-exports everything
│ ├── DealsPopup.jsx ← Deals/coupons popup component
│ ├── StoreSelectionModal.jsx← Store picker modal component
│ └── assets.js ← Embedded assets (base64 coin icon, etc.)
│
├── dist/ ← Built output (what npm ships)
│ ├── afto-ui.es.js ← ES Module format (modern bundlers)
│ └── afto-ui.umd.js ← UMD format (legacy / CDN)
│
├── vite.config.js ← Build configuration (library mode)
└── package.json ← npm metadata, peer deps, scriptsBuild pipeline
src/index.js
│
▼
Vite (library mode)
│
├── afto-ui.es.js ← imported by Next.js / Vite apps
└── afto-ui.umd.js ← fallback for older setupsVite bundles all .jsx files together but excludes peer dependencies (react, react-dom, lucide-react). This keeps the package small — the consumer app provides those.
3. Installation & peer dependencies
npm install afto-uiYou also need these peer dependencies in your app (they are NOT bundled):
npm install react react-dom lucide-reactWhy peer dependencies? If the library bundled React inside itself, your app would end up with two copies of React — this causes bugs. Peer deps let both the library and the app share the same single React instance.
4. Available components
DealsPopup
A smart promotional popup that appears automatically when a shopper scrolls down 20% of the page. It shows:
- Coupons tab — coupon codes with discount labels; eligible ones can be applied in one tap
- Rewards tab — loyalty reward offers redeemable with points
- Points banner — displays the user's available loyalty points
- Login nudge — prompts guest users to sign in to unlock rewards
Auto-show behaviour:
- Waits until user scrolls ≥ 20% of the page
- Then waits an additional 20 seconds (
POPUP_DELAY_MS) - Only shows once every 12 hours (
COOLDOWN_HOURS) — stored inlocalStorage - Never shows on
/cart,/checkout,/payment,/order,/account
Props:
| Prop | Type | Required | Description |
|---|---|---|---|
| colors | object | yes | UI color tokens (see Theming) |
| theme | object | yes | Brand color config { brand: { DEFAULT: "#hex" } } |
| isLoggedIn | boolean | yes | Whether the current user is authenticated |
| pathname | string | yes | Current URL path (from router) |
| openCart | () => void | no | Called when user clicks "Shop & Apply Deals" |
| openAuthModal | () => void | no | Called when guest user clicks login nudge |
| getCoupons | () => Promise | no | Fetches coupons; resolves to { data: { coupons: [] } } |
| getOffers | () => Promise | no | Fetches loyalty rewards; resolves to { data: { offers: [] } } |
| getLoyaltyDashboard | () => Promise | no | Fetches points; resolves to { data: { availablePoints: number } } |
| applyOffer | (orderId, offerId) => Promise | no | Applies a loyalty offer to a cart |
| addToCart | (productId, qty) => Promise | no | Adds a product to cart; resolves to { success, orderId } |
Coupon object shape:
{
id: "coupon_123",
code: "SAVE20",
name: "Save 20% on your order",
discountType: "percentage", // "percentage" | "flat" | "buy_x_get_y" | "free_delivery" | "free_gift"
discountValue: 20,
minOrderValue: 30,
maxDiscountAmount: 15,
previewDiscount: 8.50, // how much the user saves right now
eligible: true, // false = locked/greyed out
eligibleReason: null, // "min_order_value_not_met" | "no_eligible_items" | etc.
}Offer (reward) object shape:
{
id: "offer_456",
offerName: "Free Chocolate Cake",
pointsCost: 500,
discountPercent: 100, // 100 = free item
dollarValue: 12.00,
targetId: "product_789", // product to add to cart
imageUrl: "https://...",
canAfford: true, // false = not enough points
}StoreSelectionModal
A fullscreen/modal overlay for picking a store before shopping. On mobile it slides up from the bottom; on desktop it centres as a modal.
Props:
| Prop | Type | Required | Description |
|---|---|---|---|
| isOpen | boolean | yes | Whether the modal is visible |
| stores | array | yes | List of store objects |
| isLoading | boolean | no | Shows a spinner instead of the list |
| onSelectStore | (id: string) => void | yes | Called when user picks a store |
| themeConfig | object | no | Theme overrides (see Theming) |
Store object shape:
{
id: "store_001",
name: "Downtown Branch",
logo: "https://...", // optional — falls back to initials avatar
address: "123 Main St", // optional
timezone: "America/New_York", // optional
}5. How to use components in your app
Next.js (App Router) example
// app/layout.jsx or any client component
"use client";
import { useState } from "react";
import { usePathname } from "next/navigation";
import { DealsPopup } from "afto-ui";
import { api } from "@/lib/api"; // your own API helper
export default function RootLayout({ children }) {
const pathname = usePathname();
return (
<html>
<body>
{children}
<DealsPopup
colors={{
bgPrimary: "#ffffff",
bgSecondary: "#f8fafb",
border: "#e5e7eb",
textPrimary: "#0f1419",
textSecondary: "#6b7280",
}}
theme={{ brand: { DEFAULT: "#6366f1" } }}
isLoggedIn={true}
pathname={pathname}
openCart={() => router.push("/cart")}
openAuthModal={() => setShowLogin(true)}
getCoupons={() => api.get("/coupons")}
getOffers={() => api.get("/loyalty/offers")}
getLoyaltyDashboard={() => api.get("/loyalty/dashboard")}
applyOffer={(orderId, offerId) =>
api.post(`/orders/${orderId}/apply-offer`, { offerId })
}
addToCart={(productId, qty) =>
api.post("/cart/add", { productId, qty })
}
/>
</body>
</html>
);
}Store Selection example
"use client";
import { useState, useEffect } from "react";
import { StoreSelectionModal } from "afto-ui";
export default function App() {
const [stores, setStores] = useState([]);
const [loading, setLoading] = useState(true);
const [storeId, setStoreId] = useState(null);
useEffect(() => {
fetch("/api/stores")
.then(r => r.json())
.then(data => { setStores(data); setLoading(false); });
}, []);
return (
<>
{/* rest of your app */}
<StoreSelectionModal
isOpen={!storeId}
stores={stores}
isLoading={loading}
onSelectStore={(id) => {
setStoreId(id);
localStorage.setItem("selectedStore", id);
}}
themeConfig={{
brand: { DEFAULT: "#059669" },
bgPrimary: "#ffffff",
textPrimary: "#0f172a",
textSecondary: "#6b7280",
border: "#e5e7eb",
}}
/>
</>
);
}6. Creating a new component
Follow these exact steps to add a new component to the library.
Step 1 — Create the component file
src/MyNewComponent.jsx// src/MyNewComponent.jsx
"use client"; // add this if the component uses hooks or browser APIs
export function MyNewComponent({ title, theme, colors }) {
const brand = theme?.brand?.DEFAULT ?? "#6366f1";
return (
<div style={{ backgroundColor: colors?.bgPrimary, padding: 16 }}>
<h2 style={{ color: colors?.textPrimary }}>{title}</h2>
<button style={{ backgroundColor: brand, color: "#fff" }}>
Click me
</button>
</div>
);
}Rules:
- Always use named exports (
export function) — not default exports. - Accept
themeandcolorsprops for theming consistency. - Use inline styles or CSS-in-JS — do not import external CSS files (they won't bundle correctly).
- Use lucide-react for icons (it's a peer dependency, so it's already available).
- Do not import npm packages that are not already peer dependencies — they would inflate the bundle.
Step 2 — Export it from index.js
// src/index.js
export { DealsPopup } from "./DealsPopup.jsx";
export { StoreSelectionModal } from "./StoreSelectionModal.jsx";
export { MyNewComponent } from "./MyNewComponent.jsx"; // ← add this lineStep 3 — Build
npm run buildThe new component is now in dist/afto-ui.es.js and ready to publish.
7. Building the library
# One-time build
npm run build
# Watch mode (rebuild on every file save — useful during development)
npm run devBuild output goes to dist/. The package.json "files" field ensures only dist/ is shipped to npm — source files and node_modules are excluded.
How Vite builds in library mode
vite.config.js uses Vite's library mode:
build: {
lib: {
entry: "src/index.js", // start here
name: "AftoUI", // global variable name for UMD
fileName: (format) => `afto-ui.${format}.js`,
},
rollupOptions: {
external: ["react", "react-dom", "react/jsx-runtime", "lucide-react"],
// ↑ These are NOT bundled — consumers provide them
},
}Result:
dist/afto-ui.es.js— tree-shakeable ES module (for Next.js, Vite apps)dist/afto-ui.umd.js— self-contained UMD (for scripts, CDN)
8. Publishing to npm
First-time setup
- Create an account at npmjs.com
- Login in terminal:
npm login - The
.npmrcfile stores your auth token for CI/CD://registry.npmjs.org/:_authToken=YOUR_TOKEN_HEREKeep this token secret — never commit it to git.
Publishing a new version
# 1. Build latest code
npm run build
# 2. Bump the version (choose one):
npm version patch # 1.1.0 → 1.1.1 (bug fix)
npm version minor # 1.1.0 → 1.2.0 (new feature)
npm version major # 1.1.0 → 2.0.0 (breaking change)
# 3. Publish
npm publishSemver version guide
| Change type | Example | Command |
|---|---|---|
| Bug fix, no new features | Fixed coupon copy button | npm version patch |
| New component added | Added BannerAlert | npm version minor |
| Removed/renamed a prop | Renamed theme → themeConfig | npm version major |
After publishing
Consuming apps update with:
npm install afto-ui@latest
# or a specific version:
npm install [email protected]9. Project file structure explained
afto-ui/
│
├── src/
│ ├── index.js — Public API of the library.
│ │ Only things exported here are usable by consumers.
│ │
│ ├── DealsPopup.jsx — Deals / coupons / loyalty popup.
│ │ ~980 lines. Contains CouponCard, RewardCard,
│ │ EmptyState, SectionLabel as internal sub-components.
│ │
│ ├── StoreSelectionModal.jsx— Store picker modal.
│ │ Contains StoreAvatar as internal sub-component.
│ │
│ └── assets.js — Static assets embedded as base64 data URIs.
│ Currently exports LOYALTY_COIN_URI (coin PNG image).
│
├── dist/ — Auto-generated by `npm run build`. Do not edit manually.
│ ├── afto-ui.es.js — ES module output
│ └── afto-ui.umd.js — UMD output
│
├── vite.config.js — Tells Vite to build in "library mode" instead of app mode.
│
├── package.json — Package metadata.
│ • name: "afto-ui" — The npm package name
│ • version: "1.1.0" — Current published version
│ • main: dist/umd — Entry for CommonJS (require())
│ • module: dist/es — Entry for ES modules (import)
│ • files: ["dist"] — Only dist/ is uploaded to npm
│
├── .npmrc — Stores npm auth token for publishing.
│ The token line is commented out for safety.
│
└── .npmignore — Files excluded from the npm package.
(src/, node_modules/, vite.config.js stay local)10. Theming / design tokens
Both components accept theme props so they adapt to any brand colour.
colors object (used by DealsPopup)
{
bgPrimary: "#ffffff", // card/modal background
bgSecondary: "#f8fafb", // subtle inner backgrounds, skeletons
border: "#e5e7eb", // dividers, card borders
textPrimary: "#0f1419", // headings, main text
textSecondary: "#6b7280", // labels, subtitles, metadata
}theme object (used by DealsPopup)
{
brand: {
DEFAULT: "#6366f1", // primary brand colour — buttons, tabs, highlights
}
}themeConfig object (used by StoreSelectionModal)
Combines both into a single prop:
{
brand: { DEFAULT: "#059669" },
bgPrimary: "#ffffff",
bgSecondary: "#f8fafb",
textPrimary: "#0f172a",
textSecondary: "#6b7280",
border: "#e5e7eb",
}11. Triggering DealsPopup manually
The popup auto-shows based on scroll + timer, but you can force it open from anywhere in your app by dispatching a custom window event:
window.dispatchEvent(new Event("openDealsPopup"));This is useful for:
- A "See My Deals" button in the header
- A floating deals badge
- Triggering after a user logs in
The component resets the hasShown guard when this event fires, so it will always open on demand.
12. Common mistakes for beginners
Mistake 1 — Forgetting to build before publishing
The src/ files are not what npm ships. You must run npm run build first to regenerate dist/. If you skip this, the old compiled code gets published.
Mistake 2 — Adding regular dependencies instead of peer dependencies
If you need to add a new library (e.g., dayjs), add it as a peer dependency in package.json and also list it in rollupOptions.external in vite.config.js. If you add it as a regular dependency, it gets bundled into the output and can conflict with the consumer's own copy.
// package.json
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0",
"lucide-react": ">=0.300.0",
"dayjs": ">=1.0.0" // ← add here
}// vite.config.js
external: ["react", "react-dom", "react/jsx-runtime", "lucide-react", "dayjs"]Mistake 3 — Using default exports
Always use named exports. Default exports make tree-shaking less effective and create import inconsistencies.
// ✅ correct
export function MyComponent() { ... }
// ❌ wrong
export default function MyComponent() { ... }Mistake 4 — Importing CSS files
// ❌ this will break
import "./MyComponent.css";
// ✅ use inline styles or CSS-in-JS instead
const styles = { color: "red" };Mistake 5 — Not bumping the version before publishing
npm will reject a publish if the version in package.json is already taken. Always run npm version patch/minor/major before npm publish.
Mistake 6 — The "use client" directive
Both components have "use client" at the top. This is a Next.js App Router directive that tells Next.js the component runs in the browser (not during server-side rendering). It does nothing in non-Next.js projects — safe to leave in.
Quick reference
# Install (in consumer app)
npm install afto-ui
# Start watch build (in this repo)
npm run dev
# Production build (in this repo)
npm run build
# Publish to npm
npm version patch && npm publish// Import in consumer app
import { DealsPopup, StoreSelectionModal } from "afto-ui";