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
96 changes: 51 additions & 45 deletions components/console/builder-hub-account-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { CircleUserRound, LogOut, User } from "lucide-react";
import { useState } from "react";
import { useSession, signOut } from "next-auth/react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import { useLoginModalTrigger } from "@/hooks/useLoginModal";
import { LoginModal } from "@/components/login/LoginModal";

export function BuilderHubAccountButton() {
const { data: session, status } = useSession();
const router = useRouter();
const [isSignOutDialogOpen, setIsSignOutDialogOpen] = useState(false);
const { openLoginModal } = useLoginModalTrigger();

const isAuthenticated = status === 'authenticated';
const isLoading = status === 'loading';
Expand All @@ -29,13 +30,13 @@ export function BuilderHubAccountButton() {
});
}

signOut({ callbackUrl: '/login' });
signOut({ redirect: false });
};

const handleLoginClick = () => {
// Store current URL as callback for after login
const currentUrl = window.location.href;
router.push(`/login?callbackUrl=${encodeURIComponent(currentUrl)}`);
openLoginModal(currentUrl);
};

if (isLoading) {
Expand All @@ -51,45 +52,50 @@ export function BuilderHubAccountButton() {
);
}

return (isAuthenticated ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-8 w-8 p-0">
{session?.user?.image ? (
<Image
src={session.user.image}
alt="User Avatar"
width={20}
height={20}
className="rounded-md"
/>
) : (
<CircleUserRound className="w-4 h-4 text-zinc-400 dark:text-zinc-500" />
)}
<span className="sr-only">Account menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem disabled>
{session?.user?.email || 'No email available'}
</DropdownMenuItem>
<DropdownMenuItem disabled>
{session?.user?.name || 'No name available'}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => router.push('/profile')}>
<User className="mr-2 h-3 w-3" />
Profile
</DropdownMenuItem>
<DropdownMenuItem onClick={handleSignOut}>
<LogOut className="mr-2 h-3 w-3" />
Sign Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button size="sm" onClick={handleLoginClick}>
Log In
</Button>
));
return (
<>
<LoginModal />
{isAuthenticated ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-8 w-8 p-0">
{session?.user?.image ? (
<Image
src={session.user.image}
alt="User Avatar"
width={20}
height={20}
className="rounded-md"
/>
) : (
<CircleUserRound className="w-4 h-4 text-zinc-400 dark:text-zinc-500" />
)}
<span className="sr-only">Account menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem disabled>
{session?.user?.email || 'No email available'}
</DropdownMenuItem>
<DropdownMenuItem disabled>
{session?.user?.name || 'No name available'}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => router.push('/profile')}>
<User className="mr-2 h-3 w-3" />
Profile
</DropdownMenuItem>
<DropdownMenuItem onClick={handleSignOut}>
<LogOut className="mr-2 h-3 w-3" />
Sign Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button size="sm" onClick={handleLoginClick}>
Log In
</Button>
)}
</>
);
}
168 changes: 168 additions & 0 deletions components/login/LoginModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"use client";

import React, { useState, useEffect } from 'react';
import Image from "next/image";
import Link from "next/link";
import { useForm, Controller } from 'react-hook-form';
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import axios from "axios";
import { Dialog, DialogOverlay, DialogContent, DialogTitle } from '../toolbox/components/ui/dialog';
import { Input } from "../ui/input";
import { LoadingButton } from "../ui/loading-button";
import SocialLogin from "./social-login/SocialLogin";
import { VerifyEmail } from "./verify/VerifyEmail";
import { useLoginModalState } from '@/hooks/useLoginModal';

const formSchema = z.object({
email: z.string().email("Please enter a valid email address"),
});

export function LoginModal() {
const { isOpen, callbackUrl = "/", closeLoginModal, subscribeToChanges } = useLoginModalState();
const [isVerifying, setIsVerifying] = useState(false);
const [email, setEmail] = useState("");

const { control, handleSubmit, setError, reset, formState: { errors, isSubmitting } } = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { email: "" },
});

// Subscribe to modal state changes
useEffect(() => {
return subscribeToChanges();
}, [subscribeToChanges]);

// Reset form when modal opens/closes
useEffect(() => {
if (!isOpen) {
setIsVerifying(false);
setEmail("");
reset();
}
}, [isOpen, reset]);

async function onSubmit(values: z.infer<typeof formSchema>) {
setEmail(values.email);

try {
await axios.post("/api/send-otp", {
email: values.email.toLowerCase(),
});
setIsVerifying(true);
} catch (error) {
setError("email", { message: "Error sending OTP" });
}
}

if (!isOpen) return null;

return (
<Dialog.Root open={true} onOpenChange={closeLoginModal}>
<Dialog.Portal>
<DialogOverlay />
<DialogContent
className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl focus:outline-none w-[90vw] max-w-[400px] max-h-[90vh] overflow-hidden z-[10000] p-0"
showCloseButton={true}
>
{/* Compact Header - Full Width */}
<div className="relative overflow-hidden bg-gradient-to-br from-zinc-900 via-zinc-900 to-zinc-950">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,_var(--tw-gradient-stops))] from-red-500/10 via-transparent to-transparent"></div>
<div className="flex py-5 items-center justify-center relative">
<Image
src="https://qizat5l3bwvomkny.public.blob.vercel-storage.com/builders-hub/hackaton-platform-images/avalancheLoginLogo-LUyz1IYs0fZrQ3tE0CUjst07LPVAv8.svg"
alt="Avalanche"
width="120"
height="147"
className="max-w-full h-auto"
/>
</div>
</div>

{/* Form Content - No Side Padding */}
{isVerifying && email ? (
<div className="px-5 py-5">
<VerifyEmail
email={email}
onBack={() => setIsVerifying(false)}
callbackUrl={callbackUrl}
/>
</div>
) : (
<div className="px-5 py-5">
<div className="space-y-4">
{/* Title */}
<div className="text-center">
<DialogTitle className="text-xl font-semibold text-zinc-900 dark:text-zinc-50 mb-1">
Sign in to your account
</DialogTitle>
<p className="text-zinc-500 dark:text-zinc-400 text-xs">
Enter your email to receive a sign-in code
</p>
</div>

{/* Form */}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-3">
<Controller
name="email"
control={control}
render={({ field }) => (
<div className="space-y-1">
<Input
className="h-10 bg-zinc-50 dark:bg-zinc-950 border-zinc-200 dark:border-zinc-800 focus:border-red-500 dark:focus:border-red-500 rounded-lg transition-colors text-sm"
placeholder="[email protected]"
type="email"
{...field}
/>
{errors.email && (
<p className="text-red-500 text-xs px-1">
{errors.email.message}
</p>
)}
</div>
)}
/>
<LoadingButton
type="submit"
variant="red"
className="w-full h-10 rounded-lg font-medium text-sm"
isLoading={isSubmitting}
loadingText="Sending..."
>
Continue with Email
</LoadingButton>
</form>

{/* Social Login */}
<SocialLogin callbackUrl={callbackUrl} />

{/* Footer */}
<footer className="pt-1">
<p className="text-zinc-500 dark:text-zinc-400 text-center text-[10px] leading-relaxed">
By signing in, you agree to our{" "}
<Link
href="https://www.avax.network/terms-of-use"
target="_blank"
className="text-zinc-700 dark:text-zinc-300 hover:text-red-500 dark:hover:text-red-400 transition-colors underline underline-offset-2"
>
Terms
</Link>{" "}
and{" "}
<Link
href="https://www.avax.network/privacy-policy"
target="_blank"
className="text-zinc-700 dark:text-zinc-300 hover:text-red-500 dark:hover:text-red-400 transition-colors underline underline-offset-2"
>
Privacy Policy
</Link>
</p>
</footer>
</div>
</div>
)}
</DialogContent>
</Dialog.Portal>
</Dialog.Root>
);
}

4 changes: 4 additions & 0 deletions components/login/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { LoginModal } from './LoginModal';
export { default as FormLogin } from './FormLogin';
export { VerifyEmail } from './verify/VerifyEmail';

78 changes: 55 additions & 23 deletions components/login/social-login/SocialLogin.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { signIn } from "next-auth/react";
import React from "react";
import { Separator } from "@/components/ui/separator";
import SocialLoginButton from "./SocialLoginButton";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { SocialLoginProps } from "@/types/socialLoginProps";

function SocialLogin({ callbackUrl = "/" }: SocialLoginProps) {
Expand All @@ -10,31 +10,63 @@ function SocialLogin({ callbackUrl = "/" }: SocialLoginProps) {
}

return (
<div>
<div className="flex items-center justify-center w-full my-4">
<Separator className="flex-1 bg-zinc-800" />
<span className="px-4 text-zinc-400 text-sm font-medium whitespace-nowrap md:px-6">
SIGN IN WITH
</span>
<Separator className="flex-1 bg-zinc-800" />
<div className="w-full space-y-3">
{/* Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-zinc-200 dark:border-zinc-800"></div>
</div>
<div className="relative flex justify-center text-[10px]">
<span className="bg-[inherit] px-2 text-zinc-500 dark:text-zinc-400 font-medium uppercase tracking-wider">
Or
</span>
</div>
</div>
<div className="flex items-center justify-center gap-4">
<SocialLoginButton
name="Google"
image="https://qizat5l3bwvomkny.public.blob.vercel-storage.com/builders-hub/hackaton-platform-images/googleLogo-OxWoKkbOlT1idr0dqcZcrsPhx2yDj5.svg"

{/* Social Buttons */}
<div className="grid grid-cols-3 gap-2.5">
<Button
variant="outline"
className="h-10 flex items-center justify-center border border-zinc-200 dark:border-zinc-800 rounded-lg bg-white dark:bg-zinc-950 hover:bg-zinc-50 dark:hover:bg-zinc-900 hover:border-zinc-300 dark:hover:border-zinc-700 transition-all duration-200 group"
onClick={() => SignInSocialMedia("google")}
/>
<SocialLoginButton
name="Github"
image="https://qizat5l3bwvomkny.public.blob.vercel-storage.com/builders-hub/hackaton-platform-images/githubLogo-0HXD6L0XWqDxRru8DDR7jHm619qtjH.svg"
>
<Image
src="/brands/google.svg"
alt="Google"
width={20}
height={20}
className="w-5 h-5"
/>
<span className="sr-only">Google</span>
</Button>
<Button
variant="outline"
className="h-10 flex items-center justify-center border border-zinc-200 dark:border-zinc-800 rounded-lg bg-white dark:bg-zinc-950 hover:bg-zinc-50 dark:hover:bg-zinc-900 hover:border-zinc-300 dark:hover:border-zinc-700 transition-all duration-200 group"
onClick={() => SignInSocialMedia("github")}
/>

<SocialLoginButton
name="X"
image="https://qizat5l3bwvomkny.public.blob.vercel-storage.com/builders-hub/hackaton-platform-images/X_X_logo-xyp7skXcigJFOHpmC3ps7MRg0d14m2.svg"
>
<Image
src="/brands/github.svg"
alt="GitHub"
width={20}
height={20}
className="w-5 h-5 dark:invert"
/>
<span className="sr-only">Github</span>
</Button>
<Button
variant="outline"
className="h-10 flex items-center justify-center border border-zinc-200 dark:border-zinc-800 rounded-lg bg-white dark:bg-zinc-950 hover:bg-zinc-50 dark:hover:bg-zinc-900 hover:border-zinc-300 dark:hover:border-zinc-700 transition-all duration-200 group"
onClick={() => SignInSocialMedia("X")}
/>
>
<Image
src="/brands/x.svg"
alt="X"
width={20}
height={20}
className="w-5 h-5 dark:invert"
/>
<span className="sr-only">X</span>
</Button>
</div>
</div>
);
Expand Down
Loading