Skip to content
Open
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@supabase/supabase-js": "^2.39.7",
"@tanstack/react-query": "^5.56.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand Down
3 changes: 2 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Index from "./pages/Index";
import NotFound from "./pages/NotFound";
import { getBasePath } from "./lib/supabase"; // Import getBasePath

const queryClient = new QueryClient();

Expand All @@ -13,7 +14,7 @@ const App = () => (
<TooltipProvider>
<Toaster />
<Sonner />
<BrowserRouter>
<BrowserRouter basename={getBasePath()}>
<Routes>
<Route path="/" element={<Index />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
Expand Down
180 changes: 168 additions & 12 deletions src/components/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/**
* @file LoginPage.tsx
* @version 1.1.0
* @version 1.2.0
* @description Main login and account creation page for Python Quest.
* Offers users options to log in with a keystore, create a new self-custody account,
* or try the platform as a guest. Emphasizes user data sovereignty and security.
* Includes password reveal functionality for improved UX.
* or try the platform as a guest. Now integrates Supabase for additional
* authentication methods like Magic Link and GitHub, and anonymous guest access.
*
* @project Python Quest - A Gamified Python Learning Platform
* @author Factory AI Development Team
* @date June 2, 2025
* @date June 10, 2025
*/

import React, { useState, FormEvent, ChangeEvent } from 'react';
Expand All @@ -17,11 +17,19 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { KeyRound, UserPlus, ShieldCheck, UploadCloud, LogIn, PlayCircle, Loader2, AlertTriangle, Eye, EyeOff } from 'lucide-react';
import { KeyRound, UserPlus, ShieldCheck, UploadCloud, LogIn, PlayCircle, Loader2, AlertTriangle, Eye, EyeOff, Mail, Github } from 'lucide-react';
import { ThemeProvider } from '@/contexts/ThemeContext';
import supabase from '@/lib/supabase'; // Ensure this import is present

const LoginPage: React.FC = () => {
const { createAccount, loginWithKeystore, switchToGuestMode, isLoading, error: authError, setError: setAuthError } = useAuth();
const {
createAccount,
loginWithKeystore,
switchToGuestMode,
isLoading,
error: authError,
setError: setAuthError
} = useAuth();

// State for active tab
const [activeTab, setActiveTab] = useState<string>("login");
Expand All @@ -39,9 +47,15 @@ const LoginPage: React.FC = () => {
const [loginError, setLoginError] = useState<string | null>(null);
const [showLoginPassword, setShowLoginPassword] = useState(false);

// State for Magic Link form
const [magicLinkEmail, setMagicLinkEmail] = useState('');
const [magicLinkError, setMagicLinkError] = useState<string | null>(null);
const [magicLinkSent, setMagicLinkSent] = useState(false);

const clearLocalErrors = () => {
setCreateError(null);
setLoginError(null);
setMagicLinkError(null);
if (setAuthError) setAuthError(null); // Clear global auth error if method is available
};

Expand All @@ -55,6 +69,8 @@ const LoginPage: React.FC = () => {
const fileInput = document.getElementById('keystoreFile') as HTMLInputElement;
if (fileInput) fileInput.value = ''; // Reset file input
setLoginPassword('');
setMagicLinkEmail('');
setMagicLinkSent(false);
};


Expand Down Expand Up @@ -98,10 +114,62 @@ const LoginPage: React.FC = () => {
}
};

const handleGuestMode = () => {
// Modified to use Supabase anonymous sign-in
const handleGuestMode = async () => {
clearLocalErrors();
switchToGuestMode();
// AuthContext handles navigation/state change
try {
const { data, error } = await supabase.auth.signInAnonymously();
if (error) throw error;
// After successful anonymous sign-in, switch to guest mode in AuthContext
switchToGuestMode();
} catch (error: any) {
if (setAuthError) setAuthError(error.message || "Failed to sign in as guest");
}
};

// New handler for Magic Link authentication
const handleMagicLinkSignIn = async (e: FormEvent) => {
e.preventDefault();
clearLocalErrors();

if (!magicLinkEmail || !/^\S+@\S+\.\S+$/.test(magicLinkEmail)) {
setMagicLinkError("Please enter a valid email address.");
return;
}

try {
const { data, error } = await supabase.auth.signInWithOtp({
email: magicLinkEmail,
options: {
emailRedirectTo: window.location.href, // Use window.location.href for dynamic redirect
}
});

if (error) throw error;

// Show success message
setMagicLinkSent(true);
} catch (error: any) {
setMagicLinkError(error.message || "Failed to send magic link");
}
};

// New handler for GitHub authentication
const handleGitHubSignIn = async () => {
clearLocalErrors();
try {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: window.location.href // Use window.location.href for dynamic redirect
}
});

if (error) throw error;
// GitHub OAuth will handle the redirect flow
} catch (error: any) {
if (setAuthError) setAuthError(error.message || "Failed to sign in with GitHub");
}
};

return (
Expand All @@ -119,7 +187,7 @@ const LoginPage: React.FC = () => {

<main className="w-full max-w-md animate-slideUp">
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
<TabsList className="grid w-full grid-cols-3 bg-gray-800/80 border border-gray-700/60 rounded-lg backdrop-blur-sm">
<TabsList className="grid w-full grid-cols-5 bg-gray-800/80 border border-gray-700/60 rounded-lg backdrop-blur-sm">
<TabsTrigger value="login" className="data-[state=active]:bg-cyan-600/80 data-[state=active]:text-white text-gray-300 hover:text-white transition-colors duration-200">
<LogIn size={18} className="mr-2" /> Login
</TabsTrigger>
Expand All @@ -129,6 +197,12 @@ const LoginPage: React.FC = () => {
<TabsTrigger value="guest" className="data-[state=active]:bg-purple-600/80 data-[state=active]:text-white text-gray-300 hover:text-white transition-colors duration-200">
<PlayCircle size={18} className="mr-2" /> Guest
</TabsTrigger>
<TabsTrigger value="magic" className="data-[state=active]:bg-blue-600/80 data-[state=active]:text-white text-gray-300 hover:text-white transition-colors duration-200">
<Mail size={18} className="mr-2" /> Magic
</TabsTrigger>
<TabsTrigger value="github" className="data-[state=active]:bg-gray-600/80 data-[state=active]:text-white text-gray-300 hover:text-white transition-colors duration-200">
<Github size={18} className="mr-2" /> GitHub
</TabsTrigger>
</TabsList>

{/* Login with Keystore Tab */}
Expand Down Expand Up @@ -268,7 +342,7 @@ const LoginPage: React.FC = () => {
</Card>
</TabsContent>

{/* Try as Guest Tab */}
{/* Try as Guest Tab (Updated to use Supabase anonymous sign-in) */}
<TabsContent value="guest">
<Card className="bg-gray-800/70 border-purple-500/30 backdrop-blur-sm shadow-xl">
<CardHeader>
Expand All @@ -279,7 +353,7 @@ const LoginPage: React.FC = () => {
</CardHeader>
<CardContent>
<p className="text-sm text-gray-400 mb-4 leading-relaxed">
Click below to start learning immediately. If you enjoy Python Quest, you can create a secure, self-custody keystore account at any time from the main dashboard to save your progress permanently across devices.
Click below to start learning immediately. If you enjoy Python Quest, you can create a secure account at any time from the main dashboard to save your progress permanently across devices.
</p>
</CardContent>
<CardFooter>
Expand All @@ -290,6 +364,88 @@ const LoginPage: React.FC = () => {
</CardFooter>
</Card>
</TabsContent>

{/* Magic Link Tab (New) */}
<TabsContent value="magic">
<Card className="bg-gray-800/70 border-blue-500/30 backdrop-blur-sm shadow-xl">
<CardHeader>
<CardTitle className="text-2xl text-blue-400">Magic Link Sign In</CardTitle>
<CardDescription className="text-gray-400">
Receive a sign-in link via email - no password needed.
</CardDescription>
</CardHeader>
<form onSubmit={handleMagicLinkSignIn}>
<CardContent className="space-y-4">
{!magicLinkSent ? (
<div className="space-y-1">
<label htmlFor="magicLinkEmail" className="text-sm font-medium text-gray-300">Email Address</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
id="magicLinkEmail"
type="email"
value={magicLinkEmail}
onChange={(e) => {setMagicLinkEmail(e.target.value); setMagicLinkError(null);}}
placeholder="Enter your email address"
className="bg-gray-700 border-gray-600 text-white placeholder-gray-500 pl-10"
disabled={isLoading}
/>
</div>
{magicLinkError && <p className="text-sm text-red-400 flex items-center pt-1"><AlertTriangle size={16} className="mr-1 flex-shrink-0" />{magicLinkError}</p>}
{authError && activeTab === 'magic' && <p className="text-sm text-red-400 flex items-center pt-1"><AlertTriangle size={16} className="mr-1 flex-shrink-0" />{authError}</p>}
</div>
) : (
<div className="text-center py-4">
<Mail className="h-12 w-12 text-blue-400 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-blue-400 mb-2">Check Your Email</h3>
<p className="text-gray-300 mb-2">We've sent a magic link to:</p>
<p className="font-semibold text-white mb-4">{magicLinkEmail}</p>
<p className="text-sm text-gray-400">Click the link in the email to sign in to Python Quest.</p>
</div>
)}
</CardContent>
{!magicLinkSent && (
<CardFooter>
<Button type="submit" className="w-full bg-gradient-to-r from-blue-500 to-indigo-500 hover:from-blue-600 hover:to-indigo-600 text-white font-semibold shadow-md hover:shadow-lg transform hover:scale-105 transition-all duration-200" disabled={isLoading}>
{isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Mail className="mr-2 h-4 w-4" />}
Send Magic Link
</Button>
</CardFooter>
)}
</form>
</Card>
</TabsContent>

{/* GitHub Tab (New) */}
<TabsContent value="github">
<Card className="bg-gray-800/70 border-gray-500/30 backdrop-blur-sm shadow-xl">
<CardHeader>
<CardTitle className="text-2xl text-gray-300">GitHub Sign In</CardTitle>
<CardDescription className="text-gray-400">
Use your GitHub account to sign in quickly and securely.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col items-center justify-center py-4">
<Github className="h-16 w-16 text-white mb-6" />
<p className="text-sm text-gray-400 mb-6 text-center">
Connect with your GitHub account to access Python Quest. Your GitHub email will be used for your account.
</p>
{authError && activeTab === 'github' && <p className="text-sm text-red-400 flex items-center mb-4"><AlertTriangle size={16} className="mr-1 flex-shrink-0" />{authError}</p>}
</div>
</CardContent>
<CardFooter>
<Button
onClick={handleGitHubSignIn}
className="w-full bg-gradient-to-r from-gray-700 to-gray-900 hover:from-gray-800 hover:to-black text-white font-semibold shadow-md hover:shadow-lg transform hover:scale-105 transition-all duration-200"
disabled={isLoading}
>
{isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Github className="mr-2 h-4 w-4" />}
Continue with GitHub
</Button>
</CardFooter>
</Card>
</TabsContent>
</Tabs>
</main>

Expand Down
Loading