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:
joungmin
2026-03-06 13:47:19 +09:00
commit 36bec10bd0
54 changed files with 9727 additions and 0 deletions

140
frontend/src/app/page.tsx Normal file
View File

@@ -0,0 +1,140 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { GoogleLogin } from "@react-oauth/google";
import { api } from "@/lib/api";
import type { Restaurant } from "@/lib/api";
import { useAuth } from "@/lib/auth-context";
import MapView from "@/components/MapView";
import SearchBar from "@/components/SearchBar";
import RestaurantList from "@/components/RestaurantList";
import RestaurantDetail from "@/components/RestaurantDetail";
export default function Home() {
const { user, login, logout, isLoading: authLoading } = useAuth();
const [restaurants, setRestaurants] = useState<Restaurant[]>([]);
const [selected, setSelected] = useState<Restaurant | null>(null);
const [loading, setLoading] = useState(false);
const [showDetail, setShowDetail] = useState(false);
// Load all restaurants on mount
useEffect(() => {
api
.getRestaurants({ limit: 200 })
.then(setRestaurants)
.catch(console.error);
}, []);
const handleSearch = useCallback(
async (query: string, mode: "keyword" | "semantic" | "hybrid") => {
setLoading(true);
try {
const results = await api.search(query, mode);
setRestaurants(results);
setSelected(null);
setShowDetail(false);
} catch (e) {
console.error("Search failed:", e);
} finally {
setLoading(false);
}
},
[]
);
const handleSelectRestaurant = useCallback((r: Restaurant) => {
setSelected(r);
setShowDetail(true);
}, []);
const handleCloseDetail = useCallback(() => {
setShowDetail(false);
}, []);
const handleReset = useCallback(() => {
setLoading(true);
api
.getRestaurants({ limit: 200 })
.then((data) => {
setRestaurants(data);
setSelected(null);
setShowDetail(false);
})
.catch(console.error)
.finally(() => setLoading(false));
}, []);
return (
<div className="h-screen flex flex-col">
{/* Header */}
<header className="bg-white border-b px-4 py-3 flex items-center gap-4 shrink-0">
<button onClick={handleReset} className="text-lg font-bold whitespace-nowrap">
Tasteby
</button>
<div className="flex-1 max-w-xl">
<SearchBar onSearch={handleSearch} isLoading={loading} />
</div>
<span className="text-sm text-gray-500">
{restaurants.length}
</span>
<div className="shrink-0">
{authLoading ? null : user ? (
<div className="flex items-center gap-2">
{user.avatar_url && (
<img
src={user.avatar_url}
alt=""
className="w-7 h-7 rounded-full"
/>
)}
<span className="text-sm">{user.nickname || user.email}</span>
<button
onClick={logout}
className="text-xs text-gray-500 hover:text-gray-700"
>
</button>
</div>
) : (
<GoogleLogin
onSuccess={(credentialResponse) => {
if (credentialResponse.credential) {
login(credentialResponse.credential).catch(console.error);
}
}}
onError={() => console.error("Google login failed")}
size="small"
/>
)}
</div>
</header>
{/* Body */}
<div className="flex-1 flex overflow-hidden">
{/* Sidebar */}
<aside className="w-80 bg-white border-r overflow-y-auto shrink-0">
{showDetail && selected ? (
<RestaurantDetail
restaurant={selected}
onClose={handleCloseDetail}
/>
) : (
<RestaurantList
restaurants={restaurants}
selectedId={selected?.id}
onSelect={handleSelectRestaurant}
/>
)}
</aside>
{/* Map */}
<main className="flex-1">
<MapView
restaurants={restaurants}
onSelectRestaurant={handleSelectRestaurant}
/>
</main>
</div>
</div>
);
}