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/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;
+}
diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx
new file mode 100644
index 0000000..dcadcf2
--- /dev/null
+++ b/src/app/auth/login/page.tsx
@@ -0,0 +1,7 @@
+'use client';
+
+import { LoginPageUI } from '@/components/auth/LoginPageUI';
+
+export default function LoginPage() {
+ return ;
+}
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...
+
+
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 3f36f7c..43257e3 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,65 +1,39 @@
-import Image from "next/image";
+'use client';
+
+import { useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import { useAuth } from '@/lib/hooks/useAuth';
+import { LoginPageUI } from '@/components/auth/LoginPageUI';
export default function Home() {
+ const router = useRouter();
+ const { session, loading } = useAuth();
+
+ useEffect(() => {
+ if (!loading && session) {
+ router.push('/dashboard');
+ }
+ }, [session, loading, router]);
+
+ // Show loading or login based on auth state
+ if (loading) {
+ return (
+
+ );
+ }
+
+ // Show login if not authenticated
+ if (!session) {
+ return ;
+ }
+
+ // If we have a session but haven't redirected yet, show loading
return (
-
-
-
-
-
- To get started, edit the page.tsx file.
-
-
- Looking for a starting point or more instructions? Head over to{" "}
-
- Templates
- {" "}
- or the{" "}
-
- Learning
- {" "}
- center.
-
-
-
-
+
);
}
+
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 (
+
+ );
+}
+
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
+
+
+
+
+
+
+ );
+}
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).*)",
],
};
+