Make sign in and sign up pages and allow for email/pwd registration

This commit is contained in:
MartinBraquet
2025-09-06 10:50:47 +02:00
parent 523bbd11cc
commit ea74e0514e
10 changed files with 501 additions and 493 deletions

View File

@@ -26,15 +26,16 @@ export const createUser: APIHandler<'create-user'> = async (
const testUserAKAEmailPasswordUser =
firebaseUser.providerData[0].providerId === 'password'
if (
testUserAKAEmailPasswordUser &&
adminToken !== process.env.TEST_CREATE_USER_KEY
) {
throw new APIError(
401,
'Must use correct TEST_CREATE_USER_KEY to create user with email/password'
)
}
// if (
// testUserAKAEmailPasswordUser &&
// adminToken !== process.env.TEST_CREATE_USER_KEY
// ) {
// throw new APIError(
// 401,
// 'Must use correct TEST_CREATE_USER_KEY to create user with email/password'
// )
// }
const host = req.get('referer')
log(`Create user from: ${host}`)

View File

@@ -1,152 +0,0 @@
"use client";
import {signIn} from "next-auth/react";
import {useRouter, useSearchParams} from "next/navigation";
import {Suspense, useEffect, useState} from "react";
import Link from "next/link";
import {FcGoogle} from "react-icons/fc";
import FavIcon from "@/components/FavIcon";
export default function LoginPage() {
return (
<Suspense fallback={<div></div>}>
<RegisterComponent/>
</Suspense>
);
}
function RegisterComponent() {
const router = useRouter();
const searchParams = useSearchParams();
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const error = searchParams.get('error');
if (error === 'OAuthAccountNotLinked') {
setError('This email is already registered with a different provider');
} else if (error) {
setError('An error occurred during login');
}
}, [searchParams]);
const handleGoogleSignIn = async () => {
try {
setIsLoading(true);
await signIn('google', {callbackUrl: '/'});
} catch {
setError('Failed to sign in with Google');
setIsLoading(false);
}
};
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
try {
event.preventDefault();
setIsLoading(true);
setError(null);
const formData = new FormData(event.currentTarget);
const response = await signIn("credentials", {
...Object.fromEntries(formData),
redirect: false,
});
if (response?.error) {
setError("Invalid email or password");
setIsLoading(false);
return;
}
router.push("/");
router.refresh();
} catch {
setError("An error occurred during login");
setIsLoading(false);
}
}
console.log('Form rendering');
return (
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<div className="flex justify-center mb-6">
<FavIcon className="dark:invert"/>
</div>
<h2 className="mt-6 text-center text-3xl font-extrabold ">
Sign in to your account
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email" className="sr-only">
Email
</label>
<input
id="email"
name="email"
type="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Email"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Password"
/>
</div>
</div>
{error && (
<div className="text-red-500 text-sm text-center">{error}</div>
)}
<div className="space-y-4">
<button
type="submit"
disabled={isLoading}
className={`group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-full text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${isLoading ? 'opacity-70 cursor-not-allowed' : ''}`}
>
{isLoading ? 'Signing in...' : 'Sign in with Email'}
</button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-gray-50 dark:bg-gray-900 text-gray-500">Or continue with</span>
</div>
</div>
<button
type="button"
onClick={handleGoogleSignIn}
disabled={isLoading}
className="w-full flex items-center justify-center gap-2 py-2 px-4 border border-gray-300 rounded-full shadow-sm text-sm font-medium text-gray-700 hover: focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-70 disabled:cursor-not-allowed"
>
<FcGoogle className="w-5 h-5"/>
Sign in with Google
</button>
</div>
</form>
<div className="text-center">
<Link href="/register" className="text-blue-600 hover:underline">
No account? Register.
</Link>
</div>
</div>
</div>
);
}

View File

@@ -1,264 +0,0 @@
"use client";
import {Suspense, useState} from "react";
import Link from "next/link";
import {signIn} from "next-auth/react";
import {FcGoogle} from "react-icons/fc";
import {useSearchParams} from "next/navigation";
import FavIcon from "@/components/FavIcon";
export default function RegisterPage() {
return (
<Suspense fallback={<div></div>}>
<RegisterComponent/>
</Suspense>
);
}
const href = '/onboarding';
function RegisterComponent() {
const searchParams = useSearchParams();
const [error, setError] = useState<string | null>(searchParams.get('error'));
const [isLoading, setIsLoading] = useState(false);
const [registrationSuccess, setRegistrationSuccess] = useState(false);
const [registeredEmail, setRegisteredEmail] = useState('');
function redirect() {
// Redirect to complete profile page
window.location.href = href;
}
const handleGoogleSignUp = async () => {
try {
setIsLoading(true);
await signIn('google', {callbackUrl: href});
} catch (error) {
console.error('Error signing up with Google:', error);
setError('Failed to sign up with Google');
setIsLoading(false);
}
};
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
function handleError(error: unknown) {
console.error("Registration error:", error);
setError(error instanceof Error ? error.message : "Registration failed");
}
try {
event.preventDefault();
setIsLoading(true);
setError(null);
const formData = new FormData(event.currentTarget);
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const name = formData.get("name") as string;
// Basic validation
if (!email || !password || !name) {
handleError("All fields are required");
}
const res = await fetch("/api/auth/signup", {
method: "POST",
body: JSON.stringify({email, password, name}),
headers: {"Content-Type": "application/json"},
});
const data = await res.json();
if (!res.ok) {
handleError(data.error || "Registration failed");
}
// Show a success message with email verification notice
// setRegistrationSuccess(true);
setRegisteredEmail(email);
// Sign in after successful registration
const response = await signIn("credentials", {
email,
password,
redirect: false,
});
if (response?.error) {
handleError("Failed to sign in after registration");
}
redirect()
} catch (error) {
handleError(error);
} finally {
setIsLoading(false);
}
}
return (
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
{registrationSuccess ? (
<div className="text-center">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
<svg
className="h-6 w-6 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h2 className="mt-6 text-3xl font-extrabold ">
Check your email
</h2>
<p className="mt-2 text-sm text-gray-600">
We have sent a verification link to <span className="font-medium">{registeredEmail}</span>.
Please click the link in the email to verify your account.
</p>
<p className="mt-4 text-sm text-gray-500">
Did not receive the email? Check your spam folder or{' '}
<button
type="button"
className="font-medium text-blue-600 hover:text-blue-500"
onClick={() => setRegistrationSuccess(false)}
>
try again
</button>
.
</p>
<div className="mt-6">
<Link
href="/login"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Back to Login
</Link>
</div>
</div>
) : (
<div>
<div>
{/*<h2 className="mt-6 text-center text-xl font-extrabold text-red-700">*/}
{/* The project is still in development...*/}
{/*</h2>*/}
<div className="flex justify-center mb-6">
<FavIcon className="dark:invert"/>
</div>
<h2 className="text-center text-3xl font-extrabold ">
Get Started
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="name" className="sr-only">
Name
</label>
<input
id="name"
name="name"
type="text"
maxLength={100}
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Full Name"
/>
</div>
<div>
<label htmlFor="email" className="sr-only">
Email
</label>
<input
id="email"
name="email"
type="email"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Email"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Password"
/>
</div>
</div>
<div>
<p className="text-xs text-gray-500 mt-2 text-center">
By signing up, I agree to the{" "}
<a href="/terms" className="underline hover:text-blue-600">
Terms and Conditions
</a>{" "}
and{" "}
<a href="/privacy" className="underline hover:text-blue-600">
Privacy Policy
</a>.
</p>
</div>
{error && (
<div className="text-red-500 text-sm text-center">{error}</div>
)}
<div className="space-y-2">
<button
type="submit"
disabled={isLoading}
className={`group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-full text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${isLoading ? 'opacity-70 cursor-not-allowed' : ''}`}
>
{isLoading ? 'Creating account...' : 'Sign up with Email'}
</button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-gray-50 dark:bg-gray-900 text-gray-500">Or sign up with</span>
</div>
</div>
<button
type="button"
onClick={handleGoogleSignUp}
disabled={isLoading}
className="w-full flex items-center justify-center gap-2 py-2 px-4 border border-gray-300 rounded-full shadow-sm text-sm font-medium text-gray-700 hover: focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-70 disabled:cursor-not-allowed"
>
<FcGoogle className="w-5 h-5"/>
Continue with Google
</button>
</div>
</form>
<div className="text-center text-sm mt-2">
<p className="text-gray-600">
Already have an account?{' '}
<Link href="/login" className="font-medium text-blue-600 hover:text-blue-500">
Sign in
</Link>
</p>
</div>
</div>
)
}
</div>
</div>
);
}

View File

@@ -1,11 +0,0 @@
import React from "react";
import Image from "next/image";
type FavIconProps = React.SVGProps<SVGSVGElement>;
const FavIcon: React.FC<FavIconProps> = ({ className }) => (
<Image src="/favicon.ico" alt="Compass logo" width={500} height={500} className={"w-12 h-12 " + className}/>
);
export default FavIcon;

View File

@@ -1,27 +1,26 @@
import { HomeIcon, QuestionMarkCircleIcon } from '@heroicons/react/outline'
import {HomeIcon, QuestionMarkCircleIcon} from '@heroicons/react/outline'
import {
QuestionMarkCircleIcon as SolidQuestionIcon,
HomeIcon as SolidHomeIcon,
QuestionMarkCircleIcon as SolidQuestionIcon,
UserCircleIcon,
} from '@heroicons/react/solid'
import clsx from 'clsx'
import { User } from 'common/user'
import { buildArray } from 'common/util/array'
import { useOnline } from 'web/hooks/use-online'
import { ReactNode, useState } from 'react'
import { Toaster } from 'react-hot-toast'
import { Col } from 'web/components/layout/col'
import { PrivateMessagesIcon } from 'web/components/messaging/messages-icon'
import { BottomNavBar } from 'web/components/nav/love-bottom-nav-bar'
import { useIsMobile } from 'web/hooks/use-is-mobile'
import { useTracking } from 'web/hooks/use-tracking'
import { useUser } from 'web/hooks/use-user'
import { GoogleOneTapLogin } from 'web/lib/firebase/google-onetap-login'
import {User} from 'common/user'
import {buildArray} from 'common/util/array'
import {useOnline} from 'web/hooks/use-online'
import {ReactNode, useState} from 'react'
import {Toaster} from 'react-hot-toast'
import {Col} from 'web/components/layout/col'
import {PrivateMessagesIcon} from 'web/components/messaging/messages-icon'
import {BottomNavBar} from 'web/components/nav/love-bottom-nav-bar'
import {useIsMobile} from 'web/hooks/use-is-mobile'
import {useTracking} from 'web/hooks/use-tracking'
import {useUser} from 'web/hooks/use-user'
import {GoogleOneTapLogin} from 'web/lib/firebase/google-onetap-login'
import Sidebar from './nav/love-sidebar'
import { signupThenMaybeRedirectToSignup } from 'web/lib/util/signup'
import { useLover } from 'web/hooks/use-lover'
import { Lover } from 'common/love/lover'
import { NotificationsIcon, SolidNotificationsIcon } from './notifications-icon'
import {useLover} from 'web/hooks/use-lover'
import {Lover} from 'common/love/lover'
import {NotificationsIcon, SolidNotificationsIcon} from './notifications-icon'
export function LovePage(props: {
trackPageView: string | false
@@ -123,11 +122,7 @@ function getBottomNavigation(user: User, lover: Lover | null | undefined) {
const signedOutNavigation = () => [
{ name: 'Profiles', href: '/', icon: SolidHomeIcon },
{ name: 'About', href: '/about', icon: SolidQuestionIcon },
{
name: 'Sign in',
onClick: signupThenMaybeRedirectToSignup,
icon: UserCircleIcon,
},
{ name: 'Sign in', href: '/signin', icon: UserCircleIcon },
]
const getDesktopNav = (user: User | null | undefined) => {
if (user)

View File

@@ -14,7 +14,7 @@ import { ProfileSummary } from './love-profile-summary'
import { Item, SidebarItem } from './love-sidebar-item'
import ManifoldLoveLogo from '../manifold-love-logo'
import { Button, ColorType, SizeType } from 'web/components/buttons/button'
import { signupThenMaybeRedirectToSignup } from 'web/lib/util/signup'
import {signupRedirect} from 'web/lib/util/signup'
import { useLover } from 'web/hooks/use-lover'
import { useTheme } from 'web/hooks/use-theme'
@@ -56,7 +56,7 @@ export default function Sidebar(props: {
))}
{user === null && <SignUpButton className="mt-4" text="Sign up" />}
{user === null && <SignUpAsMatchmaker className="mt-2" />}
{/*{user === null && <SignUpAsMatchmaker className="mt-2" />}*/}
{user && lover === null && (
<Button className="mt-2" onClick={() => router.push('signup')}>
@@ -86,7 +86,7 @@ const bottomNav = (
toggleTheme: () => void
) =>
buildArray<Item>(
!loggedIn && { name: 'Sign in', icon: LoginIcon, onClick: firebaseLogin },
!loggedIn && { name: 'Sign in', icon: LoginIcon, href: '/signin' },
{
name: theme ?? 'auto',
children:
@@ -129,7 +129,7 @@ export const SignUpButton = (props: {
<Button
color={color ?? 'gradient'}
size={size ?? 'xl'}
onClick={signupThenMaybeRedirectToSignup}
onClick={signupRedirect}
className={clsx('w-full', className)}
>
{text ?? 'Sign up now'}

View File

@@ -10,6 +10,12 @@ export const signupThenMaybeRedirectToSignup = async () => {
const lover = await getLoverRow(userId, db)
if (!lover) {
await Router.push('/signup')
} else {
await Router.push('/')
}
}
}
export async function signupRedirect() {
await Router.push('/register')
}

View File

@@ -2,11 +2,9 @@ import {Lover} from 'common/love/lover'
import {removeNullOrUndefinedProps} from 'common/util/object'
import {Search} from 'web/components/filters/search'
import {LovePage} from 'web/components/love-page'
import {SignUpAsMatchmaker} from 'web/components/nav/love-sidebar'
import {useLover} from 'web/hooks/use-lover'
import {useCompatibleLovers} from 'web/hooks/use-lovers'
import {getStars} from 'web/lib/supabase/stars'
import {signupThenMaybeRedirectToSignup} from 'web/lib/util/signup'
import Router from 'next/router'
import {useCallback, useEffect, useRef, useState} from 'react'
import {Button} from 'web/components/buttons/button'
@@ -23,6 +21,8 @@ import {useUser} from 'web/hooks/use-user'
import {api} from 'web/lib/api'
import {debounce, omit} from 'lodash'
import {PREF_AGE_MAX, PREF_AGE_MIN,} from 'web/components/filters/location-filter'
import {signupRedirect} from "web/lib/util/signup";
export default function ProfilesPage() {
const you = useLover()
@@ -197,44 +197,44 @@ export default function ProfilesPage() {
className="flex-1"
color="gradient"
size="xl"
onClick={signupThenMaybeRedirectToSignup}
onClick={signupRedirect}
>
Sign up
</Button>
<SignUpAsMatchmaker className="flex-1"/>
{/*<SignUpAsMatchmaker className="flex-1"/>*/}
</Col>
)}
{!user && <>
<h1
className="pt-12 pb-2 text-7xl md:text-8xl xs:text-6xl font-extrabold max-w-4xl leading-tight xl:whitespace-nowrap md:whitespace-nowrap ">
Don't Swipe.<br/><span id="typewriter"></span><span id="cursor" className="animate-pulse">|</span>
</h1>
<div className="w-full bg-gray-50 dark:bg-gray-900 py-8 mt-20">
<div className="max-w-6xl mx-auto px-4">
<div className="grid md:grid-cols-3 gap-8 text-center">
<div className="space-y-2">
<h3 className="text-lg font-bold">Radically Transparent</h3>
<p className="text-gray-600 dark:text-gray-400">
No algorithms. Every profile searchable.
</p>
</div>
<h1
className="pt-12 pb-2 text-7xl md:text-8xl xs:text-6xl font-extrabold max-w-4xl leading-tight xl:whitespace-nowrap md:whitespace-nowrap ">
Don't Swipe.<br/><span id="typewriter"></span><span id="cursor" className="animate-pulse">|</span>
</h1>
<div className="w-full bg-gray-50 dark:bg-gray-900 py-8 mt-20">
<div className="max-w-6xl mx-auto px-4">
<div className="grid md:grid-cols-3 gap-8 text-center">
<div className="space-y-2">
<h3 className="text-lg font-bold">Radically Transparent</h3>
<p className="text-gray-600 dark:text-gray-400">
No algorithms. Every profile searchable.
</p>
</div>
<div className="space-y-2">
<h3 className="text-lg font-bold">Built for Depth</h3>
<p className="text-gray-600 dark:text-gray-400">
Filter by any keyword and what matters most.
</p>
</div>
<div className="space-y-2">
<h3 className="text-lg font-bold">Built for Depth</h3>
<p className="text-gray-600 dark:text-gray-400">
Filter by any keyword and what matters most.
</p>
</div>
<div className="space-y-2">
<h3 className="text-lg font-bold">Community Owned</h3>
<p className="text-gray-600 dark:text-gray-400">
Free forever. Built by users, for users.
</p>
<div className="space-y-2">
<h3 className="text-lg font-bold">Community Owned</h3>
<p className="text-gray-600 dark:text-gray-400">
Free forever. Built by users, for users.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</>}
{user &&
<>

252
web/pages/register.tsx Normal file
View File

@@ -0,0 +1,252 @@
"use client";
import {Suspense, useState} from "react";
import Link from "next/link";
import {FcGoogle} from "react-icons/fc";
import {useSearchParams} from "next/navigation";
import {signupThenMaybeRedirectToSignup} from "web/lib/util/signup";
import {createUserWithEmailAndPassword} from "firebase/auth";
import {auth} from "web/lib/firebase/users";
import FavIcon from "web/public/FavIcon";
import {LovePage} from "web/components/love-page";
import {getLoverRow} from "common/love/lover";
import {db} from "web/lib/supabase/db";
import Router from "next/router";
const handleEmailPasswordSignUp = async (email: string, password: string) => {
try {
const creds = await createUserWithEmailAndPassword(auth, email, password);
console.log("User signed up:", creds.user);
const userId = creds?.user.uid
if (userId) {
const lover = await getLoverRow(userId, db)
if (!lover) {
await Router.push('/signup')
} else {
await Router.push('/')
}
}
} catch (error) {
console.error("Error signing up:", error);
}
};
export default function RegisterPage() {
return (
<Suspense fallback={<div></div>}>
<RegisterComponent/>
</Suspense>
);
}
const href = '/signup';
function RegisterComponent() {
const searchParams = useSearchParams();
const [error, setError] = useState<string | null>(searchParams.get('error'));
const [isLoading, setIsLoading] = useState(false);
const [registrationSuccess, setRegistrationSuccess] = useState(false);
const [registeredEmail, setRegisteredEmail] = useState('');
// function redirect() {
// // Redirect to complete profile page
// window.location.href = href;
// }
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
function handleError(error: unknown) {
console.error("Registration error:", error);
setError(error instanceof Error ? error.message : "Registration failed");
}
try {
event.preventDefault();
setIsLoading(true);
setError(null);
const formData = new FormData(event.currentTarget);
const email = formData.get("email") as string;
const password = formData.get("password") as string;
// Basic validation
if (!email || !password) {
handleError("All fields are required");
}
await handleEmailPasswordSignUp(email, password);
// Show a success message with email verification notice
// setRegistrationSuccess(true);
// setRegisteredEmail(email);
// Sign in after successful registration
// ...
// if (response?.error) {
// handleError("Failed to sign in after registration");
// }
// redirect()
} catch (error) {
handleError(error);
} finally {
setIsLoading(false);
}
}
return (
<LovePage trackPageView={'register'}>
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
{registrationSuccess ? (
<div className="text-center">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
<svg
className="h-6 w-6 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h2 className="mt-6 text-3xl font-extrabold ">
Check your email
</h2>
<p className="mt-2 text-sm text-gray-600">
We have sent a verification link to <span className="font-medium">{registeredEmail}</span>.
Please click the link in the email to verify your account.
</p>
<p className="mt-4 text-sm text-gray-500">
Did not receive the email? Check your spam folder or{' '}
<button
type="button"
className="font-medium text-blue-600 hover:text-blue-500"
onClick={() => setRegistrationSuccess(false)}
>
try again
</button>
.
</p>
<div className="mt-6">
<Link
href="/signin"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Back to Login
</Link>
</div>
</div>
) : (
<div>
<div>
{/*<h2 className="mt-6 text-center text-xl font-extrabold text-red-700">*/}
{/* The project is still in development...*/}
{/*</h2>*/}
<div className="flex justify-center mb-6">
<FavIcon className="dark:invert"/>
</div>
<h2 className="text-center text-3xl font-extrabold ">
Get Started
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email" className="sr-only">
Email
</label>
<input
id="email"
name="email"
type="email"
required
className="bg-primary-50 appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Email"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
required
className="bg-primary-50 appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Password"
/>
</div>
</div>
<div>
<p className="text-xs text-gray-500 mt-2 text-center">
By signing up, I agree to the{" "}
<a href="/terms" className="underline hover:text-blue-600">
Terms and Conditions
</a>{" "}
and{" "}
<a href="/privacy" className="underline hover:text-blue-600">
Privacy Policy
</a>.
</p>
</div>
{error && (
<div className="text-red-500 text-sm text-center">{error}</div>
)}
<div className="space-y-2">
<button
type="submit"
disabled={isLoading}
className={`group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-full text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${isLoading ? 'opacity-70 cursor-not-allowed' : ''}`}
>
{isLoading ? 'Creating account...' : 'Sign up with Email'}
</button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-gray-50 dark:bg-gray-900 text-gray-500">Or sign up with</span>
</div>
</div>
<button
type="button"
onClick={signupThenMaybeRedirectToSignup}
disabled={isLoading}
className="w-full flex items-center justify-center gap-2 py-2 px-4 border border-gray-300 rounded-full shadow-sm text-sm font-medium text-gray-700 hover: focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-70 disabled:cursor-not-allowed"
>
<FcGoogle className="w-5 h-5"/>
Continue with Google
</button>
</div>
</form>
<div className="text-center text-sm mt-2">
<p className="text-gray-600">
Already have an account?{' '}
<Link href="/signin" className="font-medium text-blue-600 hover:text-blue-500">
Sign in
</Link>
</p>
</div>
</div>
)
}
</div>
</div>
</LovePage>
);
}

181
web/pages/signin.tsx Normal file
View File

@@ -0,0 +1,181 @@
"use client";
import {useSearchParams} from "next/navigation";
import {Suspense, useEffect, useState} from "react";
import Link from "next/link";
import {FcGoogle} from "react-icons/fc";
import {auth, firebaseLogin} from "web/lib/firebase/users";
import FavIcon from "web/public/FavIcon";
import {signInWithEmailAndPassword} from "firebase/auth";
import {getLoverRow} from "common/love/lover";
import {db} from "web/lib/supabase/db";
import Router from "next/router";
import {LovePage} from "web/components/love-page";
async function redirectSignin(creds) {
console.log("User signed in:", creds.user);
const userId = creds?.user.uid
if (userId) {
const lover = await getLoverRow(userId, db)
if (!lover) {
await Router.push('/signup')
} else {
await Router.push('/')
}
}
}
const handleEmailPasswordSignIn = async (email: string, password: string) => {
try {
const creds = await signInWithEmailAndPassword(auth, email, password);
await redirectSignin(creds)
} catch (error) {
console.error("Error signing in:", error);
}
};
const handleGoogleSignIn = async () => {
try {
const creds = await firebaseLogin();
await redirectSignin(creds)
} catch (error) {
console.error("Error signing in:", error);
}
};
export default function LoginPage() {
return (
<Suspense fallback={<div></div>}>
<RegisterComponent/>
</Suspense>
);
}
function RegisterComponent() {
const searchParams = useSearchParams();
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const error = searchParams.get('error');
if (error === 'OAuthAccountNotLinked') {
setError('This email is already registered with a different provider');
} else if (error) {
setError('An error occurred during login');
}
}, [searchParams]);
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
try {
event.preventDefault();
setIsLoading(true);
setError(null);
const formData = new FormData(event.currentTarget);
const email = formData.get("email") as string;
const password = formData.get("password") as string;
await handleEmailPasswordSignIn(email, password);
// if (response?.error) {
// setError("Invalid email or password");
// setIsLoading(false);
// return;
// }
// router.push("/");
// router.refresh();
} catch {
setError("An error occurred during login");
setIsLoading(false);
}
}
console.log('Form rendering');
return (
<LovePage trackPageView={'signin'}>
<div className="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<div className="flex justify-center mb-6">
<FavIcon className="dark:invert"/>
</div>
<h2 className="mt-6 text-center text-3xl font-extrabold ">
Sign in to your account
</h2>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="rounded-md shadow-sm -space-y-px">
<div>
<label htmlFor="email" className="sr-only">
Email
</label>
<input
id="email"
name="email"
type="email"
required
className="bg-primary-50 appearance-none rounded-none relative block w-full px-3 py-2 border rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Email"
/>
</div>
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
id="password"
name="password"
type="password"
required
className="bg-primary-50 appearance-none rounded-none relative block w-full px-3 py-2 border rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
placeholder="Password"
/>
</div>
</div>
{error && (
<div className="text-red-500 text-sm text-center">{error}</div>
)}
<div className="space-y-4">
<button
type="submit"
disabled={isLoading}
className={`group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-full text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${isLoading ? 'opacity-70 cursor-not-allowed' : ''}`}
>
{isLoading ? 'Signing in...' : 'Sign in with Email'}
</button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-gray-50 dark:bg-gray-900 text-gray-500">Or continue with</span>
</div>
</div>
<button
type="button"
onClick={handleGoogleSignIn}
disabled={isLoading}
className="w-full flex items-center justify-center gap-2 py-2 px-4 border border-gray-300 rounded-full shadow-sm text-sm font-medium text-gray-700 hover: focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-70 disabled:cursor-not-allowed"
>
<FcGoogle className="w-5 h-5"/>
Sign in with Google
</button>
</div>
</form>
<div className="text-center">
<Link href="/register" className="text-blue-600 hover:underline">
No account? Register.
</Link>
</div>
</div>
</div>
</LovePage>
);
}