Skip to content

YeboMart Frontend — App Pages & Components

React application structure with pages, stores, and component library.


Technology Stack

ComponentTechnology
FrameworkReact 18
LanguageTypeScript
RoutingReact Router 6
StateZustand
Data FetchingTanStack Query
StylingTailwind CSS
IconsHeroicons
Offline StorageIndexedDB (Dexie)
BuildVite
HostingCloudflare 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.json

Pages

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 lookup

Stock

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 trail

Sales

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 summary

Reports

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 PIN

State 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}</>;
}

One chat. Everything done.