42d4cb896a
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>
101 lines
4.0 KiB
TypeScript
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) ?? [];
|
|
}
|