diff --git a/app/globals.css b/app/globals.css index ac68442..845aae8 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,10 +2,6 @@ @tailwind components; @tailwind utilities; -body { - font-family: Arial, Helvetica, sans-serif; -} - @layer utilities { .text-balance { text-wrap: balance; @@ -14,73 +10,31 @@ body { @layer base { :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; + --background: 220 20% 7%; + --foreground: 210 20% 95%; + --card: 220 18% 10%; + --card-foreground: 210 20% 95%; + --popover: 220 18% 10%; + --popover-foreground: 210 20% 95%; + --primary: 24 100% 55%; + --primary-foreground: 220 20% 7%; + --secondary: 220 15% 16%; + --secondary-foreground: 210 20% 90%; + --muted: 220 15% 14%; + --muted-foreground: 215 15% 55%; + --accent: 24 80% 50%; + --accent-foreground: 220 20% 7%; + --destructive: 0 72% 51%; + --destructive-foreground: 210 20% 95%; + --border: 220 15% 18%; + --input: 220 15% 18%; + --ring: 24 100% 55%; + --chart-1: 24 100% 55%; --chart-2: 173 58% 39%; --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; - --radius: 0.5rem; - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; - } - .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; + --radius: 0.75rem; } } @@ -92,3 +46,25 @@ body { @apply bg-background text-foreground; } } + +/* Smooth scrolling */ +html { + scroll-behavior: smooth; + -webkit-tap-highlight-color: transparent; +} + +/* Hide scrollbar for mobile app feel */ +.hide-scrollbar::-webkit-scrollbar { + display: none; +} +.hide-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; +} + +/* Safe area for PWA */ +@supports(padding: max(0px)) { + .safe-bottom { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); + } +} diff --git a/app/layout.tsx b/app/layout.tsx index fd10913..8c3207a 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,15 +1,33 @@ -import type { Metadata } from 'next' -import { Geist, Geist_Mono } from 'next/font/google' +import React from "react" +import type { Metadata, Viewport } from "next" +import { Inter, Space_Grotesk } from "next/font/google" -import './globals.css' +import "./globals.css" -const _geist = Geist({ subsets: ['latin'] }) -const _geistMono = Geist_Mono({ subsets: ['latin'] }) +const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }) +const spaceGrotesk = Space_Grotesk({ + subsets: ["latin"], + variable: "--font-space-grotesk", +}) export const metadata: Metadata = { - title: 'v0 App', - description: 'Created with v0', - generator: 'v0.app', + title: "InFocus - Familienfilm-Tagebuch", + description: + "Teile deine Filmerlebnisse mit deiner Familie. Bewerte, logge und entdecke Filme zusammen.", + manifest: "/manifest.json", + appleWebApp: { + capable: true, + statusBarStyle: "black-translucent", + title: "InFocus", + }, +} + +export const viewport: Viewport = { + themeColor: "#111318", + width: "device-width", + initialScale: 1, + maximumScale: 1, + userScalable: false, } export default function RootLayout({ @@ -18,8 +36,12 @@ export default function RootLayout({ children: React.ReactNode }>) { return ( - -
{children} + + + {children} + ) } diff --git a/lib/supabase/client.ts b/lib/supabase/client.ts new file mode 100644 index 0000000..c48a435 --- /dev/null +++ b/lib/supabase/client.ts @@ -0,0 +1,8 @@ +import { createBrowserClient } from '@supabase/ssr' + +export function createClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + ) +} diff --git a/lib/supabase/middleware.ts b/lib/supabase/middleware.ts new file mode 100644 index 0000000..874d983 --- /dev/null +++ b/lib/supabase/middleware.ts @@ -0,0 +1,62 @@ +import { createServerClient } from '@supabase/ssr' +import { NextResponse, type NextRequest } from 'next/server' + +export async function updateSession(request: NextRequest) { + let supabaseResponse = NextResponse.next({ + request, + }) + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll() + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value }) => + request.cookies.set(name, value), + ) + supabaseResponse = NextResponse.next({ + request, + }) + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options), + ) + }, + }, + }, + ) + + const { + data: { user }, + } = await supabase.auth.getUser() + + // Redirect unauthenticated users trying to access the app + const isAuthRoute = request.nextUrl.pathname.startsWith('/auth') + const isApiRoute = request.nextUrl.pathname.startsWith('/api') + const isPublicRoute = request.nextUrl.pathname === '/' + + if (!user && !isAuthRoute && !isApiRoute && !isPublicRoute) { + const url = request.nextUrl.clone() + url.pathname = '/auth/login' + return NextResponse.redirect(url) + } + + // Redirect authenticated users away from auth pages to the feed + if (user && isAuthRoute) { + const url = request.nextUrl.clone() + url.pathname = '/feed' + return NextResponse.redirect(url) + } + + // Redirect root to feed if authenticated + if (user && isPublicRoute) { + const url = request.nextUrl.clone() + url.pathname = '/feed' + return NextResponse.redirect(url) + } + + return supabaseResponse +} diff --git a/lib/supabase/server.ts b/lib/supabase/server.ts new file mode 100644 index 0000000..e60931a --- /dev/null +++ b/lib/supabase/server.ts @@ -0,0 +1,29 @@ +import { createServerClient } from '@supabase/ssr' +import { cookies } from 'next/headers' + +export async function createClient() { + const cookieStore = await cookies() + + return createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return cookieStore.getAll() + }, + setAll(cookiesToSet) { + try { + cookiesToSet.forEach(({ name, value, options }) => + cookieStore.set(name, value, options), + ) + } catch { + // The "setAll" method was called from a Server Component. + // This can be ignored if you have middleware refreshing + // user sessions. + } + }, + }, + }, + ) +} diff --git a/lib/tmdb.ts b/lib/tmdb.ts new file mode 100644 index 0000000..25c41c2 --- /dev/null +++ b/lib/tmdb.ts @@ -0,0 +1,64 @@ +const TMDB_BASE = 'https://api.themoviedb.org/3' +const TMDB_IMAGE_BASE = 'https://image.tmdb.org/t/p' + +export function posterUrl(path: string | null, size: 'w185' | 'w342' | 'w500' | 'original' = 'w342') { + if (!path) return null + return `${TMDB_IMAGE_BASE}/${size}${path}` +} + +export function backdropUrl(path: string | null, size: 'w780' | 'w1280' | 'original' = 'w1280') { + if (!path) return null + return `${TMDB_IMAGE_BASE}/${size}${path}` +} + +async function tmdbFetch(endpoint: string, params: Record