From 358bfc7ac91c5827461623740372426d2c779f51 Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sat, 25 Apr 2026 14:49:25 +0800 Subject: [PATCH] feat(api): add shop management clients --- lib/api/index.ts | 15 +++- lib/api/shops.ts | 161 ++++++++++++++++++++++++++++++++++-- lib/domain/shop-template.ts | 24 ++++++ 3 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 lib/domain/shop-template.ts diff --git a/lib/api/index.ts b/lib/api/index.ts index 520bf83..91c5c20 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -21,7 +21,20 @@ export { listServicesByPlayer, updatePlayerService, } from "./services" -export { getShopById, getShopByOwnerId, listShops } from "./shops" +export { + addShopAnnouncement, + deleteShopAnnouncement, + getMyShop, + getShopById, + getShopByOwnerId, + getShopIncomeStats, + inviteShopPlayer, + listShopInvitations, + listShops, + removeShopPlayer, + updateShop, + updateShopTemplate, +} from "./shops" export { getWalletBalance, listWalletTransactions, diff --git a/lib/api/shops.ts b/lib/api/shops.ts index 5e2fdd9..d794228 100644 --- a/lib/api/shops.ts +++ b/lib/api/shops.ts @@ -1,10 +1,11 @@ +import { getDefaultShopSections } from "@/lib/domain/shop-template" import { isApiError } from "@/lib/errors" -import type { Shop } from "@/lib/types" +import type { Shop, ShopTemplateConfig } from "@/lib/types" import { httpJson } from "./http" type Paginated = { - items: T[] + items: T[] | null meta: { total: number offset: number @@ -12,18 +13,101 @@ type Paginated = { } } +type ShopResponse = Omit & { + announcements?: string[] | null + templateConfig?: ShopTemplateConfig | null +} + +export type ShopUpdateInput = Pick< + Shop, + | "name" + | "description" + | "commissionType" + | "commissionValue" + | "allowMultiShop" + | "allowIndependentOrders" + | "dispatchMode" +> + +export type ShopInvitation = { + id: number + shopId: number + playerId: number + status: string + invitedBy: number + createdAt: number + respondedAt?: number +} + +export type ShopIncomeStats = { + monthlyIncome: string + pendingSettlement: string + totalWithdrawn: string + totalOrders: number + completedOrders: number +} + +type ShopInvitationList = { + items: ShopInvitation[] | null +} + +function normalizeAnnouncement(value: string): string { + const trimmed = value.trim() + if (!trimmed.startsWith('"') || !trimmed.endsWith('"')) return value + + try { + const parsed = JSON.parse(trimmed) as unknown + return typeof parsed === "string" ? parsed : value + } catch { + return value + } +} + +function normalizeShop(shop: ShopResponse): Shop { + const templateConfig = shop.templateConfig ?? { sections: getDefaultShopSections() } + + return { + ...shop, + announcements: (shop.announcements ?? []).map(normalizeAnnouncement), + templateConfig: { + ...templateConfig, + sections: Array.isArray(templateConfig.sections) + ? templateConfig.sections + : getDefaultShopSections(), + }, + } +} + export async function listShops(): Promise { - const res = await httpJson>("/api/v1/shops?offset=0&limit=100", { + const res = await httpJson>("/api/v1/shops?offset=0&limit=100", { cache: "no-store", }) - return res.items + return (res.items ?? []).map(normalizeShop) +} + +export async function getMyShop(): Promise { + try { + const shop = await httpJson("/api/v1/shops/mine", { + cache: "no-store", + }) + return normalizeShop(shop) + } catch (error) { + if (error instanceof Error && error.message === "UNAUTHORIZED") { + throw error + } + if (isApiError(error) && error.code === 404) { + return undefined + } + throw error + } } export async function getShopById(shopId: string): Promise { try { - return await httpJson(`/api/v1/shops/${encodeURIComponent(shopId)}`, { + const shop = await httpJson(`/api/v1/shops/${encodeURIComponent(shopId)}`, { cache: "no-store", }) + return normalizeShop(shop) } catch (error) { if (error instanceof Error && error.message === "UNAUTHORIZED") { throw error @@ -37,9 +121,10 @@ export async function getShopById(shopId: string): Promise { export async function getShopByOwnerId(ownerId: string): Promise { try { - return await httpJson(`/api/v1/users/${encodeURIComponent(ownerId)}/shop`, { + const shop = await httpJson(`/api/v1/users/${encodeURIComponent(ownerId)}/shop`, { cache: "no-store", }) + return normalizeShop(shop) } catch (error) { if (error instanceof Error && error.message === "UNAUTHORIZED") { throw error @@ -50,3 +135,67 @@ export async function getShopByOwnerId(ownerId: string): Promise { + const shop = await httpJson(`/api/v1/shops/${encodeURIComponent(shopId)}`, { + method: "PUT", + json: input, + }) + return normalizeShop(shop) +} + +export async function updateShopTemplate( + shopId: string, + templateConfig: ShopTemplateConfig, +): Promise { + await httpJson(`/api/v1/shops/${encodeURIComponent(shopId)}/template`, { + method: "PUT", + json: { + sections: JSON.stringify(templateConfig), + }, + }) +} + +export async function addShopAnnouncement(shopId: string, content: string): Promise { + await httpJson(`/api/v1/shops/${encodeURIComponent(shopId)}/announcements`, { + method: "POST", + json: { content }, + }) +} + +export async function deleteShopAnnouncement(shopId: string, index: number): Promise { + await httpJson( + `/api/v1/shops/${encodeURIComponent(shopId)}/announcements/${encodeURIComponent(String(index))}`, + { method: "DELETE" }, + ) +} + +export async function inviteShopPlayer(shopId: string, playerId: string): Promise { + await httpJson(`/api/v1/shops/${encodeURIComponent(shopId)}/invitations`, { + method: "POST", + json: { + playerId: Number(playerId), + }, + }) +} + +export async function listShopInvitations(shopId: string): Promise { + const res = await httpJson( + `/api/v1/shops/${encodeURIComponent(shopId)}/invitations`, + { cache: "no-store" }, + ) + return res.items ?? [] +} + +export async function removeShopPlayer(shopId: string, playerId: string): Promise { + await httpJson( + `/api/v1/shops/${encodeURIComponent(shopId)}/players/${encodeURIComponent(playerId)}`, + { method: "DELETE" }, + ) +} + +export async function getShopIncomeStats(shopId: string): Promise { + return httpJson(`/api/v1/shops/${encodeURIComponent(shopId)}/income-stats`, { + cache: "no-store", + }) +} diff --git a/lib/domain/shop-template.ts b/lib/domain/shop-template.ts new file mode 100644 index 0000000..299f667 --- /dev/null +++ b/lib/domain/shop-template.ts @@ -0,0 +1,24 @@ +import type { Shop, ShopSection } from "@/lib/types" + +const defaultSectionTypes: ShopSection["type"][] = [ + "banner", + "intro", + "services", + "players", + "announcements", + "reviews", +] + +export function getDefaultShopSections(): ShopSection[] { + return defaultSectionTypes.map((type, order) => ({ + type, + enabled: true, + order, + })) +} + +export function getShopSections(shop: Pick): ShopSection[] { + const sections = shop.templateConfig.sections + if (!Array.isArray(sections) || sections.length === 0) return getDefaultShopSections() + return [...sections].sort((a, b) => a.order - b.order) +}