From ea74e0514eb6b1de4478aed7c171be4f4a3f2cbc Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Sat, 6 Sep 2025 10:50:47 +0200 Subject: [PATCH] Make sign in and sign up pages and allow for email/pwd registration --- backend/api/src/create-user.ts | 19 +- old/app/login/page.tsx | 152 ---------------- old/app/register/page.tsx | 264 ---------------------------- old/components/FavIcon.tsx | 11 -- web/components/love-page.tsx | 41 ++--- web/components/nav/love-sidebar.tsx | 8 +- web/lib/util/signup.ts | 6 + web/pages/index.tsx | 60 +++---- web/pages/register.tsx | 252 ++++++++++++++++++++++++++ web/pages/signin.tsx | 181 +++++++++++++++++++ 10 files changed, 501 insertions(+), 493 deletions(-) delete mode 100644 old/app/login/page.tsx delete mode 100644 old/app/register/page.tsx delete mode 100644 old/components/FavIcon.tsx create mode 100644 web/pages/register.tsx create mode 100644 web/pages/signin.tsx diff --git a/backend/api/src/create-user.ts b/backend/api/src/create-user.ts index 76492ee..aa2b821 100644 --- a/backend/api/src/create-user.ts +++ b/backend/api/src/create-user.ts @@ -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}`) diff --git a/old/app/login/page.tsx b/old/app/login/page.tsx deleted file mode 100644 index b88aae1..0000000 --- a/old/app/login/page.tsx +++ /dev/null @@ -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 ( - }> - - - ); -} - -function RegisterComponent() { - const router = useRouter(); - const searchParams = useSearchParams(); - const [error, setError] = useState(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) { - 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 ( -
-
-
-
- -
-

- Sign in to your account -

-
-
-
-
- - -
-
- - -
-
- - {error && ( -
{error}
- )} - -
- - -
-
-
-
-
- Or continue with -
-
- - -
-
-
- - No account? Register. - -
-
-
- ); -} diff --git a/old/app/register/page.tsx b/old/app/register/page.tsx deleted file mode 100644 index 1c9f906..0000000 --- a/old/app/register/page.tsx +++ /dev/null @@ -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 ( - }> - - - ); -} - -const href = '/onboarding'; - -function RegisterComponent() { - const searchParams = useSearchParams(); - const [error, setError] = useState(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) { - 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 ( -
-
- {registrationSuccess ? ( -
-
- - - -
-

- Check your email -

-

- We have sent a verification link to {registeredEmail}. - Please click the link in the email to verify your account. -

-

- Did not receive the email? Check your spam folder or{' '} - - . -

-
- - Back to Login - -
-
- ) : ( -
-
- {/*

*/} - {/* The project is still in development...*/} - {/*

*/} -
- -
-

- Get Started -

-
-
-
-
- - -
-
- - -
-
- - -
-
- -
-

- By signing up, I agree to the{" "} - - Terms and Conditions - {" "} - and{" "} - - Privacy Policy - . -

-
- - {error && ( -
{error}
- )} - -
- - -
-
-
-
-
- Or sign up with -
-
- - -
-
-
-

- Already have an account?{' '} - - Sign in - -

-
-
- ) - } -
-
- ); -} diff --git a/old/components/FavIcon.tsx b/old/components/FavIcon.tsx deleted file mode 100644 index dfcfaf9..0000000 --- a/old/components/FavIcon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; -import Image from "next/image"; - -type FavIconProps = React.SVGProps; - -const FavIcon: React.FC = ({ className }) => ( - Compass logo -); - -export default FavIcon; - diff --git a/web/components/love-page.tsx b/web/components/love-page.tsx index e20c911..8ff7cda 100644 --- a/web/components/love-page.tsx +++ b/web/components/love-page.tsx @@ -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) diff --git a/web/components/nav/love-sidebar.tsx b/web/components/nav/love-sidebar.tsx index e749480..5f9b432 100644 --- a/web/components/nav/love-sidebar.tsx +++ b/web/components/nav/love-sidebar.tsx @@ -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 && } - {user === null && } + {/*{user === null && }*/} {user && lover === null && ( - + {/**/} )} {!user && <> -

- Don't Swipe.
| -

-
-
-
-
-

Radically Transparent

-

- No algorithms. Every profile searchable. -

-
+

+ Don't Swipe.
| +

+
+
+
+
+

Radically Transparent

+

+ No algorithms. Every profile searchable. +

+
-
-

Built for Depth

-

- Filter by any keyword and what matters most. -

-
+
+

Built for Depth

+

+ Filter by any keyword and what matters most. +

+
-
-

Community Owned

-

- Free forever. Built by users, for users. -

+
+

Community Owned

+

+ Free forever. Built by users, for users. +

+
+
-
-
} {user && <> diff --git a/web/pages/register.tsx b/web/pages/register.tsx new file mode 100644 index 0000000..974e307 --- /dev/null +++ b/web/pages/register.tsx @@ -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 ( +
}> + + + ); +} + +const href = '/signup'; + +function RegisterComponent() { + const searchParams = useSearchParams(); + const [error, setError] = useState(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) { + 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 ( + +
+
+ {registrationSuccess ? ( +
+
+ + + +
+

+ Check your email +

+

+ We have sent a verification link to {registeredEmail}. + Please click the link in the email to verify your account. +

+

+ Did not receive the email? Check your spam folder or{' '} + + . +

+
+ + Back to Login + +
+
+ ) : ( +
+
+ {/*

*/} + {/* The project is still in development...*/} + {/*

*/} +
+ +
+

+ Get Started +

+
+
+
+
+ + +
+
+ + +
+
+ +
+

+ By signing up, I agree to the{" "} + + Terms and Conditions + {" "} + and{" "} + + Privacy Policy + . +

+
+ + {error && ( +
{error}
+ )} + +
+ + +
+
+
+
+
+ Or sign up with +
+
+ + +
+
+
+

+ Already have an account?{' '} + + Sign in + +

+
+
+ ) + } +
+
+
+ ); +} diff --git a/web/pages/signin.tsx b/web/pages/signin.tsx new file mode 100644 index 0000000..b210fb4 --- /dev/null +++ b/web/pages/signin.tsx @@ -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 ( +
}> + + + ); +} + +function RegisterComponent() { + const searchParams = useSearchParams(); + const [error, setError] = useState(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) { + 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 ( + +
+
+
+
+ +
+

+ Sign in to your account +

+
+
+
+
+ + +
+
+ + +
+
+ + {error && ( +
{error}
+ )} + +
+ + +
+
+
+
+
+ Or continue with +
+
+ + +
+
+
+ + No account? Register. + +
+
+
+
+ ); +}