cellex-web / src /lib /api.ts
eeshaAI
feat: Migrate frontend from static HTML to Next.js 16 App Router
a09d2a4
Raw
History Blame Contribute Delete
11.7 kB
// Cellex API Client
// For web: uses relative paths (/api/) — handled by Next.js route handlers
// For Capacitor (APK/IPA): uses absolute URL to the live HF Space
// Detection: checks if running on localhost with Capacitor's native bridge
import { Capacitor } from '@capacitor/core';
const API_BASE = Capacitor.isNativePlatform()
? 'https://eesha-learn.onrender.com'
: '';
export { Capacitor };
export { API_BASE };
// Owncast streaming server — handles live video for seller go-live sessions.
// Sellers stream to this RTMP endpoint via OBS or their phone's streaming app.
// Viewers watch via the embed URL (embedded in /live-watch).
// Admin panel is at /admin (admin / Cellex2026!).
export const OWNCAST_URL = 'https://ai-module-tester.onrender.com';
export const OWNCAST_RTMP_SERVER = 'rtmp://ai-module-tester.onrender.com/live';
export const OWNCAST_STREAM_KEY = 'cellex-live-2026';
export const OWNCAST_EMBED_URL = `${OWNCAST_URL}/embed/stream`;
export const OWNCAST_HLS_URL = `${OWNCAST_URL}/hls/stream.m3u8`;
export interface Product {
id: number;
name: string;
price: number;
description?: string;
image_url?: string;
category?: string;
seller_id?: string;
units_sold?: number;
created_at?: string;
additional_images?: string[];
video_url?: string;
group_buy_enabled?: boolean;
group_buy_target_count?: number;
group_buy_discount_pct?: number;
}
export interface Seller {
id: string;
business_name?: string;
farm_name?: string;
business_category?: string;
business_location?: string;
profile_image?: string;
seller_type?: string;
created_at?: string;
}
export interface CartItem {
id: string;
product_id: number;
quantity: number;
products?: Product;
}
export interface Review {
id: string;
user_id: string;
product_id: number;
rating: number;
title?: string;
comment?: string;
images?: string[];
verified_purchase?: boolean;
helpful_count?: number;
created_at?: string;
reviewer_name?: string;
}
async function apiCall(path: string, body: Record<string, unknown> = {}) {
const resp = await fetch(`${API_BASE}/api/${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
credentials: 'include',
});
const data = await resp.json();
if (!resp.ok) {
return { success: false, error: data.error || 'Request failed', status: resp.status };
}
return data;
}
export const api = {
// ===== Auth =====
auth: {
login: (email: string, password: string) =>
apiCall('auth', { op: 'login', email, password }),
signup: (email: string, password: string) =>
apiCall('auth', { op: 'signup', email, password }),
logout: () => apiCall('auth', { op: 'logout' }),
session: () => apiCall('auth', { op: 'session' }),
},
// ===== Products =====
products: {
home: () => apiCall('products', { op: 'home' }),
search: (query: string, maxPrice?: number | null) =>
apiCall('products', { op: 'search', query, maxPrice }),
category: (category: string, sort = 'newest', page = 1) =>
apiCall('products', { op: 'category', category, sort, page }),
byId: (id: number) => apiCall('products', { op: 'by_id', id }),
all: (limit = 100) => apiCall('products', { op: 'all', limit }),
},
// ===== Cart =====
cart: {
get: () => apiCall('cart', { op: 'get' }),
count: () => apiCall('cart', { op: 'count' }),
add: (productId: number, quantity = 1) => apiCall('cart', { op: 'add', productId, quantity }),
remove: (cartItemId: string) => apiCall('cart', { op: 'remove', cartItemId }),
update: (cartItemId: string, quantity: number) => apiCall('cart', { op: 'update', cartItemId, quantity }),
clear: () => apiCall('cart', { op: 'clear' }),
},
// ===== Wishlist =====
wishlist: {
get: () => apiCall('wishlist', { op: 'get' }),
add: (productId: number) => apiCall('wishlist', { op: 'add', productId }),
remove: (wishlistItemId: string) => apiCall('wishlist', { op: 'remove', wishlistItemId }),
},
// ===== Orders =====
orders: {
list: () => apiCall('orders', { op: 'list' }),
details: (orderId: string) => apiCall('orders', { op: 'details', orderId }),
},
// ===== Checkout =====
checkout: {
prepare: () => apiCall('checkout', { op: 'prepare' }),
placeOrder: (shippingAddress: Record<string, unknown>) =>
apiCall('checkout', { op: 'place_order', shippingAddress }),
},
// ===== Reviews =====
reviews: {
byProduct: (productId: number) => apiCall('reviews', { op: 'by_product', productId }),
bySeller: (sellerId: string) => apiCall('reviews', { op: 'by_seller', sellerId }),
create: (data: { productId: number; rating: number; title?: string; comment?: string; images?: string[] }) =>
apiCall('reviews', { op: 'create', ...data }),
helpful: (reviewId: string) => apiCall('reviews', { op: 'helpful', reviewId }),
},
// ===== Social =====
social: {
publicProfile: (sellerId: string, viewerId?: string | null) =>
apiCall('social', { op: 'public_profile', sellerId, viewerId }),
sellerFeed: (sellerId: string, limit = 20) => apiCall('social', { op: 'seller_feed', sellerId, limit }),
discover: (limit = 12) => apiCall('social', { op: 'discover', limit }),
follow: (sellerId: string) => apiCall('social', { op: 'follow', sellerId }),
unfollow: (sellerId: string) => apiCall('social', { op: 'unfollow', sellerId }),
following: () => apiCall('social', { op: 'following' }),
feed: (limit = 20, offset = 0) => apiCall('social', { op: 'feed', limit, offset }),
},
// ===== Group Buy =====
groupBuy: {
create: (productId: number, targetCount = 3, discountPct = 20) =>
apiCall('group-buy', { op: 'create', productId, targetCount, discountPct }),
join: (groupBuyId: string) => apiCall('group-buy', { op: 'join', groupBuyId }),
status: (groupBuyId: string) => apiCall('group-buy', { op: 'status', groupBuyId }),
active: (productId: number) => apiCall('group-buy', { op: 'active', productId }),
mine: () => apiCall('group-buy', { op: 'mine' }),
cancel: (groupBuyId: string) => apiCall('group-buy', { op: 'cancel', groupBuyId }),
},
// ===== Videos =====
videos: {
feed: (limit = 20) => apiCall('videos', { op: 'feed', limit }),
byProduct: (productId: number) => apiCall('videos', { op: 'by_product', productId }),
bySeller: (sellerId: string) => apiCall('videos', { op: 'by_seller', sellerId }),
get: (videoId: number) => apiCall('videos', { op: 'get', videoId }),
like: (videoId: number) => apiCall('videos', { op: 'like', videoId }),
unlike: (videoId: number) => apiCall('videos', { op: 'unlike', videoId }),
},
// ===== Live Shopping =====
live: {
list: (status = 'live') => apiCall('live', { op: 'list', status }),
get: (sessionId: string) => apiCall('live', { op: 'get', sessionId }),
messages: (sessionId: string, afterId = 0) => apiCall('live', { op: 'messages', sessionId, afterId }),
join: (sessionId: string, name?: string) => apiCall('live', { op: 'join', sessionId, name }),
leave: (sessionId: string) => apiCall('live', { op: 'leave', sessionId }),
message: (sessionId: string, message: string) => apiCall('live', { op: 'message', sessionId, message }),
},
// ===== Stories =====
stories: {
activeBar: () => apiCall('stories', { op: 'active_bar' }),
bySeller: (sellerId: string) => apiCall('stories', { op: 'by_seller', sellerId }),
get: (storyId: number) => apiCall('stories', { op: 'get', storyId }),
},
// ===== Trending =====
trending: {
list: (limit = 20, hours = 24) => apiCall('trending', { op: 'list', limit, hours }),
},
// ===== AI Discover =====
discover: {
recommend: (limit = 10) => apiCall('discover', { op: 'recommend', limit }),
},
// ===== Cross-platform =====
crossPlatform: {
generateLinkCode: (phone: string) => apiCall('cross-platform', { op: 'generate_link_code', phone }),
myPhoneLinks: () => apiCall('cross-platform', { op: 'my_phone_links' }),
unlinkPhone: (phone: string) => apiCall('cross-platform', { op: 'unlink_phone', phone }),
},
// ===== Telegram =====
telegram: {
channelInfo: () => apiCall('telegram', { op: 'channel_info' }),
subscribe: (chatId: string) => apiCall('telegram', { op: 'subscribe', chatId }),
unsubscribe: (chatId: string) => apiCall('telegram', { op: 'unsubscribe', chatId }),
recent: (limit = 10) => apiCall('telegram', { op: 'recent', limit }),
},
// ===== Payment =====
payment: {
createOrder: (data: {
buyerName: string;
buyerEmail: string;
buyerPhone?: string;
buyerBankName?: string;
itemsSummary: string;
itemCount: number;
total: number;
}) => apiCall('payment', { op: 'create_order', ...data }),
confirmSent: (orderId: string) => apiCall('payment', { op: 'confirm_sent', orderId }),
checkStatus: (orderId: string) => apiCall('payment', { op: 'check_status', orderId }),
},
// ===== Profile =====
profile: {
get: () => apiCall('profile', { op: 'get' }),
update: (data: { fullName?: string; phone?: string; address?: string; profileImage?: string }) =>
apiCall('profile', { op: 'update', ...data }),
},
// ===== Seller =====
sellerDashboard: {
stats: () => apiCall('seller-dashboard', { op: 'stats' }),
recent: () => apiCall('seller-dashboard', { op: 'recent' }),
},
sellerProducts: {
list: () => apiCall('seller-products', { op: 'list' }),
create: (data: { name: string; price: number; description?: string; category?: string; image_url?: string }) =>
apiCall('seller-products', { op: 'create', ...data }),
update: (id: number, data: Record<string, unknown>) => apiCall('seller-products', { op: 'update', id, ...data }),
delete: (id: number) => apiCall('seller-products', { op: 'delete', id }),
},
sellerOrders: {
list: () => apiCall('seller-orders', { op: 'list' }),
},
// ===== AI Recommendations (Gorse + NVIDIA + Chroma) =====
recommend: {
home: (limit?: number) =>
apiCall('recommend', { op: 'home', limit }),
category: (category: string, limit?: number) =>
apiCall('recommend', { op: 'category', category, limit }),
shorts: (limit?: number) =>
apiCall('recommend', { op: 'shorts', limit }),
neighbors: (itemId: string | number, limit?: number) =>
apiCall('recommend', { op: 'neighbors', itemId: String(itemId), limit }),
},
// ===== Smart Search (NVIDIA + Chroma) =====
smartSearch: (query: string, limit?: number) =>
apiCall('smart-search', { query, limit }),
// ===== Feedback Sync (non-blocking) =====
feedback: (itemId: string | number, type: string, score?: number, metadata?: any) =>
apiCall('feedback', { itemId: String(itemId), type, score, metadata }),
};
// ===== Utility functions =====
export function formatPrice(n: number): string {
return `₦${Number(n || 0).toLocaleString('en-NG', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
}
export function timeAgo(iso?: string): string {
if (!iso) return '';
const d = new Date(iso);
const diff = (Date.now() - d.getTime()) / 1000;
if (diff < 60) return 'just now';
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
if (diff < 604800) return Math.floor(diff / 86400) + 'd ago';
return d.toLocaleDateString();
}
export function escapeHtml(s: unknown): string {
return String(s ?? '').replace(/[&<>"']/g, c => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[c] as string));
}