- Shared src/lib/api.ts fetch wrapper -> /api with Cache-Control: no-store - Storefront: home hero, shop grid (category filter + search), product detail, cart, checkout (POST /api/orders -> confirmation + order number), order tracking - Admin: JWT login, today KPI dashboard (revenue/orders/gross/net/margin/aov/refunds), top products, supplier performance, product import, lifecycle view, order list with tracking simulation - Multi-stage Dockerfile (Vite build -> nginx static + /api proxy -> api:8000) - nginx.conf with SPA fallback and no-store HTML caching - mock/mock_api.py for local verification (excluded from image)
130 lines
5.2 KiB
TypeScript
130 lines
5.2 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { api, asList } from '../lib/api';
|
|
import type { Product } from '../lib/types';
|
|
import ProductCard from '../components/ProductCard';
|
|
import { EmptyState } from '../components/EmptyState';
|
|
import { Spinner } from '../components/Spinner';
|
|
import {
|
|
IconArrowRight,
|
|
IconBoxes,
|
|
IconShield,
|
|
IconSparkles,
|
|
IconTruck,
|
|
IconZap,
|
|
} from '../components/Icons';
|
|
|
|
const pillars = [
|
|
{ icon: IconZap, title: 'Automated sourcing', body: 'Suppliers are vetted, scored and routed automatically.' },
|
|
{ icon: IconSparkles, title: 'Dynamic pricing', body: 'Every listing is priced against the market, continuously.' },
|
|
{ icon: IconShield, title: 'Quality checked', body: 'Products pass a lifecycle gate before they ever go live.' },
|
|
{ icon: IconTruck, title: 'Tracked fulfillment', body: 'Live tracking on every order, from supplier to doorstep.' },
|
|
];
|
|
|
|
const steps = [
|
|
{ n: '01', title: 'Browse & order', body: 'Shop curated products at market-fair prices.' },
|
|
{ n: '02', title: 'Automated fulfillment', body: 'Polaris routes your order to the best-scoring supplier.' },
|
|
{ n: '03', title: 'Track to your door', body: 'Follow the shipment with live status and tracking.' },
|
|
];
|
|
|
|
export default function Home() {
|
|
const [featured, setFeatured] = useState<Product[] | null>(null);
|
|
|
|
useEffect(() => {
|
|
api
|
|
.get<Product[]>('/products?status=published&limit=4')
|
|
.then((res) => setFeatured(asList(res)))
|
|
.catch(() => setFeatured([]));
|
|
}, []);
|
|
|
|
return (
|
|
<div className="mx-auto max-w-7xl px-4 sm:px-6">
|
|
{/* ── Hero ── */}
|
|
<section className="relative py-20 text-center sm:py-28">
|
|
<div className="pointer-events-none absolute left-1/2 top-0 h-[420px] w-[720px] -translate-x-1/2 rounded-full bg-indigo-600/20 blur-[120px]" />
|
|
<span className="chip">
|
|
<IconSparkles className="h-3.5 w-3.5 text-amber-300" />
|
|
The North Star of automated e-commerce
|
|
</span>
|
|
<h1 className="mx-auto mt-6 max-w-3xl text-4xl font-extrabold leading-tight tracking-tight text-white sm:text-6xl">
|
|
Curated products.
|
|
<br />
|
|
<span className="text-gradient">Automated</span> commerce.
|
|
</h1>
|
|
<p className="mx-auto mt-5 max-w-xl text-base text-slate-400 sm:text-lg">
|
|
Polaris sources, prices and fulfills products from vetted suppliers — so every order
|
|
ships fast, at a fair price, with tracking you can follow.
|
|
</p>
|
|
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
|
|
<Link to="/shop" className="btn-primary !px-6 !py-3 text-base">
|
|
Shop the collection
|
|
<IconArrowRight className="h-4 w-4" />
|
|
</Link>
|
|
<Link to="/track" className="btn-ghost !px-6 !py-3 text-base">
|
|
<IconTruck className="h-4 w-4" />
|
|
Track an order
|
|
</Link>
|
|
</div>
|
|
</section>
|
|
|
|
{/* ── Pillars ── */}
|
|
<section className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
|
{pillars.map((p) => (
|
|
<div key={p.title} className="glass glass-hover p-5">
|
|
<p.icon className="h-6 w-6 text-indigo-300" />
|
|
<h3 className="mt-3 font-semibold text-white">{p.title}</h3>
|
|
<p className="mt-1 text-sm leading-relaxed text-slate-400">{p.body}</p>
|
|
</div>
|
|
))}
|
|
</section>
|
|
|
|
{/* ── Featured ── */}
|
|
<section className="py-16">
|
|
<div className="mb-6 flex items-end justify-between gap-4">
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-white">Featured now</h2>
|
|
<p className="mt-1 text-sm text-slate-400">Fresh from the pipeline, live today.</p>
|
|
</div>
|
|
<Link to="/shop" className="btn-ghost !py-2 text-sm">
|
|
View all
|
|
<IconArrowRight className="h-4 w-4" />
|
|
</Link>
|
|
</div>
|
|
{featured === null ? (
|
|
<div className="flex justify-center py-16">
|
|
<Spinner className="h-7 w-7 text-indigo-400" />
|
|
</div>
|
|
) : featured.length === 0 ? (
|
|
<EmptyState
|
|
icon={<IconBoxes className="h-10 w-10" />}
|
|
title="Nothing published yet"
|
|
hint="The product pipeline is warming up — check back soon."
|
|
/>
|
|
) : (
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
{featured.map((p) => (
|
|
<ProductCard key={p.id} product={p} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
{/* ── How it works ── */}
|
|
<section className="pb-20">
|
|
<div className="glass p-8 sm:p-10">
|
|
<h2 className="text-center text-2xl font-bold text-white">How Polaris works</h2>
|
|
<div className="mt-8 grid gap-8 sm:grid-cols-3">
|
|
{steps.map((s) => (
|
|
<div key={s.n} className="text-center">
|
|
<div className="text-gradient mx-auto text-3xl font-black">{s.n}</div>
|
|
<h3 className="mt-2 font-semibold text-white">{s.title}</h3>
|
|
<p className="mx-auto mt-1 max-w-[240px] text-sm text-slate-400">{s.body}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|