Files
meezi/web/finder/src/lib/api.ts
T
soroush.asadi 42d4cb896a feat(finder): AI-powered cafe finder PWA with Next.js 16
Public cafe discovery app:
- SEO-optimised pages: home, /cafe/[slug], /search, /city/[city]
- AI search bar with natural language queries
- Structured data (JSON-LD) for Google rich results
- City browsing, rating/filter sidebar, similar cafes
- Review listing, full menu preview, working-hours card
- Web App Manifest + offline fallback page (PWA)
- Next.js 16: params/searchParams typed as Promise<{}>
- Fix Lucide icon title→aria-label (type removed upstream)
- "use client" on offline page (onClick handler)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-27 21:34:47 +03:30

101 lines
4.0 KiB
TypeScript

import type {
ApiResponse,
CafeDiscoverDto,
CafePublicDto,
PublicMenuDto,
CafeReviewDto,
NlpHints,
DiscoverFilters,
} from "@/lib/types";
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://api.meezi.ir";
async function get<T>(path: string, opts?: RequestInit): Promise<T | null> {
try {
const res = await fetch(`${API_URL}${path}`, {
next: { revalidate: 60 },
...opts,
});
if (!res.ok) return null;
const json = (await res.json()) as ApiResponse<T>;
return json.success ? json.data : null;
} catch {
return null;
}
}
// ── Discover / search ─────────────────────────────────────────────────────────
export async function discoverCafes(
filters: DiscoverFilters
): Promise<CafeDiscoverDto[]> {
const params = new URLSearchParams();
if (filters.city) params.set("city", filters.city);
if (filters.q) params.set("q", filters.q);
if (filters.minRating) params.set("minRating", String(filters.minRating));
if (filters.sort) params.set("sort", filters.sort);
if (filters.themes?.length) params.set("themes", filters.themes.join(","));
if (filters.vibes?.length) params.set("vibes", filters.vibes.join(","));
if (filters.occasions?.length) params.set("occasions", filters.occasions.join(","));
if (filters.spaceFeatures?.length) params.set("spaceFeatures", filters.spaceFeatures.join(","));
if (filters.noise) params.set("noise", filters.noise);
if (filters.priceTier) params.set("priceTier", filters.priceTier);
if (filters.size) params.set("size", filters.size);
if (filters.openNow) params.set("openNow", "true");
const qs = params.toString();
const result = await get<CafeDiscoverDto[]>(
`/api/public/discover${qs ? `?${qs}` : ""}`,
{ next: { revalidate: 30 } }
);
return result ?? [];
}
// ── Individual cafe ───────────────────────────────────────────────────────────
export async function getCafe(slug: string): Promise<CafePublicDto | null> {
return get<CafePublicDto>(`/api/public/cafes/${slug}`, {
next: { revalidate: 300 },
});
}
// ── Menu ──────────────────────────────────────────────────────────────────────
export async function getCafeMenu(slug: string): Promise<PublicMenuDto | null> {
return get<PublicMenuDto>(`/api/public/cafes/${slug}/menu`, {
next: { revalidate: 300 },
});
}
// ── Reviews ───────────────────────────────────────────────────────────────────
export async function getCafeReviews(
slug: string,
page = 1
): Promise<CafeReviewDto[]> {
const result = await get<CafeReviewDto[]>(
`/api/public/cafes/${slug}/reviews?page=${page}&pageSize=10`,
{ next: { revalidate: 120 } }
);
return result ?? [];
}
// ── NLP parse (server-side only; for ISR hint pre-population) ─────────────────
export async function nlpParse(q: string): Promise<NlpHints | null> {
return get<NlpHints>(
`/api/public/discover/nlp-parse?q=${encodeURIComponent(q)}`,
{ cache: "no-store" }
);
}
// ── Slugs for static generation ───────────────────────────────────────────────
export async function getAllCafeSlugs(): Promise<string[]> {
// Fetch all cafes without filters to get slugs for static generation
const cafes = await get<CafeDiscoverDto[]>("/api/public/discover?requireProfile=false", {
next: { revalidate: 3600 },
});
return cafes?.map((c) => c.slug) ?? [];
}