mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-01-06 04:48:14 -05:00
Make sign in and sign up pages and allow for email/pwd registration
This commit is contained in:
@@ -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}`)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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
252
web/pages/register.tsx
Normal 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
181
web/pages/signin.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user