chore: auth flow cleanup and verification
This commit is contained in:
+9
-12
@@ -7,7 +7,6 @@ import {
|
||||
ExternalLinkIcon,
|
||||
Gamepad2Icon,
|
||||
MessageSquareIcon,
|
||||
MonitorIcon,
|
||||
SettingsIcon,
|
||||
TrendingUpIcon,
|
||||
DatabaseIcon,
|
||||
@@ -50,13 +49,11 @@ function SearchContent() {
|
||||
const [results, setResults] = useState<UnifiedResult[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const isValidQuery = query && query.length >= 2
|
||||
|
||||
// Handle direct navigation / browser back-forward
|
||||
useEffect(() => {
|
||||
if (!query || query.length < 2) {
|
||||
setResults([])
|
||||
setError(null)
|
||||
return
|
||||
}
|
||||
if (!isValidQuery) return
|
||||
|
||||
let cancelled = false
|
||||
|
||||
@@ -85,7 +82,7 @@ function SearchContent() {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [query])
|
||||
}, [isValidQuery, query])
|
||||
|
||||
function handleClick(result: UnifiedResult) {
|
||||
const path = result.appId
|
||||
@@ -114,7 +111,7 @@ function SearchContent() {
|
||||
: "Enter a game name or AppID to find benchmarks, settings, and reviews."}
|
||||
</motion.p>
|
||||
|
||||
{!query && (
|
||||
{!isValidQuery && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
@@ -127,7 +124,7 @@ function SearchContent() {
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{query && loading && (
|
||||
{isValidQuery && loading && (
|
||||
<div className="flex flex-col items-center justify-center py-20 gap-4">
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
@@ -140,7 +137,7 @@ function SearchContent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{query && !loading && error && (
|
||||
{isValidQuery && !loading && error && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
@@ -150,7 +147,7 @@ function SearchContent() {
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{query && !loading && !error && results.length === 0 && (
|
||||
{isValidQuery && !loading && !error && results.length === 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
@@ -163,7 +160,7 @@ function SearchContent() {
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{query && !loading && !error && results.length > 0 && (
|
||||
{isValidQuery && !loading && !error && results.length > 0 && (
|
||||
<motion.div
|
||||
className="flex flex-col gap-3"
|
||||
initial="hidden"
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Lock, Loader2, ArrowLeft } from "lucide-react"
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
import {
|
||||
forgotPasswordSchema,
|
||||
type ForgotPasswordInput,
|
||||
} from "@/lib/auth/validation"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
@@ -51,7 +50,7 @@ export default function ForgotPasswordForm() {
|
||||
Forgot your password?
|
||||
</h1>
|
||||
<p className="text-xs text-[#ebe4f1]/50 mt-1 leading-relaxed">
|
||||
Enter your email and we'll send you a verification code to
|
||||
Enter your email and we'll send you a verification code to
|
||||
reset your password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
import {
|
||||
loginEmailSchema,
|
||||
loginSchema,
|
||||
type LoginInput,
|
||||
} from "@/lib/auth/validation"
|
||||
import SocialButtons from "./social-buttons"
|
||||
import Link from "next/link"
|
||||
@@ -221,7 +220,7 @@ export default function LoginForm() {
|
||||
)}
|
||||
|
||||
<p className="text-center text-xs text-[#ebe4f1]/50">
|
||||
Don't have an account?{" "}
|
||||
Don't have an account?{" "}
|
||||
<Link
|
||||
href="/signup"
|
||||
className="text-[#eb3779] hover:underline"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useRef, useState, useCallback, useEffect } from "react"
|
||||
import { useRef, useCallback, useMemo } from "react"
|
||||
|
||||
interface OtpInputProps {
|
||||
length?: number
|
||||
@@ -17,23 +17,18 @@ export default function OtpInput({
|
||||
disabled = false,
|
||||
error,
|
||||
}: OtpInputProps) {
|
||||
const [digits, setDigits] = useState<string[]>(
|
||||
value.split("").concat(Array(length).fill("")).slice(0, length),
|
||||
const digits = useMemo(
|
||||
() =>
|
||||
value
|
||||
.split("")
|
||||
.concat(Array(length).fill(""))
|
||||
.slice(0, length),
|
||||
[value, length],
|
||||
)
|
||||
const refs = useRef<(HTMLInputElement | null)[]>([])
|
||||
|
||||
// Sync with external value
|
||||
useEffect(() => {
|
||||
const newDigits = value
|
||||
.split("")
|
||||
.concat(Array(length).fill(""))
|
||||
.slice(0, length)
|
||||
setDigits(newDigits)
|
||||
}, [value, length])
|
||||
|
||||
const updateDigits = useCallback(
|
||||
(newDigits: string[]) => {
|
||||
setDigits(newDigits)
|
||||
onChange(newDigits.join(""))
|
||||
},
|
||||
[onChange],
|
||||
|
||||
@@ -20,14 +20,11 @@ export default function OtpVerificationStep({
|
||||
const [error, setError] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [resendTimer, setResendTimer] = useState(300) // 5 minutes
|
||||
const [canResend, setCanResend] = useState(false)
|
||||
const canResend = resendTimer <= 0
|
||||
|
||||
// Countdown timer
|
||||
useEffect(() => {
|
||||
if (resendTimer <= 0) {
|
||||
setCanResend(true)
|
||||
return
|
||||
}
|
||||
if (resendTimer <= 0) return
|
||||
const interval = setInterval(() => {
|
||||
setResendTimer((prev) => prev - 1)
|
||||
}, 1000)
|
||||
@@ -40,15 +37,16 @@ export default function OtpVerificationStep({
|
||||
return `${mins}:${secs.toString().padStart(2, "0")}`
|
||||
}
|
||||
|
||||
const handleVerify = useCallback(async () => {
|
||||
if (otp.length !== 6) return
|
||||
const handleVerify = useCallback(
|
||||
async (otpValue: string) => {
|
||||
if (otpValue.length !== 6) return
|
||||
|
||||
setIsLoading(true)
|
||||
setError("")
|
||||
|
||||
const { error } = await authClient.emailOtp.verifyEmail({
|
||||
email,
|
||||
otp,
|
||||
otp: otpValue,
|
||||
})
|
||||
|
||||
setIsLoading(false)
|
||||
@@ -63,14 +61,9 @@ export default function OtpVerificationStep({
|
||||
}
|
||||
|
||||
onSuccess()
|
||||
}, [otp, email, onSuccess])
|
||||
|
||||
// Auto-submit when all digits entered
|
||||
useEffect(() => {
|
||||
if (otp.length === 6) {
|
||||
handleVerify()
|
||||
}
|
||||
}, [otp, handleVerify])
|
||||
},
|
||||
[email, onSuccess],
|
||||
)
|
||||
|
||||
const handleResend = async () => {
|
||||
setError("")
|
||||
@@ -79,7 +72,6 @@ export default function OtpVerificationStep({
|
||||
type: "email-verification",
|
||||
})
|
||||
setResendTimer(300)
|
||||
setCanResend(false)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -101,7 +93,12 @@ export default function OtpVerificationStep({
|
||||
</label>
|
||||
<OtpInput
|
||||
value={otp}
|
||||
onChange={setOtp}
|
||||
onChange={(value) => {
|
||||
setOtp(value)
|
||||
if (value.length === 6) {
|
||||
handleVerify(value)
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
error={error}
|
||||
/>
|
||||
@@ -126,7 +123,7 @@ export default function OtpVerificationStep({
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleVerify}
|
||||
onClick={() => handleVerify(otp)}
|
||||
disabled={isLoading || otp.length !== 6}
|
||||
className="w-full py-2.5 rounded-lg bg-[#eb3779] text-white text-sm font-semibold hover:bg-[#eb3779]/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function PasskeySetupStep({
|
||||
Set up a passkey
|
||||
</h1>
|
||||
<p className="text-xs text-[#ebe4f1]/50 mt-1 leading-relaxed">
|
||||
Sign in faster with biometrics or your device's security
|
||||
Sign in faster with biometrics or your device's security
|
||||
key. No password needed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import {
|
||||
Mail,
|
||||
CheckCircle2,
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
import {
|
||||
resetPasswordSchema,
|
||||
type ResetPasswordInput,
|
||||
} from "@/lib/auth/validation"
|
||||
import OtpInput from "./otp-input"
|
||||
import PasswordStrengthMeter from "./password-strength"
|
||||
@@ -30,14 +29,11 @@ export default function ResetPasswordForm({
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isSuccess, setIsSuccess] = useState(false)
|
||||
const [resendTimer, setResendTimer] = useState(300)
|
||||
const [canResend, setCanResend] = useState(false)
|
||||
const canResend = resendTimer <= 0
|
||||
|
||||
// Countdown timer
|
||||
useEffect(() => {
|
||||
if (resendTimer <= 0) {
|
||||
setCanResend(true)
|
||||
return
|
||||
}
|
||||
if (resendTimer <= 0) return
|
||||
const interval = setInterval(() => {
|
||||
setResendTimer((prev) => prev - 1)
|
||||
}, 1000)
|
||||
@@ -65,9 +61,10 @@ export default function ResetPasswordForm({
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
const { error } = await authClient.resetPassword({
|
||||
newPassword,
|
||||
const { error } = await authClient.emailOtp.resetPassword({
|
||||
email,
|
||||
otp,
|
||||
password: newPassword,
|
||||
})
|
||||
setIsLoading(false)
|
||||
|
||||
@@ -87,7 +84,6 @@ export default function ResetPasswordForm({
|
||||
setError("")
|
||||
await authClient.emailOtp.requestPasswordReset({ email })
|
||||
setResendTimer(300)
|
||||
setCanResend(false)
|
||||
}
|
||||
|
||||
if (isSuccess) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState } from "react"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
import { signupSchema, type SignupInput } from "@/lib/auth/validation"
|
||||
import { signupSchema } from "@/lib/auth/validation"
|
||||
import SocialButtons from "./social-buttons"
|
||||
import PasswordStrengthMeter from "./password-strength"
|
||||
import Link from "next/link"
|
||||
@@ -61,7 +61,7 @@ export default function SignupFormStep({ onSuccess }: SignupFormStepProps) {
|
||||
Create your account
|
||||
</h1>
|
||||
<p className="text-xs text-[#ebe4f1]/50 mt-1">
|
||||
Choose how you'd like to sign up
|
||||
Choose how you'd like to sign up
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
+17
-7
@@ -9,7 +9,7 @@ import { CircleXIcon, Gamepad2Icon, MenuIcon, XIcon } from "lucide-react"
|
||||
import { routes } from "@/lib/routes"
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import { useDebounce } from "@/lib/hooks/useDebounce"
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
import { authClient, useSession } from "@/lib/auth-client"
|
||||
import { LogOut, User } from "lucide-react"
|
||||
|
||||
export default function Navbar() {
|
||||
@@ -27,13 +27,14 @@ export default function Navbar() {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const { data: session, isPending: isSessionLoading } =
|
||||
authClient.useSession()
|
||||
useSession()
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false)
|
||||
|
||||
// Sync search query with URL ?q= param
|
||||
useEffect(() => {
|
||||
const q = searchParams.get("q") || ""
|
||||
setSearchQuery(q)
|
||||
const timeout = setTimeout(() => setSearchQuery(q), 0)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [searchParams])
|
||||
|
||||
// Update URL when debounced query changes (skip if already matches)
|
||||
@@ -61,13 +62,16 @@ export default function Navbar() {
|
||||
sessionStorage.getItem("focusSearch") === "true"
|
||||
) {
|
||||
sessionStorage.removeItem("focusSearch")
|
||||
setForceFocusStyles(true)
|
||||
const focusTimeout = setTimeout(() => setForceFocusStyles(true), 0)
|
||||
// Focus the input after the layout animation element mounts
|
||||
requestAnimationFrame(() => {
|
||||
inputRef.current?.focus()
|
||||
})
|
||||
const timer = setTimeout(() => setForceFocusStyles(false), 450)
|
||||
return () => clearTimeout(timer)
|
||||
return () => {
|
||||
clearTimeout(focusTimeout)
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
}, [isLanding, searchQuery])
|
||||
|
||||
@@ -200,9 +204,12 @@ export default function Navbar() {
|
||||
className='flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-white/[0.05] transition-colors'
|
||||
>
|
||||
{session.user.image ? (
|
||||
<img
|
||||
<Image
|
||||
src={session.user.image}
|
||||
alt=''
|
||||
width={28}
|
||||
height={28}
|
||||
unoptimized
|
||||
className='h-7 w-7 rounded-full'
|
||||
/>
|
||||
) : (
|
||||
@@ -320,9 +327,12 @@ export default function Navbar() {
|
||||
<div className='space-y-3'>
|
||||
<div className='flex items-center gap-2'>
|
||||
{session.user.image ? (
|
||||
<img
|
||||
<Image
|
||||
src={session.user.image}
|
||||
alt=''
|
||||
width={32}
|
||||
height={32}
|
||||
unoptimized
|
||||
className='h-8 w-8 rounded-full'
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -53,7 +53,7 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
|
||||
)
|
||||
|
||||
// Fetch platform support + anti-cheat for local games
|
||||
let platformSupportMap = new Map<
|
||||
const platformSupportMap = new Map<
|
||||
string,
|
||||
{
|
||||
isSupported: boolean
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { createAuthClient } from 'better-auth/client'
|
||||
import { createAuthClient } from 'better-auth/react'
|
||||
import { adminClient, emailOTPClient, lastLoginMethodClient } from 'better-auth/client/plugins'
|
||||
import { passkeyClient } from '@better-auth/passkey/client'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user