feat: aggrigated all api calls
This commit is contained in:
parent
524c504b70
commit
3a075c66ec
32 changed files with 597 additions and 167 deletions
|
|
@ -1,26 +1,8 @@
|
|||
import { winofyFetch, type PaginatedResponse } from "@/lib/api/winofy";
|
||||
import { requireSession } from "@/lib/auth/session";
|
||||
import type { Address, City, Neighborhood } from "@/types/api";
|
||||
import { getAddresses } from "@/lib/api/addresses";
|
||||
import { getCities, getNeighborhoods } from "@/lib/api/locations";
|
||||
import { AddressList } from "@/components/customer/address-list";
|
||||
|
||||
async function getAddresses() {
|
||||
const res = await winofyFetch<PaginatedResponse<Address> | Address[]>("/v1/addresses/");
|
||||
return Array.isArray(res) ? res : res.results;
|
||||
}
|
||||
|
||||
async function getCities() {
|
||||
const res = await winofyFetch<PaginatedResponse<City> | City[]>("/v1/cities/", { auth: false });
|
||||
return Array.isArray(res) ? res : res.results;
|
||||
}
|
||||
|
||||
async function getNeighborhoods() {
|
||||
const res = await winofyFetch<PaginatedResponse<Neighborhood> | Neighborhood[]>("/v1/neighborhoods/", {
|
||||
auth: false,
|
||||
params: { limit: 200 },
|
||||
});
|
||||
return Array.isArray(res) ? res : res.results;
|
||||
}
|
||||
|
||||
export default async function AddressesPage() {
|
||||
await requireSession("/addresses");
|
||||
const [addresses, cities, neighborhoods] = await Promise.all([getAddresses(), getCities(), getNeighborhoods()]);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Link from "next/link";
|
||||
import { getCart } from "@/lib/actions/cart";
|
||||
import { getCart } from "@/lib/api/cart";
|
||||
import { requireSession } from "@/lib/auth/session";
|
||||
import { CartItemRow } from "@/components/customer/cart-item-row";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
import { redirect } from "next/navigation";
|
||||
import { getCart } from "@/lib/actions/cart";
|
||||
import { getCart } from "@/lib/api/cart";
|
||||
import { requireSession } from "@/lib/auth/session";
|
||||
import { winofyFetch, type PaginatedResponse } from "@/lib/api/winofy";
|
||||
import type { Address } from "@/types/api";
|
||||
import { getAddresses } from "@/lib/api/addresses";
|
||||
import { CheckoutForm } from "@/components/customer/checkout-form";
|
||||
|
||||
async function getAddresses() {
|
||||
const res = await winofyFetch<PaginatedResponse<Address> | Address[]>("/v1/addresses/");
|
||||
return Array.isArray(res) ? res : res.results;
|
||||
}
|
||||
|
||||
export default async function CheckoutPage() {
|
||||
await requireSession("/checkout");
|
||||
const [cart, addresses] = await Promise.all([getCart(), getAddresses()]);
|
||||
|
|
|
|||
|
|
@ -1,23 +1,17 @@
|
|||
import { notFound } from "next/navigation";
|
||||
import { winofyFetch, ApiError } from "@/lib/api/winofy";
|
||||
import { ApiError } from "@/lib/api/winofy";
|
||||
import { requireSession } from "@/lib/auth/session";
|
||||
import { getOrderGroup } from "@/lib/api/orders";
|
||||
import { OrderCard } from "@/components/customer/order-card";
|
||||
import { PAYMENT_STATUS_LABELS_FA } from "@/lib/order-status";
|
||||
import type { OrderGroup } from "@/types/api";
|
||||
|
||||
async function getOrderGroup(uuid: string) {
|
||||
try {
|
||||
return await winofyFetch<OrderGroup>(`/v1/order-groups/${uuid}/`);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 404) return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function OrderGroupPage({ params }: PageProps<"/order-groups/[uuid]">) {
|
||||
const { uuid } = await params;
|
||||
await requireSession(`/order-groups/${uuid}`);
|
||||
const orderGroup = await getOrderGroup(uuid);
|
||||
const orderGroup = await getOrderGroup(uuid).catch((err) => {
|
||||
if (err instanceof ApiError && err.status === 404) return null;
|
||||
throw err;
|
||||
});
|
||||
if (!orderGroup) notFound();
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
import Link from "next/link";
|
||||
import { winofyFetch, type PaginatedResponse } from "@/lib/api/winofy";
|
||||
import { requireSession } from "@/lib/auth/session";
|
||||
import { getOrderGroups } from "@/lib/api/orders";
|
||||
import { PAYMENT_STATUS_LABELS_FA } from "@/lib/order-status";
|
||||
import type { OrderGroup } from "@/types/api";
|
||||
|
||||
async function getOrderGroups() {
|
||||
const res = await winofyFetch<PaginatedResponse<OrderGroup>>("/v1/order-groups/");
|
||||
return res.results;
|
||||
}
|
||||
|
||||
export default async function OrdersPage() {
|
||||
await requireSession("/orders");
|
||||
|
|
|
|||
|
|
@ -1,23 +1,17 @@
|
|||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { winofyFetch, ApiError } from "@/lib/api/winofy";
|
||||
import type { ProductDetail } from "@/types/api";
|
||||
import { ApiError } from "@/lib/api/winofy";
|
||||
import { getProduct } from "@/lib/api/catalog";
|
||||
import { AddToCartButton } from "@/components/customer/add-to-cart-button";
|
||||
|
||||
const UNIT_LABELS_FA: Record<string, string> = { gram: "گرم", ml: "میلیلیتر", piece: "عدد" };
|
||||
|
||||
async function getProduct(uuid: string) {
|
||||
try {
|
||||
return await winofyFetch<ProductDetail>(`/v1/products/${uuid}/`, { auth: false });
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 404) return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function ProductDetailPage({ params }: PageProps<"/products/[uuid]">) {
|
||||
const { uuid } = await params;
|
||||
const product = await getProduct(uuid);
|
||||
const product = await getProduct(uuid).catch((err) => {
|
||||
if (err instanceof ApiError && err.status === 404) return null;
|
||||
throw err;
|
||||
});
|
||||
if (!product) notFound();
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,34 +1,21 @@
|
|||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { winofyFetch, ApiError, type PaginatedResponse } from "@/lib/api/winofy";
|
||||
import type { ProductListItem, StoreDetail } from "@/types/api";
|
||||
import { ApiError } from "@/lib/api/winofy";
|
||||
import { getStore } from "@/lib/api/stores";
|
||||
import { getProducts } from "@/lib/api/catalog";
|
||||
import { AddToCartButton } from "@/components/customer/add-to-cart-button";
|
||||
|
||||
const WEEKDAY_LABELS_FA = ["شنبه", "یکشنبه", "دوشنبه", "سهشنبه", "چهارشنبه", "پنجشنبه", "جمعه"];
|
||||
|
||||
async function getStore(uuid: string) {
|
||||
try {
|
||||
return await winofyFetch<StoreDetail>(`/v1/stores/${uuid}/`, { auth: false });
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 404) return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function getStoreProducts(uuid: string) {
|
||||
const res = await winofyFetch<PaginatedResponse<ProductListItem>>("/v1/products/", {
|
||||
auth: false,
|
||||
params: { store: uuid, limit: 100 },
|
||||
});
|
||||
return res.results;
|
||||
}
|
||||
|
||||
export default async function StoreDetailPage({ params }: PageProps<"/stores/[uuid]">) {
|
||||
const { uuid } = await params;
|
||||
const store = await getStore(uuid);
|
||||
const store = await getStore(uuid).catch((err) => {
|
||||
if (err instanceof ApiError && err.status === 404) return null;
|
||||
throw err;
|
||||
});
|
||||
if (!store) notFound();
|
||||
|
||||
const products = await getStoreProducts(uuid);
|
||||
const products = await getProducts({ store: uuid, limit: 100 });
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-6 px-4 py-6">
|
||||
|
|
|
|||
|
|
@ -1,17 +1,8 @@
|
|||
import { Suspense } from "react";
|
||||
import { winofyFetch, type PaginatedResponse } from "@/lib/api/winofy";
|
||||
import { getLocationServer } from "@/lib/location/server";
|
||||
import type { Neighborhood } from "@/types/api";
|
||||
import { getNeighborhoods } from "@/lib/api/locations";
|
||||
import { NeighborhoodSearch } from "@/components/location/neighborhood-search";
|
||||
|
||||
async function getNeighborhoods() {
|
||||
const res = await winofyFetch<PaginatedResponse<Neighborhood> | Neighborhood[]>("/v1/neighborhoods/", {
|
||||
auth: false,
|
||||
params: { limit: 200 },
|
||||
});
|
||||
return Array.isArray(res) ? res : res.results;
|
||||
}
|
||||
|
||||
export default async function NeighborhoodSearchPage() {
|
||||
const [neighborhoods, location] = await Promise.all([getNeighborhoods(), getLocationServer()]);
|
||||
const selectedNeighborhoodUuid = location?.mode === "manual" ? location.neighborhoodUuid : null;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { Suspense } from "react";
|
||||
import { getValidSession } from "@/lib/auth/session";
|
||||
import { getLocationServer } from "@/lib/location/server";
|
||||
import { winofyFetch, type PaginatedResponse } from "@/lib/api/winofy";
|
||||
import { getCart } from "@/lib/actions/cart";
|
||||
import type { StoreCategory, StoreListItem } from "@/types/api";
|
||||
import { getStoreCategories, getStores } from "@/lib/api/stores";
|
||||
import { getCart } from "@/lib/api/cart";
|
||||
import { SiteHeader } from "@/components/home/site-header";
|
||||
import { SearchBar } from "@/components/home/search-bar";
|
||||
import { LocationReminderBanner } from "@/components/home/location-reminder-banner";
|
||||
|
|
@ -12,21 +11,6 @@ import { StoreCard } from "@/components/home/store-card";
|
|||
import { SellerPromoCard } from "@/components/home/seller-promo-card";
|
||||
import { BottomNav } from "@/components/home/bottom-nav";
|
||||
|
||||
async function getStoreCategories() {
|
||||
const res = await winofyFetch<PaginatedResponse<StoreCategory> | StoreCategory[]>("/v1/store-categories/", {
|
||||
auth: false,
|
||||
});
|
||||
return Array.isArray(res) ? res : res.results;
|
||||
}
|
||||
|
||||
async function getStores(params: { category?: string; search?: string; neighborhood?: string; lat?: number; lng?: number }) {
|
||||
const res = await winofyFetch<PaginatedResponse<StoreListItem>>("/v1/stores/", {
|
||||
auth: false,
|
||||
params: { limit: 20, ...params },
|
||||
});
|
||||
return res.results;
|
||||
}
|
||||
|
||||
export default async function HomePage({ searchParams }: PageProps<"/">) {
|
||||
const params = await searchParams;
|
||||
const category = typeof params.category === "string" ? params.category : undefined;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useState, useTransition } from "react";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { addToCart } from "@/lib/actions/cart";
|
||||
import { addToCart } from "@/lib/api/cart";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils/cn";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useMemo, useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { createAddress, deleteAddress, type AddressInput } from "@/lib/actions/addresses";
|
||||
import { createAddress, deleteAddress, type AddressInput } from "@/lib/api/addresses";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { Address, AddressLabel, City, Neighborhood } from "@/types/api";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { updateCartItemQuantity, removeCartItem } from "@/lib/actions/cart";
|
||||
import { updateCartItemQuantity, removeCartItem } from "@/lib/api/cart";
|
||||
import type { CartItem } from "@/types/api";
|
||||
|
||||
export function CartItemRow({ item }: { item: CartItem }) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { useState, useTransition } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { submitCheckout } from "@/lib/actions/checkout";
|
||||
import { submitCheckout } from "@/lib/api/checkout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Address, Cart, DeliveryType, PaymentMethod } from "@/types/api";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { cancelOrder } from "@/lib/actions/orders";
|
||||
import { cancelOrder } from "@/lib/api/orders";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ORDER_STATUS_LABELS_FA, ORDER_STATUS_SEQUENCE } from "@/lib/order-status";
|
||||
import type { Order } from "@/types/api";
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { winofyFetch } from "@/lib/api/winofy";
|
||||
import { callApi, type ActionResult } from "./result";
|
||||
import type { Order } from "@/types/api";
|
||||
|
||||
export async function cancelOrder(uuid: string, reason: string = ""): Promise<ActionResult<Order>> {
|
||||
const result = await callApi(() =>
|
||||
winofyFetch<Order>(`/v1/orders/${uuid}/cancel/`, { method: "POST", body: { reason } }),
|
||||
);
|
||||
if (result.ok) {
|
||||
revalidatePath(`/orders/${uuid}`);
|
||||
revalidatePath("/orders");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { winofyFetch } from "@/lib/api/winofy";
|
||||
import { winofyFetch, type PaginatedResponse } from "./winofy";
|
||||
import { callApi, type ActionResult } from "./result";
|
||||
import type { Address, AddressLabel } from "@/types/api";
|
||||
|
||||
export async function getAddresses(): Promise<Address[]> {
|
||||
const res = await winofyFetch<PaginatedResponse<Address>>("/v1/addresses/");
|
||||
return res.results;
|
||||
}
|
||||
|
||||
export interface AddressInput {
|
||||
label: AddressLabel;
|
||||
title: string;
|
||||
|
|
@ -22,9 +27,7 @@ export interface AddressInput {
|
|||
}
|
||||
|
||||
export async function createAddress(input: AddressInput): Promise<ActionResult<Address>> {
|
||||
const result = await callApi(() =>
|
||||
winofyFetch<Address>("/v1/addresses/", { method: "POST", body: input }),
|
||||
);
|
||||
const result = await callApi(() => winofyFetch<Address>("/v1/addresses/", { method: "POST", body: input }));
|
||||
if (result.ok) revalidatePath("/addresses");
|
||||
return result;
|
||||
}
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { winofyFetch } from "@/lib/api/winofy";
|
||||
import { winofyFetch } from "./winofy";
|
||||
import { callApi, type ActionResult } from "./result";
|
||||
import type { Cart } from "@/types/api";
|
||||
|
||||
export async function getCart(): Promise<Cart> {
|
||||
return winofyFetch<Cart>("/v1/cart/");
|
||||
}
|
||||
|
||||
export async function addToCart(productUuid: string, quantity: number = 1): Promise<ActionResult> {
|
||||
const result = await callApi(() =>
|
||||
winofyFetch<undefined>("/v1/cart/items/", {
|
||||
method: "POST",
|
||||
body: { product_uuid: productUuid, quantity },
|
||||
}),
|
||||
winofyFetch<undefined>("/v1/cart/items/", { method: "POST", body: { product_uuid: productUuid, quantity } }),
|
||||
);
|
||||
if (result.ok) revalidatePath("/cart");
|
||||
return result;
|
||||
|
|
@ -18,10 +19,7 @@ export async function addToCart(productUuid: string, quantity: number = 1): Prom
|
|||
|
||||
export async function updateCartItemQuantity(itemUuid: string, quantity: number): Promise<ActionResult> {
|
||||
const result = await callApi(() =>
|
||||
winofyFetch<undefined>(`/v1/cart/items/${itemUuid}/`, {
|
||||
method: "PATCH",
|
||||
body: { quantity },
|
||||
}),
|
||||
winofyFetch<undefined>(`/v1/cart/items/${itemUuid}/`, { method: "PATCH", body: { quantity } }),
|
||||
);
|
||||
if (result.ok) revalidatePath("/cart");
|
||||
return result;
|
||||
|
|
@ -38,7 +36,3 @@ export async function clearCart(): Promise<ActionResult> {
|
|||
if (result.ok) revalidatePath("/cart");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getCart(): Promise<Cart> {
|
||||
return winofyFetch<Cart>("/v1/cart/");
|
||||
}
|
||||
30
src/lib/api/catalog.ts
Normal file
30
src/lib/api/catalog.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import "server-only";
|
||||
import { winofyFetch, type PaginatedResponse } from "./winofy";
|
||||
import type { ProductCategory, ProductDetail, ProductListItem } from "@/types/api";
|
||||
|
||||
export async function getProductCategories(): Promise<ProductCategory[]> {
|
||||
const res = await winofyFetch<PaginatedResponse<ProductCategory>>("/v1/product-categories/", {
|
||||
auth: false,
|
||||
params: { limit: 100 },
|
||||
});
|
||||
return res.results;
|
||||
}
|
||||
|
||||
export interface GetProductsParams {
|
||||
store?: string;
|
||||
category?: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export async function getProducts(params: GetProductsParams = {}): Promise<ProductListItem[]> {
|
||||
const res = await winofyFetch<PaginatedResponse<ProductListItem>>("/v1/products/", {
|
||||
auth: false,
|
||||
params: { limit: 100, ...params },
|
||||
});
|
||||
return res.results;
|
||||
}
|
||||
|
||||
export async function getProduct(uuid: string): Promise<ProductDetail> {
|
||||
return winofyFetch<ProductDetail>(`/v1/products/${uuid}/`, { auth: false });
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
"use server";
|
||||
|
||||
import { winofyFetch } from "@/lib/api/winofy";
|
||||
import { winofyFetch } from "./winofy";
|
||||
import { callApi, type ActionResult } from "./result";
|
||||
import type { DeliveryType, OrderGroup, PaymentMethod } from "@/types/api";
|
||||
|
||||
24
src/lib/api/locations.ts
Normal file
24
src/lib/api/locations.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import "server-only";
|
||||
import { winofyFetch, type PaginatedResponse } from "./winofy";
|
||||
import type { City, Neighborhood } from "@/types/api";
|
||||
|
||||
export async function getCities(): Promise<City[]> {
|
||||
const res = await winofyFetch<PaginatedResponse<City>>("/v1/cities/", { auth: false, params: { limit: 100 } });
|
||||
return res.results;
|
||||
}
|
||||
|
||||
export async function getCity(uuid: string): Promise<City> {
|
||||
return winofyFetch<City>(`/v1/cities/${uuid}/`, { auth: false });
|
||||
}
|
||||
|
||||
export async function getNeighborhoods(params: { city?: string; limit?: number } = {}): Promise<Neighborhood[]> {
|
||||
const res = await winofyFetch<PaginatedResponse<Neighborhood>>("/v1/neighborhoods/", {
|
||||
auth: false,
|
||||
params: { limit: 200, ...params },
|
||||
});
|
||||
return res.results;
|
||||
}
|
||||
|
||||
export async function getNeighborhood(uuid: string): Promise<Neighborhood> {
|
||||
return winofyFetch<Neighborhood>(`/v1/neighborhoods/${uuid}/`, { auth: false });
|
||||
}
|
||||
22
src/lib/api/media.ts
Normal file
22
src/lib/api/media.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
"use server";
|
||||
|
||||
import { winofyFetch } from "./winofy";
|
||||
import { callApi, type ActionResult } from "./result";
|
||||
|
||||
export interface MediaPresignResult {
|
||||
upload_url: string;
|
||||
object_key: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 1 of the upload flow (§2 of the API guide): get a presigned MinIO PUT
|
||||
* URL. Step 2 — PUT the raw file bytes to `upload_url` directly (client-side,
|
||||
* no auth header, this is a MinIO URL not a Winofy one) — happens in the
|
||||
* browser, not here. Step 3 — send the returned `object_key` as the value of
|
||||
* whatever field you're setting (Store.logo, Product.image, etc.).
|
||||
*/
|
||||
export async function presignUpload(filename: string): Promise<ActionResult<MediaPresignResult>> {
|
||||
return callApi(() =>
|
||||
winofyFetch<MediaPresignResult>("/media/presign/", { method: "POST", body: { filename } }),
|
||||
);
|
||||
}
|
||||
31
src/lib/api/notifications.ts
Normal file
31
src/lib/api/notifications.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { winofyFetch, type PaginatedResponse } from "./winofy";
|
||||
import { callApi, type ActionResult } from "./result";
|
||||
import type { Notification } from "@/types/api";
|
||||
|
||||
export async function getNotifications(): Promise<Notification[]> {
|
||||
const res = await winofyFetch<PaginatedResponse<Notification>>("/v1/notifications/");
|
||||
return res.results;
|
||||
}
|
||||
|
||||
export async function getNotification(uuid: string): Promise<Notification> {
|
||||
return winofyFetch<Notification>(`/v1/notifications/${uuid}/`);
|
||||
}
|
||||
|
||||
export async function markNotificationRead(uuid: string): Promise<ActionResult<Notification>> {
|
||||
const result = await callApi(() =>
|
||||
winofyFetch<Notification>(`/v1/notifications/${uuid}/mark-read/`, { method: "POST", body: {} }),
|
||||
);
|
||||
if (result.ok) revalidatePath("/notifications");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function markAllNotificationsRead(): Promise<ActionResult> {
|
||||
const result = await callApi(() =>
|
||||
winofyFetch<undefined>("/v1/notifications/mark-all-read/", { method: "POST", body: {} }),
|
||||
);
|
||||
if (result.ok) revalidatePath("/notifications");
|
||||
return result;
|
||||
}
|
||||
28
src/lib/api/orders.ts
Normal file
28
src/lib/api/orders.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { winofyFetch, type PaginatedResponse } from "./winofy";
|
||||
import { callApi, type ActionResult } from "./result";
|
||||
import type { Order, OrderGroup } from "@/types/api";
|
||||
|
||||
export async function getOrderGroups(): Promise<OrderGroup[]> {
|
||||
const res = await winofyFetch<PaginatedResponse<OrderGroup>>("/v1/order-groups/");
|
||||
return res.results;
|
||||
}
|
||||
|
||||
export async function getOrderGroup(uuid: string): Promise<OrderGroup> {
|
||||
return winofyFetch<OrderGroup>(`/v1/order-groups/${uuid}/`);
|
||||
}
|
||||
|
||||
/** Single-order tracking — one store's slice of an OrderGroup. */
|
||||
export async function getOrder(uuid: string): Promise<Order> {
|
||||
return winofyFetch<Order>(`/v1/orders/${uuid}/`);
|
||||
}
|
||||
|
||||
export async function cancelOrder(uuid: string, cancelReason: string = ""): Promise<ActionResult<Order>> {
|
||||
const result = await callApi(() =>
|
||||
winofyFetch<Order>(`/v1/orders/${uuid}/cancel/`, { method: "POST", body: { cancel_reason: cancelReason } }),
|
||||
);
|
||||
if (result.ok) revalidatePath("/orders");
|
||||
return result;
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import "server-only";
|
||||
import { ApiError } from "@/lib/api/winofy";
|
||||
import { ApiError } from "./winofy";
|
||||
|
||||
export type ActionResult<T = undefined> =
|
||||
| { ok: true; data: T }
|
||||
| { ok: false; status: number; message: string; fieldErrors: Record<string, string[]> | null };
|
||||
|
||||
/** Wraps a winofyFetch call for use in a Server Action — never throws, always returns a typed result. */
|
||||
export async function callApi<T>(fn: () => Promise<T>): Promise<ActionResult<T>> {
|
||||
try {
|
||||
const data = await fn();
|
||||
23
src/lib/api/reviews.ts
Normal file
23
src/lib/api/reviews.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
"use server";
|
||||
|
||||
import { winofyFetch, type PaginatedResponse } from "./winofy";
|
||||
import { callApi, type ActionResult } from "./result";
|
||||
import type { Review } from "@/types/api";
|
||||
|
||||
export async function getReviews(params: { store?: string; limit?: number } = {}): Promise<Review[]> {
|
||||
const res = await winofyFetch<PaginatedResponse<Review>>("/v1/reviews/", {
|
||||
auth: false,
|
||||
params: { limit: 50, ...params },
|
||||
});
|
||||
return res.results;
|
||||
}
|
||||
|
||||
export interface CreateReviewInput {
|
||||
order_uuid: string;
|
||||
rating: number;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
export async function createReview(input: CreateReviewInput): Promise<ActionResult<Review>> {
|
||||
return callApi(() => winofyFetch<Review>("/v1/reviews/", { method: "POST", body: input }));
|
||||
}
|
||||
8
src/lib/api/seller/analytics.ts
Normal file
8
src/lib/api/seller/analytics.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import "server-only";
|
||||
import { winofyFetch } from "../winofy";
|
||||
import type { SellerAnalytics } from "@/types/api";
|
||||
|
||||
/** No date-range params are documented — the API returns one fixed reporting window. */
|
||||
export async function getSellerAnalytics(): Promise<SellerAnalytics> {
|
||||
return winofyFetch<SellerAnalytics>("/v1/seller/analytics/");
|
||||
}
|
||||
60
src/lib/api/seller/orders.ts
Normal file
60
src/lib/api/seller/orders.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { winofyFetch, type PaginatedResponse } from "../winofy";
|
||||
import { callApi, type ActionResult } from "../result";
|
||||
import type { Order, OrderStatus } from "@/types/api";
|
||||
|
||||
export async function getSellerOrders(status?: OrderStatus): Promise<Order[]> {
|
||||
const res = await winofyFetch<PaginatedResponse<Order>>("/v1/seller/orders/", {
|
||||
params: { limit: 100, status },
|
||||
});
|
||||
return res.results;
|
||||
}
|
||||
|
||||
export async function getSellerOrder(uuid: string): Promise<Order> {
|
||||
return winofyFetch<Order>(`/v1/seller/orders/${uuid}/`);
|
||||
}
|
||||
|
||||
async function sellerOrderAction(
|
||||
uuid: string,
|
||||
action: string,
|
||||
body: Record<string, unknown> = {},
|
||||
): Promise<ActionResult<Order>> {
|
||||
const result = await callApi(() =>
|
||||
winofyFetch<Order>(`/v1/seller/orders/${uuid}/${action}/`, { method: "POST", body }),
|
||||
);
|
||||
if (result.ok) {
|
||||
revalidatePath("/seller/orders");
|
||||
revalidatePath(`/seller/orders/${uuid}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function cancelSellerOrder(uuid: string, cancelReason: string = ""): Promise<ActionResult<Order>> {
|
||||
return sellerOrderAction(uuid, "cancel", { cancel_reason: cancelReason });
|
||||
}
|
||||
|
||||
/** placed → preparing */
|
||||
export async function confirmSellerOrder(uuid: string): Promise<ActionResult<Order>> {
|
||||
return sellerOrderAction(uuid, "confirm");
|
||||
}
|
||||
|
||||
/** preparing → ready_to_ship */
|
||||
export async function markSellerOrderReady(uuid: string): Promise<ActionResult<Order>> {
|
||||
return sellerOrderAction(uuid, "mark-ready");
|
||||
}
|
||||
|
||||
/** ready_to_ship → handed_to_courier */
|
||||
export async function markSellerOrderShipped(
|
||||
uuid: string,
|
||||
courierName: string,
|
||||
courierPhone: string,
|
||||
): Promise<ActionResult<Order>> {
|
||||
return sellerOrderAction(uuid, "mark-shipped", { courier_name: courierName, courier_phone: courierPhone });
|
||||
}
|
||||
|
||||
/** handed_to_courier → delivered */
|
||||
export async function markSellerOrderDelivered(uuid: string): Promise<ActionResult<Order>> {
|
||||
return sellerOrderAction(uuid, "mark-delivered");
|
||||
}
|
||||
65
src/lib/api/seller/products.ts
Normal file
65
src/lib/api/seller/products.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { winofyFetch, type PaginatedResponse } from "../winofy";
|
||||
import { callApi, type ActionResult } from "../result";
|
||||
import type { SellerProduct } from "@/types/api";
|
||||
|
||||
export async function getSellerProducts(): Promise<SellerProduct[]> {
|
||||
const res = await winofyFetch<PaginatedResponse<SellerProduct>>("/v1/seller/products/", { params: { limit: 100 } });
|
||||
return res.results;
|
||||
}
|
||||
|
||||
export async function getSellerProduct(uuid: string): Promise<SellerProduct> {
|
||||
return winofyFetch<SellerProduct>(`/v1/seller/products/${uuid}/`);
|
||||
}
|
||||
|
||||
export interface SellerProductInput {
|
||||
name: string;
|
||||
description: string;
|
||||
image?: string | null;
|
||||
category_uuid: string;
|
||||
price: number;
|
||||
unit_type: SellerProduct["unit_type"];
|
||||
unit_value: number;
|
||||
stock_quantity: number;
|
||||
low_stock_threshold: number;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export async function createSellerProduct(input: SellerProductInput): Promise<ActionResult<SellerProduct>> {
|
||||
const result = await callApi(() =>
|
||||
winofyFetch<SellerProduct>("/v1/seller/products/", { method: "POST", body: input }),
|
||||
);
|
||||
if (result.ok) revalidatePath("/seller/products");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function updateSellerProduct(
|
||||
uuid: string,
|
||||
input: Partial<SellerProductInput>,
|
||||
): Promise<ActionResult<SellerProduct>> {
|
||||
const result = await callApi(() =>
|
||||
winofyFetch<SellerProduct>(`/v1/seller/products/${uuid}/`, { method: "PATCH", body: input }),
|
||||
);
|
||||
if (result.ok) revalidatePath("/seller/products");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function deleteSellerProduct(uuid: string): Promise<ActionResult> {
|
||||
const result = await callApi(() => winofyFetch<undefined>(`/v1/seller/products/${uuid}/`, { method: "DELETE" }));
|
||||
if (result.ok) revalidatePath("/seller/products");
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Focused inventory-only update — returns 204, no body, unlike the general PATCH above. */
|
||||
export async function updateSellerProductStock(
|
||||
uuid: string,
|
||||
input: { stock_quantity: number; low_stock_threshold?: number },
|
||||
): Promise<ActionResult> {
|
||||
const result = await callApi(() =>
|
||||
winofyFetch<undefined>(`/v1/seller/products/${uuid}/stock/`, { method: "PATCH", body: input }),
|
||||
);
|
||||
if (result.ok) revalidatePath("/seller/products");
|
||||
return result;
|
||||
}
|
||||
26
src/lib/api/seller/reviews.ts
Normal file
26
src/lib/api/seller/reviews.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { winofyFetch, type PaginatedResponse } from "../winofy";
|
||||
import { callApi, type ActionResult } from "../result";
|
||||
import type { Review } from "@/types/api";
|
||||
|
||||
export async function getSellerReviews(): Promise<Review[]> {
|
||||
const res = await winofyFetch<PaginatedResponse<Review>>("/v1/seller/reviews/", { params: { limit: 100 } });
|
||||
return res.results;
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ The spec documents this action's request body as the full customer-facing
|
||||
* review-write shape (order_uuid/rating/comment), which doesn't make sense for
|
||||
* a reply. Sending `seller_reply` here as the best guess (matches the response
|
||||
* field name) — confirm the real field name with backend (or from a 400's
|
||||
* field-error keys) before relying on this.
|
||||
*/
|
||||
export async function replySellerReview(uuid: string, replyText: string): Promise<ActionResult<Review>> {
|
||||
const result = await callApi(() =>
|
||||
winofyFetch<Review>(`/v1/seller/reviews/${uuid}/reply/`, { method: "POST", body: { seller_reply: replyText } }),
|
||||
);
|
||||
if (result.ok) revalidatePath("/seller/reviews");
|
||||
return result;
|
||||
}
|
||||
65
src/lib/api/seller/store.ts
Normal file
65
src/lib/api/seller/store.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { winofyFetch, ApiError } from "../winofy";
|
||||
import { callApi, type ActionResult } from "../result";
|
||||
import type { SellerStore, WorkingHours } from "@/types/api";
|
||||
|
||||
/** `null` means the seller hasn't created a store yet (404) — not an error state to surface. */
|
||||
export async function getSellerStore(): Promise<SellerStore | null> {
|
||||
try {
|
||||
return await winofyFetch<SellerStore>("/v1/seller/store/");
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 404) return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SellerStoreInput {
|
||||
name: string;
|
||||
category_uuid: string;
|
||||
description: string;
|
||||
logo?: string | null;
|
||||
cover_image?: string | null;
|
||||
phone_number: string;
|
||||
city_uuid: string;
|
||||
address: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
service_neighborhood_uuids: string[];
|
||||
delivery_radius_km: string;
|
||||
min_order_amount: number;
|
||||
delivery_fee: number;
|
||||
free_delivery_threshold?: number | null;
|
||||
accepts_wallet: boolean;
|
||||
accepts_online: boolean;
|
||||
accepts_cash_on_delivery: boolean;
|
||||
is_open: boolean;
|
||||
}
|
||||
|
||||
export async function createSellerStore(input: SellerStoreInput): Promise<ActionResult<SellerStore>> {
|
||||
const result = await callApi(() => winofyFetch<SellerStore>("/v1/seller/store/", { method: "POST", body: input }));
|
||||
if (result.ok) revalidatePath("/seller");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function updateSellerStore(input: Partial<SellerStoreInput>): Promise<ActionResult<SellerStore>> {
|
||||
const result = await callApi(() =>
|
||||
winofyFetch<SellerStore>("/v1/seller/store/", { method: "PATCH", body: input }),
|
||||
);
|
||||
if (result.ok) revalidatePath("/seller");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getSellerWorkingHours(): Promise<WorkingHours[]> {
|
||||
return winofyFetch<WorkingHours[]>("/v1/seller/store/working-hours/");
|
||||
}
|
||||
|
||||
/** Full replace — send all 7 days (weekday 0–6) in one call. */
|
||||
export async function setSellerWorkingHours(hours: WorkingHours[]): Promise<ActionResult<WorkingHours[]>> {
|
||||
const result = await callApi(() =>
|
||||
winofyFetch<WorkingHours[]>("/v1/seller/store/working-hours/", { method: "PUT", body: hours }),
|
||||
);
|
||||
if (result.ok) revalidatePath("/seller");
|
||||
return result;
|
||||
}
|
||||
32
src/lib/api/stores.ts
Normal file
32
src/lib/api/stores.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import "server-only";
|
||||
import { winofyFetch, type PaginatedResponse } from "./winofy";
|
||||
import type { StoreCategory, StoreDetail, StoreListItem } from "@/types/api";
|
||||
|
||||
export async function getStoreCategories(): Promise<StoreCategory[]> {
|
||||
const res = await winofyFetch<PaginatedResponse<StoreCategory>>("/v1/store-categories/", {
|
||||
auth: false,
|
||||
params: { limit: 100 },
|
||||
});
|
||||
return res.results;
|
||||
}
|
||||
|
||||
export interface GetStoresParams {
|
||||
category?: string;
|
||||
search?: string;
|
||||
neighborhood?: string;
|
||||
lat?: number;
|
||||
lng?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export async function getStores(params: GetStoresParams = {}): Promise<StoreListItem[]> {
|
||||
const res = await winofyFetch<PaginatedResponse<StoreListItem>>("/v1/stores/", {
|
||||
auth: false,
|
||||
params: { limit: 20, ...params },
|
||||
});
|
||||
return res.results;
|
||||
}
|
||||
|
||||
export async function getStore(uuid: string): Promise<StoreDetail> {
|
||||
return winofyFetch<StoreDetail>(`/v1/stores/${uuid}/`, { auth: false });
|
||||
}
|
||||
149
src/types/api.ts
149
src/types/api.ts
|
|
@ -1,13 +1,17 @@
|
|||
/**
|
||||
* Shared API types mirrored from FRONTEND_GUIDE.md. Money fields are integer Toman.
|
||||
* Shared API types, mirrored from winofyapifrontendintegrationguide.md + the
|
||||
* OpenAPI spec (source of truth for field names/shapes). Money fields are
|
||||
* integer Toman. Grouped to match src/lib/api/'s feature layout.
|
||||
*
|
||||
* Media fields (`icon`, `image`, `logo`, `cover_image`) are MinIO object keys, not
|
||||
* URLs — per FRONTEND_GUIDE.md §4.7 (feature/minio-integration), every one of them
|
||||
* has a `<field>_url` sibling that's a freshly presigned, already-absolute URL.
|
||||
* Always render the `_url` field directly; never resolve the raw key yourself, and
|
||||
* don't cache `_url` past the response — it's re-signed (and can change) on every read.
|
||||
* Media fields (`icon`, `image`, `logo`, `cover_image`) are MinIO object keys,
|
||||
* not URLs — every one has a `<field>_url` sibling that's a freshly presigned,
|
||||
* already-absolute URL. Always render the `_url` field directly; never resolve
|
||||
* the raw key yourself, and don't cache `_url` past the response — it's
|
||||
* re-signed (and can change) on every read.
|
||||
*/
|
||||
|
||||
// ---- Locations --------------------------------------------------------------
|
||||
|
||||
export interface City {
|
||||
uuid: string;
|
||||
name: string;
|
||||
|
|
@ -17,10 +21,13 @@ export interface City {
|
|||
export interface Neighborhood {
|
||||
uuid: string;
|
||||
name: string;
|
||||
slug?: string;
|
||||
/** Nested object, confirmed against staging — not a bare uuid string. */
|
||||
city: City;
|
||||
}
|
||||
|
||||
// ---- Stores / Catalog ---------------------------------------------------
|
||||
|
||||
export interface StoreCategory {
|
||||
uuid: string;
|
||||
name: string;
|
||||
|
|
@ -46,11 +53,10 @@ export interface StoreListItem {
|
|||
is_open: boolean;
|
||||
}
|
||||
|
||||
|
||||
export interface WorkingHours {
|
||||
weekday: 0 | 1 | 2 | 3 | 4 | 5 | 6; // 0 = Saturday .. 6 = Friday
|
||||
opens_at: string;
|
||||
closes_at: string;
|
||||
opens_at: string | null;
|
||||
closes_at: string | null;
|
||||
is_closed: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +98,8 @@ export interface ProductDetail extends ProductListItem {
|
|||
store: StoreListItem;
|
||||
}
|
||||
|
||||
// ---- Addresses ----------------------------------------------------------
|
||||
|
||||
export type AddressLabel = "home" | "work" | "other";
|
||||
|
||||
export interface Address {
|
||||
|
|
@ -112,6 +120,8 @@ export interface Address {
|
|||
created_at: string;
|
||||
}
|
||||
|
||||
// ---- Cart -----------------------------------------------------------------
|
||||
|
||||
export interface CartItem {
|
||||
uuid: string;
|
||||
product: ProductListItem;
|
||||
|
|
@ -133,20 +143,16 @@ export interface Cart {
|
|||
grand_total: number;
|
||||
}
|
||||
|
||||
// ---- Checkout / Orders ---------------------------------------------------
|
||||
|
||||
export type DeliveryType = "express" | "scheduled";
|
||||
export type PaymentMethod = "wallet" | "online" | "cash_on_delivery";
|
||||
export type PaymentStatus = "pending" | "paid" | "failed";
|
||||
export type OrderStatus =
|
||||
| "placed"
|
||||
| "preparing"
|
||||
| "ready_to_ship"
|
||||
| "handed_to_courier"
|
||||
| "delivered"
|
||||
| "cancelled";
|
||||
export type OrderStatus = "placed" | "preparing" | "ready_to_ship" | "handed_to_courier" | "delivered" | "cancelled";
|
||||
|
||||
export interface OrderItem {
|
||||
uuid: string;
|
||||
product: string;
|
||||
product: string | null;
|
||||
product_name_snapshot: string;
|
||||
unit_price_snapshot: number;
|
||||
quantity: number;
|
||||
|
|
@ -195,3 +201,112 @@ export interface OrderGroup {
|
|||
/** `feature/payment` only. */
|
||||
payment?: { method: PaymentMethod; status: string; payment_url?: string };
|
||||
}
|
||||
|
||||
// ---- Reviews --------------------------------------------------------------
|
||||
|
||||
export interface Review {
|
||||
uuid: string;
|
||||
store: string;
|
||||
product: string | null;
|
||||
rating: number;
|
||||
comment: string;
|
||||
seller_reply: string | null;
|
||||
customer_name: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// ---- Notifications --------------------------------------------------------
|
||||
|
||||
export type NotificationType = "new_order" | "settlement_done" | "new_review" | "order_cancelled";
|
||||
|
||||
export interface Notification {
|
||||
uuid: string;
|
||||
type: NotificationType;
|
||||
title: string;
|
||||
body: string;
|
||||
related_order: string | null;
|
||||
is_read: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// ---- Seller · Store ---------------------------------------------------
|
||||
|
||||
export type SellerStoreStatus = "pending" | "approved" | "suspended";
|
||||
|
||||
export interface StoreBankAccount {
|
||||
iban: string;
|
||||
account_holder_name: string;
|
||||
}
|
||||
|
||||
export interface SellerStore {
|
||||
uuid: string;
|
||||
name: string;
|
||||
category: StoreCategory;
|
||||
description: string;
|
||||
logo: string | null;
|
||||
logo_url: string | null;
|
||||
cover_image: string | null;
|
||||
cover_image_url: string | null;
|
||||
phone_number: string;
|
||||
city: City;
|
||||
address: string;
|
||||
service_neighborhoods: Neighborhood[];
|
||||
/** DRF decimal-as-string, e.g. "5.00". */
|
||||
delivery_radius_km: string;
|
||||
min_order_amount: number;
|
||||
delivery_fee: number;
|
||||
free_delivery_threshold: number | null;
|
||||
accepts_wallet: boolean;
|
||||
accepts_online: boolean;
|
||||
accepts_cash_on_delivery: boolean;
|
||||
rating_avg: string;
|
||||
rating_count: number;
|
||||
status: SellerStoreStatus;
|
||||
is_open: boolean;
|
||||
bank_account: StoreBankAccount | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// ---- Seller · Products ---------------------------------------------------
|
||||
|
||||
export interface SellerProduct {
|
||||
uuid: string;
|
||||
name: string;
|
||||
description: string;
|
||||
image: string | null;
|
||||
image_url: string | null;
|
||||
category: ProductCategory;
|
||||
price: number;
|
||||
unit_type: UnitType;
|
||||
unit_value: number;
|
||||
stock_quantity: number;
|
||||
low_stock_threshold: number;
|
||||
is_active: boolean;
|
||||
sold_count: number;
|
||||
is_out_of_stock: boolean;
|
||||
is_low_stock: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// ---- Seller · Analytics ---------------------------------------------------
|
||||
|
||||
export interface SalesChartPoint {
|
||||
bucket: string;
|
||||
sales: number;
|
||||
}
|
||||
|
||||
export interface TopProduct {
|
||||
name: string;
|
||||
units_sold: number;
|
||||
revenue: number;
|
||||
}
|
||||
|
||||
export interface SellerAnalytics {
|
||||
period: string;
|
||||
order_count: number;
|
||||
total_sales: number;
|
||||
cancellation_rate: number;
|
||||
average_order_value: number;
|
||||
chart: SalesChartPoint[];
|
||||
top_products: TopProduct[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue