feature/WNPT-733 #1
5 changed files with 74 additions and 29 deletions
|
|
@ -46,7 +46,7 @@ export default async function HomePage({ searchParams }: PageProps<"/">) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<section className="flex flex-col gap-3 pt-4">
|
||||
<section className="flex flex-col gap-4 pt-4">
|
||||
<h2 className="px-4 text-2xl font-extrabold text-foreground">فروشگاههای نزدیک</h2>
|
||||
<Suspense fallback={null}>
|
||||
<CategoryChips categories={categories} />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* don't invent any rather than guess.
|
||||
*/
|
||||
:root {
|
||||
--background: #f7f2fa; /* پسزمینه — app background */
|
||||
--background: #FFFFFF; /* پسزمینه — app background */
|
||||
--surface: #fffbfe; /* سفید — card/sheet surface */
|
||||
--foreground: #111827; /* gray-900 */
|
||||
|
||||
|
|
|
|||
|
|
@ -18,13 +18,15 @@ export function CategoryChips({ categories }: { categories: StoreCategory[] }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 overflow-x-auto px-4 pb-1">
|
||||
<div className="flex gap-2 overflow-x-auto px-4 min-h-full my-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => select(null)}
|
||||
className={cn(
|
||||
"shrink-0 rounded-full px-4 py-2.5 text-body-medium font-medium",
|
||||
!activeUuid ? "bg-primary-action text-white" : "border border-gray-300 bg-white text-foreground",
|
||||
"shrink-0 rounded-full px-4 py-2 mb-4 text-body-medium font-medium flex items-center",
|
||||
!activeUuid
|
||||
? "bg-primary-action text-white"
|
||||
: "border border-gray-300 bg-white text-foreground",
|
||||
)}
|
||||
>
|
||||
همه
|
||||
|
|
@ -35,7 +37,7 @@ export function CategoryChips({ categories }: { categories: StoreCategory[] }) {
|
|||
type="button"
|
||||
onClick={() => select(category.uuid)}
|
||||
className={cn(
|
||||
"shrink-0 rounded-full px-4 py-2.5 text-body-medium font-medium",
|
||||
"shrink-0 rounded-full px-3 py-2 text-body-medium font-medium flex items-center mb-4",
|
||||
activeUuid === category.uuid
|
||||
? "bg-primary-action text-white"
|
||||
: "border border-gray-300 bg-white text-foreground",
|
||||
|
|
|
|||
62
src/lib/api/dev-logger.ts
Normal file
62
src/lib/api/dev-logger.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import "server-only";
|
||||
|
||||
const DEBUG = process.env.NODE_ENV !== "production";
|
||||
const MAX_ARRAY_ITEMS = 3;
|
||||
|
||||
const RESET = "\x1b[0m";
|
||||
const CYAN = "\x1b[36m";
|
||||
const GREEN = "\x1b[32m";
|
||||
const YELLOW = "\x1b[33m";
|
||||
const RED = "\x1b[31m";
|
||||
const BOLD = "\x1b[1m";
|
||||
const DIM = "\x1b[2m";
|
||||
|
||||
function statusColor(status: number): string {
|
||||
if (status >= 500) return RED;
|
||||
if (status >= 400) return YELLOW;
|
||||
return GREEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps big paginated lists (e.g. a 19-store `results` array) from flooding the
|
||||
* terminal — shows the first few items of any array plus a count, recursively.
|
||||
*/
|
||||
function truncateForLog(value: unknown): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
const shown = value.slice(0, MAX_ARRAY_ITEMS).map(truncateForLog);
|
||||
if (value.length > MAX_ARRAY_ITEMS) shown.push(`… ${value.length - MAX_ARRAY_ITEMS} more item(s)`);
|
||||
return shown;
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(Object.entries(value).map(([key, v]) => [key, truncateForLog(v)]));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function prettyJson(value: unknown): string {
|
||||
return JSON.stringify(truncateForLog(value), null, 2);
|
||||
}
|
||||
|
||||
function indent(text: string): string {
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Dev-only request/response logging — the terminal-equivalent of Chrome's Network
|
||||
* tab for server-side calls, since they run on the Next.js server and the browser
|
||||
* never sees them (no CORS on the Winofy API). Never runs in production.
|
||||
*/
|
||||
export function logWinofyRequest(method: string, url: string, body: unknown) {
|
||||
if (!DEBUG) return;
|
||||
console.log(`\n${CYAN}→ [winofy]${RESET} ${BOLD}${method}${RESET} ${url}`);
|
||||
if (body !== undefined) console.log(`${DIM} body:${RESET}\n${indent(prettyJson(body))}`);
|
||||
}
|
||||
|
||||
export function logWinofyResponse(method: string, url: string, status: number, data: unknown) {
|
||||
if (!DEBUG) return;
|
||||
console.log(`${statusColor(status)}← [winofy] ${status}${RESET} ${BOLD}${method}${RESET} ${url}`);
|
||||
if (data !== null && data !== undefined) console.log(`${DIM} data:${RESET}\n${indent(prettyJson(data))}`);
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import "server-only";
|
||||
import { getValidSession } from "@/lib/auth/session";
|
||||
import { ApiError, type WinofyErrorDetails } from "./errors";
|
||||
import { logWinofyRequest, logWinofyResponse } from "./dev-logger";
|
||||
|
||||
const BASE_URL = process.env.WINOFY_API_BASE_URL!;
|
||||
const DEBUG = process.env.NODE_ENV !== "production";
|
||||
|
||||
interface WinofyFetchOptions extends Omit<RequestInit, "body"> {
|
||||
/** Attach the caller's bearer token. Defaults to true — set false for public endpoints. */
|
||||
|
|
@ -19,25 +19,6 @@ export interface PaginatedResponse<T> {
|
|||
results: T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dev-only request/response logging — this is the terminal-equivalent of Chrome's
|
||||
* Network tab for these calls, since they run server-side and the browser never
|
||||
* sees them (no CORS on the Winofy API). Prints method/URL/body going out and
|
||||
* status/body coming back. Never runs in production (checked once via DEBUG,
|
||||
* and nothing here writes to a file or leaves the process).
|
||||
*/
|
||||
function logRequest(method: string, url: string, body: unknown) {
|
||||
if (!DEBUG) return;
|
||||
console.log(`\n→ [winofy] ${method} ${url}`);
|
||||
if (body !== undefined) console.log(" body:", JSON.stringify(body));
|
||||
}
|
||||
|
||||
function logResponse(method: string, url: string, status: number, data: unknown) {
|
||||
if (!DEBUG) return;
|
||||
console.log(`← [winofy] ${status} ${method} ${url}`);
|
||||
console.log(" data:", JSON.stringify(data));
|
||||
}
|
||||
|
||||
export async function winofyFetch<T>(path: string, options: WinofyFetchOptions = {}): Promise<T> {
|
||||
const { auth = true, params, headers, body, ...rest } = options;
|
||||
const method = rest.method ?? "GET";
|
||||
|
|
@ -60,7 +41,7 @@ export async function winofyFetch<T>(path: string, options: WinofyFetchOptions =
|
|||
if (session) finalHeaders.set("Authorization", `Bearer ${session.accessToken}`);
|
||||
}
|
||||
|
||||
logRequest(method, url.toString(), body);
|
||||
logWinofyRequest(method, url.toString(), body);
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
...rest,
|
||||
|
|
@ -70,12 +51,12 @@ export async function winofyFetch<T>(path: string, options: WinofyFetchOptions =
|
|||
});
|
||||
|
||||
if (res.status === 204) {
|
||||
logResponse(method, url.toString(), res.status, null);
|
||||
logWinofyResponse(method, url.toString(), res.status, null);
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
const data = await res.json().catch(() => null);
|
||||
logResponse(method, url.toString(), res.status, data);
|
||||
logWinofyResponse(method, url.toString(), res.status, data);
|
||||
|
||||
if (!res.ok) {
|
||||
const envelope = data as { status_message?: string; details?: WinofyErrorDetails } | null;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue