Перейти к содержимому

27. Реальный проект: E-commerce

Собираем всё изученное вместе в полноценный e-commerce интерфейс. Каталог товаров с фильтрами, поиском, корзиной с оптимистичными обновлениями и имитацией loader/action паттернов Remix.

app/
├── routes/
│ ├── _store.tsx # Общий layout магазина
│ ├── _store._index.tsx # Главная / каталог
│ ├── _store.products.$id.tsx # Страница товара
│ ├── _store.cart.tsx # Корзина
│ └── api.cart.ts # Resource Route для корзины
├── models/
│ ├── product.server.ts # Работа с товарами
│ └── cart.server.ts # Работа с корзиной
└── session.server.ts # Сессии для корзины
app/routes/_store._index.tsx
export async function loader({ request }) {
const url = new URL(request.url);
const category = url.searchParams.get("category");
const q = url.searchParams.get("q");
const sort = url.searchParams.get("sort") ?? "popular";
const [products, categories] = await Promise.all([
getProducts({ category, q, sort }),
getCategories(),
]);
return json({ products, categories, q, category, sort });
}
// app/routes/api.cart.ts (Resource Route)
export async function action({ request }) {
const session = await getSession(request.headers.get("Cookie"));
const formData = await request.formData();
const intent = formData.get("intent") as string;
switch (intent) {
case "add":
addToCart(session, formData.get("productId") as string);
break;
case "remove":
removeFromCart(session, formData.get("productId") as string);
break;
case "clear":
clearCart(session);
break;
}
return json({ ok: true }, {
headers: { "Set-Cookie": await commitSession(session) },
});
}
function AddToCartButton({ product }) {
const fetcher = useFetcher();
// Оптимистично определяем состояние
const isAdding = fetcher.state !== "idle" &&
fetcher.formData?.get("intent") === "add";
return (
<fetcher.Form method="post" action="/api/cart">
<input type="hidden" name="intent" value="add" />
<input type="hidden" name="productId" value={product.id} />
<button type="submit" disabled={isAdding}>
{isAdding ? "Добавляется..." : "В корзину"}
</button>
</fetcher.Form>
);
}