Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/app/api/auth/logout/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
);
}
}
7 changes: 7 additions & 0 deletions src/app/auth/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}
7 changes: 7 additions & 0 deletions src/app/auth/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use client';

import { LoginPageUI } from '@/components/auth/LoginPageUI';

export default function LoginPage() {
return <LoginPageUI />;
}
32 changes: 32 additions & 0 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="space-y-8">
<div>
<h2 className="text-3xl font-bold text-white mb-2">Welcome, {user?.email}!</h2>
<p className="text-zinc-400">This is your dashboard. We&apos;re building the live data here.</p>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-6 rounded-lg bg-zinc-900 border border-zinc-800">
<h3 className="text-lg font-semibold text-white mb-2">Live Matches</h3>
<p className="text-zinc-400">Coming soon...</p>
</div>

<div className="p-6 rounded-lg bg-zinc-900 border border-zinc-800">
<h3 className="text-lg font-semibold text-white mb-2">World Cup Hub</h3>
<p className="text-zinc-400">Coming soon...</p>
</div>
</div>
</div>
);
}
92 changes: 33 additions & 59 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen flex items-center justify-center bg-black">
<div className="text-zinc-400">Loading...</div>
</div>
);
}

// Show login if not authenticated
if (!session) {
return <LoginPageUI />;
}

// If we have a session but haven't redirected yet, show loading
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
<div className="min-h-screen flex items-center justify-center bg-black">
<div className="text-zinc-400">Redirecting...</div>
</div>
);
}

103 changes: 103 additions & 0 deletions src/components/auth/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(null);

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
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 (
<form onSubmit={handleSubmit} className="space-y-4">
{/* Email Field */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-white mb-2">
Email
</label>
<input
id="email"
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => 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"
/>
</div>

{/* Password Field */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-white mb-2">
Password
</label>
<input
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => 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"
/>
</div>

{/* Error Message */}
{error && (
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
{error}
</div>
)}

{/* Submit Button */}
<button
type="submit"
disabled={isLoading || !email || !password}
className="w-full py-3 mt-6 bg-emerald-600 hover:bg-emerald-500 disabled:bg-zinc-800 disabled:text-zinc-500 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-colors"
>
{isLoading ? 'Signing in...' : 'Sign in'}
</button>

{/* Forgot Password Link */}
<div className="text-center pt-2">
<a href="#" className="text-xs text-zinc-500 hover:text-zinc-400 transition-colors">
Forgot password?
</a>
</div>
</form>
);
}

76 changes: 76 additions & 0 deletions src/components/auth/LoginPageUI.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use client';

import Link from 'next/link';
import LoginForm from '@/components/auth/LoginForm';

export function LoginPageUI() {
return (
<div className="min-h-screen flex bg-black">
{/* Left side - Branding */}
<div className="hidden lg:flex lg:w-1/2 flex-col justify-between p-12 bg-gradient-to-br from-black via-black to-zinc-900">
<div>
<h1 className="text-5xl font-black tracking-tighter text-white mb-4">
Swale
</h1>
<p className="text-lg text-zinc-400 max-w-md leading-relaxed">
Football intelligence for every competition, every player, every matchday.
</p>
</div>

<div className="space-y-12">
<div>
<h2 className="text-sm font-semibold text-zinc-500 uppercase tracking-widest mb-6">
What you get
</h2>
<ul className="space-y-4">
<li className="flex gap-3">
<div className="w-1.5 h-1.5 bg-emerald-500 rounded-full mt-2 flex-shrink-0" />
<span className="text-sm text-zinc-300">Live scores & match timelines</span>
</li>
<li className="flex gap-3">
<div className="w-1.5 h-1.5 bg-emerald-500 rounded-full mt-2 flex-shrink-0" />
<span className="text-sm text-zinc-300">Player form & advanced stats</span>
</li>
<li className="flex gap-3">
<div className="w-1.5 h-1.5 bg-emerald-500 rounded-full mt-2 flex-shrink-0" />
<span className="text-sm text-zinc-300">World Cup hub & history back to 1930</span>
</li>
<li className="flex gap-3">
<div className="w-1.5 h-1.5 bg-emerald-500 rounded-full mt-2 flex-shrink-0" />
<span className="text-sm text-zinc-300">AI-powered insights & previews</span>
</li>
</ul>
</div>

<p className="text-xs text-zinc-500">
Open source • Built with Next.js, Supabase & OpenAI
</p>
</div>
</div>

{/* Right side - Login Form */}
<div className="w-full lg:w-1/2 flex flex-col justify-center px-6 sm:px-12 py-12 bg-black">
<div className="w-full max-w-sm mx-auto lg:mx-0">
<div className="mb-12">
<h2 className="text-3xl font-bold text-white mb-2">Welcome back</h2>
<p className="text-zinc-400">Sign in to your Swale account</p>
</div>

<LoginForm />

<div className="mt-8 pt-8 border-t border-zinc-800 text-center text-sm">
<p className="text-zinc-400">
Don&apos;t have an account?{' '}
<Link
href="/auth/signup"
className="text-emerald-500 hover:text-emerald-400 font-medium transition-colors"
>
Sign up
</Link>
</p>
</div>
</div>
</div>
</div>
);
}
14 changes: 10 additions & 4 deletions src/lib/supabase/client.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Loading
Loading