Further improve auth experience

This commit is contained in:
Jared Schoeny
2025-10-09 19:58:58 -10:00
parent 86dcc9279e
commit 3e3a58354f
13 changed files with 234 additions and 23 deletions

View File

@@ -1,28 +1,42 @@
"use client";
import React, { useActionState} from "react";
import React, { useActionState, useEffect} from "react";
import { FiEye, FiEyeOff } from "react-icons/fi";
import { AuthActionState, login } from "@/app/login/actions";
import { useSearchParams } from "next/navigation";
import { redirect, useSearchParams } from "next/navigation";
import { useAuthContext } from "@/contexts/AuthContext";
export default function LoginForm() {
const { setUser } = useAuthContext();
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const [showPassword, setShowPassword] = React.useState(false);
const searchParams = useSearchParams();
const urlError = searchParams.get("error");
const [state, formAction] = useActionState<AuthActionState, FormData>(login, { error: null });
const [state, formAction] = useActionState<AuthActionState, FormData>(login, null);
const errorMessage = urlError === "EMAIL_CONFIRMATION_ERROR" ?
"Email verification failed. Try again or request a new link." :
state?.error || null;
const redirectTo = searchParams.get("redirectTo");
const emailValid = /.+@.+\..+/.test(email);
const passwordValid = password.length > 1;
const isValid = emailValid && passwordValid;
useEffect(() => {
if (state && state.error === null) {
setUser(state.user);
if (state.redirectTo) {
redirect(state.redirectTo);
}
}
}, [state]);
return (
<form className="grid gap-5 group">
{redirectTo && (
<input type="hidden" name="redirectTo" value={redirectTo} />
)}
{(errorMessage) && (
<div className="rounded-md bg-red-500/10 ring-1 ring-red-600/40 px-3 py-2 text-sm text-red-300">
{errorMessage}

View File

@@ -1,11 +1,13 @@
"use client";
import React, { useActionState, useEffect } from "react";
import { useSearchParams } from "next/navigation";
import { FiEye, FiEyeOff } from "react-icons/fi";
import { AuthActionState, signup } from "@/app/signup/actions";
import { validateEmail, validatePassword } from "@/utils/auth";
export default function SignupForm() {
const searchParams = useSearchParams();
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const [confirm, setConfirm] = React.useState("");
@@ -27,8 +29,13 @@ export default function SignupForm() {
setPasswordError(error);
}, [password]);
const redirectTo = searchParams.get("redirectTo");
return (
<form className="grid gap-5 group">
{redirectTo && (
<input type="hidden" name="redirectTo" value={redirectTo} />
)}
{(state?.error) && (
<div className="rounded-md bg-red-500/10 ring-1 ring-red-600/40 px-3 py-2 text-sm text-red-300">
{state?.error}

View File

@@ -12,6 +12,9 @@ export default function Footer() {
<Link href="/submit" className="hover:underline">
Submit
</Link>
<Link href="/login" className="hover:underline font-medium text-foreground">
Already a creator? Log in
</Link>
</div>
</div>
</footer>

View File

@@ -4,6 +4,9 @@ import Link from "next/link";
import React from "react";
import { usePathname } from "next/navigation";
import { useBaseRoms } from "@/contexts/BaseRomContext";
import { useAuthContext } from "@/contexts/AuthContext";
import { createClient } from "@/utils/supabase/client";
import Avatar from "@/components/Account/Avatar";
function NavLink({ href, label, className = "" }: { href: string; label: React.ReactNode; className?: string }) {
const pathname = usePathname();
@@ -23,7 +26,38 @@ function NavLink({ href, label, className = "" }: { href: string; label: React.R
export default function Header() {
const { countReady } = useBaseRoms();
const { user } = useAuthContext();
const pathname = usePathname();
const supabase = createClient();
const [isAuthenticated, setIsAuthenticated] = React.useState<boolean>(false);
const [userId, setUserId] = React.useState<string | null>(null);
const [avatarUrl, setAvatarUrl] = React.useState<string | null>(null);
React.useEffect(() => {
let isMounted = true;
(async () => {
const { data } = await supabase.auth.getUser();
if (!isMounted) return;
const authed = Boolean(data.user);
setIsAuthenticated(authed);
setUserId(data.user?.id ?? null);
// Best-effort fetch profile avatar_url for header avatar
if (authed && data.user?.id) {
const { data: profile } = await supabase
.from('profiles')
.select('avatar_url')
.eq('id', data.user.id)
.single();
setAvatarUrl(profile?.avatar_url ?? null);
} else {
setAvatarUrl(null);
}
})();
return () => {
isMounted = false;
};
}, [supabase, user]);
return (
<header className="sticky top-0 z-40 w-full border-b border-[var(--border)] backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="mx-auto flex h-16 max-w-screen-2xl items-center justify-between px-6">
@@ -52,6 +86,16 @@ export default function Header() {
>
Submit
</Link>
{isAuthenticated && (
<Link
href="/account"
className="ml-1 inline-flex items-center justify-center rounded-full ring-1 ring-[var(--border)] p-[2px] hover:bg-[var(--surface-2)]"
aria-label="Open account"
title="Account"
>
<Avatar uid={userId} url={avatarUrl} size={28} />
</Link>
)}
</nav>
</div>
</header>

View File

@@ -0,0 +1,64 @@
"use client";
import React, { useEffect } from 'react'
import Link from 'next/link'
const SubmitAuthOverlay: React.FC = () => {
useEffect(() => {
const html = document.documentElement;
const body = document.body;
const previousHtmlOverflow = html.style.overflow;
const previousBodyOverflow = body.style.overflow;
const previousBodyPaddingRight = body.style.paddingRight;
const scrollBarWidth = window.innerWidth - html.clientWidth;
html.style.overflow = 'hidden';
body.style.overflow = 'hidden';
if (scrollBarWidth > 0) {
body.style.paddingRight = `${scrollBarWidth}px`;
}
return () => {
html.style.overflow = previousHtmlOverflow;
body.style.overflow = previousBodyOverflow;
body.style.paddingRight = previousBodyPaddingRight;
};
}, []);
return (
<div className="fixed left-0 right-0 top-16 bottom-0 z-[100] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-sm" />
<div
role="dialog"
aria-modal="true"
aria-label="Creators only"
className="relative z-[101] mb-16 card p-6 max-w-md w-full rounded-lg"
>
<div className="flex flex-col gap-4">
<div>
<div className="text-lg font-semibold">Creators only</div>
<p className="mt-1 text-sm text-foreground/80">
You need an account to submit new romhacks for others to play. It only takes a minute.
</p>
</div>
<div className="flex flex-wrap justify-center items-center gap-3">
<Link
href="/signup"
className="shine-wrap btn-premium h-11 min-w-[7.5rem] text-sm font-semibold rounded-md text-[var(--accent-foreground)]"
>
<span>Create account</span>
</Link>
<Link
href="/login?redirectTo=%2Fsubmit"
className="inline-flex h-11 items-center justify-center rounded-md px-4 text-sm font-semibold ring-1 ring-[var(--border)] hover:bg-[var(--surface-2)]"
>
Log in
</Link>
</div>
</div>
</div>
</div>
)
}
export default SubmitAuthOverlay;