Refine new user auth experience

This commit is contained in:
Sam Becker 2025-03-11 21:35:16 -05:00
parent 11af89065b
commit eaecfae7c9
8 changed files with 106 additions and 76 deletions

View File

@ -1,36 +0,0 @@
'use client';
import PhotoUploadWithStatus from '@/photo/PhotoUploadWithStatus';
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
import { useAppState } from '@/state/AppState';
import Link from 'next/link';
import { FaArrowRight } from 'react-icons/fa';
export default function AdminCTA({
shouldResize,
onLastUpload,
}: {
shouldResize: boolean
onLastUpload: () => Promise<void>
}) {
const { isUserSignedIn } = useAppState();
return (
<div className="flex justify-center pt-4">
{isUserSignedIn
? <PhotoUploadWithStatus
inputId="admin-cta"
shouldResize={shouldResize}
onLastUpload={onLastUpload}
showStatusText={false}
/>
: <Link
href={PATH_ADMIN_PHOTOS}
className="button primary"
>
<span>Admin Dashboard</span>
<FaArrowRight size={10} />
</Link>}
</div>
);
}

View File

@ -0,0 +1,45 @@
'use client';
import { useAppState } from '@/state/AppState';
import SignInForm from '@/auth/SignInForm';
import clsx from 'clsx';
import PhotoUploadWithStatus from '@/photo/PhotoUploadWithStatus';
export default function SignInOrUploadClient({
shouldResize,
onLastUpload,
}: {
shouldResize: boolean
onLastUpload: () => Promise<void>
}) {
const { isUserSignedIn, isCheckingAuth } = useAppState();
return (
<div className={clsx(
'flex justify-center items-center flex-col gap-4',
)}>
<div>
{isCheckingAuth
? 'Loading ...'
: isUserSignedIn
? 'Add your first photo'
: 'Sign in to upload photos'}
</div>
{!isCheckingAuth && isUserSignedIn === false &&
<div className="flex justify-center my-2 sm:my-4">
<SignInForm
className="max-w-[90%] sm:max-w-none"
includeTitle={false}
shouldRedirect={false}
/>
</div>}
{isUserSignedIn === true &&
<PhotoUploadWithStatus
inputId="admin-cta"
shouldResize={shouldResize}
onLastUpload={onLastUpload}
showStatusText={false}
/>}
</div>
);
}

View File

@ -11,13 +11,26 @@ import {
} from 'react';
import { getAuthAction, signInAction } from './actions';
import ErrorNote from '@/components/ErrorNote';
import { KEY_CALLBACK_URL, KEY_CREDENTIALS_SIGN_IN_ERROR } from '.';
import {
KEY_CALLBACK_URL,
KEY_CREDENTIALS_SIGN_IN_ERROR,
KEY_CREDENTIALS_SUCCESS,
} from '.';
import { useSearchParams } from 'next/navigation';
import { useAppState } from '@/state/AppState';
import { clsx } from 'clsx/lite';
import { FiLock } from 'react-icons/fi';
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
export default function SignInForm() {
export default function SignInForm({
includeTitle = true,
shouldRedirect = true,
className,
}: {
includeTitle?: boolean
shouldRedirect?: boolean
className?: string
}) {
const params = useSearchParams();
const { setUserEmail } = useAppState();
@ -33,12 +46,15 @@ export default function SignInForm() {
}, []);
useEffect(() => {
if (response === KEY_CREDENTIALS_SUCCESS) {
setUserEmail?.(email);
}
return () => {
// Capture user email before unmounting
getAuthAction().then(auth =>
setUserEmail?.(auth?.user?.email ?? undefined));
};
}, [setUserEmail]);
}, [setUserEmail, response, email]);
const isFormValid =
email.length > 0 &&
@ -48,20 +64,19 @@ export default function SignInForm() {
<Container className={clsx(
'w-[calc(100vw-1.5rem)] sm:w-[min(360px,90vw)]',
'px-6 py-5',
className,
)}>
<h1 className={clsx(
'flex gap-3 items-center justify-center',
'self-start text-2xl mb-3.5',
)}>
<FiLock className="text-main translate-y-[0.5px]" />
<span className="text-main">
Sign in
</span>
</h1>
<form
action={action}
className="w-full"
>
{includeTitle &&
<h1 className={clsx(
'flex gap-3 items-center justify-center',
'self-start text-2xl mb-3.5',
)}>
<FiLock className="text-main translate-y-[0.5px]" />
<span className="text-main">
Sign in
</span>
</h1>}
<form action={action} className="w-full">
<div className="space-y-6 w-full -translate-y-0.5">
{response === KEY_CREDENTIALS_SIGN_IN_ERROR &&
<ErrorNote>
@ -83,11 +98,12 @@ export default function SignInForm() {
value={password}
onChange={setPassword}
/>
<input
type="hidden"
name={KEY_CALLBACK_URL}
value={params.get(KEY_CALLBACK_URL) ?? ''}
/>
{shouldRedirect &&
<input
type="hidden"
name={KEY_CALLBACK_URL}
value={params.get(KEY_CALLBACK_URL) || PATH_ADMIN_PHOTOS}
/>}
</div>
<SubmitButtonWithStatus disabled={!isFormValid}>
Sign in

View File

@ -5,12 +5,12 @@ import {
KEY_CREDENTIALS_CALLBACK_ROUTE_ERROR_URL,
KEY_CREDENTIALS_SIGN_IN_ERROR,
KEY_CREDENTIALS_SIGN_IN_ERROR_URL,
KEY_CREDENTIALS_SUCCESS,
auth,
generateAuthSecret,
signIn,
signOut,
} from '@/auth';
import { PATH_ADMIN_PHOTOS } from '@/app/paths';
import type { Session } from 'next-auth';
import { redirect } from 'next/navigation';
@ -38,7 +38,10 @@ export const signInAction = async (
throw error;
}
}
redirect(formData.get(KEY_CALLBACK_URL) as string || PATH_ADMIN_PHOTOS);
if (formData.get(KEY_CALLBACK_URL)) {
redirect(formData.get(KEY_CALLBACK_URL) as string);
}
return KEY_CREDENTIALS_SUCCESS;
};
export const signOutAction = async () =>

View File

@ -7,6 +7,7 @@ export const KEY_CREDENTIALS_SIGN_IN_ERROR_URL =
'https://errors.authjs.dev#credentialssignin';
export const KEY_CREDENTIALS_CALLBACK_ROUTE_ERROR_URL =
'https://errors.authjs.dev#callbackrouteerror';
export const KEY_CREDENTIALS_SUCCESS = 'success';
export const KEY_CALLBACK_URL = 'callbackUrl';
export const {

View File

@ -1,13 +1,13 @@
import AdminCTA from '@/admin/AdminCTA';
import Container from '@/components/Container';
import SiteGrid from '@/components/SiteGrid';
import { IS_SITE_READY, PRESERVE_ORIGINAL_UPLOADS } from '@/app/config';
import { PATH_ADMIN_CONFIGURATION } from '@/app/paths';
import AdminAppConfiguration from '@/admin/AdminAppConfiguration';
import { clsx } from 'clsx/lite';
import Link from 'next/link';
import { HiOutlinePhotograph } from 'react-icons/hi';
import { revalidatePath } from 'next/cache';
import SignInOrUploadClient from '@/admin/SignInOrUploadClient';
import Link from 'next/link';
import { PATH_ADMIN_CONFIGURATION } from '@/app/paths';
export default function PhotosEmptyState() {
return (
@ -30,19 +30,14 @@ export default function PhotosEmptyState() {
{!IS_SITE_READY
? <AdminAppConfiguration simplifiedView />
: <div className="max-w-md text-center space-y-6">
<div className="space-y-2">
<div>
Add your first photo:
</div>
<AdminCTA
shouldResize={!PRESERVE_ORIGINAL_UPLOADS}
onLastUpload={async () => {
'use server';
// Update upload count in admin nav
revalidatePath('/admin', 'layout');
}}
/>
</div>
<SignInOrUploadClient
shouldResize={!PRESERVE_ORIGINAL_UPLOADS}
onLastUpload={async () => {
'use server';
// Update upload count in admin nav
revalidatePath('/admin', 'layout');
}}
/>
<div>
Change the name of this blog and other configuration
by editing environment variables referenced in

View File

@ -44,6 +44,7 @@ export type AppStateContext = {
isUserSignedInEager?: boolean
clearAuthStateAndRedirect?: () => void
// ADMIN
isCheckingAuth?: boolean
adminUpdateTimes?: Date[]
registerAdminUpdate?: () => void
refreshAdminData?: () => void

View File

@ -91,7 +91,11 @@ export default function AppStateProvider({
const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []);
const { data: auth, error: authError } = useSWR('getAuth', getAuthAction);
const {
data: auth,
error: authError,
isLoading: isCheckingAuth,
} = useSWR('getAuth', getAuthAction);
useEffect(() => {
setIsUserSignedInEager(hasAuthEmailCookie());
if (!authError) {
@ -176,6 +180,7 @@ export default function AppStateProvider({
recipeModalProps,
setRecipeModalProps,
// AUTH
isCheckingAuth,
userEmail,
setUserEmail,
isUserSignedIn,