feat: add purchase wizard flow at /get-started
5-step guided wizard: find business → confirm → email → payment → success. Includes Google Places search API, Stripe checkout API, i18n (EN/ES), and CTA links updated to point to the new wizard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
149
apps/web/src/app/api/business/search/route.ts
Normal file
149
apps/web/src/app/api/business/search/route.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
interface Business {
|
||||
place_id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
rating: number;
|
||||
review_count: number;
|
||||
category: string;
|
||||
maps_url: string;
|
||||
}
|
||||
|
||||
const GOOGLE_MAPS_URL_PATTERN =
|
||||
/(?:google\.com\/maps|maps\.google\.com|goo\.gl\/maps|maps\.app\.goo\.gl)/i;
|
||||
|
||||
function isGoogleMapsUrl(input: string): boolean {
|
||||
return GOOGLE_MAPS_URL_PATTERN.test(input);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { query } = body as { query: string };
|
||||
|
||||
if (!query || typeof query !== "string" || query.trim().length < 2) {
|
||||
return NextResponse.json(
|
||||
{ error: "Query must be at least 2 characters" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const trimmed = query.trim();
|
||||
|
||||
if (isGoogleMapsUrl(trimmed)) {
|
||||
return handleUrlSearch(trimmed);
|
||||
}
|
||||
|
||||
return handleTextSearch(trimmed);
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid request body" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUrlSearch(url: string): Promise<NextResponse> {
|
||||
const engineUrl = process.env.NEXT_PUBLIC_ENGINE_URL;
|
||||
|
||||
if (!engineUrl) {
|
||||
return NextResponse.json(
|
||||
{ error: "Engine service not configured" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${engineUrl}/api/check-reviews`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ url }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: "Could not find business from URL" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as Record<string, unknown>;
|
||||
|
||||
const business: Business = {
|
||||
place_id: (data.place_id as string) || "",
|
||||
name: (data.name as string) || "Unknown Business",
|
||||
address: (data.address as string) || "",
|
||||
rating: (data.rating as number) || 0,
|
||||
review_count: (data.review_count as number) || (data.reviews_count as number) || 0,
|
||||
category: (data.category as string) || (data.type as string) || "",
|
||||
maps_url: url,
|
||||
};
|
||||
|
||||
return NextResponse.json({ businesses: [business] });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch business details" },
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTextSearch(query: string): Promise<NextResponse> {
|
||||
const apiKey = process.env.GOOGLE_PLACES_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
return NextResponse.json(
|
||||
{ error: "Places search not configured" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://places.googleapis.com/v1/places:searchText",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Goog-Api-Key": apiKey,
|
||||
"X-Goog-FieldMask":
|
||||
"places.id,places.displayName,places.formattedAddress,places.rating,places.userRatingCount,places.primaryTypeDisplayName,places.googleMapsUri",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
textQuery: query,
|
||||
languageCode: "en",
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: "Places search failed" },
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as Record<string, unknown>;
|
||||
|
||||
const businesses: Business[] = ((data.places as any[]) || [])
|
||||
.slice(0, 5)
|
||||
.map((place: any) => ({
|
||||
place_id: place.id || "",
|
||||
name: place.displayName?.text || "Unknown",
|
||||
address: place.formattedAddress || "",
|
||||
rating: place.rating || 0,
|
||||
review_count: place.userRatingCount || 0,
|
||||
category: place.primaryTypeDisplayName?.text || "",
|
||||
maps_url: place.googleMapsUri || "",
|
||||
}));
|
||||
|
||||
return NextResponse.json({ businesses });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to search businesses" },
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user