feature/WNPT-733 #1

Merged
afra.khalaj merged 6 commits from feature/WNPT-733 into main 2026-08-26 04:17:36 -04:00
5 changed files with 74 additions and 29 deletions
Showing only changes of commit 0dc0cec4a6 - Show all commits

View file

@ -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} />

View file

@ -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 */

View file

@ -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
View 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))}`);
}

View file

@ -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;