diff --git a/app/(app)/diary/page.tsx b/app/(app)/diary/page.tsx new file mode 100644 index 0000000..8ed5e68 --- /dev/null +++ b/app/(app)/diary/page.tsx @@ -0,0 +1,35 @@ +import { createClient } from "@/lib/supabase/server" +import { DiaryContent } from "@/components/diary-content" + +export default async function DiaryPage() { + const supabase = await createClient() + const { + data: { user }, + } = await supabase.auth.getUser() + + const { data: entries } = await supabase + .from("diary_entries") + .select("*") + .eq("user_id", user!.id) + .order("watched_at", { ascending: false }) + + const { data: watchlist } = await supabase + .from("watchlist") + .select("*") + .eq("user_id", user!.id) + .order("added_at", { ascending: false }) + + const { data: lists } = await supabase + .from("lists") + .select("*, list_items(count)") + .eq("user_id", user!.id) + .order("created_at", { ascending: false }) + + return ( + + ) +} diff --git a/app/(app)/feed/page.tsx b/app/(app)/feed/page.tsx new file mode 100644 index 0000000..495435b --- /dev/null +++ b/app/(app)/feed/page.tsx @@ -0,0 +1,38 @@ +import { createClient } from "@/lib/supabase/server" +import { getTrending } from "@/lib/tmdb" +import { FeedContent } from "@/components/feed-content" + +export default async function FeedPage() { + const supabase = await createClient() + + const { data: { user } } = await supabase.auth.getUser() + + const { data: profile } = await supabase + .from("profiles") + .select("*") + .eq("id", user!.id) + .single() + + // Get family diary entries with profiles + const { data: entries } = await supabase + .from("diary_entries") + .select("*, profiles(display_name, avatar_url)") + .order("created_at", { ascending: false }) + .limit(20) + + // Get trending movies + let trending = { results: [] } + try { + trending = await getTrending() + } catch { + // TMDB might fail, show empty + } + + return ( + + ) +} diff --git a/app/(app)/layout.tsx b/app/(app)/layout.tsx new file mode 100644 index 0000000..45774e4 --- /dev/null +++ b/app/(app)/layout.tsx @@ -0,0 +1,11 @@ +import React from "react" +import { BottomNav } from "@/components/bottom-nav" + +export default function AppLayout({ children }: { children: React.ReactNode }) { + return ( +
+ {children} + +
+ ) +} diff --git a/app/(app)/lists/[id]/page.tsx b/app/(app)/lists/[id]/page.tsx new file mode 100644 index 0000000..ac11c4a --- /dev/null +++ b/app/(app)/lists/[id]/page.tsx @@ -0,0 +1,28 @@ +import { createClient } from "@/lib/supabase/server" +import { ListDetailContent } from "@/components/list-detail-content" +import { redirect } from "next/navigation" + +export default async function ListDetailPage({ + params, +}: { + params: Promise<{ id: string }> +}) { + const { id } = await params + const supabase = await createClient() + + const { data: list } = await supabase + .from("lists") + .select("*") + .eq("id", id) + .single() + + if (!list) redirect("/diary") + + const { data: items } = await supabase + .from("list_items") + .select("*") + .eq("list_id", id) + .order("added_at", { ascending: false }) + + return +} diff --git a/app/(app)/log/loading.tsx b/app/(app)/log/loading.tsx new file mode 100644 index 0000000..f15322a --- /dev/null +++ b/app/(app)/log/loading.tsx @@ -0,0 +1,3 @@ +export default function Loading() { + return null +} diff --git a/app/(app)/log/page.tsx b/app/(app)/log/page.tsx new file mode 100644 index 0000000..816272b --- /dev/null +++ b/app/(app)/log/page.tsx @@ -0,0 +1,304 @@ +"use client" + +import React from "react" + +import { useState, useEffect, useCallback, Suspense } from "react" +import { useRouter, useSearchParams } from "next/navigation" +import Image from "next/image" +import { createClient } from "@/lib/supabase/client" +import { posterUrl } from "@/lib/tmdb" +import type { TMDBMovie } from "@/lib/tmdb" +import { StarRating } from "@/components/star-rating" +import { Search, Film, Check, Loader2 } from "lucide-react" + +function LogPageContent() { + const router = useRouter() + const searchParams = useSearchParams() + + const preselectedId = searchParams.get("tmdb_id") + const preselectedTitle = searchParams.get("title") + const preselectedPoster = searchParams.get("poster_path") + + const [step, setStep] = useState<"search" | "rate">( + preselectedId ? "rate" : "search" + ) + const [query, setQuery] = useState("") + const [results, setResults] = useState([]) + const [searching, setSearching] = useState(false) + + const [selectedMovie, setSelectedMovie] = useState<{ + id: number + title: string + poster_path: string | null + } | null>( + preselectedId + ? { + id: Number(preselectedId), + title: preselectedTitle || "", + poster_path: preselectedPoster || null, + } + : null + ) + + const [rating, setRating] = useState(0) + const [review, setReview] = useState("") + const [watchedAt, setWatchedAt] = useState( + new Date().toISOString().slice(0, 10) + ) + const [saving, setSaving] = useState(false) + + const doSearch = useCallback(async (q: string) => { + if (!q.trim()) { + setResults([]) + return + } + setSearching(true) + try { + const res = await fetch(`/api/tmdb/search?q=${encodeURIComponent(q)}`) + const data = await res.json() + setResults(data.results || []) + } catch { + setResults([]) + } + setSearching(false) + }, []) + + useEffect(() => { + const timer = setTimeout(() => doSearch(query), 400) + return () => clearTimeout(timer) + }, [query, doSearch]) + + function selectMovie(movie: TMDBMovie) { + setSelectedMovie({ + id: movie.id, + title: movie.title, + poster_path: movie.poster_path, + }) + setStep("rate") + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + if (!selectedMovie) return + setSaving(true) + + const supabase = createClient() + const { + data: { user }, + } = await supabase.auth.getUser() + if (!user) return + + const { error } = await supabase.from("diary_entries").insert({ + user_id: user.id, + tmdb_id: selectedMovie.id, + title: selectedMovie.title, + poster_path: selectedMovie.poster_path, + rating: rating || null, + review: review.trim() || null, + watched_at: watchedAt, + }) + + if (!error) { + router.push("/diary") + router.refresh() + } + setSaving(false) + } + + const posterImage = selectedMovie?.poster_path + ? posterUrl(selectedMovie.poster_path, "w342") + : null + + if (step === "search") { + return ( +
+
+

+ Film loggen +

+
+ + setQuery(e.target.value)} + placeholder="Welchen Film hast du gesehen?" + className="h-11 w-full rounded-lg border border-border bg-secondary pl-10 pr-4 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring" + autoFocus + /> +
+
+ +
+ {searching && ( +
+ +
+ )} + {!searching && results.length === 0 && query.trim() && ( +

+ Keine Filme gefunden +

+ )} +
+ {results.map((movie) => { + const url = posterUrl(movie.poster_path, "w185") + return ( + + ) + })} +
+
+
+ ) + } + + return ( +
+
+ +

+ Bewerten +

+
+
+ +
+ {/* Selected movie */} +
+
+ {posterImage ? ( + {selectedMovie?.title + ) : ( +
+ +
+ )} +
+
+

+ {selectedMovie?.title} +

+
+
+ + {/* Rating */} +
+ + +
+ + {/* Watch date */} +
+ + setWatchedAt(e.target.value)} + className="h-11 w-full rounded-lg border border-border bg-secondary px-4 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring" + /> +
+ + {/* Review */} +
+ +