Initial commit: Tasteby - YouTube restaurant map service
Backend (FastAPI + Oracle ADB), Frontend (Next.js), daemon worker. Features: channel/video/restaurant management, semantic search, Google OAuth, user reviews. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
161
frontend/src/lib/api.ts
Normal file
161
frontend/src/lib/api.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
||||
|
||||
const TOKEN_KEY = "tasteby_token";
|
||||
|
||||
export function getToken(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function setToken(token: string) {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
async function fetchApi<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...((init?.headers as Record<string, string>) || {}),
|
||||
};
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
...init,
|
||||
headers,
|
||||
});
|
||||
if (!res.ok) throw new Error(`API ${res.status}: ${res.statusText}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface Restaurant {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string | null;
|
||||
region: string | null;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
cuisine_type: string | null;
|
||||
price_range: string | null;
|
||||
google_place_id: string | null;
|
||||
}
|
||||
|
||||
export interface VideoLink {
|
||||
video_id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
published_at: string | null;
|
||||
foods_mentioned: string[];
|
||||
evaluation: Record<string, string>;
|
||||
guests: string[];
|
||||
}
|
||||
|
||||
export interface Channel {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
channel_name: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string | null;
|
||||
nickname: string | null;
|
||||
avatar_url: string | null;
|
||||
}
|
||||
|
||||
export interface Review {
|
||||
id: string;
|
||||
user_id: string;
|
||||
rating: number;
|
||||
review_text: string | null;
|
||||
visited_at: string | null;
|
||||
created_at: string;
|
||||
user_nickname: string | null;
|
||||
user_avatar_url: string | null;
|
||||
}
|
||||
|
||||
export interface ReviewsResponse {
|
||||
reviews: Review[];
|
||||
avg_rating: number | null;
|
||||
review_count: number;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
getRestaurants(params?: {
|
||||
cuisine?: string;
|
||||
region?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) {
|
||||
const sp = new URLSearchParams();
|
||||
if (params?.cuisine) sp.set("cuisine", params.cuisine);
|
||||
if (params?.region) sp.set("region", params.region);
|
||||
if (params?.limit) sp.set("limit", String(params.limit));
|
||||
if (params?.offset) sp.set("offset", String(params.offset));
|
||||
const qs = sp.toString();
|
||||
return fetchApi<Restaurant[]>(`/api/restaurants${qs ? `?${qs}` : ""}`);
|
||||
},
|
||||
|
||||
getRestaurant(id: string) {
|
||||
return fetchApi<Restaurant>(`/api/restaurants/${id}`);
|
||||
},
|
||||
|
||||
getRestaurantVideos(id: string) {
|
||||
return fetchApi<VideoLink[]>(`/api/restaurants/${id}/videos`);
|
||||
},
|
||||
|
||||
search(q: string, mode: "keyword" | "semantic" | "hybrid" = "keyword") {
|
||||
return fetchApi<Restaurant[]>(
|
||||
`/api/search?q=${encodeURIComponent(q)}&mode=${mode}`
|
||||
);
|
||||
},
|
||||
|
||||
getChannels() {
|
||||
return fetchApi<Channel[]>("/api/channels");
|
||||
},
|
||||
|
||||
loginWithGoogle(idToken: string) {
|
||||
return fetchApi<{ access_token: string; user: User }>("/api/auth/google", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id_token: idToken }),
|
||||
});
|
||||
},
|
||||
|
||||
getMe() {
|
||||
return fetchApi<User>("/api/auth/me");
|
||||
},
|
||||
|
||||
getReviews(restaurantId: string) {
|
||||
return fetchApi<ReviewsResponse>(`/api/restaurants/${restaurantId}/reviews`);
|
||||
},
|
||||
|
||||
createReview(
|
||||
restaurantId: string,
|
||||
data: { rating: number; review_text?: string; visited_at?: string }
|
||||
) {
|
||||
return fetchApi<Review>(`/api/restaurants/${restaurantId}/reviews`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
|
||||
updateReview(
|
||||
reviewId: string,
|
||||
data: { rating: number; review_text?: string; visited_at?: string }
|
||||
) {
|
||||
return fetchApi<Review>(`/api/reviews/${reviewId}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
},
|
||||
|
||||
deleteReview(reviewId: string) {
|
||||
return fetchApi<void>(`/api/reviews/${reviewId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user