perf: design enhancements implemented

This commit is contained in:
Afra ‌ 2026-08-26 11:45:36 +03:30
parent 2490e9a21f
commit 0dc0cec4a6
5 changed files with 74 additions and 29 deletions

View file

@ -46,7 +46,7 @@ export default async function HomePage({ searchParams }: PageProps<"/">) {
</div> </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> <h2 className="px-4 text-2xl font-extrabold text-foreground">فروشگاه‌های نزدیک</h2>
<Suspense fallback={null}> <Suspense fallback={null}>
<CategoryChips categories={categories} /> <CategoryChips categories={categories} />

View file

@ -6,7 +6,7 @@
* don't invent any rather than guess. * don't invent any rather than guess.
*/ */
:root { :root {
--background: #f7f2fa; /* پس‌زمینه — app background */ --background: #FFFFFF; /* پس‌زمینه — app background */
--surface: #fffbfe; /* سفید — card/sheet surface */ --surface: #fffbfe; /* سفید — card/sheet surface */
--foreground: #111827; /* gray-900 */ --foreground: #111827; /* gray-900 */

View file

@ -18,13 +18,15 @@ export function CategoryChips({ categories }: { categories: StoreCategory[] }) {
} }
return ( 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 <button
type="button" type="button"
onClick={() => select(null)} onClick={() => select(null)}
className={cn( className={cn(
"shrink-0 rounded-full px-4 py-2.5 text-body-medium font-medium", "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", !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" type="button"
onClick={() => select(category.uuid)} onClick={() => select(category.uuid)}
className={cn( 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 activeUuid === category.uuid
? "bg-primary-action text-white" ? "bg-primary-action text-white"
: "border border-gray-300 bg-white text-foreground", : "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 "server-only";
import { getValidSession } from "@/lib/auth/session"; import { getValidSession } from "@/lib/auth/session";
import { ApiError, type WinofyErrorDetails } from "./errors"; import { ApiError, type WinofyErrorDetails } from "./errors";
import { logWinofyRequest, logWinofyResponse } from "./dev-logger";
const BASE_URL = process.env.WINOFY_API_BASE_URL!; const BASE_URL = process.env.WINOFY_API_BASE_URL!;
const DEBUG = process.env.NODE_ENV !== "production";
interface WinofyFetchOptions extends Omit<RequestInit, "body"> { interface WinofyFetchOptions extends Omit<RequestInit, "body"> {
/** Attach the caller's bearer token. Defaults to true — set false for public endpoints. */ /** Attach the caller's bearer token. Defaults to true — set false for public endpoints. */
@ -19,25 +19,6 @@ export interface PaginatedResponse<T> {
results: 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> { export async function winofyFetch<T>(path: string, options: WinofyFetchOptions = {}): Promise<T> {
const { auth = true, params, headers, body, ...rest } = options; const { auth = true, params, headers, body, ...rest } = options;
const method = rest.method ?? "GET"; 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}`); if (session) finalHeaders.set("Authorization", `Bearer ${session.accessToken}`);
} }
logRequest(method, url.toString(), body); logWinofyRequest(method, url.toString(), body);
const res = await fetch(url.toString(), { const res = await fetch(url.toString(), {
...rest, ...rest,
@ -70,12 +51,12 @@ export async function winofyFetch<T>(path: string, options: WinofyFetchOptions =
}); });
if (res.status === 204) { if (res.status === 204) {
logResponse(method, url.toString(), res.status, null); logWinofyResponse(method, url.toString(), res.status, null);
return undefined as T; return undefined as T;
} }
const data = await res.json().catch(() => null); const data = await res.json().catch(() => null);
logResponse(method, url.toString(), res.status, data); logWinofyResponse(method, url.toString(), res.status, data);
if (!res.ok) { if (!res.ok) {
const envelope = data as { status_message?: string; details?: WinofyErrorDetails } | null; const envelope = data as { status_message?: string; details?: WinofyErrorDetails } | null;