121 lines
3.6 KiB
TypeScript
121 lines
3.6 KiB
TypeScript
import { useCallback } from 'react';
|
|
import useAuthStore from '../../../store/authStore';
|
|
|
|
export type CoffeeShippingFeePieceCount = 60 | 120;
|
|
|
|
export type CoffeeShippingFee = {
|
|
pieceCount: CoffeeShippingFeePieceCount;
|
|
price: number;
|
|
};
|
|
|
|
function isFormData(body: any): body is FormData {
|
|
return typeof FormData !== 'undefined' && body instanceof FormData;
|
|
}
|
|
|
|
export default function useCoffeeShippingFees() {
|
|
const base = process.env.NEXT_PUBLIC_API_BASE_URL || '';
|
|
const getState = useAuthStore.getState;
|
|
|
|
const authorizedFetch = useCallback(
|
|
async <T = any>(
|
|
path: string,
|
|
init: RequestInit = {},
|
|
responseType: 'json' | 'text' | 'blob' = 'json'
|
|
): Promise<T> => {
|
|
let token = getState().accessToken;
|
|
if (!token) {
|
|
const ok = await getState().refreshAuthToken();
|
|
if (ok) token = getState().accessToken;
|
|
}
|
|
|
|
const headers: Record<string, string> = {
|
|
...((init.headers as Record<string, string>) || {}),
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
};
|
|
if (!isFormData(init.body) && init.method && init.method !== 'GET') {
|
|
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
|
|
}
|
|
|
|
const res = await fetch(`${base}${path}`, {
|
|
credentials: 'include',
|
|
...init,
|
|
headers,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => '');
|
|
throw new Error(text || `HTTP ${res.status}`);
|
|
}
|
|
|
|
if (responseType === 'blob') return (await res.blob()) as unknown as T;
|
|
if (responseType === 'text') return (await res.text()) as unknown as T;
|
|
const text = await res.text();
|
|
try {
|
|
return JSON.parse(text) as T;
|
|
} catch {
|
|
return {} as T;
|
|
}
|
|
},
|
|
[base, getState]
|
|
);
|
|
|
|
const listShippingFees = useCallback(async (): Promise<CoffeeShippingFee[]> => {
|
|
const res = await fetch(`${base}/api/shipping-fees`, {
|
|
method: 'GET',
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => '');
|
|
throw new Error(text || `HTTP ${res.status}`);
|
|
}
|
|
|
|
const raw = (await res.json().catch(() => [])) as any;
|
|
if (!Array.isArray(raw)) return [];
|
|
|
|
const normalizePieceCount = (v: any): CoffeeShippingFeePieceCount | null => {
|
|
const n = typeof v === 'number' ? v : (typeof v === 'string' && /^\d+$/.test(v) ? Number(v) : NaN);
|
|
if (n === 60 || n === 120) return n;
|
|
return null;
|
|
};
|
|
|
|
return raw
|
|
.map((r: any) => {
|
|
const pieceCount = normalizePieceCount(r?.pieceCount);
|
|
if (!pieceCount) return null;
|
|
const price = r?.price != null && r?.price !== '' ? Number(r.price) : 0;
|
|
return { pieceCount, price } satisfies CoffeeShippingFee;
|
|
})
|
|
.filter(Boolean) as CoffeeShippingFee[];
|
|
}, [base]);
|
|
|
|
const updateShippingFee = useCallback(
|
|
async (pieceCount: CoffeeShippingFeePieceCount, price: number): Promise<CoffeeShippingFee> => {
|
|
if (!(pieceCount === 60 || pieceCount === 120)) {
|
|
throw new Error('pieceCount must be 60 or 120');
|
|
}
|
|
if (!Number.isFinite(price) || price < 0) {
|
|
throw new Error('price must be a number >= 0');
|
|
}
|
|
|
|
const row = await authorizedFetch<any>(`/api/admin/shipping-fees/${pieceCount}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ price }),
|
|
});
|
|
|
|
const normalized: CoffeeShippingFee = {
|
|
pieceCount,
|
|
price: row?.price != null && row?.price !== '' ? Number(row.price) : price,
|
|
};
|
|
|
|
return normalized;
|
|
},
|
|
[authorizedFetch]
|
|
);
|
|
|
|
return {
|
|
listShippingFees,
|
|
updateShippingFee,
|
|
};
|
|
}
|