From 5cdfa2327d99b024f087891faf293adcf3e00a42 Mon Sep 17 00:00:00 2001 From: Ori Marx Date: Wed, 22 Apr 2026 17:32:02 -0400 Subject: [PATCH 1/8] first mock login page --- src/app/auth/login/page.tsx | 76 +++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/app/auth/login/page.tsx diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx new file mode 100644 index 0000000..0b5d5aa --- /dev/null +++ b/src/app/auth/login/page.tsx @@ -0,0 +1,76 @@ +'use client'; + +import Link from 'next/link'; +import LoginForm from '@/components/auth/LoginForm'; + +export default function LoginPage() { + return ( +
+ {/* Left side - Branding */} +
+
+

+ Swale +

+

+ Football intelligence for every competition, every player, every matchday. +

+
+ +
+
+

+ What you get +

+
    +
  • +
    + Live scores & match timelines +
  • +
  • +
    + Player form & advanced stats +
  • +
  • +
    + World Cup hub & history back to 1930 +
  • +
  • +
    + AI-powered insights & previews +
  • +
+
+ +

+ Open source • Built with Next.js, Supabase & OpenAI +

+
+
+ + {/* Right side - Login Form */} +
+
+
+

Welcome back

+

Sign in to your Swale account

+
+ + + +
+

+ Don't have an account?{' '} + + Sign up + +

+
+
+
+
+ ); +} From cf7183bc66b9e26ba1d744477a816db84afe00fe Mon Sep 17 00:00:00 2001 From: Ori Marx Date: Wed, 22 Apr 2026 18:36:10 -0400 Subject: [PATCH 2/8] login page related-changes --- src/components/auth/LoginForm.tsx | 103 ++++++++++++++++++++++++++++ src/components/auth/LoginPageUI.tsx | 76 ++++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 src/components/auth/LoginForm.tsx create mode 100644 src/components/auth/LoginPageUI.tsx diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx new file mode 100644 index 0000000..400ec6a --- /dev/null +++ b/src/components/auth/LoginForm.tsx @@ -0,0 +1,103 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; + +export default function LoginForm() { + const router = useRouter(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setIsLoading(true); + + try { + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, password }), + }); + + const data = await response.json(); + + if (!response.ok) { + setError(data.error || 'Login failed'); + setIsLoading(false); + return; + } + + // Login successful, redirect to dashboard + router.push('/dashboard'); + router.refresh(); + } catch { + setError('An error occurred. Please try again.'); + setIsLoading(false); + } + }; + + return ( +
+ {/* Email Field */} +
+ + setEmail(e.target.value)} + disabled={isLoading} + className="w-full px-4 py-3 rounded-lg bg-zinc-900 border border-zinc-800 text-white placeholder-zinc-500 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" + /> +
+ + {/* Password Field */} +
+ + setPassword(e.target.value)} + disabled={isLoading} + className="w-full px-4 py-3 rounded-lg bg-zinc-900 border border-zinc-800 text-white placeholder-zinc-500 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" + /> +
+ + {/* Error Message */} + {error && ( +
+ {error} +
+ )} + + {/* Submit Button */} + + + {/* Forgot Password Link */} + +
+ ); +} + diff --git a/src/components/auth/LoginPageUI.tsx b/src/components/auth/LoginPageUI.tsx new file mode 100644 index 0000000..184e806 --- /dev/null +++ b/src/components/auth/LoginPageUI.tsx @@ -0,0 +1,76 @@ +'use client'; + +import Link from 'next/link'; +import LoginForm from '@/components/auth/LoginForm'; + +export function LoginPageUI() { + return ( +
+ {/* Left side - Branding */} +
+
+

+ Swale +

+

+ Football intelligence for every competition, every player, every matchday. +

+
+ +
+
+

+ What you get +

+
    +
  • +
    + Live scores & match timelines +
  • +
  • +
    + Player form & advanced stats +
  • +
  • +
    + World Cup hub & history back to 1930 +
  • +
  • +
    + AI-powered insights & previews +
  • +
+
+ +

+ Open source • Built with Next.js, Supabase & OpenAI +

+
+
+ + {/* Right side - Login Form */} +
+
+
+

Welcome back

+

Sign in to your Swale account

+
+ + + +
+

+ Don't have an account?{' '} + + Sign up + +

+
+
+
+
+ ); +} From 3857912aa03be33d2bb05eaac63db8bd970a0a52 Mon Sep 17 00:00:00 2001 From: Ori Marx Date: Wed, 22 Apr 2026 18:36:29 -0400 Subject: [PATCH 3/8] auth-layout --- src/app/auth/layout.tsx | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/app/auth/layout.tsx diff --git a/src/app/auth/layout.tsx b/src/app/auth/layout.tsx new file mode 100644 index 0000000..a51a71f --- /dev/null +++ b/src/app/auth/layout.tsx @@ -0,0 +1,7 @@ +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return children; +} From cf81a8d5df12c518d1e2e626da0c9d2682e47030 Mon Sep 17 00:00:00 2001 From: Ori Marx Date: Wed, 22 Apr 2026 18:36:47 -0400 Subject: [PATCH 4/8] login page ui / logout route --- src/app/api/auth/logout/route.ts | 33 +++++++++++++++ src/app/auth/login/page.tsx | 73 +------------------------------- 2 files changed, 35 insertions(+), 71 deletions(-) create mode 100644 src/app/api/auth/logout/route.ts diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..d13c897 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,33 @@ +import { createServerSupabaseClient } from "@/lib/supabase/server"; +import { NextResponse } from "next/server"; + +export async function POST() { + const supabase = await createServerSupabaseClient(); + + try { + await supabase.auth.signOut(); + return NextResponse.json({ success: true }); + } catch { + return NextResponse.json( + { error: "Failed to sign out" }, + { status: 500 } + ); + } +} + +export async function GET(request: Request) { + const supabase = await createServerSupabaseClient(); + + try { + await supabase.auth.signOut(); + // Redirect to home page after logout + return NextResponse.redirect(new URL("/", request.url), { + status: 302, + }); + } catch { + return NextResponse.json( + { error: "Failed to sign out" }, + { status: 500 } + ); + } +} diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 0b5d5aa..dcadcf2 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -1,76 +1,7 @@ 'use client'; -import Link from 'next/link'; -import LoginForm from '@/components/auth/LoginForm'; +import { LoginPageUI } from '@/components/auth/LoginPageUI'; export default function LoginPage() { - return ( -
- {/* Left side - Branding */} -
-
-

- Swale -

-

- Football intelligence for every competition, every player, every matchday. -

-
- -
-
-

- What you get -

-
    -
  • -
    - Live scores & match timelines -
  • -
  • -
    - Player form & advanced stats -
  • -
  • -
    - World Cup hub & history back to 1930 -
  • -
  • -
    - AI-powered insights & previews -
  • -
-
- -

- Open source • Built with Next.js, Supabase & OpenAI -

-
-
- - {/* Right side - Login Form */} -
-
-
-

Welcome back

-

Sign in to your Swale account

-
- - - -
-

- Don't have an account?{' '} - - Sign up - -

-
-
-
-
- ); + return ; } From 8b424ad5428e1e3f29a38912cff3403d88caf16c Mon Sep 17 00:00:00 2001 From: Ori Marx Date: Wed, 22 Apr 2026 18:37:27 -0400 Subject: [PATCH 5/8] home page (for routing purposes) --- src/app/page.tsx | 85 +++++++++++++----------------------------------- 1 file changed, 22 insertions(+), 63 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index 3f36f7c..b6f98d3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,24 @@ -import Image from "next/image"; +import { redirect } from 'next/navigation'; +import { createServerSupabaseClient } from '@/lib/supabase/server'; +import { LoginPageUI } from '@/components/auth/LoginPageUI'; -export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- -
-
- ); +export default async function Home() { + const supabase = await createServerSupabaseClient(); + + try { + const { + data: { user }, + } = await supabase.auth.getUser(); + + // If user is logged in, redirect to dashboard + if (user) { + redirect('/dashboard'); + } + } catch { + // If error getting user, fall through to show login + } + + // Show login page if not authenticated + return ; } + From 5c03bf894a4936a2f93c2924ed682caef3f84b5b Mon Sep 17 00:00:00 2001 From: Ori Marx Date: Wed, 22 Apr 2026 18:37:43 -0400 Subject: [PATCH 6/8] middleware/client/server-side configs --- src/lib/supabase/client.ts | 14 +++++++---- src/lib/supabase/server.ts | 49 +++++++++++++++++++++----------------- src/middleware.ts | 42 ++++++++++++++++++++++++++++---- 3 files changed, 74 insertions(+), 31 deletions(-) diff --git a/src/lib/supabase/client.ts b/src/lib/supabase/client.ts index 9f2891b..4666003 100644 --- a/src/lib/supabase/client.ts +++ b/src/lib/supabase/client.ts @@ -1,8 +1,14 @@ import { createBrowserClient } from "@supabase/ssr"; export function createClient() { - return createBrowserClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! - ); + const url = process.env.NEXT_PUBLIC_SUPABASE_URL; + const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!url || !key) { + throw new Error( + "Missing Supabase environment variables. Please set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in your .env.local" + ); + } + + return createBrowserClient(url, key); } diff --git a/src/lib/supabase/server.ts b/src/lib/supabase/server.ts index b0827ca..f06eab0 100644 --- a/src/lib/supabase/server.ts +++ b/src/lib/supabase/server.ts @@ -2,29 +2,34 @@ import { createServerClient } from "@supabase/ssr"; import { cookies } from "next/headers"; export async function createServerSupabaseClient() { + const url = process.env.NEXT_PUBLIC_SUPABASE_URL; + const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!url || !key) { + throw new Error( + "Missing Supabase environment variables. Please set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in your .env.local" + ); + } + 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 or Edge Routes that - // will handle the set-cookie header. If this gets logged to help - // you debug, you can ignore it. - } - }, + return createServerClient(url, 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 or Edge Routes that + // will handle the set-cookie header. If this gets logged to help + // you debug, you can ignore it. + } }, - } - ); + }, + }); } diff --git a/src/middleware.ts b/src/middleware.ts index 5102fde..38d1a26 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -6,9 +6,17 @@ export async function middleware(request: NextRequest) { request, }); + // Skip middleware if Supabase env vars are not configured (dev without setup) + if ( + !process.env.NEXT_PUBLIC_SUPABASE_URL || + !process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY + ) { + return supabaseResponse; + } + const supabase = createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + process.env.NEXT_PUBLIC_SUPABASE_URL, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, { cookies: { getAll() { @@ -23,11 +31,34 @@ export async function middleware(request: NextRequest) { } ); - // Check session + // Check session and refresh if needed + let user = null; try { - await supabase.auth.getUser(); + const { + data: { user: authUser }, + } = await supabase.auth.getUser(); + user = authUser; } catch { - return supabaseResponse; + // User is not authenticated + } + + // Protect dashboard routes + if (request.nextUrl.pathname.startsWith("/dashboard")) { + if (!user) { + // Redirect to login if trying to access protected route + return NextResponse.redirect(new URL("/", request.url)); + } + } + + // Redirect authenticated users away from auth pages + if ( + request.nextUrl.pathname === "/" || + request.nextUrl.pathname.startsWith("/auth/") + ) { + if (user) { + // Redirect to dashboard if already authenticated + return NextResponse.redirect(new URL("/dashboard", request.url)); + } } return supabaseResponse; @@ -45,3 +76,4 @@ export const config = { "/((?!_next/static|_next/image|favicon.ico|.*\\.svg).*)", ], }; + From 5a591d766ccc8708d661cb59f0aa96dadb03caba Mon Sep 17 00:00:00 2001 From: Ori Marx Date: Wed, 22 Apr 2026 18:41:09 -0400 Subject: [PATCH 7/8] vercel-error address --- src/app/page.tsx | 47 +++++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index b6f98d3..43257e3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,24 +1,39 @@ -import { redirect } from 'next/navigation'; -import { createServerSupabaseClient } from '@/lib/supabase/server'; -import { LoginPageUI } from '@/components/auth/LoginPageUI'; +'use client'; -export default async function Home() { - const supabase = await createServerSupabaseClient(); +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useAuth } from '@/lib/hooks/useAuth'; +import { LoginPageUI } from '@/components/auth/LoginPageUI'; - try { - const { - data: { user }, - } = await supabase.auth.getUser(); +export default function Home() { + const router = useRouter(); + const { session, loading } = useAuth(); - // If user is logged in, redirect to dashboard - if (user) { - redirect('/dashboard'); + useEffect(() => { + if (!loading && session) { + router.push('/dashboard'); } - } catch { - // If error getting user, fall through to show login + }, [session, loading, router]); + + // Show loading or login based on auth state + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + // Show login if not authenticated + if (!session) { + return ; } - // Show login page if not authenticated - return ; + // If we have a session but haven't redirected yet, show loading + return ( +
+
Redirecting...
+
+ ); } From 89e5fd98e4c0fc47a1c94f7b47d31f8a29773a1d Mon Sep 17 00:00:00 2001 From: Ori Marx Date: Wed, 22 Apr 2026 18:43:41 -0400 Subject: [PATCH 8/8] prevent prerender of dashboard --- src/app/dashboard/page.tsx | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/app/dashboard/page.tsx diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..b15fbbb --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,32 @@ +import { createServerSupabaseClient } from '@/lib/supabase/server'; + +export const dynamic = 'force-dynamic'; + +export default async function DashboardPage() { + const supabase = await createServerSupabaseClient(); + + const { + data: { user }, + } = await supabase.auth.getUser(); + + return ( +
+
+

Welcome, {user?.email}!

+

This is your dashboard. We're building the live data here.

+
+ +
+
+

Live Matches

+

Coming soon...

+
+ +
+

World Cup Hub

+

Coming soon...

+
+
+
+ ); +}