YeboMart Frontend — App Pages & Components
React application structure with pages, stores, and component library.
Technology Stack
| Component | Technology |
|---|---|
| Framework | React 18 |
| Language | TypeScript |
| Routing | React Router 6 |
| State | Zustand |
| Data Fetching | TanStack Query |
| Styling | Tailwind CSS |
| Icons | Heroicons |
| Offline Storage | IndexedDB (Dexie) |
| Build | Vite |
| Hosting | Cloudflare Pages |
Application Structure
app/
├── src/
│ ├── api/
│ │ └── client.ts # API client with auth
│ ├── components/
│ │ ├── layout/ # Layout, Sidebar, Navbar
│ │ ├── scanner/ # Barcode scanner
│ │ ├── subscription/ # Feature gates
│ │ └── ui/ # Card, Button, Modal, Input, Badge
│ ├── data/
│ │ └── shopTypes.ts # Business types config
│ ├── lib/
│ │ └── db.ts # IndexedDB for offline
│ ├── pages/
│ │ ├── Dashboard.tsx
│ │ ├── POS.tsx
│ │ ├── Products.tsx
│ │ ├── ProductForm.tsx
│ │ ├── Stock.tsx
│ │ ├── Sales.tsx
│ │ ├── Reports.tsx
│ │ ├── Staff.tsx
│ │ ├── StaffDetail.tsx
│ │ ├── Returns.tsx
│ │ ├── Suppliers.tsx
│ │ ├── AIChat.tsx
│ │ ├── Settings.tsx
│ │ ├── Onboarding.tsx
│ │ └── Login.tsx
│ ├── stores/
│ │ ├── authStore.ts # Authentication
│ │ ├── inventoryStore.ts # Products, sales, alerts
│ │ ├── cartStore.ts # POS cart
│ │ ├── subscriptionStore.ts # Tier & features
│ │ └── localeStore.ts # Country & currency
│ ├── types/
│ │ └── index.ts # TypeScript types
│ ├── App.tsx # Root component
│ └── index.css # Global styles
└── package.jsonPages
Dashboard
Main dashboard with sales stats, alerts, and quick actions.
tsx
// src/pages/Dashboard.tsx
export function Dashboard() {
const { shop } = useAuthStore();
const { loadAll, alerts, insights, sales, products } = useInventoryStore();
const [metrics, setMetrics] = useState<DashboardMetrics | null>(null);
useEffect(() => {
if (shop) {
loadAll(shop.id);
getDashboardMetrics(shop.id).then(setMetrics);
}
}, [shop]);
return (
<div className="space-y-6">
{/* Welcome Header */}
<div className="flex justify-between">
<div>
<h1>Good morning, {shop?.ownerName}! 👋</h1>
<p>Here's what's happening at {shop?.name}</p>
</div>
<Link to="/pos">
<Button variant="primary">Open POS</Button>
</Link>
</div>
{/* Stats Grid - Today's Sales, Transactions, Profit, Low Stock */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{stats.map(stat => <StatCard key={stat.label} {...stat} />)}
</div>
{/* Quick Actions */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Link to="/products/new">Add Product</Link>
<Link to="/stock/receive">Receive Stock</Link>
<Link to="/reports">View Reports</Link>
<Link to="/assistant">Ask AI</Link>
</div>
{/* Recent Sales + AI Insights + Low Stock */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<RecentSalesCard sales={metrics?.recentSales} />
<AIInsightsCard insights={insights} />
<LowStockCard alerts={alerts} />
</div>
</div>
);
}POS (Point of Sale)
Full-featured checkout interface.
tsx
// src/pages/POS.tsx
export function POS() {
const { products } = useInventoryStore();
const { items, addItem, removeItem, updateQuantity, checkout } = useCartStore();
const [showScanner, setShowScanner] = useState(false);
const [showCashModal, setShowCashModal] = useState(false);
const [showReceipt, setShowReceipt] = useState(false);
// Filter products by search
const filteredProducts = searchQuery
? products.filter(p =>
p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.barcode?.includes(searchQuery)
)
: products;
// Group by category
const productsByCategory = filteredProducts.reduce((acc, product) => {
const category = product.category || 'Other';
if (!acc[category]) acc[category] = [];
acc[category].push(product);
return acc;
}, {});
const handlePayment = async (method) => {
if (method === 'cash') {
setShowCashModal(true);
return;
}
await processPayment(method);
};
return (
<div className="h-full flex lg:flex-row gap-4">
{/* Products Section */}
<div className="flex-1 flex flex-col">
<SearchBar onScan={() => setShowScanner(true)} />
<div className="flex-1 overflow-y-auto">
{Object.entries(productsByCategory).map(([category, products]) => (
<ProductGrid
key={category}
category={category}
products={products}
onSelect={addItem}
/>
))}
</div>
</div>
{/* Cart Section */}
<div className="lg:w-96 bg-slate-800/50 rounded-2xl">
<CartHeader items={items} onClear={clear} />
<CartItems items={items} onUpdate={updateQuantity} onRemove={removeItem} />
<CartFooter
subtotal={subtotal}
discount={discount}
total={total}
onPayment={handlePayment}
/>
</div>
{/* Modals */}
<BarcodeScanner show={showScanner} onScan={handleBarcodeScan} />
<CashPaymentModal show={showCashModal} total={total} onComplete={processCashPayment} />
<ReceiptModal show={showReceipt} sale={lastSale} />
</div>
);
}AI Chat
Conversational AI assistant.
tsx
// src/pages/AIChat.tsx
export function AIChat() {
const { shop } = useAuthStore();
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const sendMessage = async () => {
if (!input.trim()) return;
const userMessage = { role: 'user', content: input };
setMessages([...messages, userMessage]);
setInput('');
setIsLoading(true);
const response = await api.aiChat({ message: input });
setMessages([
...messages,
userMessage,
{ role: 'assistant', content: response.message }
]);
setIsLoading(false);
};
return (
<div className="flex flex-col h-full">
<div className="flex items-center gap-3 mb-4">
<SparklesIcon className="w-8 h-8 text-purple-400" />
<div>
<h1>Ask {shop?.assistantName || 'Yebo'}</h1>
<p className="text-slate-400">Your AI business assistant</p>
</div>
</div>
{/* Suggested Questions */}
<SuggestedQuestions onSelect={setInput} />
{/* Chat Messages */}
<div className="flex-1 overflow-y-auto space-y-4">
{messages.map((msg, i) => (
<ChatMessage key={i} {...msg} />
))}
{isLoading && <TypingIndicator />}
</div>
{/* Input */}
<div className="flex gap-2 mt-4">
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask anything about your business..."
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
/>
<Button onClick={sendMessage}>Send</Button>
</div>
</div>
);
}Products
Product management with CRUD operations.
tsx
// Features:
// - List products with search, category filter, low stock filter
// - Add/edit product form
// - Bulk import from CSV
// - Export to CSV
// - Barcode scanning for quick lookupStock
Inventory management.
tsx
// Features:
// - Current stock levels with value calculation
// - Low stock alerts (critical, low, warning)
// - Stock adjustment (add/remove with reason)
// - Bulk restock (receive shipment)
// - Movement history with audit trailSales
Transaction history.
tsx
// Features:
// - List sales with date filters, payment method filter
// - Sale detail view with items
// - Void sale (managers only)
// - Email/print receipt
// - Daily summaryReports
Business analytics.
tsx
// Features:
// - Daily report (sales, profit, payment breakdown)
// - Weekly report with trend chart
// - Product performance (top sellers, slow movers, margins)
// - Staff performance (sales by cashier)Staff
Staff management.
tsx
// Features:
// - List staff with role badges
// - Add staff with role selection
// - Set permissions (discount, void, reports, stock)
// - Staff detail with sales history
// - Reset PINState Stores
AuthStore
Authentication and session management.
typescript
// src/stores/authStore.ts
interface AuthState {
user: User | null;
shop: Shop | null;
subscription: Subscription | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (phone: string, password: string) => Promise<boolean>;
staffLogin: (phone: string, pin: string) => Promise<boolean>;
logout: () => void;
register: (data: RegisterData) => Promise<boolean>;
loadUser: () => Promise<void>;
updateShop: (updates: Partial<Shop>) => Promise<void>;
}
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
// State
user: null,
shop: null,
isAuthenticated: false,
// Actions
login: async (phone, password) => {
const { data } = await api.login(phone, password);
if (!data) return false;
api.setToken(data.token);
set({ user: data.user, shop: data.shop, isAuthenticated: true });
syncShopLocale(data.shop);
return true;
},
logout: async () => {
api.clearToken();
await clearDatabase(); // Clear IndexedDB
set({ user: null, shop: null, isAuthenticated: false });
},
// ...
}),
{ name: 'yebomart-auth' }
)
);InventoryStore
Products, sales, and inventory data.
typescript
// src/stores/inventoryStore.ts
interface InventoryState {
products: Product[];
sales: Sale[];
alerts: StockAlert[];
insights: AIInsight[];
isLoading: boolean;
loadAll: (shopId: string) => Promise<void>;
getProductByBarcode: (barcode: string) => Product | undefined;
getDashboardMetrics: (shopId: string) => Promise<DashboardMetrics>;
clearAll: () => void;
}
export const useInventoryStore = create<InventoryState>((set, get) => ({
products: [],
sales: [],
alerts: [],
insights: [],
loadAll: async (shopId) => {
set({ isLoading: true });
const [productsRes, salesRes, alertsRes, insightsRes] = await Promise.all([
api.getProducts(),
api.getSales({ limit: 50 }),
api.getStockAlerts(),
api.getAIInsights(),
]);
set({
products: productsRes.data?.products || [],
sales: salesRes.data?.sales || [],
alerts: alertsRes.data?.items?.critical || [],
insights: insightsRes.data?.insights || [],
isLoading: false,
});
},
// ...
}));CartStore
POS shopping cart.
typescript
// src/stores/cartStore.ts
interface CartItem {
productId: string;
product: Product;
quantity: number;
isPack?: boolean;
}
interface CartState {
items: CartItem[];
paymentMethod: PaymentMethod | null;
discount: { percent?: number; amount: number; reason?: string } | null;
error: string | null;
addItem: (product: Product, isPack?: boolean) => void;
removeItem: (productId: string, isPack?: boolean) => void;
updateQuantity: (productId: string, quantity: number, isPack?: boolean) => void;
setPaymentMethod: (method: PaymentMethod) => void;
setDiscountPercent: (percent: number, reason?: string) => void;
setDiscountAmount: (amount: number, reason?: string) => void;
clearDiscount: () => void;
clear: () => void;
checkout: (userId: string, shopId: string) => Promise<Sale | null>;
}
// Derived state hooks
export const useCartTotal = () => useCartStore(state => {
const subtotal = state.items.reduce((sum, item) => {
const price = item.isPack && item.product.packPrice
? item.product.packPrice
: item.product.sellPrice;
return sum + (price * item.quantity);
}, 0);
const discountAmount = state.discount?.amount || 0;
return Math.max(0, subtotal - discountAmount);
});SubscriptionStore
Tier and feature management.
typescript
// src/stores/subscriptionStore.ts
export type Feature =
| 'pos' | 'stock_management' | 'basic_reports'
| 'barcode_scanning' | 'low_stock_alerts' | 'staff_accounts'
| 'whatsapp_reports' | 'advanced_reports'
| 'ai_assistant' | 'multi_location' | 'accounting_module'
| 'api_access' | 'dedicated_support';
export const FEATURES: Record<Feature, FeatureInfo> = {
pos: { id: 'pos', name: 'Point of Sale', minTier: 'lite' },
barcode_scanning: { id: 'barcode_scanning', name: 'Barcode Scanning', minTier: 'starter' },
ai_assistant: { id: 'ai_assistant', name: 'AI Assistant', minTier: 'lite' },
// ...
};
interface SubscriptionState {
currentTier: SubscriptionTier;
countryCode: string;
hasFeature: (feature: Feature) => boolean;
getUpgradeTier: (feature: Feature) => SubscriptionTier | null;
}Components
UI Components
tsx
// Card
<Card gradient="emerald">
<CardHeader title="Sales" subtitle="Today" />
<CardContent>...</CardContent>
</Card>
// Button
<Button variant="primary" size="lg" leftIcon={<PlusIcon />}>
Add Product
</Button>
// Input
<Input
label="Product Name"
placeholder="Enter name..."
leftIcon={<SearchIcon />}
error="Required"
/>
// Modal
<Modal isOpen={show} onClose={onClose} title="Confirm" size="md">
<p>Are you sure?</p>
<Button onClick={confirm}>Yes</Button>
</Modal>
// Badge
<Badge variant="success">Active</Badge>
<Badge variant="warning">Low Stock</Badge>
<Badge variant="danger">Out of Stock</Badge>Feature Gate
Conditionally render based on tier.
tsx
// src/components/subscription/FeatureGate.tsx
export function FeatureGate({
feature,
children,
fallback = 'locked'
}: FeatureGateProps) {
const { hasFeature, getUpgradeTier, currentTier } = useSubscriptionStore();
if (hasFeature(feature)) {
return <>{children}</>;
}
if (fallback === 'hidden') {
return null;
}
return (
<div className="relative">
<div className="opacity-50 pointer-events-none">{children}</div>
<div className="absolute inset-0 flex items-center justify-center bg-slate-900/80">
<LockIcon />
<p>Upgrade to {TIERS[getUpgradeTier(feature)].name}</p>
</div>
</div>
);
}
// Usage
<FeatureGate feature="barcode_scanning">
<BarcodeScanner />
</FeatureGate>FeatureCheck
Render prop pattern for more control.
tsx
<FeatureCheck feature="ai_assistant">
{({ isAvailable, requiredTier }) => (
<Link
to="/assistant"
className={!isAvailable ? 'opacity-50' : ''}
>
Ask AI {!isAvailable && `(${requiredTier}+ only)`}
</Link>
)}
</FeatureCheck>Offline Support
IndexedDB Storage
typescript
// src/lib/db.ts
import Dexie from 'dexie';
class YeboMartDB extends Dexie {
products!: Dexie.Table<Product, string>;
sales!: Dexie.Table<Sale, string>;
syncQueue!: Dexie.Table<SyncItem, string>;
constructor() {
super('YeboMartDB');
this.version(1).stores({
products: 'id, shopId, barcode, localId',
sales: 'id, shopId, localId, syncedAt',
syncQueue: '++id, entityType, status',
});
}
}
export const db = new YeboMartDB();
export async function clearDatabase() {
await db.products.clear();
await db.sales.clear();
await db.syncQueue.clear();
}Initial Sync
tsx
// src/components/InitialSync.tsx
export function InitialSync({ children }) {
const { shop } = useAuthStore();
const [synced, setSynced] = useState(false);
useEffect(() => {
if (shop) {
syncProducts(shop.id).then(() => setSynced(true));
}
}, [shop]);
if (!synced) {
return <SyncingSpinner />;
}
return <>{children}</>;
}