diff --git a/src/components/auth/AuthButtons.tsx b/src/components/auth/AuthButtons.tsx index 913cdf1f..472c9628 100644 --- a/src/components/auth/AuthButtons.tsx +++ b/src/components/auth/AuthButtons.tsx @@ -3,9 +3,11 @@ import { useNavigate } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; -import { User, Settings, LogOut, Trophy } from 'lucide-react'; +import { User, Settings, LogOut } from 'lucide-react'; import { useAuth } from '@/hooks/useAuth'; import { useToast } from '@/hooks/use-toast'; +import { AuthModal } from './AuthModal'; + export function AuthButtons() { const { user, @@ -17,6 +19,8 @@ export function AuthButtons() { toast } = useToast(); const [loggingOut, setLoggingOut] = useState(false); + const [authModalOpen, setAuthModalOpen] = useState(false); + const [authModalTab, setAuthModalTab] = useState<'signin' | 'signup'>('signin'); const handleSignOut = async () => { setLoggingOut(true); try { @@ -37,14 +41,36 @@ export function AuthButtons() { } }; if (!user) { - return <> - - - ; + + + ); } return diff --git a/src/components/auth/AuthModal.tsx b/src/components/auth/AuthModal.tsx new file mode 100644 index 00000000..b0675fdb --- /dev/null +++ b/src/components/auth/AuthModal.tsx @@ -0,0 +1,496 @@ +import { useState } from 'react'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Separator } from '@/components/ui/separator'; +import { Zap, Mail, Lock, User, Eye, EyeOff } from 'lucide-react'; +import { supabase } from '@/integrations/supabase/client'; +import { useToast } from '@/hooks/use-toast'; +import { TurnstileCaptcha } from './TurnstileCaptcha'; +import { notificationService } from '@/lib/notificationService'; + +interface AuthModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + defaultTab?: 'signin' | 'signup'; +} + +export function AuthModal({ open, onOpenChange, defaultTab = 'signin' }: AuthModalProps) { + const { toast } = useToast(); + const [loading, setLoading] = useState(false); + const [magicLinkLoading, setMagicLinkLoading] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [captchaToken, setCaptchaToken] = useState(null); + const [captchaKey, setCaptchaKey] = useState(0); + const [signInCaptchaToken, setSignInCaptchaToken] = useState(null); + const [signInCaptchaKey, setSignInCaptchaKey] = useState(0); + const [formData, setFormData] = useState({ + email: '', + password: '', + confirmPassword: '', + username: '', + displayName: '' + }); + + const handleInputChange = (e: React.ChangeEvent) => { + setFormData(prev => ({ + ...prev, + [e.target.name]: e.target.value + })); + }; + + const handleSignIn = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + if (!signInCaptchaToken) { + toast({ + variant: "destructive", + title: "CAPTCHA required", + description: "Please complete the CAPTCHA verification." + }); + setLoading(false); + return; + } + + const tokenToUse = signInCaptchaToken; + setSignInCaptchaToken(null); + + try { + const { error } = await supabase.auth.signInWithPassword({ + email: formData.email, + password: formData.password, + options: { + captchaToken: tokenToUse + } + }); + if (error) throw error; + + toast({ + title: "Welcome back!", + description: "You've been signed in successfully." + }); + onOpenChange(false); + } catch (error: any) { + setSignInCaptchaKey(prev => prev + 1); + toast({ + variant: "destructive", + title: "Sign in failed", + description: error.message + }); + } finally { + setLoading(false); + } + }; + + const handleSignUp = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + if (formData.password !== formData.confirmPassword) { + toast({ + variant: "destructive", + title: "Passwords don't match", + description: "Please make sure your passwords match." + }); + setLoading(false); + return; + } + + if (formData.password.length < 6) { + toast({ + variant: "destructive", + title: "Password too short", + description: "Password must be at least 6 characters long." + }); + setLoading(false); + return; + } + + if (!captchaToken) { + toast({ + variant: "destructive", + title: "CAPTCHA required", + description: "Please complete the CAPTCHA verification." + }); + setLoading(false); + return; + } + + const tokenToUse = captchaToken; + setCaptchaToken(null); + + try { + const { data, error } = await supabase.auth.signUp({ + email: formData.email, + password: formData.password, + options: { + captchaToken: tokenToUse, + data: { + username: formData.username, + display_name: formData.displayName + } + } + }); + + if (error) throw error; + + if (data.user) { + notificationService.createSubscriber({ + subscriberId: data.user.id, + email: formData.email, + firstName: formData.username, + data: { + username: formData.username, + } + }).catch(err => { + console.error('Failed to register Novu subscriber:', err); + }); + } + + toast({ + title: "Welcome to ThrillWiki!", + description: "Please check your email to verify your account." + }); + onOpenChange(false); + } catch (error: any) { + setCaptchaKey(prev => prev + 1); + toast({ + variant: "destructive", + title: "Sign up failed", + description: error.message + }); + } finally { + setLoading(false); + } + }; + + const handleMagicLinkSignIn = async (email: string) => { + if (!email) { + toast({ + variant: "destructive", + title: "Email required", + description: "Please enter your email address to receive a magic link." + }); + return; + } + + setMagicLinkLoading(true); + + try { + const { error } = await supabase.auth.signInWithOtp({ + email, + options: { + emailRedirectTo: `${window.location.origin}/` + } + }); + + if (error) throw error; + + toast({ + title: "Magic link sent!", + description: "Check your email for a sign-in link." + }); + onOpenChange(false); + } catch (error: any) { + toast({ + variant: "destructive", + title: "Failed to send magic link", + description: error.message + }); + } finally { + setMagicLinkLoading(false); + } + }; + + const handleSocialSignIn = async (provider: 'google' | 'discord') => { + try { + const { error } = await supabase.auth.signInWithOAuth({ + provider, + options: { + redirectTo: `${window.location.origin}/` + } + }); + if (error) throw error; + } catch (error: any) { + toast({ + variant: "destructive", + title: "Social sign in failed", + description: error.message + }); + } + }; + + return ( + + + + + ThrillWiki + + + Join the ultimate theme park community + + + + + + Sign In + Sign Up + + + +
+
+ +
+ + +
+
+ +
+ +
+ + + +
+
+ +
+ + setSignInCaptchaToken(null)} + onExpire={() => setSignInCaptchaToken(null)} + siteKey={import.meta.env.VITE_TURNSTILE_SITE_KEY} + theme="auto" + /> +
+ + +
+ +
+ +

+ Enter your email above and click to receive a sign-in link +

+
+ +
+
+
+ +
+
+ + Or continue with + +
+
+ +
+ + +
+
+ +

+ + Prefer full page? + +

+
+ + +
+
+
+ +
+ + +
+
+
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + + +
+
+ +
+ +
+ + +
+
+ +
+ + setCaptchaToken(null)} + onExpire={() => setCaptchaToken(null)} + siteKey={import.meta.env.VITE_TURNSTILE_SITE_KEY} + theme="auto" + /> +
+ + +
+ +

+ By signing up, you agree to our{' '} + Terms + {' '}and{' '} + Privacy Policy +

+ +

+ + Prefer full page? + +

+
+
+
+
+ ); +}