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

View File

@@ -0,0 +1,113 @@
"use client";
import { useEffect, useState } from "react";
import { api } from "@/lib/api";
import type { Restaurant, VideoLink } from "@/lib/api";
import ReviewSection from "@/components/ReviewSection";
interface RestaurantDetailProps {
restaurant: Restaurant;
onClose: () => void;
}
export default function RestaurantDetail({
restaurant,
onClose,
}: RestaurantDetailProps) {
const [videos, setVideos] = useState<VideoLink[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
api
.getRestaurantVideos(restaurant.id)
.then(setVideos)
.catch(() => setVideos([]))
.finally(() => setLoading(false));
}, [restaurant.id]);
return (
<div className="p-4 space-y-4">
<div className="flex justify-between items-start">
<h2 className="text-lg font-bold">{restaurant.name}</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 text-xl leading-none"
>
x
</button>
</div>
<div className="space-y-1 text-sm">
{restaurant.cuisine_type && (
<p>
<span className="text-gray-500">:</span> {restaurant.cuisine_type}
</p>
)}
{restaurant.address && (
<p>
<span className="text-gray-500">:</span> {restaurant.address}
</p>
)}
{restaurant.region && (
<p>
<span className="text-gray-500">:</span> {restaurant.region}
</p>
)}
{restaurant.price_range && (
<p>
<span className="text-gray-500">:</span> {restaurant.price_range}
</p>
)}
</div>
<div>
<h3 className="font-semibold text-sm mb-2"> </h3>
{loading ? (
<p className="text-sm text-gray-500"> ...</p>
) : videos.length === 0 ? (
<p className="text-sm text-gray-500"> </p>
) : (
<div className="space-y-3">
{videos.map((v) => (
<div key={v.video_id} className="border rounded-lg p-3">
<a
href={v.url}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-blue-600 hover:underline"
>
{v.title}
</a>
{v.foods_mentioned.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{v.foods_mentioned.map((f, i) => (
<span
key={i}
className="px-2 py-0.5 bg-orange-50 text-orange-700 rounded text-xs"
>
{f}
</span>
))}
</div>
)}
{v.evaluation?.text && (
<p className="mt-1 text-xs text-gray-600">
{v.evaluation.text}
</p>
)}
{v.guests.length > 0 && (
<p className="mt-1 text-xs text-gray-500">
: {v.guests.join(", ")}
</p>
)}
</div>
))}
</div>
)}
</div>
<ReviewSection restaurantId={restaurant.id} />
</div>
);
}