diff --git a/package-lock.json b/package-lock.json index eb315d66..0205913e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,10 +25,12 @@ "@visx/vendor": "^3.5.0", "ethers": "^6.13.1", "framer-motion": "^11.3.17", + "input-otp": "^1.2.4", "loglevel": "^1.9.1", "mutative": "^1.0.7", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", "react-router-dom": "^6.25.1", "use-mutative": "^1.1.5" }, @@ -6833,6 +6835,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/input-otp": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.2.4.tgz", + "integrity": "sha512-md6rhmD+zmMnUh5crQNSQxq3keBRYvE3odbr4Qb9g2NWzQv9azi+t1a3X4TBTbh98fsGHgEEJlzbe1q860uGCA==", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/internal-slot": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", @@ -8212,6 +8223,21 @@ "react": "^18.3.1" } }, + "node_modules/react-hook-form": { + "version": "7.53.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz", + "integrity": "sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 451f1794..5b3b803a 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,12 @@ "@visx/vendor": "^3.5.0", "ethers": "^6.13.1", "framer-motion": "^11.3.17", + "input-otp": "^1.2.4", "loglevel": "^1.9.1", "mutative": "^1.0.7", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", "react-router-dom": "^6.25.1", "use-mutative": "^1.1.5" }, diff --git a/public/images/google.svg b/public/images/google.svg new file mode 100644 index 00000000..ad9d7f7e --- /dev/null +++ b/public/images/google.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/onboarding/Login.jsx b/src/onboarding/Login.jsx index bae5f72d..c03dd1be 100644 --- a/src/onboarding/Login.jsx +++ b/src/onboarding/Login.jsx @@ -1,7 +1,65 @@ +import { Button, Divider, Input } from '@nextui-org/react'; +import { useSignIn } from '@clerk/clerk-react'; import { Link } from 'react-router-dom'; -import { SignIn } from '@clerk/clerk-react'; +import { reduceState } from '../shared/helpers'; +import { useForm } from 'react-hook-form'; +import { useMutativeReducer } from 'use-mutative'; export default function Login() { + const { + register, + handleSubmit, + setError, + formState: { errors } + } = useForm(); + + const { isLoaded: isClerkLoaded, signIn, setActive } = useSignIn(); + const [state, dispatch] = useMutativeReducer(reduceState, { + isLoading: false + }); + + const handleLogin = async data => { + if (!isClerkLoaded) { + return; + } + try { + dispatch({ isLoading: true }); + const signInAttempt = await signIn.create({ + identifier: data.email, + password: data.password + }); + if (signInAttempt.status === 'complete') { + await setActive({ session: signInAttempt.createdSessionId }); + } else { + console.error(JSON.stringify(signInAttempt, null, 2)); + } + } catch (err) { + err.errors.forEach(e => { + if (e.meta.paramName === 'identifier') { + setError('email', { + message: e.message + }); + } + if (e.meta.paramName === 'password') { + setError('password', { + message: e.message + }); + } + }); + } finally { + dispatch({ isLoading: false }); + } + }; + + // const handleGoogleLogin = async () => { + // await signIn.authenticateWithRedirect({ + // strategy: 'oauth_google', + // redirectUrl: + // 'https://hip-primate-84.clerk.accounts.dev/v1/oauth_callback', + // redirectUrlComplete: '/' + // }); + // }; + return (
@@ -12,44 +70,97 @@ export default function Login() { Let's sign in to your account or if you don't have one, sign up

-
- */} + +
+

+ Log In to your account +

+ + + + -

Don't have an account?

- + +

or

+ +
+ {/*
+ Continue with Google + */} + + + +
+

Don't have an account?

+ + Create account + +
+ ); } diff --git a/src/onboarding/Register.jsx b/src/onboarding/Register.jsx index 8ea934c5..87df9dd7 100644 --- a/src/onboarding/Register.jsx +++ b/src/onboarding/Register.jsx @@ -1,7 +1,164 @@ -import { Link } from 'react-router-dom'; -import { SignUp } from '@clerk/clerk-react'; +import { Button, Checkbox, cn, Input } from '@nextui-org/react'; +import { useSignUp } from '@clerk/clerk-react'; +import { Link, useNavigate } from 'react-router-dom'; +import { OTPInput } from 'input-otp'; +import { reduceState } from '../shared/helpers'; +import { useForm } from 'react-hook-form'; +import { useMutativeReducer } from 'use-mutative'; export default function Register() { + const { + register, + handleSubmit, + getValues, + setError, + // clearErrors, + // reset, + formState: { errors } + } = useForm(); + const { isLoaded: isClerkLoaded, signUp, setActive } = useSignUp(); + const [state, dispatch] = useMutativeReducer(reduceState, { + isLoading: false, + verifying: false, + code: '', + verificationError: undefined + }); + const navigate = useNavigate(); + + const handleSignUp = async data => { + if (!isClerkLoaded) return; + + try { + dispatch({ isLoading: true }); + await signUp.create({ + emailAddress: data.email, + password: data.password + }); + + await signUp.prepareEmailAddressVerification({ + strategy: 'email_code' + }); + + dispatch({ verifying: true }); + } catch (err) { + err.errors.forEach(e => { + if (e.meta.paramName === 'email_address') { + setError('email', { + message: e.message + }); + } + if (e.meta.paramName === 'password') { + setError('password', { + message: e.message + }); + } + }); + } finally { + dispatch({ isLoading: false }); + } + }; + + const handleVerify = async e => { + e.preventDefault(); + if (!isClerkLoaded) return; + try { + dispatch({ isLoading: true }); + const completeSignUp = await signUp.attemptEmailAddressVerification({ + code: state.code + }); + if (completeSignUp.status === 'complete') { + await setActive({ session: completeSignUp.createdSessionId }); + navigate('/'); + } else { + console.error(JSON.stringify(completeSignUp, null, 2)); + } + } catch (err) { + err.errors.forEach(e => { + if (e.meta.paramName === 'code') { + dispatch({ verificationError: e.longMessage }); + } + }); + } finally { + dispatch({ isLoading: false }); + } + }; + + // const handleGoogleLogin = async () => { + // reset(); + // if (!getValues('terms')) { + // setError('terms', { + // message: 'Please accept terms and conditions', + // type: 'required' + // }); + // return; + // } + // clearErrors('terms'); + // await signUp.authenticateWithRedirect({ + // strategy: 'oauth_google', + // redirectUrl: + // 'https://hip-primate-84.clerk.accounts.dev/v1/oauth_callback', + // redirectUrlComplete: '/' + // }); + // }; + + if (state.verifying) { + return ( +
+
+

+ Register +

+

+ Let's create your account or if you have one already, sign in +

+
+
+

+ Verify your email +

+

+ {getValues('email')} +

+ + dispatch({ code: e })} + render={({ slots }) => ( + <> +
+ {slots.map((slot, i) => ( + + ))} +
+ + )} + required + value={state.code} + /> + + {state.verificationError && !state.isLoading && ( +

{state.verificationError}

+ )} + + + +
+ ); + } + return (
@@ -12,44 +169,141 @@ export default function Register() { Let's create your account or if you have one already, sign in

-
- */} + +
+

+ Create your account +

+ + + + -

Already have an account?

- + +

or

+ +
*/} + + {/*
+ Continue with Google + */} + +
+ + Accept Terms & conditions + +

+ {errors?.terms && errors.terms.message} +

+
+ + + +
+

Already have an account?

+ + Log in + +
+ + + ); +} + +function Slot(props) { + return ( +
+ {props.char !== null &&
{props.char}
} + {props.hasFakeCaret && } +
+ ); +} + +function FakeCaret() { + return ( +
+
); } diff --git a/tailwind.config.js b/tailwind.config.js index 4641ffc3..dce05a12 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -8,7 +8,17 @@ export default { './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}' ], theme: { - extend: {}, + extend: { + keyframes: { + 'caret-blink': { + '0%,70%,100%': { opacity: '1' }, + '20%,50%': { opacity: '0' } + } + }, + animation: { + 'caret-blink': 'caret-blink 1.2s ease-out infinite' + } + }, fontFamily: { sans: ['"DM Sans", system-ui, sans-serif', { fontOpticalSizing: 'auto' }], display: ['Exo, system-ui, sans-serif', { fontOpticalSizing: 'auto' }] @@ -62,7 +72,8 @@ export default { outline: '#293041', primary: '#cad7f9', secondary: '#ffcc80', - success: '#7ccb69' + success: '#7ccb69', + danger: '#d24646' } } }